Runnable refactor (#932)

* fix(frontend): Support runnable everywhere

* fix(frontend): Runnable component working

* fix(frontend): Simplify implementation

* fix(frontend): User inputs working

* fix(frontend): Fix connections

* fix(frontend): auto refresh

* fix(frontend): revert changes
This commit is contained in:
Faton Ramadani
2022-11-23 10:30:58 +01:00
committed by GitHub
parent df97121527
commit 682c44a39e
15 changed files with 236 additions and 122 deletions
@@ -17,16 +17,10 @@
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
// ComponentInput: Static/dynamic
// ScriptInput: Run form: Static/Dynamic/User
// paramInput: Search : configurable only at component level (toggle)
export const staticOutputs: string[] = ['selectedRow', 'loading', 'result']
$: outputs = $worldStore?.outputsById[id] as {
selectedRow: Output<any>
result: Output<Array<string>>
loading: Output<boolean>
}
let selectedRowIndex = -1
@@ -49,6 +43,10 @@
let result: Array<Record<string, any>> = []
$: headers = Object.keys(result[0] || {}) || []
const extraQueryParams = { search, page }
export const reservedKeys: string[] = Object.keys(extraQueryParams)
</script>
<ComponentInputValue input={componentInputs.searchEnabled} bind:value={searchEnabledValue} />
@@ -42,10 +42,8 @@
let result: ChartData<'pie', number[], unknown> | undefined = undefined
</script>
<RunnableComponent {id} {path} {runType} {inlineScriptName} {inputs} bind:result>
<RunnableComponent {id} {path} {runType} {inlineScriptName} bind:inputs bind:result>
{#if result}
<Pie data={result} {options} />
{:else}
<span>No dataset</span>
{/if}
</RunnableComponent>
@@ -18,17 +18,26 @@
let labelValue: string = 'Default label'
let tick = 0
let runnableComponent: RunnableComponent
</script>
<ComponentInputValue input={componentInputs.label} bind:value={labelValue} />
<RunnableComponent bind:inputs {path} {runType} {inlineScriptName} {id} shouldTick={tick}>
<RunnableComponent
bind:this={runnableComponent}
bind:inputs
{path}
{runType}
{inlineScriptName}
{id}
autoRefresh={false}
>
<AlignWrapper {horizontalAlignement} {verticalAlignement}>
<Button
on:click={() => {
tick = tick + 1
runnableComponent?.runComponent()
}}
btnClasses="h-full"
>
{labelValue}
</Button>
@@ -2,26 +2,17 @@
import { getContext } from 'svelte'
import type { StaticInput, DynamicInput, AppEditorContext } from '../../types'
type T = $$Generic
export let input: DynamicInput | StaticInput
export let value: any
export let value: T
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
$: hasConnection = input.type === 'output' && input.id && input.name
$: input.type === 'static' && (value = input.value)
$: input.type === 'output' && $worldStore?.connect<any>(input, onValueChange)
$: inputResult = hasConnection
? $worldStore?.connect<any>(input, () => updateValue())
: {
peak: () => {
if (input.type === 'static') {
return input.value
}
}
}
function updateValue() {
value = inputResult?.peak()
function onValueChange(newValue: T): void {
value = newValue
}
$: !hasConnection && input && updateValue()
</script>
@@ -5,8 +5,9 @@
let timer: NodeJS.Timeout
function debounce(event: KeyboardEvent) {
function debounce(event: KeyboardEvent): void {
clearTimeout(timer)
timer = setTimeout(() => {
const target = event.target as HTMLInputElement
value = target.value
@@ -22,13 +22,12 @@
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 autoRefresh: boolean = true
export let result: any = undefined
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
let pagePath = $page.params.path
// Local state
let pagePath = $page.params.path
let args: Record<string, any> = {}
let schema: Schema | undefined = undefined
let testIsLoading = false
@@ -36,8 +35,24 @@
$: mergedArgs = { ...args, ...extraQueryParams, ...runnableInputValues }
function isMergedArgsValid(mergedArgs: Record<string, any>) {
if (Object.keys(inputs).length !== Object.keys(runnableInputValues).length) {
// TODO: Review
function setStaticInputsToArgs() {
Object.entries(inputs).forEach(([key, value]) => {
if (value.type === 'static') {
args[key] = value.value
}
})
args = args
}
$: inputs && setStaticInputsToArgs()
function argMergedArgsValid(mergedArgs: Record<string, any>) {
if (
Object.keys(inputs).filter((k) => inputs[k].type !== 'user').length !==
Object.keys(runnableInputValues).length
) {
return false
}
@@ -45,16 +60,14 @@
(arg) => arg !== undefined && arg !== null
)
debugger
if (areAllArgsValid) {
if (areAllArgsValid && autoRefresh) {
executeComponent()
}
return areAllArgsValid
}
$: isValid = isMergedArgsValid(mergedArgs)
$: isValid = argMergedArgsValid(mergedArgs)
// Test job internal state
let testJob: CompletedJob | undefined = undefined
@@ -65,13 +78,6 @@
loading: Output<boolean>
}
/**
* Args are built from 3 sources:
* 1. The inputs spec ( )
* 2. The schema input transform with user submitted values§
* 3. The extra query params
*/
async function loadSchemaFromTriggerable(
workspace: string,
path: string,
@@ -91,8 +97,15 @@
// 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)
$: if (schema && Object.keys(schema.properties).length !== Object.keys(inputs).length) {
let schemaWithoutExtraQueries: Schema = JSON.parse(JSON.stringify(schema))
// Remove extra query params from the schema, which are not directly configurable by the user
Object.keys(extraQueryParams).forEach((key) => {
delete schemaWithoutExtraQueries.properties[key]
})
inputs = schemaToInputsSpec(schemaWithoutExtraQueries)
}
let schemaStripped: Schema | undefined = undefined
@@ -123,14 +136,19 @@
$: schema && stripSchema(schema)
$: disabledArgs = Object.keys(inputs).reduce((a: string[], c: string) => {
if (inputs[c].type === 'static') {
a = [...a, c]
}
return a
}, [])
$: disabledArgs = Object.keys(inputs).reduce(
(disabledArgsAccumulator: string[], inputName: string) => {
if (inputs[inputName].type === 'static') {
disabledArgsAccumulator = [...disabledArgsAccumulator, inputName]
}
return disabledArgsAccumulator
},
[]
)
async function executeComponent() {
outputs?.loading.set(true)
await testJobLoader?.abstractRun(() => {
const requestBody = {
args: mergedArgs,
@@ -154,6 +172,10 @@
})
})
}
export function runComponent() {
executeComponent()
}
</script>
{#each Object.keys(inputs) as key}
@@ -164,6 +186,8 @@
on:done={() => {
if (testJob) {
outputs.result.set(testJob?.result)
outputs?.loading.set(false)
result = testJob?.result
}
}}
@@ -176,7 +200,7 @@
<SchemaForm schema={schemaStripped} bind:args {isValid} {disabledArgs} shouldHideNoInputs />
{/if}
{#if shouldTick === undefined}
{#if autoRefresh === true}
{#if isValid}
<Button size="xs" color="dark" on:click={() => executeComponent()} disabled={!isValid}>
<div>
@@ -192,4 +216,6 @@
Please fill in all the inputs
</Alert>
{/if}
{:else}
<slot />
{/if}
@@ -2,26 +2,33 @@
import { getContext } from 'svelte'
import type { StaticInput, DynamicInput, AppEditorContext, UserInput } from '../../types'
type T = $$Generic
export let input: DynamicInput | StaticInput | UserInput
export let value: any
export let value: T
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
$: hasConnection = input.type === 'output' && input.id !== undefined && input.name !== undefined
$: input && handleConnection()
$: inputResult = hasConnection
? $worldStore?.connect<any>(input, () => updateValue())
: {
peak: () => {
if (input.type === 'static') {
return input.value
}
}
}
function updateValue() {
value = inputResult?.peak()
function handleConnection() {
if (input.type === 'output') {
$worldStore?.connect<any>(input, onValueChange)
} else if (input.type === 'static') {
setValue()
}
}
$: !hasConnection && input && input.type === 'static' && updateValue()
function setValue() {
if (input.type === 'static') {
value = input.value
}
}
function onValueChange(newValue: T): void {
if (input.type === 'output') {
value = newValue
} else {
// TODO: handle disconnect
}
}
</script>
@@ -33,7 +33,7 @@
class={classNames(
'p-2 border overflow-auto cursor-pointer h-full bg-white',
selected ? 'border-blue-500' : 'border-white',
$mode === 'preview' ? 'border-white' : 'hover:border-blue-500 '
$mode === 'preview' ? 'border-white' : 'hover:border-blue-500'
)}
>
{#if component.type === 'runformcomponent'}
@@ -3,6 +3,7 @@
import type { AppEditorContext } from '../types'
import Grid from 'svelte-grid'
import ComponentEditor from './ComponentEditor.svelte'
import { classNames } from '$lib/utils'
const { selectedComponent, app, mode } = getContext<AppEditorContext>('AppEditorContext')
@@ -29,7 +30,7 @@
{@const index = $app.grid.findIndex((c) => c.data.id === dataItem.data.id)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="h-full w-full flex justify-center align-center border border-gray-100"
class={classNames('h-full w-full flex justify-center align-center border border-gray-100')}
on:click={() => {
$selectedComponent = dataItem.data.id
}}
@@ -45,6 +46,6 @@
<style>
:global(.svlt-grid-shadow) {
/* Back shadow */
background: lightblue !important;
background: rgb(147 197 253) !important;
}
</style>
@@ -91,12 +91,15 @@ const chartComponents = {
...defaultProps,
id: 'piechartcomponent',
type: 'piechartcomponent',
runnable: true
runnable: true,
card: true
},
{
...defaultProps,
id: 'barchartcomponent',
type: 'barchartcomponent'
type: 'barchartcomponent',
runnable: true,
card: true
}
] as AppComponent[]
}
@@ -120,7 +123,8 @@ const tableComponents = {
fieldType: 'boolean'
}
},
runnable: true
runnable: true,
card: true
}
] as AppComponent[]
}
@@ -16,8 +16,7 @@
export let appPath: string
const { connectingInput, staticOutputs, app, worldStore } =
getContext<AppEditorContext>('AppEditorContext')
const { connectingInput, staticOutputs, app } = getContext<AppEditorContext>('AppEditorContext')
function connectInput(id: string, name: string) {
if ($connectingInput) {
@@ -55,8 +54,6 @@
scriptCreationDrawer.closeDrawer()
}
// Inline DENO, Inline Python, Inline GO, Inline SQL
let selectedScript:
| { content: string; language: Preview.language; path: string; schema: Schema }
| undefined = undefined
@@ -6,6 +6,8 @@
faAlignCenter,
faAlignLeft,
faAlignRight,
faClose,
faPen,
faTrashAlt
} from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
@@ -31,52 +33,85 @@
{#if component}
<div class="flex flex-col w-full divide-y">
<PanelSection title="Context">
{#if component.inputs}
<InputsSpecsEditor bind:inputSpecs={component.inputs} />
{/if}
{#if component.runnable}
<PanelSection title="Runnable">
{#if component['inlineScriptName']}
<span class="text-xs">{component['inlineScriptName']}</span>
<div class="w-full flex flex-row gap-2">
<Button
size="xs"
color="dark"
startIcon={{ icon: faPen }}
on:click={() => {
alert('TODO')
}}
>
Edit
</Button>
{#if component.runnable && component['path'] === undefined && component['inlineScriptName'] === undefined}
<span class="text-sm">Select a script or a flow to continue</span>
<PickScript
kind="script"
on:pick={({ detail }) => {
if (component && component.type === 'runformcomponent') {
component.path = detail.path
component.runType = 'script'
}
}}
/>
<PickFlow
on:pick={({ detail }) => {
if (component && component.type === 'runformcomponent') {
component.path = detail.path
component.runType = 'flow'
}
}}
/>
{/if}
<Button
size="xs"
color="light"
variant="border"
startIcon={{ icon: faClose }}
on:click={() => {
alert('TODO')
}}
>
Clear
</Button>
</div>
{/if}
{#if component.runnable && component['path'] === undefined && component['inlineScriptName'] === undefined}
{#each Object.keys($app.inlineScripts ?? {}) as inlineScriptName}
<Button
on:click={() => {
if (component?.runnable) {
// @ts-ignore
component.inlineScriptName = inlineScriptName
{#if component.runnable && component['path'] === undefined && component['inlineScriptName'] === undefined}
<span class="text-sm">Select a script or a flow to continue</span>
<PickScript
kind="script"
on:pick={({ detail }) => {
if (component && component.type === 'runformcomponent') {
component.path = detail.path
component.runType = 'script'
}
}}
size="xs"
>
Link {inlineScriptName}
</Button>
{/each}
{/if}
/>
<PickFlow
on:pick={({ detail }) => {
if (component && component.type === 'runformcomponent') {
component.path = detail.path
component.runType = 'flow'
}
}}
/>
{/if}
{#if component.componentInputs}
{#if component.runnable && component['path'] === undefined && component['inlineScriptName'] === undefined}
{#each Object.keys($app.inlineScripts ?? {}) as inlineScriptName}
<Button
on:click={() => {
if (component?.runnable) {
// @ts-ignore
component.inlineScriptName = inlineScriptName
}
}}
size="xs"
>
Link {inlineScriptName}
</Button>
{/each}
{/if}
</PanelSection>
{/if}
{#if Object.values(component.inputs).length > 0}
<PanelSection title="Runnable inputs">
<InputsSpecsEditor bind:inputSpecs={component.inputs} />
</PanelSection>
{/if}
{#if Object.values(component.componentInputs).length > 0}
<PanelSection title="Component parameters">
<ComponentInputsSpecsEditor bind:componentInputSpecs={component.componentInputs} />
{/if}
</PanelSection>
</PanelSection>
{/if}
{#if component.verticalAlignement !== undefined}
<PanelSection title="Alignement">
@@ -7,6 +7,38 @@
export let inputSpecs: InputsSpec
const userTypeKeys = ['schemaProperty', 'defaultValue', 'value']
const staticTypeKeys = ['visible', 'value', 'fieldType']
const dynamicTypeKeys = ['id', 'name', 'defaultValue']
function sanitizeInputSpec(type: 'user' | 'static' | 'output', inputSpecKey: string) {
const inputSpec = inputSpecs[inputSpecKey]
if (type === 'user') {
for (const key of staticTypeKeys) {
delete inputSpec[key]
}
for (const key of dynamicTypeKeys) {
delete inputSpec[key]
}
} else if (type === 'static') {
for (const key of userTypeKeys) {
delete inputSpec[key]
}
for (const key of dynamicTypeKeys) {
delete inputSpec[key]
}
} else if (type === 'output') {
for (const key of userTypeKeys) {
delete inputSpec[key]
}
for (const key of staticTypeKeys) {
delete inputSpec[key]
}
}
inputSpecs[inputSpecKey] = inputSpec
}
let openedProp = Object.keys(inputSpecs)[0]
</script>
@@ -26,7 +58,10 @@
</div>
{#if inputSpecKey === openedProp}
<div class="flex flex-col w-full gap-2">
<ToggleButtonGroup bind:selected={inputSpecs[inputSpecKey].type}>
<ToggleButtonGroup
bind:selected={inputSpecs[inputSpecKey].type}
on:select={(x) => sanitizeInputSpec(x.detail, inputSpecKey)}
>
<ToggleButton position="left" value="static" startIcon={{ icon: faBolt }} size="xs">
Static
</ToggleButton>
+11 -1
View File
@@ -24,7 +24,16 @@ export type StaticInput = {
type: 'static'
value: any
visible?: boolean
fieldType: 'text' | 'textarea' | 'number' | 'boolean' | 'select' | 'date' | 'time' | 'datetime'
fieldType:
| 'text'
| 'textarea'
| 'number'
| 'boolean'
| 'select'
| 'date'
| 'time'
| 'datetime'
| 'object'
}
export type AppInputTransform = DynamicInput | StaticInput | UserInput
@@ -92,6 +101,7 @@ export type AppComponent =
// Only dynamic inputs (Result of display)
componentInputs: ComponentInputsSpec
runnable?: boolean | undefined
card?: boolean | undefined
// TODO: add min/max width/height
}
+3 -1
View File
@@ -77,11 +77,13 @@ export async function loadSchema(
export function schemaToInputsSpec(schema: Schema): InputsSpec {
return Object.keys(schema.properties).reduce((accu, key) => {
const property = schema.properties[key]
accu[key] = {
type: 'static',
defaultValue: property.default,
value: undefined,
visible: true
visible: true,
fieldType: property.type
}
return accu
}, {})