mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-12 00:06:14 +00:00
Refactor flow UI/UX + added fork and create script from inline script (#175)
* Refactor flow UI/UX + added fork and create script from inline script * Prevent infinite loop when remove steps * Fix forking a script from the Hub * Fix viewing code of a script from the Hub * Fix PR comments * Fix code highlight * Fix path * Find next available path * Fix copy first step schema
This commit is contained in:
@@ -89,6 +89,30 @@
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
.default-secondary-button-v2 {
|
||||
@apply text-gray-900;
|
||||
@apply bg-white;
|
||||
@apply border;
|
||||
@apply border-gray-200;
|
||||
@apply font-medium;
|
||||
@apply rounded-lg;
|
||||
@apply text-sm;
|
||||
@apply px-5;
|
||||
@apply py-2.5;
|
||||
@apply text-center;
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
}
|
||||
|
||||
.default-secondary-button-v2:focus {
|
||||
@apply ring-4;
|
||||
@apply outline-none;
|
||||
@apply ring-gray-100;
|
||||
}
|
||||
.default-secondary-button-v2:hover {
|
||||
@apply bg-gray-100
|
||||
}
|
||||
|
||||
.input-error {
|
||||
@apply border-red-500 !important;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { FlowService, ScriptService, type Flow } from '$lib/gen'
|
||||
import { clearPreviewResults, workspaceStore, hubScripts } from '$lib/stores'
|
||||
import { clearPreviewResults, hubScripts, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { onMount } from 'svelte'
|
||||
import SvelteMarkdown from 'svelte-markdown'
|
||||
import FlowEditor from './FlowEditor.svelte'
|
||||
import { flowStore, initFlow } from './flows/flowStore'
|
||||
import Path from './Path.svelte'
|
||||
import Required from './Required.svelte'
|
||||
import ScriptSchema from './ScriptSchema.svelte'
|
||||
@@ -26,7 +27,7 @@
|
||||
}
|
||||
|
||||
async function saveFlow(): Promise<void> {
|
||||
if (initialPath == '') {
|
||||
if (initialPath === '') {
|
||||
await FlowService.createFlow({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
@@ -40,7 +41,7 @@
|
||||
} else {
|
||||
await FlowService.updateFlow({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath,
|
||||
path: flow.path,
|
||||
requestBody: {
|
||||
path: flow.path,
|
||||
summary: flow.summary,
|
||||
@@ -58,10 +59,12 @@
|
||||
goto(`?step=${step}`)
|
||||
}
|
||||
|
||||
$: {
|
||||
flowStore.subscribe((flow: Flow) => {
|
||||
$page.url.searchParams.set('state', btoa(JSON.stringify(flow)))
|
||||
history.replaceState({}, '', $page.url)
|
||||
}
|
||||
})
|
||||
|
||||
$: flow && initFlow(flow)
|
||||
|
||||
onMount(() => {
|
||||
loadSearchData()
|
||||
@@ -107,12 +110,14 @@
|
||||
class="default-button px-6 max-h-8"
|
||||
on:click={() => {
|
||||
changeStep(step + 1)
|
||||
}}>Next</button
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
{#if step == 2}
|
||||
<button class="default-button-secondary px-6 max-h-8 mr-2" on:click={saveFlow}
|
||||
>Save</button
|
||||
>
|
||||
<button class="default-button-secondary px-6 max-h-8 mr-2" on:click={saveFlow}>
|
||||
Save
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<button class="default-button px-6 self-end" on:click={saveFlow}>Save</button>
|
||||
@@ -187,7 +192,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else if step === 2}
|
||||
<FlowEditor bind:flow />
|
||||
<FlowEditor />
|
||||
{:else if step === 3}
|
||||
<ScriptSchema
|
||||
synchronizedHeader={false}
|
||||
|
||||
@@ -1,73 +1,47 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { FlowModuleValue, type Flow } from '$lib/gen'
|
||||
import { loadSchema } from '$lib/scripts'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import FlowPreview from './FlowPreview.svelte'
|
||||
import CopyFirstStepSchema from './flows/CopyFirstStepSchema.svelte'
|
||||
import { addModule, flowStore } from './flows/flowStore'
|
||||
import ModuleStep from './ModuleStep.svelte'
|
||||
import SchemaEditor from './SchemaEditor.svelte'
|
||||
import type SchemaForm from './SchemaForm.svelte'
|
||||
|
||||
export let flow: Flow
|
||||
|
||||
let args: Record<string, any> = {}
|
||||
let schemas: Schema[] = []
|
||||
let schemaForms: (SchemaForm | undefined)[] = []
|
||||
|
||||
function addModule() {
|
||||
schemaForms.push(undefined)
|
||||
|
||||
let newModule = {
|
||||
value: { type: FlowModuleValue.type.SCRIPT, path: '' },
|
||||
input_transform: {}
|
||||
}
|
||||
flow.value.modules = flow.value.modules.concat(newModule)
|
||||
schemas.push(emptySchema())
|
||||
}
|
||||
$: numberOfSteps = $flowStore?.value.modules.length - 1
|
||||
</script>
|
||||
|
||||
<!-- <PageHeader title="Flow" /> -->
|
||||
<div class="flow-root bg-gray-50 rounded-xl border border-gray-200">
|
||||
<ul class="relative -mt-10">
|
||||
<span class="absolute top-0 left-1/2 h-full w-1 bg-gray-400" aria-hidden="true" />
|
||||
<div class="relative">
|
||||
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-20">
|
||||
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-20">
|
||||
<div class="bg-white border border-gray xl-rounded shadow-lg w-full mx-4 xl:mx-20">
|
||||
<div
|
||||
class="flex items-center justify-between flex-wra px-4 py-5 border-b border-gray-200 sm:px-6"
|
||||
>
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">Flow Input</h3>
|
||||
<button
|
||||
class="text-xs default-button-secondary max-h-6 place-self-end"
|
||||
disabled={flow.value.modules.length == 0 ||
|
||||
flow.value.modules[0].value.path == undefined}
|
||||
on:click={async () => {
|
||||
flow.schema = await loadSchema(flow.value.modules[0].value.path ?? '')
|
||||
}}
|
||||
>Copy from step 1's schema
|
||||
</button>
|
||||
<CopyFirstStepSchema />
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<SchemaEditor bind:schema={flow.schema} />
|
||||
<SchemaEditor bind:schema={$flowStore.schema} />
|
||||
<div class="my-4" />
|
||||
<FlowPreview {flow} i={flow.value.modules.length - 1} bind:args />
|
||||
<FlowPreview bind:flow={$flowStore} i={numberOfSteps} bind:args />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{#each flow.value.modules as mod, i}
|
||||
<ModuleStep bind:flow bind:mod bind:schemas bind:schemaForms bind:args {i} />
|
||||
{#each $flowStore?.value.modules as mod, i}
|
||||
<ModuleStep bind:mod bind:args {i} />
|
||||
{/each}
|
||||
<li class="relative m-20 ">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div class="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div class="relative flex justify-center">
|
||||
<button
|
||||
class="default-button h-10 w-10 shadow-blue-600/40 border-blue-600 shadow"
|
||||
on:click={addModule}><Icon class="text-white mb-1" data={faPlus} scale={0.9} /></button
|
||||
on:click={() => addModule()}
|
||||
>
|
||||
<Icon class="text-white mb-1" data={faPlus} />
|
||||
Add step
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
@@ -85,10 +85,9 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<h2 class="mb-5 mt-2">
|
||||
<h2 class="flex justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
class="underline text-gray-700 inline-flex items-center"
|
||||
class="default-button "
|
||||
on:click={() => {
|
||||
viewPreview = !viewPreview
|
||||
}}
|
||||
|
||||
@@ -1,173 +1,86 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { FlowModuleValue, type Flow, type FlowModule } from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { loadSchema as UloadSchema } from '$lib/scripts'
|
||||
import { DENO_INIT_CODE, PYTHON_INIT_CODE } from '$lib/script_helpers'
|
||||
import { addPreviewResult, previewResults, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
buildExtraLib,
|
||||
emptySchema,
|
||||
objectToTsType,
|
||||
schemaToObject,
|
||||
schemaToTsType
|
||||
} from '$lib/utils'
|
||||
import { language, mehO } from 'svelte-awesome/icons'
|
||||
import { FlowModuleValue, type FlowModule } from '$lib/gen'
|
||||
import { addPreviewResult, previewResults } from '$lib/stores'
|
||||
import { buildExtraLib, objectToTsType, schemaToObject, schemaToTsType } from '$lib/utils'
|
||||
import Editor from './Editor.svelte'
|
||||
import FlowPreview from './FlowPreview.svelte'
|
||||
import RadioButton from './RadioButton.svelte'
|
||||
import FlowInputs from './flows/FlowInputs.svelte'
|
||||
import FlowModuleHeader from './flows/FlowModuleHeader.svelte'
|
||||
import {
|
||||
createInlineScriptModule,
|
||||
flowStore,
|
||||
loadSchema,
|
||||
pickScript,
|
||||
schemasStore
|
||||
} from './flows/flowStore'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import ScriptPicker from './ScriptPicker.svelte'
|
||||
|
||||
export let flow: Flow
|
||||
export let i: number
|
||||
export let mod: FlowModule
|
||||
export let args: Record<string, any> = {}
|
||||
|
||||
export let schemas: Schema[] = []
|
||||
export let schemaForms: (SchemaForm | undefined)[] = []
|
||||
|
||||
let editor: Editor
|
||||
let flowPreview: FlowPreview
|
||||
|
||||
$: previousSchema = i === 0 ? schemaToObject(flow.schema) : $previewResults[i]
|
||||
|
||||
$: schema = $schemasStore[i]
|
||||
$: shouldPick = mod.value.path === '' && mod.value.language === undefined
|
||||
$: previousSchema = i === 0 ? schemaToObject($flowStore?.schema) : $previewResults[i]
|
||||
$: extraLib = buildExtraLib(
|
||||
i == 0 ? schemaToTsType(flow.schema) : objectToTsType($previewResults[i])
|
||||
i === 0 ? schemaToTsType($flowStore?.schema) : objectToTsType($previewResults[i])
|
||||
)
|
||||
|
||||
function initContent(lang: string) {
|
||||
const newStart = lang == 'deno' ? DENO_INIT_CODE : PYTHON_INIT_CODE
|
||||
if (editor) {
|
||||
editor.setCode(newStart)
|
||||
} else {
|
||||
mod.value.content = newStart
|
||||
}
|
||||
}
|
||||
|
||||
$: mod.value.type == 'rawscript' &&
|
||||
mod.value.language == undefined &&
|
||||
(mod.value.language = FlowModuleValue.language.DENO)
|
||||
|
||||
$: mod.value.type == 'rawscript' && mod.value.language && initContent(mod.value.language)
|
||||
|
||||
export async function loadSchema() {
|
||||
let isRaw = mod.value.type == 'rawscript'
|
||||
if ((!isRaw && mod.value.path) || isRaw) {
|
||||
let schema: Schema
|
||||
if (isRaw) {
|
||||
schema = emptySchema()
|
||||
await inferArgs(mod.value.language!, mod.value.content!, schema)
|
||||
} else {
|
||||
schema = await UloadSchema(mod.value.path!)
|
||||
}
|
||||
if (
|
||||
JSON.stringify(Object.keys(schema?.properties ?? {}).sort()) !=
|
||||
JSON.stringify(Object.keys(mod.input_transform).sort())
|
||||
) {
|
||||
let it = {}
|
||||
Object.keys(schema?.properties ?? {}).map(
|
||||
(x) =>
|
||||
(it[x] = {
|
||||
type: 'static',
|
||||
value: ''
|
||||
})
|
||||
)
|
||||
schemaForms[i]?.setArgs(it)
|
||||
}
|
||||
schemas[i] = schema ?? emptySchema()
|
||||
} else {
|
||||
schemaForms[i]?.setArgs({})
|
||||
schemas[i] = emptySchema()
|
||||
}
|
||||
schemas = schemas
|
||||
}
|
||||
|
||||
$: $workspaceStore && loadSchema()
|
||||
</script>
|
||||
|
||||
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-20">
|
||||
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-20">
|
||||
<div class="bg-white border border-gray xl-rounded shadow-lg w-full max-w-4xl mx-4 md:mx-auto">
|
||||
<div
|
||||
class="flex items-center justify-between flex-wra px-4 py-5 border-b border-gray-200 sm:px-6"
|
||||
>
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">Step {i + 1}</h3>
|
||||
<button
|
||||
class="text-xs default-button-secondary max-h-6 place-self-end"
|
||||
on:click={() => {
|
||||
flow.value.modules.splice(i, 1)
|
||||
schemas.splice(i, 1)
|
||||
schemaForms.splice(i, 1)
|
||||
flow = flow
|
||||
}}
|
||||
>Remove this step
|
||||
</button>
|
||||
<div class="flex items-center justify-between flex-wra p-4 sm:px-6">
|
||||
<FlowModuleHeader bind:i bind:shouldPick>
|
||||
<h3 class="text-lg font-bold text-gray-900">Step {i + 1}</h3>
|
||||
<p>{mod.value.path ?? ''}</p>
|
||||
</FlowModuleHeader>
|
||||
</div>
|
||||
<div class="p-10">
|
||||
<h2 class="mb-4">Step script</h2>
|
||||
<RadioButton
|
||||
small={true}
|
||||
options={[
|
||||
['Pick from an existing script', 'script'],
|
||||
['Edit in-place', 'rawscript']
|
||||
]}
|
||||
bind:value={mod.value.type}
|
||||
/>
|
||||
{#if mod.value.type == 'script'}
|
||||
<ScriptPicker allowHub={true} bind:scriptPath={mod.value.path} on:select={loadSchema} />
|
||||
{:else}
|
||||
<div class="mt-2" />
|
||||
<RadioButton
|
||||
label="Language"
|
||||
small={true}
|
||||
options={[
|
||||
['Python 3.10', 'python3'],
|
||||
['Typescript (Deno)', 'deno']
|
||||
]}
|
||||
bind:value={mod.value.language}
|
||||
<div class="border-b border-gray-200" />
|
||||
<div class="p-6">
|
||||
{#if shouldPick}
|
||||
<FlowInputs
|
||||
on:pick={(e) => pickScript(e.detail.path, i)}
|
||||
on:new={(e) => createInlineScriptModule(e.detail.language, i)}
|
||||
/>
|
||||
{/if}
|
||||
{#if mod.value.type === FlowModuleValue.type.RAWSCRIPT}
|
||||
<div class="h-96">
|
||||
<Editor
|
||||
class="h-full"
|
||||
bind:code={mod.value.content}
|
||||
deno={mod.value.language === FlowModuleValue.language.DENO}
|
||||
/>
|
||||
</div>
|
||||
<button class="default-button w-full p-1 mt-4" on:click={() => loadSchema(i)}>
|
||||
Infer schema
|
||||
</button>
|
||||
{/if}
|
||||
{#if !shouldPick}
|
||||
<h2 class="mb-4">Step inputs</h2>
|
||||
<SchemaForm
|
||||
inputTransform={true}
|
||||
{schema}
|
||||
{extraLib}
|
||||
{i}
|
||||
{previousSchema}
|
||||
bind:args={mod.input_transform}
|
||||
/>
|
||||
<div class="h-96 mt-4">
|
||||
{#if mod.value.language == 'deno'}
|
||||
<Editor
|
||||
bind:this={editor}
|
||||
class="h-full"
|
||||
bind:code={mod.value.content}
|
||||
deno={true}
|
||||
cmdEnterAction={() => flowPreview.runPreview(mod.input_transform)}
|
||||
/>
|
||||
{:else}
|
||||
<Editor
|
||||
bind:this={editor}
|
||||
class="h-full"
|
||||
bind:code={mod.value.content}
|
||||
deno={false}
|
||||
cmdEnterAction={() => flowPreview.runPreview(mod.input_transform)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="default-button w-full p-1 mt-4" on:click={loadSchema}>Infer schema</button>
|
||||
{/if}
|
||||
<div class="my-4" />
|
||||
<h2 class="mb-4">Step inputs</h2>
|
||||
<SchemaForm
|
||||
bind:this={schemaForms[i]}
|
||||
inputTransform={true}
|
||||
schema={schemas[i]}
|
||||
{extraLib}
|
||||
{i}
|
||||
{previousSchema}
|
||||
bind:args={mod.input_transform}
|
||||
/>
|
||||
<FlowPreview
|
||||
bind:this={flowPreview}
|
||||
{flow}
|
||||
{i}
|
||||
bind:args
|
||||
{schemas}
|
||||
on:change={(e) => {
|
||||
addPreviewResult(e.detail.result, i + 1)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if !shouldPick}
|
||||
<div class="border-b border-gray-200" />
|
||||
<div class="p-3">
|
||||
<FlowPreview
|
||||
bind:args
|
||||
bind:flow={$flowStore}
|
||||
{i}
|
||||
bind:schemas={$schemasStore}
|
||||
on:change={(e) => {
|
||||
addPreviewResult(e.detail.result, i + 1)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import type { InputTransform } from '$lib/gen'
|
||||
import { allTrue } from '$lib/utils'
|
||||
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { slide } from 'svelte/transition'
|
||||
import ArgInput from './ArgInput.svelte'
|
||||
import Editor from './Editor.svelte'
|
||||
import FieldHeader from './FieldHeader.svelte'
|
||||
import DynamicInputHelpBox from './flows/DynamicInputHelpBox.svelte'
|
||||
import PropPicker from './flows/PropPicker.svelte'
|
||||
import RadioButton from './RadioButton.svelte'
|
||||
|
||||
export let inputTransform = false
|
||||
export let schema: Schema
|
||||
export let args: Record<string, any> = {}
|
||||
|
||||
export let isValid: boolean = true
|
||||
export let args: Record<string, InputTransform> = {}
|
||||
export let editableSchema = false
|
||||
|
||||
export let extraLib: string
|
||||
export let isValid: boolean = true
|
||||
|
||||
export let i: number
|
||||
export let previousSchema: Object
|
||||
|
||||
let inputCheck: { [id: string]: boolean } = {}
|
||||
let seeHelp: { [id: string]: boolean } = {}
|
||||
let editor: Editor
|
||||
|
||||
export function setArgs(nargs: Record<string, any>) {
|
||||
args = nargs
|
||||
}
|
||||
|
||||
function getDefaultExpr(i: number, key: string = 'myfield') {
|
||||
return `import { previous_result, flow_input, step, variable, resource, params } from 'windmill@${i}'
|
||||
|
||||
@@ -113,73 +106,7 @@ previous_result.${key}`
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="text-xs flex flex-row-reverse">
|
||||
<span
|
||||
class="underline mr-4"
|
||||
on:click={() => {
|
||||
seeHelp[argName] = seeHelp[argName] == undefined ? true : !seeHelp[argName]
|
||||
}}
|
||||
>Help<Icon
|
||||
class="ml-2"
|
||||
data={seeHelp[argName] ? faChevronUp : faChevronDown}
|
||||
scale={0.7}
|
||||
/></span
|
||||
>
|
||||
</div>
|
||||
{#if seeHelp[argName]}
|
||||
<div
|
||||
transition:slide
|
||||
class="bg-gray-100 border-l-4 border-gray-600 text-gray-700 p-4 m-4"
|
||||
role="alert"
|
||||
>
|
||||
<p class="font-bold">Dynamic arg help</p>
|
||||
<p>
|
||||
When a field is "dynamic", its value is computed dynamically as the evaluation of
|
||||
its corresponding typescript snippet.
|
||||
</p>
|
||||
That snippet can be single line:
|
||||
<pre><code>last_result.myarg</code></pre>
|
||||
or multiline:
|
||||
<pre><code
|
||||
>let x = 5;
|
||||
x + 2</code
|
||||
></pre>
|
||||
<p>
|
||||
If it is multiline, the last statement before the final expression<b
|
||||
>MUST END WITH ; and a newline</b
|
||||
>
|
||||
</p>
|
||||
The snippet can also be a string template:
|
||||
<pre><code
|
||||
>`Hello ${params.name}, all your base ${previous_result.base_name}
|
||||
belong to us`</code
|
||||
></pre>
|
||||
However, the last line must always be the final expression.
|
||||
<p>
|
||||
The snippet can use any typescript primitives, and the following flow specific
|
||||
objects and functions:
|
||||
</p>
|
||||
<ul class="ml-4">
|
||||
<li>
|
||||
<b>previous_result</b>: the object containing the result of the previous step
|
||||
</li>
|
||||
<li><b>flow_input</b>: the object containing the flow input arguments</li>
|
||||
<li><b>params</b>: the object containing the current step static values</li>
|
||||
<li>
|
||||
<b>step(n)</b>: the function returning the result of step number n. One can also
|
||||
use a negative n. step(0) == flow_input , step(-1) == previous_result
|
||||
</li>
|
||||
<li>
|
||||
<b>variable(path)</b>: the function returning the variable (including secrets)
|
||||
at given path as a string
|
||||
</li>
|
||||
<li>
|
||||
<b>resource(path)</b>: the function returning the resource at a given path as an
|
||||
object
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
<DynamicInputHelpBox />
|
||||
{/if}
|
||||
{:else}
|
||||
<p>Not recognized arg type {args[argName].type}</p>
|
||||
@@ -199,7 +126,8 @@ belong to us`</code
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
bind:itemsType={schema.properties[argName].items}
|
||||
{editableSchema}
|
||||
/>{/if}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="italic text-sm">No settable input</p>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { copyFirstStepSchema, isCopyFirstStepSchemaDisabled } from './flowStore'
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="default-secondary-button-v2"
|
||||
disabled={$isCopyFirstStepSchemaDisabled}
|
||||
on:click={copyFirstStepSchema}
|
||||
>
|
||||
Copy from step 1's schema
|
||||
</button>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { slide } from 'svelte/transition'
|
||||
|
||||
$: opened = false
|
||||
</script>
|
||||
|
||||
<div class="text-xs flex flex-row-reverse">
|
||||
<span
|
||||
class="underline mr-4"
|
||||
on:click={() => {
|
||||
opened = !opened
|
||||
}}
|
||||
>
|
||||
Help
|
||||
<Icon class="ml-2" data={opened ? faChevronUp : faChevronDown} scale={0.7} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if opened}
|
||||
<div
|
||||
transition:slide
|
||||
class="bg-gray-100 border-l-4 border-gray-600 text-gray-700 p-4 m-4"
|
||||
role="alert"
|
||||
>
|
||||
<p class="font-bold">Dynamic arg help</p>
|
||||
<p>
|
||||
When a field is "dynamic", its value is computed dynamically as the evaluation of its
|
||||
corresponding typescript snippet.
|
||||
</p>
|
||||
That snippet can be single line:
|
||||
<pre><code>last_result.myarg</code></pre>
|
||||
or multiline:
|
||||
<pre><code
|
||||
>let x = 5;
|
||||
x + 2</code
|
||||
></pre>
|
||||
<p>
|
||||
If it is multiline, the last statement before the final expression<b
|
||||
>MUST END WITH ; and a newline</b
|
||||
>
|
||||
</p>
|
||||
The snippet can also be a string template:
|
||||
<pre><code
|
||||
>`Hello ${params.name}, all your base ${previous_result.base_name}
|
||||
belong to us`</code
|
||||
></pre>
|
||||
However, the last line must always be the final expression.
|
||||
<p>
|
||||
The snippet can use any typescript primitives, and the following flow specific objects and
|
||||
functions:
|
||||
</p>
|
||||
<ul class="ml-4">
|
||||
<li>
|
||||
<b>previous_result</b>: the object containing the result of the previous step
|
||||
</li>
|
||||
<li><b>flow_input</b>: the object containing the flow input arguments</li>
|
||||
<li><b>params</b>: the object containing the current step static values</li>
|
||||
<li>
|
||||
<b>step(n)</b>: the function returning the result of step number n. One can also use a
|
||||
negative n. step(0) == flow_input , step(-1) == previous_result
|
||||
</li>
|
||||
<li>
|
||||
<b>variable(path)</b>: the function returning the variable (including secrets) at given path
|
||||
as a string
|
||||
</li>
|
||||
<li>
|
||||
<b>resource(path)</b>: the function returning the resource at a given path as an object
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { FlowModuleValue } from '$lib/gen/models/FlowModuleValue'
|
||||
|
||||
import { faCode } from '@fortawesome/free-solid-svg-icons'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import FlowScriptPicker from './pickers/FlowScriptPicker.svelte'
|
||||
import PickHubScript from './pickers/PickHubScript.svelte'
|
||||
import PickScript from './pickers/PickScript.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="columns-2">
|
||||
<PickScript on:pick />
|
||||
<PickHubScript on:pick />
|
||||
<FlowScriptPicker
|
||||
label="New Typescript script (Deno)"
|
||||
icon={faCode}
|
||||
iconColor="text-blue-800"
|
||||
on:click={() => dispatch('new', { language: FlowModuleValue.language.DENO })}
|
||||
/>
|
||||
<FlowScriptPicker
|
||||
label="New Python script (3.10)"
|
||||
icon={faCode}
|
||||
iconColor="text-yellow-500"
|
||||
on:click={() => dispatch('new', { language: FlowModuleValue.language.PYTHON3 })}
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import { FlowModuleValue, Script } from '$lib/gen'
|
||||
import { faCode, faCodeBranch, faPlus, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { Highlight } from 'svelte-highlight'
|
||||
import { python, typescript } from 'svelte-highlight/languages'
|
||||
import github from 'svelte-highlight/styles/github'
|
||||
import Modal from '../Modal.svelte'
|
||||
import { createScriptFromInlineScript, flowStore, fork, removeModule } from './flowStore'
|
||||
import { getScriptByPath } from './utils'
|
||||
|
||||
export let i: number
|
||||
export let shouldPick = false
|
||||
|
||||
$: mod = $flowStore.value.modules[i]
|
||||
let modalViewer: Modal
|
||||
let modalViewerContent = ''
|
||||
let modalViewerLanguage: Script.language = Script.language.DENO
|
||||
|
||||
async function viewCode() {
|
||||
const { content, language } = await getScriptByPath(mod.value.path!)
|
||||
modalViewerContent = content
|
||||
modalViewerLanguage = language
|
||||
modalViewer.openModal()
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{@html github}
|
||||
</svelte:head>
|
||||
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
<div>
|
||||
{#if mod.value.type === FlowModuleValue.type.SCRIPT && !shouldPick}
|
||||
<button type="button" on:click={() => fork(i)} class="default-secondary-button-v2 text-xs">
|
||||
<Icon data={faCodeBranch} class={`w-4 mr-2 h-4`} />
|
||||
Fork
|
||||
</button>
|
||||
<button type="button" on:click={viewCode} class="default-secondary-button-v2 text-xs">
|
||||
<Icon data={faCode} class="w-4 mr-2 h-4" />
|
||||
View code
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if mod.value.type === FlowModuleValue.type.RAWSCRIPT && !shouldPick}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => createScriptFromInlineScript(i)}
|
||||
class="default-secondary-button-v2 text-sm"
|
||||
>
|
||||
<Icon data={faPlus} class="w-4 mr-2" />
|
||||
Create script
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => removeModule(i)}
|
||||
class="default-secondary-button-v2 text-xs"
|
||||
>
|
||||
<Icon data={faTrashAlt} class="w-4 mr-2 h-4" />
|
||||
Remove step
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Modal bind:this={modalViewer}>
|
||||
<div slot="title">Script {mod?.value.path}</div>
|
||||
<div slot="content">
|
||||
{#if modalViewerLanguage === 'python3'}
|
||||
<Highlight language={python} code={modalViewerContent} />
|
||||
{:else if modalViewerLanguage === 'deno'}
|
||||
<Highlight language={typescript} code={modalViewerContent} />
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { derived, writable } from 'svelte/store'
|
||||
import ObjectViewer from './ObjectViewer.svelte'
|
||||
import { keepByKey } from './utils'
|
||||
@@ -7,7 +6,6 @@
|
||||
export let props: Object = {}
|
||||
|
||||
const EMPTY_STRING = ''
|
||||
const dispatch = createEventDispatcher()
|
||||
const search = writable(EMPTY_STRING)
|
||||
|
||||
$: propsFiltered = derived(search, ($search: string) => {
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import { FlowModuleValue, ScriptService, type Flow, type FlowModule } from '$lib/gen'
|
||||
import { DENO_INIT_CODE, PYTHON_INIT_CODE } from '$lib/script_helpers'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { derived, get, writable } from 'svelte/store'
|
||||
import { createInlineScriptModuleFromPath, getFirstStepSchema, loadSchemaFromModule } from './utils'
|
||||
|
||||
export const flowStore = writable<Flow>(undefined)
|
||||
export const schemasStore = writable<Schema[]>([])
|
||||
|
||||
export function initFlow(flow: Flow) {
|
||||
flowStore.set(flow)
|
||||
// For each module in flow, we should load the corresponding schema
|
||||
flow.value.modules.forEach((_, index) => {
|
||||
loadSchema(index)
|
||||
})
|
||||
}
|
||||
|
||||
export const isCopyFirstStepSchemaDisabled = derived(flowStore, (flow: Flow) => {
|
||||
const modules = flow.value.modules
|
||||
const [firstModule] = modules
|
||||
return (
|
||||
modules.length === 0 || (firstModule.value.path === '' && firstModule.value.type === 'script')
|
||||
)
|
||||
})
|
||||
|
||||
export function addModule() {
|
||||
const newModule: FlowModule = {
|
||||
value: { type: FlowModuleValue.type.SCRIPT, path: '' },
|
||||
input_transform: {}
|
||||
}
|
||||
|
||||
flowStore.update((flow: Flow) => {
|
||||
flow.value.modules = flow.value.modules.concat(newModule)
|
||||
return flow
|
||||
})
|
||||
}
|
||||
|
||||
export async function pickScript(path: string, step: number) {
|
||||
flowStore.update((flow: Flow) => {
|
||||
if (flow.value.modules[step]) {
|
||||
flow.value.modules[step].value.path = path
|
||||
}
|
||||
|
||||
return flow
|
||||
})
|
||||
|
||||
await loadSchema(step)
|
||||
}
|
||||
|
||||
export async function createInlineScriptModule(language: FlowModuleValue.language, step: number) {
|
||||
const code = language === FlowModuleValue.language.DENO ? DENO_INIT_CODE : PYTHON_INIT_CODE
|
||||
flowStore.update((flow: Flow) => {
|
||||
flow.value.modules[step].value = {
|
||||
type: FlowModuleValue.type.RAWSCRIPT,
|
||||
content: code,
|
||||
language
|
||||
}
|
||||
|
||||
return flow
|
||||
})
|
||||
await loadSchema(step)
|
||||
}
|
||||
|
||||
export async function loadSchema(step: number) {
|
||||
const flow = get(flowStore)
|
||||
const module = flow.value.modules[step]
|
||||
|
||||
const { input_transform, schema } = await loadSchemaFromModule(module)
|
||||
|
||||
flowStore.update((flow: Flow) => {
|
||||
flow.value.modules[step].input_transform = input_transform
|
||||
return flow
|
||||
})
|
||||
schemasStore.update((schemas) => {
|
||||
schemas[step] = schema
|
||||
return schemas
|
||||
})
|
||||
}
|
||||
|
||||
export async function fork(step: number) {
|
||||
const flow = get(flowStore)
|
||||
const flowModuleValue = flow.value.modules[step].value
|
||||
|
||||
if (flowModuleValue.path) {
|
||||
const moduleValue = await createInlineScriptModuleFromPath(flowModuleValue.path)
|
||||
flowStore.update((flow: Flow) => {
|
||||
flow.value.modules[step].value = moduleValue
|
||||
return flow
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function createScriptFromInlineScript(step: number) {
|
||||
const flow = get(flowStore)
|
||||
const schemas = get(schemasStore)
|
||||
const user = get(userStore)
|
||||
|
||||
const flowModuleValue = flow.value.modules[step].value
|
||||
|
||||
const originalScriptPath = flowModuleValue.path
|
||||
const wasForked = Boolean(originalScriptPath)
|
||||
|
||||
let suffix = `step-${step}`
|
||||
|
||||
if (wasForked && originalScriptPath) {
|
||||
const [first, second, ...others] = originalScriptPath.split('/')
|
||||
suffix = others.join('/')
|
||||
}
|
||||
|
||||
const path = `${flow.path}/${suffix}`
|
||||
const forkedDescription = wasForked ? `as a fork of ${originalScriptPath}` : ''
|
||||
const description = `This script was edited in place of flow ${flow.path} ${forkedDescription} by ${user?.username} at step ${step}.`
|
||||
|
||||
const availablePath = await findNextAvailablePath(path)
|
||||
|
||||
await ScriptService.createScript({
|
||||
workspace: get(workspaceStore)!,
|
||||
requestBody: {
|
||||
path: availablePath,
|
||||
summary: '',
|
||||
description,
|
||||
content: flowModuleValue.content!,
|
||||
parent_hash: undefined,
|
||||
schema: schemas[step],
|
||||
is_template: false,
|
||||
language: flowModuleValue.language!
|
||||
}
|
||||
})
|
||||
|
||||
flowStore.update((flow: Flow) => {
|
||||
flow.value.modules[step].value = {
|
||||
type: FlowModuleValue.type.SCRIPT,
|
||||
path: availablePath
|
||||
}
|
||||
|
||||
return flow
|
||||
})
|
||||
await loadSchema(step)
|
||||
}
|
||||
|
||||
export function removeModule(step: number) {
|
||||
flowStore.update((flow: Flow) => {
|
||||
flow.value.modules.splice(step, 1)
|
||||
return flow
|
||||
})
|
||||
|
||||
schemasStore.update((schemas: Schema[]) => {
|
||||
schemas.splice(step, 1)
|
||||
return schemas
|
||||
})
|
||||
}
|
||||
|
||||
export async function copyFirstStepSchema() {
|
||||
const flow = get(flowStore)
|
||||
|
||||
const flowSchema = await getFirstStepSchema(flow)
|
||||
|
||||
flowStore.update((flow: Flow) => {
|
||||
flow.schema = flowSchema
|
||||
return flow
|
||||
})
|
||||
}
|
||||
|
||||
export async function findNextAvailablePath(path: string): Promise<string> {
|
||||
try {
|
||||
await ScriptService.getScriptByPath({
|
||||
workspace: get(workspaceStore)!,
|
||||
path
|
||||
})
|
||||
|
||||
const [_, version] = path.split(/.*_([0-9]*)/)
|
||||
|
||||
if (version.length > 0) {
|
||||
path = path.slice(0, -(version.length + 1))
|
||||
}
|
||||
|
||||
path = `${path}_${Number(version) + 1}`
|
||||
|
||||
return findNextAvailablePath(path)
|
||||
} catch (e) {
|
||||
// Catching an error means the path is available
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldPickOrCreateScript(flow: Flow, step: number): boolean {
|
||||
const module = flow.value.modules[step]
|
||||
return module.value.path === '' && module.value.language === undefined
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { IconDefinition } from '@fortawesome/free-brands-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
|
||||
export let icon: IconDefinition
|
||||
export let label: string
|
||||
export let iconColor: string
|
||||
</script>
|
||||
|
||||
<button type="button" on:click class="default-secondary-button-v2 mb-2 w-full">
|
||||
<Icon data={icon} class={`w-4 h-4 mr-2 -ml-2 ${iconColor}`} />
|
||||
{label}
|
||||
</button>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { faUserGroup } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import FlowScriptPicker from '$lib/components/flows/pickers/FlowScriptPicker.svelte'
|
||||
import ItemPicker from '$lib/components/ItemPicker.svelte'
|
||||
import { hubScripts } from '$lib/stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
type Item = { summary: String; path: String; version?: String }
|
||||
|
||||
const items: Item[] = $hubScripts ?? []
|
||||
let itemPicker: ItemPicker
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<ItemPicker
|
||||
bind:this={itemPicker}
|
||||
pickCallback={(path) => {
|
||||
dispatch('pick', { path })
|
||||
}}
|
||||
itemName={'Script'}
|
||||
extraField="summary"
|
||||
loadItems={async () => {
|
||||
return items
|
||||
}}
|
||||
/>
|
||||
|
||||
<FlowScriptPicker
|
||||
label={'Pick a script from the Hub'}
|
||||
icon={faUserGroup}
|
||||
iconColor="text-blue-500"
|
||||
on:click={() => itemPicker.openModal()}
|
||||
/>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import ItemPicker from '$lib/components/ItemPicker.svelte'
|
||||
import { faUserGroup } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { ScriptService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import FlowScriptPicker from './FlowScriptPicker.svelte'
|
||||
|
||||
type Item = { summary: String; path: String; version?: String }
|
||||
|
||||
let items: Item[] = []
|
||||
let itemPicker: ItemPicker
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function loadItems(): Promise<void> {
|
||||
items = await ScriptService.listScripts({ workspace: $workspaceStore! })
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
loadItems()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ItemPicker
|
||||
bind:this={itemPicker}
|
||||
pickCallback={(path) => {
|
||||
dispatch('pick', { path })
|
||||
}}
|
||||
itemName={'Script'}
|
||||
extraField="summary"
|
||||
loadItems={async () => {
|
||||
return items
|
||||
}}
|
||||
/>
|
||||
|
||||
<FlowScriptPicker
|
||||
label={'Pick a script from your workspace'}
|
||||
icon={faUserGroup}
|
||||
iconColor="text-blue-500"
|
||||
on:click={() => itemPicker.openModal()}
|
||||
/>
|
||||
@@ -1,3 +1,18 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import {
|
||||
FlowModuleValue,
|
||||
InputTransform,
|
||||
ScriptService,
|
||||
type Flow,
|
||||
type FlowModule
|
||||
} from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { loadSchema } from '$lib/scripts'
|
||||
import { DENO_INIT_CODE, PYTHON_INIT_CODE } from '$lib/script_helpers'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
function filterByKey(obj: Object, key: string): Object {
|
||||
if (Object(obj) !== obj) {
|
||||
return obj
|
||||
@@ -51,3 +66,97 @@ export function formatValue(arg: any) {
|
||||
}
|
||||
return arg
|
||||
}
|
||||
|
||||
export async function getFirstStepSchema(flow: Flow): Promise<Schema> {
|
||||
const [firstModule] = flow.value.modules
|
||||
if (firstModule.value.type === FlowModuleValue.type.RAWSCRIPT) {
|
||||
const { language, content } = firstModule.value
|
||||
if (language && content) {
|
||||
const schema = emptySchema()
|
||||
await inferArgs(language, content, schema)
|
||||
return schema
|
||||
}
|
||||
} else if (firstModule.value.path) {
|
||||
return await loadSchema(firstModule.value.path)
|
||||
}
|
||||
return emptySchema()
|
||||
}
|
||||
|
||||
export function createInlineScriptModule(language: FlowModuleValue.language): FlowModuleValue {
|
||||
const code = language === FlowModuleValue.language.DENO ? DENO_INIT_CODE : PYTHON_INIT_CODE
|
||||
|
||||
return {
|
||||
type: FlowModuleValue.type.RAWSCRIPT,
|
||||
content: code,
|
||||
language
|
||||
}
|
||||
}
|
||||
|
||||
export async function getScriptByPath(path: string): Promise<{
|
||||
content: string
|
||||
language: FlowModuleValue.language
|
||||
}> {
|
||||
if (path.startsWith('hub/')) {
|
||||
const content = await ScriptService.getHubScriptContentByPath({ path })
|
||||
return {
|
||||
content,
|
||||
language: FlowModuleValue.language.DENO
|
||||
}
|
||||
} else {
|
||||
const script = await ScriptService.getScriptByPath({
|
||||
workspace: get(workspaceStore)!,
|
||||
path: path ?? ''
|
||||
})
|
||||
return {
|
||||
content: script.content,
|
||||
language: script.language
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createInlineScriptModuleFromPath(path: string): Promise<FlowModuleValue> {
|
||||
const { content, language } = await getScriptByPath(path)
|
||||
|
||||
return {
|
||||
type: FlowModuleValue.type.RAWSCRIPT,
|
||||
language: language,
|
||||
content: content,
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadSchemaFromModule(module: FlowModule): Promise<{
|
||||
input_transform: Record<string, InputTransform>
|
||||
schema: Schema
|
||||
}> {
|
||||
const isRaw = module?.value.type === FlowModuleValue.type.RAWSCRIPT
|
||||
|
||||
if (isRaw || Boolean(module.value.path)) {
|
||||
let schema: Schema
|
||||
if (isRaw) {
|
||||
schema = emptySchema()
|
||||
await inferArgs(module.value.language!, module.value.content!, schema)
|
||||
} else {
|
||||
schema = await loadSchema(module.value.path!)
|
||||
}
|
||||
|
||||
const keys = Object.keys(schema?.properties ?? {})
|
||||
const inputTransform = keys.reduce((accu, key) => {
|
||||
accu[key] = {
|
||||
type: 'static',
|
||||
value: ''
|
||||
}
|
||||
return accu
|
||||
}, {})
|
||||
|
||||
return {
|
||||
input_transform: inputTransform,
|
||||
schema: schema ?? emptySchema()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
input_transform: {},
|
||||
schema: emptySchema()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { FlowService, type Flow } from '$lib/gen'
|
||||
|
||||
import { page } from '$app/stores'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
|
||||
const initialState = $page.url.searchParams.get('state')
|
||||
|
||||
Reference in New Issue
Block a user