feat(frontend): Rich result by id component (#4069)

* feat(frontend): Rich result by id component

* feat(frontend): add waitJob helper

* feat(frontend): improve code

* feat(frontend): improve code

* feat(frontend): fix build
This commit is contained in:
Faton Ramadani
2024-07-15 09:06:45 +02:00
committed by GitHub
parent 83c717bba0
commit ae9d73c1f0
10 changed files with 268 additions and 12 deletions
@@ -25,6 +25,7 @@
const requireHtmlApproval = getContext<boolean | undefined>(IS_APP_PUBLIC_CONTEXT_KEY)
const { app, worldStore, componentControl } = getContext<AppViewerContext>('AppViewerContext')
let result: any = undefined
const resolvedConfig = initConfig(
@@ -75,7 +76,7 @@
)}
style={css?.header?.style}
>
{resolvedConfig?.title ?? 'Result'}
{resolvedConfig?.title ? resolvedConfig?.title : 'Result'}
</div>
<div
style={twMerge(
@@ -0,0 +1,116 @@
<script lang="ts">
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { initConfig, initOutput } from '../../editor/appUtils'
import {
IS_APP_PUBLIC_CONTEXT_KEY,
type AppViewerContext,
type ComponentCustomCSS,
type RichConfigurations
} from '../../types'
import { initCss } from '../../utils'
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
import type { Job } from '$lib/gen'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import DisplayResult from '$lib/components/DisplayResult.svelte'
export let id: string
export let initializing: boolean | undefined = false
export let customCss: ComponentCustomCSS<'jobiddisplaycomponent'> | undefined = undefined
export let configuration: RichConfigurations
export let render: boolean
const { app, worldStore, workspace } = getContext<AppViewerContext>('AppViewerContext')
const requireHtmlApproval = getContext<boolean | undefined>(IS_APP_PUBLIC_CONTEXT_KEY)
let resolvedConfig = initConfig(
components['jobiddisplaycomponent'].initialData.configuration,
configuration
)
const outputs = initOutput($worldStore, id, {
result: undefined,
loading: false,
jobId: undefined
})
initializing = false
let css = initCss($app.css?.jobiddisplaycomponent, customCss)
let testJobLoader: TestJobLoader | undefined = undefined
let testIsLoading: boolean = false
let testJob: Job | undefined = undefined
$: if (resolvedConfig.jobId) {
outputs.loading.set(true)
testJobLoader?.watchJob(resolvedConfig?.['jobId'])
}
let result: any = undefined
</script>
{#each Object.keys(components['jobiddisplaycomponent'].initialData.configuration) as key (key)}
<ResolveConfig
{id}
{key}
bind:resolvedConfig={resolvedConfig[key]}
configuration={configuration[key]}
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.jobiddisplaycomponent}
/>
{/each}
<TestJobLoader
workspaceOverride={workspace}
bind:this={testJobLoader}
bind:isLoading={testIsLoading}
bind:job={testJob}
on:done={(e) => {
outputs.loading.set(false)
outputs.jobId.set(e.detail.id)
outputs.result.set(e.detail.result)
result = e.detail.result
}}
/>
<InitializeComponent {id} />
{#if render}
<div class="flex flex-col w-full h-full component-wrapper">
<div
class={twMerge(
'w-full border-b p-2 text-xs font-semibold text-primary bg-surface-secondary',
css?.header?.class
)}
style={css?.header?.style}
>
{resolvedConfig?.title ? resolvedConfig?.title : 'Result'}
</div>
<div
style={twMerge(
$app.css?.['displaycomponent']?.['container']?.style,
customCss?.container?.style,
'wm-rich-result-container'
)}
class={twMerge(
'p-2 grow overflow-auto',
$app.css?.['displaycomponent']?.['container']?.class,
customCss?.container?.class
)}
>
<DisplayResult {result} {requireHtmlApproval} disableExpand={resolvedConfig?.hideDetails} />
</div>
</div>
{/if}
@@ -1,5 +1,6 @@
import type { World } from '../../rx'
import { sendUserToast } from '$lib/toast'
import { waitJob } from '$lib/components/waitJob'
export function computeGlobalContext(world: World | undefined, extraContext: any = {}) {
return {
@@ -24,7 +25,7 @@ function create_context_function_template(
) {
let hasReturnAsLastLine = noReturn || eval_string.split('\n').some((x) => x.startsWith('return '))
return `
return async function (context, state, goto, setTab, recompute, getAgGrid, setValue, setSelectedIndex, openModal, closeModal, open, close, validate, invalidate, validateAll, clearFiles, showToast) {
return async function (context, state, goto, setTab, recompute, getAgGrid, setValue, setSelectedIndex, openModal, closeModal, open, close, validate, invalidate, validateAll, clearFiles, showToast, waitJob) {
"use strict";
${
contextKeys && contextKeys.length > 0
@@ -59,7 +60,8 @@ type WmFunctor = (
invalidate,
validateAll,
clearFiles,
showToast
showToast,
waitJob
) => Promise<any>
let functorCache: Record<number, WmFunctor> = {}
@@ -108,6 +110,7 @@ export async function eval_like(
validateAll?: () => void
clearFiles?: () => void
showToast?: (message: string, error?: boolean) => void
waitJob?: (jobId: string) => void
}
>,
worldStore: World | undefined,
@@ -185,6 +188,7 @@ export async function eval_like(
},
(message, error) => {
sendUserToast(message, error)
}
},
async (id) => waitJob(id)
)
}
@@ -77,6 +77,7 @@
import AppNumberInput from '../../components/inputs/AppNumberInput.svelte'
import AppNavbar from '../../components/display/AppNavbar.svelte'
import AppDateSelect from '../../components/inputs/AppDateSelect.svelte'
import AppDisplayComponentByJobId from '../../components/display/AppDisplayComponentByJobId.svelte'
export let component: AppComponent
export let selected: boolean
@@ -847,6 +848,14 @@
verticalAlignment={component.verticalAlignment}
{render}
/>
{:else if component.type === 'jobiddisplaycomponent'}
<AppDisplayComponentByJobId
id={component.id}
customCss={component.customCss}
bind:initializing
configuration={component.configuration}
{render}
/>
{/if}
</div>
</div>
@@ -164,6 +164,7 @@ export type AggridInfiniteComponentEe = BaseComponent<'aggridinfinitecomponentee
}
export type DisplayComponent = BaseComponent<'displaycomponent'>
export type JobIdDisplayComponent = BaseComponent<'jobiddisplaycomponent'>
export type LogComponent = BaseComponent<'logcomponent'>
export type JobIdLogComponent = BaseComponent<'jobidlogcomponent'>
export type FlowStatusComponent = BaseComponent<'flowstatuscomponent'>
@@ -354,6 +355,7 @@ export type TypedComponent =
| MultiSelectComponentV2
| NavBarComponent
| DateSelectComponent
| JobIdDisplayComponent
export type AppComponent = BaseAppComponent & TypedComponent
@@ -3995,6 +3997,38 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
}
}
}
},
jobiddisplaycomponent: {
name: 'Rich result by Job Id',
icon: Monitor,
documentationLink: `${documentationBaseUrl}/rich_result_by_job_id`,
dims: '2:8-6:8' as AppComponentDimensions,
customCss: {
header: { class: '', style: '' },
container: { class: '', style: '' }
},
initialData: {
configuration: {
jobId: {
type: 'static',
fieldType: 'text',
value: '',
tooltip: 'Job id to display logs from'
},
title: {
type: 'static',
fieldType: 'text',
value: 'Result'
},
hideDetails: {
type: 'static',
fieldType: 'boolean',
value: false,
tooltip:
'Hide the details section: the object keys, the clipboard button and the maximise button'
}
}
}
}
} as const
@@ -71,6 +71,7 @@ const display: ComponentSet = {
'displaycomponent',
'jobidlogcomponent',
'jobidflowstatuscomponent',
'jobiddisplaycomponent',
'statcomponent',
'menucomponent',
'alertcomponent'
@@ -792,5 +792,9 @@ export const quickStyleProperties: Record<
},
dateselectcomponent: {
input: inputDefaultProps
},
jobiddisplaycomponent: {
header: [...containerDefaultProps, typographyGrouping],
container: containerDefaultProps
}
}
@@ -258,6 +258,15 @@ declare function clearFiles(id: string): void;
* @param message message to display
*/
declare function showToast(message: string, error?: boolean): void;
/**
* Wait for a job to finish
* @param id job id
* @returns the result of the job
*/
declare async function waitJob(id: string): Promise<any>;
`
: ''
}
+85
View File
@@ -0,0 +1,85 @@
import { JobService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { get } from 'svelte/store'
const ITERATIONS_BEFORE_SLOW_REFRESH = 10
const ITERATIONS_BEFORE_SUPER_SLOW_REFRESH = 100
export async function waitJob(id: string) {
const workspace = get(workspaceStore)
if (!id) {
return
}
if (!workspace) {
throw new Error('Workspace not found')
}
let syncIteration: number = 0
let errorIteration: number = 0
let job: any
return new Promise((resolve, reject) => {
async function checkJob() {
try {
const maybeJob = await JobService.getCompletedJobResultMaybe({
workspace: workspace!,
id,
getStarted: false
})
if (maybeJob.completed) {
job = { ...maybeJob, id }
if (!job.success && typeof job.result === 'object' && 'error' in job.result) {
reject(job.result.error)
} else {
resolve(job.result)
}
return
}
} catch (err) {
errorIteration += 1
if (errorIteration === 5) {
try {
await cancelJob(id, workspace!)
} catch (err) {
console.error(err)
}
}
}
syncIteration++
let nextIteration = 50
if (syncIteration > ITERATIONS_BEFORE_SLOW_REFRESH) {
nextIteration = 500
} else if (syncIteration > ITERATIONS_BEFORE_SUPER_SLOW_REFRESH) {
nextIteration = 2000
}
setTimeout(checkJob, nextIteration)
}
job = undefined
checkJob()
})
}
async function cancelJob(id: string, workspace: string) {
if (id) {
try {
await JobService.cancelQueuedJob({
workspace,
id,
requestBody: {}
})
} catch (err) {
console.error(err)
}
}
}
@@ -480,14 +480,7 @@
</div>
</div>
</div>
<EditableSchemaWrapper
watchChanges
on:change={() => {
console.log('change')
}}
bind:schema={newResourceType.schema}
fullHeight
/>
<EditableSchemaWrapper bind:schema={newResourceType.schema} fullHeight />
</div>
</DrawerContent>
</Drawer>