mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 16:03:27 +00:00
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:
@@ -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(
|
||||
|
||||
+116
@@ -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>;
|
||||
|
||||
|
||||
`
|
||||
: ''
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user