mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-09 00:02:30 +00:00
Runnable refactor (#929)
* fix(frontend): Support runnable everywhere * fix(frontend): Runnable component working
This commit is contained in:
@@ -16,6 +16,9 @@
|
||||
export let extraLib: string = 'missing extraLib'
|
||||
export let autofocus = false
|
||||
export let previousModuleId: string | undefined = undefined
|
||||
|
||||
export let shouldHideNoInputs: boolean = false
|
||||
|
||||
let clazz: string = ''
|
||||
export { clazz as class }
|
||||
|
||||
@@ -73,7 +76,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="italic text-sm">No inputs</p>
|
||||
{:else if !shouldHideNoInputs}
|
||||
<div class="text-gray-500 text-sm">No inputs</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -13,11 +13,16 @@
|
||||
ArcElement
|
||||
} from 'chart.js'
|
||||
import type { ChartData } from 'chart.js'
|
||||
import ComponentInputValue from '../helpers/ComponentInputValue.svelte'
|
||||
import type { ComponentInputsSpec } from '../../types'
|
||||
import type { InputsSpec } from '../../types'
|
||||
import RunnableComponent from '../helpers/RunnableComponent.svelte'
|
||||
|
||||
export let componentInputs: ComponentInputsSpec
|
||||
export const staticOutputs: string[] = []
|
||||
export let id: string
|
||||
export let inputs: InputsSpec
|
||||
export let path: string | undefined = undefined
|
||||
export let runType: 'script' | 'flow' | undefined = undefined
|
||||
export let inlineScriptName: string | undefined = undefined
|
||||
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
@@ -34,33 +39,13 @@
|
||||
responsive: true
|
||||
}
|
||||
|
||||
let dataSetValue: Record<string, number> | undefined = undefined
|
||||
|
||||
let data: ChartData<'pie', number[], unknown> = { datasets: [], labels: [] }
|
||||
|
||||
function populateDataSet() {
|
||||
if (dataSetValue) {
|
||||
data.datasets = [
|
||||
{
|
||||
data: Object.values(dataSetValue).filter((val) => typeof val === 'number'),
|
||||
backgroundColor: ['#F7464A', '#46BFBD', '#FDB45C', '#949FB1', '#4D5360', '#AC64AD'],
|
||||
hoverBackgroundColor: ['#FF5A5E', '#5AD3D1', '#FFC870', '#A8B3C5', '#616774', '#DA92DB']
|
||||
}
|
||||
]
|
||||
data.labels = Object.keys(dataSetValue)
|
||||
.filter((key) => typeof dataSetValue?.[key] === 'number')
|
||||
.map((s) => s.replace(/([A-Z]+)*([A-Z][a-z])/g, '$1 $2'))
|
||||
|
||||
data = data
|
||||
}
|
||||
}
|
||||
|
||||
$: dataSetValue && populateDataSet()
|
||||
let result: ChartData<'pie', number[], unknown> | undefined = undefined
|
||||
</script>
|
||||
|
||||
<ComponentInputValue input={componentInputs.dataset} bind:value={dataSetValue} />
|
||||
{#if data.datasets.length > 0}
|
||||
<Pie {data} {options} />
|
||||
{:else}
|
||||
<span>No dataset</span>
|
||||
{/if}
|
||||
<RunnableComponent {id} {path} {runType} {inlineScriptName} {inputs} bind:result>
|
||||
{#if result}
|
||||
<Pie data={result} {options} />
|
||||
{:else}
|
||||
<span>No dataset</span>
|
||||
{/if}
|
||||
</RunnableComponent>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import type { Schema } from '$lib/common'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
|
||||
@@ -11,7 +12,8 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, InputsSpec } from '../../types'
|
||||
import { buildArgs, loadSchema, schemaToInputsSpec } from '../../utils'
|
||||
import { loadSchema, schemaToInputsSpec } from '../../utils'
|
||||
import RunnableInputValue from './RunnableInputValue.svelte'
|
||||
|
||||
// Component props
|
||||
export let id: string
|
||||
@@ -20,9 +22,7 @@
|
||||
export let runType: 'script' | 'flow' | undefined = undefined
|
||||
export let inlineScriptName: string | undefined = undefined
|
||||
export let extraQueryParams: Record<string, any> = {}
|
||||
|
||||
export let shouldTick: number | undefined = undefined
|
||||
|
||||
export let result: any = undefined
|
||||
|
||||
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
@@ -31,71 +31,97 @@
|
||||
// Local state
|
||||
let args: Record<string, any> = {}
|
||||
let schema: Schema | undefined = undefined
|
||||
let schemaClone: Schema | undefined = undefined
|
||||
|
||||
let isValid = true
|
||||
let testIsLoading = false
|
||||
let runnableInputValues: Record<string, any> = {}
|
||||
|
||||
$: if (outputs) {
|
||||
outputs.loading.set(testIsLoading)
|
||||
$: mergedArgs = { ...args, ...extraQueryParams, ...runnableInputValues }
|
||||
|
||||
function isMergedArgsValid(mergedArgs: Record<string, any>) {
|
||||
if (Object.keys(inputs).length !== Object.keys(runnableInputValues).length) {
|
||||
return false
|
||||
}
|
||||
|
||||
const areAllArgsValid = Object.values(mergedArgs).every(
|
||||
(arg) => arg !== undefined && arg !== null
|
||||
)
|
||||
|
||||
debugger
|
||||
|
||||
if (areAllArgsValid) {
|
||||
executeComponent()
|
||||
}
|
||||
|
||||
return areAllArgsValid
|
||||
}
|
||||
|
||||
$: isValid = isMergedArgsValid(mergedArgs)
|
||||
|
||||
// Test job internal state
|
||||
let testJob: CompletedJob | undefined = undefined
|
||||
let testJobLoader: TestJobLoader | undefined = undefined
|
||||
|
||||
$: if ($workspaceStore && path && runType) {
|
||||
loadSchemaFromTriggerable($workspaceStore, path, runType)
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<Array<any>>
|
||||
loading: Output<boolean>
|
||||
}
|
||||
|
||||
$: if (inlineScriptName && $app.inlineScripts[inlineScriptName]) {
|
||||
schema = $app.inlineScripts[inlineScriptName].schema
|
||||
/**
|
||||
* Args are built from 3 sources:
|
||||
* 1. The inputs spec ( )
|
||||
* 2. The schema input transform with user submitted values§
|
||||
* 3. The extra query params
|
||||
*/
|
||||
|
||||
Object.keys(extraQueryParams).forEach((key) => {
|
||||
if (schema?.properties[key]) {
|
||||
delete schema.properties[key]
|
||||
}
|
||||
})
|
||||
|
||||
reloadSchemaAndArgs()
|
||||
}
|
||||
|
||||
$: if (inputs && schema !== undefined) {
|
||||
if (Object.keys(schema.properties).length !== Object.keys(inputs).length) {
|
||||
inputs = schemaToInputsSpec(schema)
|
||||
}
|
||||
|
||||
reloadSchemaAndArgs()
|
||||
}
|
||||
|
||||
// Load once
|
||||
async function loadSchemaFromTriggerable(
|
||||
workspace: string,
|
||||
path: string,
|
||||
runType: 'script' | 'flow'
|
||||
) {
|
||||
schema = await loadSchema(workspace, path, runType)
|
||||
}
|
||||
|
||||
Object.keys(extraQueryParams).forEach((key) => {
|
||||
if (schema?.properties[key]) {
|
||||
delete schema.properties[key]
|
||||
// Only loads the schema
|
||||
$: if ($workspaceStore && path && runType) {
|
||||
// Remote schema needs to be loaded
|
||||
loadSchemaFromTriggerable($workspaceStore, path, runType)
|
||||
} else if (inlineScriptName && $app.inlineScripts[inlineScriptName]) {
|
||||
// Inline scripts directly provide the schema
|
||||
schema = $app.inlineScripts[inlineScriptName].schema
|
||||
}
|
||||
|
||||
// When the schema is loaded, we need to update the inputs spec
|
||||
// in order to render the inputs the component panel
|
||||
$: if (schema && Object.keys(schema.properties ?? {}).length !== Object.keys(inputs).length) {
|
||||
inputs = schemaToInputsSpec(schema)
|
||||
}
|
||||
|
||||
let schemaStripped: Schema | undefined = undefined
|
||||
|
||||
function stripSchema(schema: Schema) {
|
||||
schemaStripped = JSON.parse(JSON.stringify(schema))
|
||||
|
||||
// Remove hidden static inputs
|
||||
Object.keys(inputs).forEach((key: string) => {
|
||||
const input = inputs[key]
|
||||
|
||||
if (input.type === 'static' && !input.visible && schemaStripped !== undefined) {
|
||||
delete schemaStripped.properties[key]
|
||||
}
|
||||
|
||||
if (input.type === 'output' && schemaStripped !== undefined) {
|
||||
delete schemaStripped.properties[key]
|
||||
}
|
||||
})
|
||||
|
||||
// Remove extra query params from schema
|
||||
Object.keys(extraQueryParams).forEach((key: string) => {
|
||||
if (schemaStripped !== undefined) {
|
||||
delete schemaStripped.properties[key]
|
||||
}
|
||||
})
|
||||
args = buildArgs(inputs, schema)
|
||||
}
|
||||
|
||||
async function reloadSchemaAndArgs() {
|
||||
schemaClone = JSON.parse(JSON.stringify(schema))
|
||||
|
||||
if (schemaClone !== undefined) {
|
||||
args = buildArgs(inputs, schemaClone)
|
||||
|
||||
Object.keys(schemaClone.properties).forEach((propKey) => {
|
||||
if (!Object.keys(args).includes(propKey)) {
|
||||
delete schemaClone!.properties[propKey]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
$: schema && stripSchema(schema)
|
||||
|
||||
$: disabledArgs = Object.keys(inputs).reduce((a: string[], c: string) => {
|
||||
if (inputs[c].type === 'static') {
|
||||
@@ -107,10 +133,7 @@
|
||||
async function executeComponent() {
|
||||
await testJobLoader?.abstractRun(() => {
|
||||
const requestBody = {
|
||||
args: {
|
||||
...args,
|
||||
...extraQueryParams
|
||||
},
|
||||
args: mergedArgs,
|
||||
force_viewer_static_fields: {}
|
||||
}
|
||||
|
||||
@@ -131,19 +154,12 @@
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$: if (testJobLoader && shouldTick) {
|
||||
executeComponent()
|
||||
}
|
||||
|
||||
$: extraQueryParams && executeComponent()
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<Array<any>>
|
||||
loading: Output<boolean>
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each Object.keys(inputs) as key}
|
||||
<RunnableInputValue input={inputs[key]} bind:value={runnableInputValues[key]} />
|
||||
{/each}
|
||||
|
||||
<TestJobLoader
|
||||
on:done={() => {
|
||||
if (testJob) {
|
||||
@@ -156,18 +172,24 @@
|
||||
bind:this={testJobLoader}
|
||||
/>
|
||||
|
||||
{#if schemaClone !== undefined}
|
||||
<SchemaForm schema={schemaClone} bind:args bind:isValid {disabledArgs} />
|
||||
{#if schemaStripped !== undefined}
|
||||
<SchemaForm schema={schemaStripped} bind:args {isValid} {disabledArgs} shouldHideNoInputs />
|
||||
{/if}
|
||||
|
||||
{#if shouldTick === undefined}
|
||||
<Button size="xs" color="dark" on:click={() => executeComponent()} disabled={!isValid}>
|
||||
<div>
|
||||
{Object.keys(args).length > 0 ? 'Submit' : 'Refresh'}
|
||||
{#if testIsLoading}
|
||||
<Icon data={faArrowsRotate} class="animate-spin ml-2" scale={0.8} />
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
{#if isValid}
|
||||
<Button size="xs" color="dark" on:click={() => executeComponent()} disabled={!isValid}>
|
||||
<div>
|
||||
{Object.keys(args).length > 0 ? 'Submit' : 'Refresh'}
|
||||
{#if testIsLoading}
|
||||
<Icon data={faArrowsRotate} class="animate-spin ml-2" scale={0.8} />
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
<slot />
|
||||
{:else}
|
||||
<Alert type="warning" size="xs" class="mt-2" title="Missing inputs">
|
||||
Please fill in all the inputs
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
<slot />
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { StaticInput, DynamicInput, AppEditorContext, UserInput } from '../../types'
|
||||
|
||||
export let input: DynamicInput | StaticInput | UserInput
|
||||
export let value: any
|
||||
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
$: hasConnection = input.type === 'output' && input.id !== undefined && input.name !== undefined
|
||||
|
||||
$: inputResult = hasConnection
|
||||
? $worldStore?.connect<any>(input, () => updateValue())
|
||||
: {
|
||||
peak: () => {
|
||||
if (input.type === 'static') {
|
||||
return input.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateValue() {
|
||||
value = inputResult?.peak()
|
||||
}
|
||||
|
||||
$: !hasConnection && input && input.type === 'static' && updateValue()
|
||||
</script>
|
||||
@@ -47,7 +47,11 @@
|
||||
{:else if component.type === 'barchartcomponent'}
|
||||
<BarChartComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
|
||||
{:else if component.type === 'piechartcomponent'}
|
||||
<PieChartComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
|
||||
<PieChartComponent
|
||||
{...component}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
bind:inputs={component.inputs}
|
||||
/>
|
||||
{:else if component.type === 'tablecomponent'}
|
||||
<TableComponent
|
||||
{...component}
|
||||
|
||||
@@ -91,14 +91,7 @@ const chartComponents = {
|
||||
...defaultProps,
|
||||
id: 'piechartcomponent',
|
||||
type: 'piechartcomponent',
|
||||
componentInputs: {
|
||||
dataset: {
|
||||
type: 'static',
|
||||
visible: true,
|
||||
value: {},
|
||||
fieldType: 'textarea'
|
||||
}
|
||||
}
|
||||
runnable: true
|
||||
},
|
||||
{
|
||||
...defaultProps,
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
let object = {}
|
||||
|
||||
outputs.forEach((output) => {
|
||||
console.log({ output })
|
||||
$worldStore?.outputsById[componentId][output].subscribe({
|
||||
next: (value) => {
|
||||
object[output] = value
|
||||
|
||||
@@ -21,7 +21,6 @@ export type World = {
|
||||
}
|
||||
|
||||
export function buildWorld(components: Record<string, string[]>) {
|
||||
console.log({ components })
|
||||
const newWorld = buildObservableWorld()
|
||||
const outputsById: Record<string, Record<string, Output<any>>> = {}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export type BarChartComponent = {
|
||||
type: 'barchartcomponent'
|
||||
}
|
||||
|
||||
export type PieChartComponent = {
|
||||
export type PieChartComponent = Runnable & {
|
||||
type: 'piechartcomponent'
|
||||
}
|
||||
|
||||
|
||||
@@ -21,25 +21,25 @@
|
||||
|
||||
const classes: Record<AlertType, Record<string, string>> = {
|
||||
info: {
|
||||
bgClass: 'bg-blue-50',
|
||||
bgClass: 'bg-blue-50 border-blue-200 border',
|
||||
iconClass: 'text-blue-500',
|
||||
titleClass: 'text-blue-800',
|
||||
descriptionClass: 'text-blue-700'
|
||||
},
|
||||
warning: {
|
||||
bgClass: 'bg-yellow-50',
|
||||
bgClass: 'bg-yellow-50 border-yellow-200 border',
|
||||
iconClass: 'text-yellow-500',
|
||||
titleClass: 'text-yellow-800',
|
||||
descriptionClass: 'text-yellow-700'
|
||||
},
|
||||
error: {
|
||||
bgClass: 'bg-red-50',
|
||||
bgClass: 'bg-red-50 border-red-200 border',
|
||||
iconClass: 'text-red-500',
|
||||
titleClass: 'text-red-800',
|
||||
descriptionClass: 'text-red-700'
|
||||
},
|
||||
success: {
|
||||
bgClass: 'bg-green-50',
|
||||
bgClass: 'bg-green-50 border-green-200 border',
|
||||
iconClass: 'text-green-500',
|
||||
titleClass: 'text-green-800',
|
||||
descriptionClass: 'text-green-700'
|
||||
@@ -47,7 +47,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={classNames('rounded-md p-4', classes[type].bgClass, $$props.class)}>
|
||||
<div class={classNames('rounded-md p-4 ', classes[type].bgClass, $$props.class)}>
|
||||
<div class="flex">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full">
|
||||
<Icon data={icons[type]} class={classes[type].iconClass} />
|
||||
|
||||
Reference in New Issue
Block a user