improve Flow UI (#297)

* Improve flow ui

* Finish reworking

* Fix formating

* Fix naviation

* Fix navigaion + adjust icon sizes

* Remove duplicate code

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
Faton Ramadani
2022-08-01 15:07:29 +02:00
committed by GitHub
co-authored by Ruben Fiszel
parent e2788a2003
commit 6b30969ae5
22 changed files with 716 additions and 541 deletions
+105 -3
View File
@@ -32,6 +32,8 @@
"eslint": "^8.20.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0",
"flowbite": "^1.5.1",
"flowbite-svelte": "^0.22.19",
"openapi-typescript-codegen": "^0.23.0",
"path-browserify": "^1.0.1",
"postcss": "^8.4.5",
@@ -188,6 +190,21 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@floating-ui/core": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz",
"integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==",
"dev": true
},
"node_modules/@floating-ui/dom": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz",
"integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==",
"dev": true,
"dependencies": {
"@floating-ui/core": "^0.7.3"
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.2.tgz",
@@ -319,7 +336,6 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==",
"dev": true,
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -1028,6 +1044,12 @@
"fsevents": "~2.3.2"
}
},
"node_modules/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==",
"dev": true
},
"node_modules/clone-regexp": {
"version": "2.2.0",
"dev": true,
@@ -1886,6 +1908,32 @@
"dev": true,
"license": "ISC"
},
"node_modules/flowbite": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/flowbite/-/flowbite-1.5.1.tgz",
"integrity": "sha512-kMQ2o/aLV629WqUVubw2KZGYcN7W5rD0tFVqxvK5/45EFIgMAlTmT+XxdIjKwzyTlb3HgmUj5h5NaqN3TJc9Zw==",
"dev": true,
"dependencies": {
"@popperjs/core": "^2.9.3",
"mini-svg-data-uri": "^1.4.3"
}
},
"node_modules/flowbite-svelte": {
"version": "0.22.19",
"resolved": "https://registry.npmjs.org/flowbite-svelte/-/flowbite-svelte-0.22.19.tgz",
"integrity": "sha512-HhLhQlHiCP6K37z3OhrDGgoiiNqq6DMmbsa8hL+1TgK0WilokTuC4iLbnaBTTNg5lN+Od4w/KmX1PYKiBUrzow==",
"dev": true,
"dependencies": {
"@floating-ui/dom": "^0.5.3",
"classnames": "^2.3.1",
"flowbite": "^1.4.2",
"svelte-heros": "^2.2.2"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/fraction.js": {
"version": "4.2.0",
"dev": true,
@@ -4188,6 +4236,12 @@
"svelte": "^3.24.0"
}
},
"node_modules/svelte-heros": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/svelte-heros/-/svelte-heros-2.3.2.tgz",
"integrity": "sha512-OlfD/TzHytc9/J9ij/YrFKS2Lj0i0N0qkOm/5F7s9zdb/YGZ9tHY4PujSXrNxEyVWjjiaLTJGVgMkXlIrvVNNg==",
"dev": true
},
"node_modules/svelte-highlight": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/svelte-highlight/-/svelte-highlight-6.2.0.tgz",
@@ -4905,6 +4959,21 @@
"strip-json-comments": "^3.1.1"
}
},
"@floating-ui/core": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz",
"integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==",
"dev": true
},
"@floating-ui/dom": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz",
"integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==",
"dev": true,
"requires": {
"@floating-ui/core": "^0.7.3"
}
},
"@fortawesome/fontawesome-common-types": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.2.tgz",
@@ -4993,8 +5062,7 @@
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==",
"dev": true,
"peer": true
"dev": true
},
"@rollup/pluginutils": {
"version": "4.2.1",
@@ -5405,6 +5473,12 @@
"readdirp": "~3.6.0"
}
},
"classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==",
"dev": true
},
"clone-regexp": {
"version": "2.2.0",
"dev": true,
@@ -5963,6 +6037,28 @@
"version": "3.2.5",
"dev": true
},
"flowbite": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/flowbite/-/flowbite-1.5.1.tgz",
"integrity": "sha512-kMQ2o/aLV629WqUVubw2KZGYcN7W5rD0tFVqxvK5/45EFIgMAlTmT+XxdIjKwzyTlb3HgmUj5h5NaqN3TJc9Zw==",
"dev": true,
"requires": {
"@popperjs/core": "^2.9.3",
"mini-svg-data-uri": "^1.4.3"
}
},
"flowbite-svelte": {
"version": "0.22.19",
"resolved": "https://registry.npmjs.org/flowbite-svelte/-/flowbite-svelte-0.22.19.tgz",
"integrity": "sha512-HhLhQlHiCP6K37z3OhrDGgoiiNqq6DMmbsa8hL+1TgK0WilokTuC4iLbnaBTTNg5lN+Od4w/KmX1PYKiBUrzow==",
"dev": true,
"requires": {
"@floating-ui/dom": "^0.5.3",
"classnames": "^2.3.1",
"flowbite": "^1.4.2",
"svelte-heros": "^2.2.2"
}
},
"fraction.js": {
"version": "4.2.0",
"dev": true
@@ -7356,6 +7452,12 @@
"typescript": "*"
}
},
"svelte-heros": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/svelte-heros/-/svelte-heros-2.3.2.tgz",
"integrity": "sha512-OlfD/TzHytc9/J9ij/YrFKS2Lj0i0N0qkOm/5F7s9zdb/YGZ9tHY4PujSXrNxEyVWjjiaLTJGVgMkXlIrvVNNg==",
"dev": true
},
"svelte-highlight": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/svelte-highlight/-/svelte-highlight-6.2.0.tgz",
+2
View File
@@ -29,6 +29,8 @@
"eslint": "^8.20.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0",
"flowbite": "^1.5.1",
"flowbite-svelte": "^0.22.19",
"openapi-typescript-codegen": "^0.23.0",
"path-browserify": "^1.0.1",
"postcss": "^8.4.5",
+6
View File
@@ -3,6 +3,12 @@
@tailwind components;
@tailwind utilities;
html {
/* Avoid content shifting */
overflow-y: overlay;
}
@layer base {
h1 {
@apply text-2xl;
+49 -32
View File
@@ -2,17 +2,18 @@
import { ResourceService, ScriptService, VariableService } from '$lib/gen'
import { getScriptByPath, loadHubScripts, sendUserToast } from '$lib/utils'
import Icon from 'svelte-awesome'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { workspaceStore, hubScripts } from '$lib/stores'
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
import ResourceEditor from './ResourceEditor.svelte'
import { hubScripts, workspaceStore } from '$lib/stores'
import { Button } from 'flowbite-svelte'
import { Highlight } from 'svelte-highlight'
import { python, typescript } from 'svelte-highlight/languages'
import Modal from './Modal.svelte'
import type Editor from './Editor.svelte'
import ItemPicker from './ItemPicker.svelte'
import Modal from './Modal.svelte'
import ResourceEditor from './ResourceEditor.svelte'
import VariableEditor from './VariableEditor.svelte'
export let lang: 'python3' | 'deno'
export let editor: Editor
@@ -172,41 +173,57 @@
<VariableEditor bind:this={variableEditor} on:create={variablePicker.openModal} />
<div class="flex flex-row justify-around w-full">
<button
class="default-button-secondary font-semibold py-px mr-2 text-xs align-middle max-h-8"
on:click|stopPropagation={() => {
<div class="flex flex-row justify-end w-full space-x-2">
<Button
size="xs"
color="blue"
outline
on:click={() => {
variablePicker.openModal()
}}
>Variable picker <Icon data={faSearch} scale={0.7} />
</button>
>
<Icon data={faSearch} scale={0.7} class="mr-2" />
Variable picker
</Button>
<button
class="default-button-secondary font-semibold py-px text-xs mr-2 align-middle max-h-8"
on:click|stopPropagation={() => {
<Button
size="xs"
color="blue"
outline
on:click={() => {
resourcePicker.openModal()
}}
>Resource picker <Icon data={faSearch} scale={0.7} />
</button>
<button
class="default-button-secondary font-semibold py-px text-xs mr-2 align-middle max-h-8"
on:click|stopPropagation={() => {
>
<Icon data={faSearch} scale={0.7} class="mr-2" />
Resource picker
</Button>
<Button
size="xs"
color="blue"
outline
on:click={() => {
scriptPicker.openModal()
}}
>Script explorer <Icon data={faSearch} scale={0.7} />
</button>
>
<Icon data={faSearch} scale={0.7} class="mr-2" />
Script explorer
</Button>
<button
class="default-button-secondary py-px max-h-8 text-xs"
on:click|stopPropagation={() => {
<Button
size="xs"
color="blue"
outline
on:click={() => {
editor.reloadWebsocket()
}}
>
Reload assistants (status: {#if lang == 'deno'}<span
class={websocketAlive.deno ? 'text-green-600' : 'text-red-600'}>deno</span
>{:else if lang == 'python3'}<span
class={websocketAlive.pyright ? 'text-green-600' : 'text-red-600'}>pyright</span
>
<span class={websocketAlive.black ? 'text-green-600' : 'text-red-600'}> black</span>{/if})
</button>
Reload assistants (status:&nbsp;
{#if lang == 'deno'}
<span class={websocketAlive.deno ? 'text-green-600' : 'text-red-600'}>Deno</span>
{:else if lang == 'python3'}
<span class={websocketAlive.pyright ? 'text-green-600' : 'text-red-600'}>Pyright</span>
<span class={websocketAlive.black ? 'text-green-600' : 'text-red-600'}>Black</span>
{/if})
</Button>
</div>
+34 -56
View File
@@ -10,6 +10,7 @@
sendUserToast,
setQueryWithoutLoad
} from '$lib/utils'
import { Breadcrumb, BreadcrumbItem } from 'flowbite-svelte'
import { onMount } from 'svelte'
import { OFFSET } from './CronInput.svelte'
import FlowEditor from './FlowEditor.svelte'
@@ -126,61 +127,48 @@
})
</script>
<div class="flex flex-col h-screen max-w-screen-lg xl:-ml-20 xl:pl-4 w-full -mt-4 pt-4 md:mx-10 ">
<div class="flex flex-col max-w-screen-lg w-full mb-96 px-8">
<!-- Nav between steps-->
<div class="flex flex-col w-full">
<div class="justify-between flex flex-row drop-shadow-sm w-full">
<div class="wizard-nav flex flex-row w-full">
<div class="justify-between flex flex-row w-full">
<Breadcrumb>
<BreadcrumbItem>
<button on:click={() => changeStep(1)} class={step === 1 ? 'font-bold' : null}
>Flow Editor</button
>
</BreadcrumbItem>
<BreadcrumbItem>
<button on:click={() => changeStep(2)} class={step === 2 ? 'font-bold' : null}
>UI customisation</button
>
</BreadcrumbItem>
</Breadcrumb>
<div class="flex flex-row-reverse ml-2">
{#if step == 1}
<button
disabled={pathError != ''}
class="{step === 1
? 'default-button-disabled text-gray-700'
: 'default-button-secondary'} min-w-max ml-2"
on:click={() => {
changeStep(1)
}}>Step 1: Flow</button
class="default-button px-6 max-h-8"
on:click={() => changeStep(2)}
>
Next
</button>
<button
disabled={pathError != ''}
class="{step === 2
? 'default-button-disabled text-gray-700'
: 'default-button-secondary'} min-w-max ml-2"
on:click={() => {
changeStep(2)
}}>Step 2: UI customisation</button
class="default-button-secondary px-6 max-h-8 mr-2"
on:click={saveFlow}
>
</div>
<div class="flex flex-row-reverse ml-2">
{#if step == 1}
<button
disabled={pathError != ''}
class="default-button px-6 max-h-8"
on:click={() => {
changeStep(step + 1)
}}
>
Next
</button>
<button
disabled={pathError != ''}
class="default-button-secondary px-6 max-h-8 mr-2"
on:click={saveFlow}
>
Save
</button>
{:else}
<button class="default-button px-6 self-end" on:click={saveFlow}>Save</button>
{/if}
</div>
</div>
<div class="flex flex-row-reverse">
<span class="my-1 text-sm text-gray-500 italic">
{#if initialPath && initialPath != $flowStore?.path} {initialPath} &rightarrow; {/if}
{$flowStore?.path}
</span>
Save
</button>
{:else}
<button class="default-button px-6 self-end" on:click={saveFlow}>Save</button>
{/if}
</div>
</div>
<div class="flex flex-row-reverse">
<span class="my-1 text-sm text-gray-500 italic">
{#if initialPath && initialPath != $flowStore?.path} {initialPath} &rightarrow; {/if}
{$flowStore?.path}
</span>
</div>
<!-- metadata -->
@@ -205,13 +193,3 @@
<p>Loading</p>
{/if}
</div>
<style>
/* .wizard-nav {
@apply w-1/2 sm:w-1/4;
} */
.wizard-nav button {
max-height: 30px;
}
</style>
+47 -234
View File
@@ -1,28 +1,16 @@
<script lang="ts">
import { ScheduleService } from '$lib/gen'
import { faFileExport, faFileImport, faGlobe, 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, initFlow, mode } from './flows/flowStore'
import ModuleStep from './ModuleStep.svelte'
import Path from './Path.svelte'
import RadioButtonV2 from './RadioButtonV2.svelte'
import SchemaEditor from './SchemaEditor.svelte'
import Required from './Required.svelte'
import { flowToHubUrl, pathIsEmpty, sendUserToast } from '$lib/utils'
import Dropdown from './Dropdown.svelte'
import Editor from './Editor.svelte'
import Modal from './Modal.svelte'
import FlowViewer from './FlowViewer.svelte'
import { flowToMode } from './flows/utils'
import CronInput from './CronInput.svelte'
import CollapseLink from './CollapseLink.svelte'
import Toggle from './Toggle.svelte'
import SchemaForm from './SchemaForm.svelte'
import Tooltip from './Tooltip.svelte'
import { workspaceStore } from '$lib/stores'
import { pathIsEmpty } from '$lib/utils'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { Button } from 'flowbite-svelte'
import Icon from 'svelte-awesome'
import FlowInput from './flows/FlowInput.svelte'
import FlowSettings from './flows/FlowSettings.svelte'
import { addModule, flowStore, mode } from './flows/flowStore'
import ModuleStep from './ModuleStep.svelte'
import Tooltip from './Tooltip.svelte'
export let pathError = ''
export let initialPath: string = ''
@@ -31,11 +19,6 @@
export let scheduleEnabled = false
export let scheduleCron: string = '0 */5 * * *'
let jsonSetter: Modal
let jsonViewer: Modal
let jsonValue: string = ''
async function loadSchedule() {
const existsSchedule = await ScheduleService.existsSchedule({
workspace: $workspaceStore ?? '',
@@ -46,7 +29,7 @@
workspace: $workspaceStore ?? '',
path: initialPath
})
scheduleEnabled = schedule.enabled
scheduleEnabled = schedule.enabled!
scheduleCron = schedule.schedule
scheduleArgs = schedule.args ?? {}
}
@@ -60,221 +43,51 @@
let args: Record<string, any> = {}
</script>
<Modal bind:this={jsonSetter}>
<div slot="title">Import JSON</div>
<div slot="content" class="h-full">
<Editor bind:code={jsonValue} lang={'json'} class="h-full" />
</div>
<div slot="submission">
<button
class="default-button px-4 py-2 font-semibold"
{#if $flowStore}
<div class="flex space-y-8 flex-col items-center line">
<FlowSettings
bind:pathError
bind:initialPath
bind:scheduleArgs
bind:scheduleCron
bind:scheduleEnabled
bind:open
/>
<FlowInput />
{#each $flowStore?.value.modules as mod, i}
<ModuleStep bind:open bind:mod bind:args {i} mode={$mode} />
{#if i == 0 && $mode == 'pull'}
<div class="flex justify-center bg-white shadow p-2">
Starting from here, the flow for loop over items from step 1's result above &nbsp;
<Tooltip>
This flow being in 'Pull' mode, the rest of the flow will for loop over the list of
items returned by the trigger script right above. Retrieve the item value using
`flow_input._value`
</Tooltip>
</div>
{/if}
{/each}
<Button
disabled={pathIsEmpty($flowStore.path)}
color="blue"
on:click={() => {
Object.assign($flowStore, JSON.parse(jsonValue))
initFlow($flowStore)
open = -1
sendUserToast('OpenFlow imported from JSON')
jsonSetter.closeModal()
addModule()
open = $flowStore?.value.modules.length - 1
}}
>
Import
</button>
</div></Modal
>
<Modal bind:this={jsonViewer}>
<div slot="title">See JSON</div>
<div slot="content" class="h-full">
<FlowViewer flow={flowToMode($flowStore, $mode)} tab="json" />
<Icon class="text-white mr-2" data={faPlus} />
Add step {pathIsEmpty($flowStore?.path) ? '(pick a name first!)' : ''}
</Button>
</div>
</Modal>
{#if $flowStore}
<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">
<div
class="bg-white border border-gray xl-rounded shadow-lg w-full max-w-4xl mx-4 md:mx-auto p-4"
>
<div class="flex flex-row-reverse mr-4">
<Dropdown
dropdownItems={[
{
displayName: 'Import from a JSON OpenFlow',
icon: faFileImport,
action: () => {
jsonSetter.openModal()
}
},
{
displayName: 'Export to a JSON OpenFlow',
icon: faFileExport,
action: () => {
jsonViewer.openModal()
}
},
{
displayName: 'Publish to Hub',
icon: faGlobe,
action: () => {
const url = flowToHubUrl(flowToMode($flowStore, $mode))
window.open(url, '_blank')?.focus()
}
}
]}
relative={false}
/>
</div>
<div class="mb-8 p-4">
<Path
bind:error={pathError}
bind:path={$flowStore.path}
{initialPath}
namePlaceholder="my_flow"
kind="flow"
>
<div slot="ownerToolkit">
Flow permissions depend on their path. Select the group <span class="font-mono"
>all</span
>
to share your flow, and <span class="font-mono">user</span> to keep it private.
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
</div>
</Path>
<label class="block mt-4">
<span class="text-gray-700">Summary <Required required={false} /></span>
<textarea
bind:value={$flowStore.summary}
class="
mt-1
block
w-full
rounded-md
border-gray-300
shadow-sm
focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50
"
placeholder="A very short summary of the flow displayed when the flow is listed"
rows="1"
/>
</label>
</div>
<RadioButtonV2
options={[
[
{
title: 'UI or webhook triggered',
desc: 'Trigger this flow through the generated UI, a manual schedule or by calling the associated webhook'
},
'push'
],
[
{
title: 'Watching changes regularly',
desc: 'The first module of this flow is a trigger script whose purpose is to pull data from an external source and return all new items since last run. This flow is meant to be scheduled very regularly to reduce latency to react to new events. It will trigger the rest of the flow once per item. If no new items, the flow will be skipped.'
},
'pull'
]
]}
bind:value={$mode}
/>
{#if $mode == 'pull'}
<div class="p-4">
<CollapseLink text="set primary schedule" open={true}>
<Tooltip
>The primary schedule of a flow is simply a schedule that has the same name as a
flow. It can be set and enabled directly within the flow editor. "Watching for
new changes" flows are meant to be watching regularly for new items in an
external systems. The primary schedule purpose is there to set the periodicity
at which you want this watcher to operate.
</Tooltip> &nbsp;
<Toggle
bind:checked={scheduleEnabled}
options={{
left: { label: 'disabled', value: false },
right: { label: 'enabled', value: true }
}}
/>
<div class="p-2 mt-2 rounded" class:bg-gray-300={!scheduleEnabled}>
{#if !scheduleEnabled}
<span class="font-black">No next scheduled run when disabled</span>
{/if}
<CronInput bind:schedule={scheduleCron} />
</div>
<SchemaForm schema={$flowStore.schema} bind:args={scheduleArgs} />
</CollapseLink>
</div>
{/if}
</div>
</li>
<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>
<CopyFirstStepSchema />
</div>
<div class="p-4">
<SchemaEditor
on:change={() => {
$flowStore = $flowStore
}}
schema={$flowStore.schema}
/>
<div class="my-4" />
<FlowPreview
mode={$mode}
flow={$flowStore}
i={$flowStore?.value.modules.length}
bind:args
/>
</div>
</div>
</li>
{#each $flowStore?.value.modules as mod, i}
<li class="relative mt-16">
<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(i)
open = i
}}
>
<Icon class="text-white mb-1" data={faPlus} />
</button>
</div>
</li>
<ModuleStep bind:open bind:mod bind:args {i} mode={$mode} />
{/each}
<li class="relative m-20 ">
<div class="relative flex justify-center">
<button
disabled={pathIsEmpty($flowStore.path)}
class="default-button h-10 w-10 shadow"
on:click={() => {
addModule()
open = $flowStore?.value.modules.length - 1
}}
>
<Icon class="text-white mb-1" data={faPlus} />
Add step {pathIsEmpty($flowStore?.path) ? '(pick a name first!)' : ''}
</button>
</div>
</li>
</div>
</ul>
</div>
<div class="py-10 bg-white" />
{:else}
<h3>Loading flow</h3>
{/if}
<style>
.shadow:not([disabled]) {
@apply border-blue-600 shadow-blue-600/40;
.line {
background-image: linear-gradient(#e5e7eb, #e5e7eb);
background-size: 2px 100%;
background-repeat: no-repeat;
background-position: center center;
}
</style>
@@ -97,7 +97,8 @@
>
<h2 class="flex justify-center text-gray-600">
<div>
Preview<Icon class="ml-1" data={viewPreview ? faChevronUp : faChevronDown} scale={1} />
Preview
<Icon class="ml-1" data={viewPreview ? faChevronUp : faChevronDown} scale={1} />
</div>
</h2>
</button>
+2 -2
View File
@@ -37,10 +37,10 @@
<div class="fixed top-0 w-screen h-screen {z}">
<div
class="fixed right-0 flex flex-col w-3/4 sm:w-2/3 lg:w-1/2 h-screen border border-gray-300 shadow-xl"
class="fixed right-0 top-0 flex flex-col w-3/4 sm:w-2/3 lg:w-1/2 h-screen border border-gray-300 shadow-xl"
>
{#if open}
<div class="flex flex-row justify-between p-2 bg-white border-b border-gray-200">
<div class="flex flex-row justify-between p-2 bg-white border-b border-gray-200">
<button
on:click={() => {
open = false
+60 -62
View File
@@ -1,15 +1,17 @@
<script lang="ts">
import { type FlowModule, RawScript } from '$lib/gen'
import { RawScript, type FlowModule } from '$lib/gen'
import { previewResults } from '$lib/stores'
import { buildExtraLib, objectToTsType, schemaToTsType } from '$lib/utils'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import { faChevronDown, faChevronUp, faPlus } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import Editor from './Editor.svelte'
import EditorBar from './EditorBar.svelte'
import FlowPreview from './FlowPreview.svelte'
import FlowBox from './flows/FlowBox.svelte'
import FlowInputs from './flows/FlowInputs.svelte'
import FlowModuleHeader from './flows/FlowModuleHeader.svelte'
import {
addModule,
createInlineScriptModule,
flowStore,
loadSchema,
@@ -42,62 +44,69 @@
)
function toResults(jobs: any) {
return jobs.map((x) => {
if (x.result) {
return x.result
} else if (x.length) {
return toResults(x)
return jobs.map((job) => {
if (job.result) {
return job.result
} else if (Array.isArray(job)) {
return toResults(job)
}
})
}
const isTrigger = mode === 'pull' && i === 0
</script>
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-16">
<div
class="bg-white border border-gray xl-rounded shadow-lg w-full max-w-4xl mx-4 md:mx-auto"
id="module-{i}"
>
<div class="flex items-center justify-between flex-wra p-4 sm:px-6 z-10">
<FlowModuleHeader bind:open {mod} {i} {shouldPick}>
<div>
<h3 class="text-lg font-bold text-gray-900">Step {i + 1}</h3>
{#if i == 0 && mode == 'pull'}
<h3 class="font-bold">
Trigger Script <Tooltip
>When a flow is 'Pull', the first step is a trigger script. Trigger scripts are
scripts that must return a list which are the new items to be treated one by one by
the rest of the flow, usually the list of new items since last time the flow was
run. One can retrieve the item in the next step using `previous_result._value`. To
easily compute the diff, windmill provides some helpers under the form of
`getInternalState` and `setInternalState`.</Tooltip
>
</h3>{/if}
<p>
{#if 'path' in mod.value && mod.value.path}
{mod.value.path}
{/if}
{#if 'language' in mod.value && mod.value.language}
Inline {mod.value.language}
{/if}
{#if !('path' in mod.value) && !('language' in mod.value)}
Select a script
{/if}
</p>
</div>
</FlowModuleHeader>
</div>
<div class="border-b border-gray-200" />
<button
class="rounded-full h-10 w-10 bg-white border-2 border-gray-400"
on:click={() => {
addModule(i)
open = i
}}
>
<Icon class="text-gray-400 mb-1" data={faPlus} />
</button>
<FlowBox>
<div id="module-{i}">
<FlowModuleHeader bind:open {mod} {i} {shouldPick}>
<div>
<h3 class="text-lg font-bold text-gray-900">Step {i + 1}</h3>
{#if isTrigger}
<h3 class="font-bold">
Trigger Script
<Tooltip>
When a flow is 'Pull', the first step is a trigger script. Trigger scripts are scripts
that must return a list which are the new items to be treated one by one by the rest
of the flow, usually the list of new items since last time the flow was run. One can
retrieve the item in the next step using `previous_result._value`. To easily compute
the diff, windmill provides some helpers under the form of `getInternalState` and
`setInternalState`.
</Tooltip>
</h3>
{/if}
<p>
{#if 'path' in mod.value && mod.value.path}
{mod.value.path}
{/if}
{#if 'language' in mod.value && mod.value.language}
Inline {mod.value.language}
{/if}
{#if !('path' in mod.value) && !('language' in mod.value)}
Select a script
{/if}
</p>
</div>
</FlowModuleHeader>
{#if open == i}
<div class="p-6">
{#if shouldPick}
<FlowInputs
isTrigger={mode == 'pull' && i == 0}
{isTrigger}
on:pick={(e) => pickScript(e.detail.path, i)}
on:new={(e) => createInlineScriptModule(e.detail.language, i, mode)}
/>
{/if}
{#if mod.value.type === 'rawscript'}
<div class="p-1 overflow-hidden">
<div class="mb-2 overflow-hidden">
<EditorBar {editor} {websocketAlive} lang={mod.value.language ?? 'deno'} />
</div>
<div>
@@ -156,27 +165,16 @@
{/if}
{#if open == i}
<div>
<button class="w-full h-full" on:click={() => (open = -1)}
><Icon data={faChevronUp} scale={1.0} /></button
>
<button class="w-full h-full" on:click={() => (open = -1)}>
<Icon data={faChevronUp} scale={1.0} />
</button>
</div>
{:else}
<div>
<button class="w-full h-full" on:click={() => (open = i)}
><Icon data={faChevronDown} scale={1.0} /></button
>
<button class="w-full h-full" on:click={() => (open = i)}>
<Icon data={faChevronDown} scale={1.0} />
</button>
</div>
{/if}
</div>
</li>
{#if i == 0 && mode == 'pull'}
<li class="relative m-20 ">
<div class="relative flex justify-center bg-white shadow p-2">
Starting from here, the flow for loop over items from step 1's result above &nbsp;<Tooltip
>This flow being in 'Pull' mode, the rest of the flow will for loop over the list of items
returned by the trigger script right above. Retrieve the item value using
`flow_input._value`</Tooltip
>
</div>
</li>
{/if}
</FlowBox>
+80 -75
View File
@@ -1,12 +1,16 @@
<script lang="ts">
import SchemaModal, { DEFAULT_PROPERTY, schemaToModal } from './SchemaModal.svelte'
import type { ModalSchemaProperty } from './SchemaModal.svelte'
import type { Schema, SchemaProperty } from '$lib/common'
import Editor from './Editor.svelte'
import { emptySchema, sendUserToast, truncate } from '$lib/utils'
import Tooltip from './Tooltip.svelte'
import TableCustom from './TableCustom.svelte'
import { emptySchema, sendUserToast } from '$lib/utils'
import { faPen, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'
import { Badge, Button } from 'flowbite-svelte'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import Editor from './Editor.svelte'
import type { ModalSchemaProperty } from './SchemaModal.svelte'
import SchemaModal, { DEFAULT_PROPERTY, schemaToModal } from './SchemaModal.svelte'
import TableCustom from './TableCustom.svelte'
import Toggle from './Toggle.svelte'
import Tooltip from './Tooltip.svelte'
const dispatch = createEventDispatcher()
@@ -101,6 +105,13 @@
try {
if (Object.keys(schema.properties).includes(argName)) {
delete schema.properties[argName]
const requiredIndex = schema.required.findIndex((arg) => arg === argName)
if (requiredIndex > -1) {
schema.required = schema.required.slice(requiredIndex, 1)
}
schema = schema
schemaString = JSON.stringify(schema, null, '\t')
dispatch('change', schema)
@@ -126,95 +137,89 @@
</script>
<div class="flex flex-col">
<div class="w-full">
<div class="flex flex-row text-base">
<button
class="text-xs sm:text-base py-1 px-6 block hover:text-blue-500 focus:outline-noneborder-gray-200 {viewJsonSchema
? 'text-gray-500 '
: 'text-gray-700 font-semibold '}"
on:click={() => (viewJsonSchema ? switchTab() : null)}
>
arguments
</button><button
class="py-1 px-6 block hover:text-blue-500 focus:outline-none border-gray-200 {viewJsonSchema
? 'text-gray-700 font-semibold '
: 'text-gray-500'}"
on:click={() => (viewJsonSchema ? null : switchTab())}
>
advanced <Tooltip
>Arguments can be edited either using the wizard, or by editing their json-schema <a
href="https://docs.windmill.dev/docs/reference/script_arguments_reference">docs</a
></Tooltip
>
</button>
<button
class="default-button-secondary grow mb-1"
on:click={() => {
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
schemaModal.openModal()
}}>Add argument</button
>
<div class="flex justify-between">
<Button
on:click={() => {
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
schemaModal.openModal()
}}
color="blue"
>
<Icon data={faPlus} class="mr-1" />
Add argument
</Button>
<div class="flex items-center">
<Toggle
on:change={() => switchTab()}
options={{
left: { label: '', value: false },
right: { label: 'Json Schema Editor', value: true }
}}
/>
<div class="ml-2">
<Tooltip>
Arguments can be edited either using the wizard, or by editing their json-schema
<a href="https://docs.windmill.dev/docs/reference/script_arguments_reference">docs</a>
</Tooltip>
</div>
</div>
</div>
<!--json schema or table view-->
<div class="border-t py-1 h-full overflow-y-auto">
<div class="h-full overflow-y-auto">
{#if !viewJsonSchema}
<div class="h-full">
{#if schema.properties && Object.keys(schema.properties).length > 0 && schema.required}
<TableCustom class="w-full min-h-full">
<tr slot="header-row" class="underline">
<th>name</th>
<th>type</th>
<th>description</th>
<th>default</th>
<th>required</th>
<TableCustom>
<tr slot="header-row">
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
<th>Required</th>
<th />
</tr>
<tbody slot="body">
{#each Object.entries(schema.properties) as [name, property] (name)}
<tr>
<td>{name}</td>
<td
>{property.type ?? 'any'}
{property.format ? `(format: ${property.format})` : ''}
{property.contentEncoding ? `(encoding: ${property.contentEncoding})` : ''}
<td class="font-bold">{name}</td>
<td>
{#if !property.type}
<Badge color="red">ANY</Badge>
{:else}
<Badge color="blue">{property.type.toLocaleUpperCase()}</Badge>
{/if}
</td>
<td>{truncate(property.description, 20)}</td>
<td>{truncate(JSON.stringify(property.default) ?? '', 20)}</td>
<td>{schema.required.includes(name) ? 'required' : 'optional'}</td>
<td class="">
<button class="mr-2" on:click={() => handleDeleteArgument(name)}
><svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 14"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg></button
<td>{property.description}</td>
<td>{JSON.stringify(property.default) ?? ''}</td>
<td>{schema.required.includes(name) ? 'Required' : 'Optional'}</td>
<td class="justify-end flex">
<Button
color="red"
outline
class="mr-2"
size="xs"
on:click={() => handleDeleteArgument(name)}
>
<button
class="default-button-secondary text-xs inline-flex"
on:click={() => {
startEditArgument(name)
}}>edit</button
></td
>
<Icon data={faTrash} class="mr-2" scale={0.8} />
Delete
</Button>
<Button color="alternative" size="xs" on:click={() => startEditArgument(name)}>
<Icon data={faPen} class="mr-2" scale={0.8} />
Edit
</Button>
</td>
</tr>
{/each}
</tbody>
</TableCustom>
{:else}
<div class="text-gray-700 text-xs italic">This script has no argument</div>
<div class="text-gray-700 text-xs italic mt-2">This schema has no arguments.</div>
{/if}
</div>
{:else}
<div>
<div class="border rounded mt-4 p-2">
<Editor
on:change={() => {
try {
@@ -1,19 +1,19 @@
<script lang="ts">
import { ScriptService, type Script } from '$lib/gen'
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { inferArgs } from '$lib/infer'
import { DENO_INIT_CODE, DENO_INIT_CODE_TRIGGER, initialCode } from '$lib/script_helpers'
import { workspaceStore } from '$lib/stores'
import { emptySchema, encodeState, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
import { onDestroy } from 'svelte'
import ScriptEditor from './ScriptEditor.svelte'
import { page } from '$app/stores'
import { goto } from '$app/navigation'
import Path from './Path.svelte'
import SvelteMarkdown from 'svelte-markdown'
import { workspaceStore } from '$lib/stores'
import ScriptSchema from './ScriptSchema.svelte'
import { inferArgs } from '$lib/infer'
import Required from './Required.svelte'
import { DENO_INIT_CODE, DENO_INIT_CODE_TRIGGER, initialCode } from '$lib/script_helpers'
import Path from './Path.svelte'
import RadioButtonV3 from './RadioButtonV3.svelte'
import Required from './Required.svelte'
import ScriptEditor from './ScriptEditor.svelte'
import ScriptSchema from './ScriptSchema.svelte'
let editor: ScriptEditor
let scriptSchema: ScriptSchema
+14 -16
View File
@@ -1,9 +1,8 @@
<script lang="ts">
import { JobService, Job, CompletedJob } from '$lib/gen'
import { emptySchema, displayDate } from '$lib/utils'
import type { Schema } from '$lib/common'
import { fade } from 'svelte/transition'
import Icon from 'svelte-awesome'
import { CompletedJob, Job, JobService } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { displayDate, emptySchema } from '$lib/utils'
import {
faCheck,
faChevronDown,
@@ -12,26 +11,25 @@
faSpinner,
faTimes
} from '@fortawesome/free-solid-svg-icons'
import Editor from './Editor.svelte'
import Tooltip from './Tooltip.svelte'
import { onDestroy, onMount } from 'svelte'
import { userStore, workspaceStore } from '$lib/stores'
import TableCustom from './TableCustom.svelte'
import Icon from 'svelte-awesome'
import { check } from 'svelte-awesome/icons'
import { fade } from 'svelte/transition'
import Editor from './Editor.svelte'
import Modal from './Modal.svelte'
import TableCustom from './TableCustom.svelte'
import Tooltip from './Tooltip.svelte'
import { inferArgs } from '$lib/infer'
// @ts-ignore
import { VSplitPane } from 'svelte-split-pane'
import SchemaForm from './SchemaForm.svelte'
import DisplayResult from './DisplayResult.svelte'
import type { Preview } from '$lib/gen/models/Preview'
import EditorBar from './EditorBar.svelte'
import { Highlight } from 'svelte-highlight'
import typescript from 'svelte-highlight/languages/typescript'
import python from 'svelte-highlight/languages/python'
import json from 'svelte-highlight/languages/json'
import { json, python, typescript } from 'svelte-highlight/languages'
import { VSplitPane } from 'svelte-split-pane'
import DisplayResult from './DisplayResult.svelte'
import EditorBar from './EditorBar.svelte'
import SchemaForm from './SchemaForm.svelte'
// Exported
export let schema: Schema = emptySchema()
@@ -423,7 +421,7 @@
{:else}No local save{/if}
</div>
{:else if previewTab === 'history'}
<TableCustom paginated={false}>
<TableCustom>
<tr slot="header-row">
<th class="text-xs">id</th>
<th class="text-xs">created at</th>
+7 -10
View File
@@ -13,7 +13,7 @@
- the second slot should be a <tbody>, containing th elements
-->
<div class="flex flex-col {$$props.class} min-w-full">
<div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div class="inline-block min-w-full py-2 align-middle">
<table id="table-custom" class="min-w-full divide-y divide-gray-300 table-auto">
<thead>
<slot name="header-row" />
@@ -25,19 +25,16 @@
<div class="flex flex-row-reverse text-gray-500 mb-6">
<button
class="ml-2 drop-shadow-md {showNext ? 'visible' : 'invisible'}"
on:click={() => {
dispatch('next')
}}>Next</button
on:click={() => dispatch('next')}
>
Next
</button>
<button
class="mx-2 drop-shadow-md {currentPage === 1 ? 'hidden' : ''}"
on:click={() => {
dispatch('previous')
}}>Previous</button
on:click={() => dispatch('previous')}
>
Previous
</button>
</div>
{/if}
</div>
<style>
</style>
+2 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import Icon from 'svelte-awesome'
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { createPopperActions } from 'svelte-popperjs'
import { fade } from 'svelte/transition'
const [popperRef, popperContent] = createPopperActions({
@@ -50,6 +50,7 @@
use:popperContent={extraOpts}
on:mouseenter={open}
on:mouseleave={close}
class="w-96 text-left"
>
<slot />
<div id="arrow" data-popper-arrow />
@@ -1,11 +1,13 @@
<script lang="ts">
import { Button } from 'flowbite-svelte'
import { copyFirstStepSchema, isCopyFirstStepSchemaDisabled } from './flowStore'
</script>
<button
class="default-secondary-button-v2"
<Button
color="alternative"
size="sm"
disabled={$isCopyFirstStepSchemaDisabled}
on:click={copyFirstStepSchema}
>
Copy from step 1's schema
</button>
Copy from first step schema
</Button>
@@ -0,0 +1,3 @@
<div class="bg-white border border-gray-300 rounded-lg w-full shadow-md">
<slot />
</div>
@@ -0,0 +1,10 @@
<script lang="ts">
export let title: string | undefined = undefined
</script>
<div class="flex items-center justify-between flex-wra px-6 py-4 border-b border-gray-300">
{#if title}
<h3 class="text-lg leading-6 font-bold text-gray-900">{title}</h3>
{/if}
<slot />
</div>
@@ -0,0 +1,21 @@
<script lang="ts">
import SchemaEditor from '../SchemaEditor.svelte'
import { flowStore } from './../flows/flowStore'
import CopyFirstStepSchema from './CopyFirstStepSchema.svelte'
import FlowBox from './FlowBox.svelte'
import FlowBoxHeader from './FlowBoxHeader.svelte'
</script>
<FlowBox>
<FlowBoxHeader title="Flow Inputs">
<CopyFirstStepSchema />
</FlowBoxHeader>
<div class="p-6 ">
<SchemaEditor
on:change={() => {
$flowStore = $flowStore
}}
schema={$flowStore.schema}
/>
</div>
</FlowBox>
@@ -2,11 +2,13 @@
import type { FlowModule } from '$lib/gen'
import { getScriptByPath } from '$lib/utils'
import { faCode, faCodeBranch, faSave, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { Button } from 'flowbite-svelte'
import Icon from 'svelte-awesome'
import { Highlight } from 'svelte-highlight'
import typescript from 'svelte-highlight/languages/typescript'
import python from 'svelte-highlight/languages/python'
import typescript from 'svelte-highlight/languages/typescript'
import Modal from '../Modal.svelte'
import FlowBoxHeader from './FlowBoxHeader.svelte'
import { createScriptFromInlineScript, fork, removeModule } from './flowStore'
import { scrollIntoView } from './utils'
@@ -36,54 +38,54 @@
}
</script>
<div class="flex flex-row w-full space-x-2">
<slot />
<a href="#module-{i}" class="grow" on:click={() => (open = i)} on:click|preventDefault={scrollTo}
><div class="w-full" /></a
<FlowBoxHeader>
<a
href="#module-{i}"
class="grow text-inherit"
on:click={() => (open = i)}
on:click|preventDefault={scrollTo}
>
<slot />
</a>
<div class="flex flex-row space-x-2">
{#if mod.value.type === 'script' && !shouldPick}
<button
type="button"
<Button
on:click={() => {
open = i
fork(i)
}}
class="default-secondary-button-v2 text-xs"
size="sm"
color="alternative"
>
<Icon data={faCodeBranch} class={`w-4 mr-2 h-4`} />
<Icon data={faCodeBranch} class="mr-2" />
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" />
</Button>
<Button size="sm" color="alternative" on:click={viewCode}>
<Icon data={faCode} class="mr-2" />
View code
</button>
</Button>
{/if}
{#if mod.value.type === 'rawscript' && !shouldPick}
<button
type="button"
on:click={() => createScriptFromInlineScript(i)}
class="default-secondary-button-v2 text-sm"
>
<Icon data={faSave} class="w-4 mr-4" />
<Button size="sm" color="alternative" on:click={() => createScriptFromInlineScript(i)}>
<Icon data={faSave} class="mr-2" />
Save to workspace
</button>
</Button>
{/if}
<button
type="button"
<Button
size="sm"
color="alternative"
on:click={() => {
open = -1
removeModule(i)
}}
class="default-secondary-button-v2 text-xs"
>
<Icon data={faTrashAlt} class="w-4 mr-2 h-4" />
<Icon data={faTrashAlt} class="mr-2" />
Remove step
</button>
</Button>
</div>
</div>
</FlowBoxHeader>
<Modal bind:this={modalViewer}>
<div slot="title">Script {'path' in mod?.value ? mod?.value.path : ''}</div>
@@ -0,0 +1,189 @@
<script lang="ts">
import { sendUserToast } from '$lib/utils'
import { faFileExport, faFileImport, faGlobe } from '@fortawesome/free-solid-svg-icons'
import { Dropdown, DropdownItem } from 'flowbite-svelte'
import Icon from 'svelte-awesome'
import Editor from '../Editor.svelte'
import FlowPreview from '../FlowPreview.svelte'
import FlowViewer from '../FlowViewer.svelte'
import Modal from '../Modal.svelte'
import CollapseLink from './../CollapseLink.svelte'
import CronInput from './../CronInput.svelte'
import FlowBox from './../flows/FlowBox.svelte'
import { flowStore, initFlow, mode } from './../flows/flowStore'
import { flowToMode } from './../flows/utils'
import Path from './../Path.svelte'
import RadioButtonV2 from './../RadioButtonV2.svelte'
import Required from './../Required.svelte'
import SchemaForm from './../SchemaForm.svelte'
import Toggle from './../Toggle.svelte'
import Tooltip from './../Tooltip.svelte'
import FlowBoxHeader from './FlowBoxHeader.svelte'
export let pathError = ''
export let initialPath: string = ''
export let scheduleArgs: Record<string, any> = {}
export let scheduleEnabled = false
export let scheduleCron: string = '0 */5 * * *'
export let open: number
let jsonSetter: Modal
let jsonViewer: Modal
let jsonValue: string = ''
</script>
<Modal bind:this={jsonSetter}>
<div slot="title">Import JSON</div>
<div slot="content" class="h-full">
<Editor bind:code={jsonValue} lang={'json'} class="h-full" />
</div>
<div slot="submission">
<button
class="default-button px-4 py-2 font-semibold"
on:click={() => {
Object.assign($flowStore, JSON.parse(jsonValue))
initFlow($flowStore)
open = -1
sendUserToast('OpenFlow imported from JSON')
jsonSetter.closeModal()
}}
>
Import
</button>
</div>
</Modal>
<Modal bind:this={jsonViewer}>
<div slot="title">See JSON</div>
<div slot="content" class="h-full">
<FlowViewer flow={flowToMode($flowStore, $mode)} tab="json" />
</div>
</Modal>
<FlowBox>
<FlowBoxHeader title="Flow Settings">
<div class="flex flex-row-reverse">
<Dropdown class="w-fit" placement="bottom-end">
<button slot="trigger" class="text-gray-900 bg-white dark:text-white dark:bg-gray-800">
...
</button>
<DropdownItem
on:click={() => {
jsonSetter.openModal()
}}
>
<Icon data={faFileImport} scale={0.6} class="inline mr-2" />
Import from a JSON OpenFlow
</DropdownItem>
<DropdownItem
on:click={() => {
jsonViewer.openModal()
}}
>
<Icon data={faFileExport} scale={0.6} class="inline mr-2" />
Export to a JSON OpenFlow
</DropdownItem>
<DropdownItem
on:click={() => {
const url = new URL('https://hub.windmill.dev/flows/add')
const openFlow = {
value: $flowStore.value,
summary: $flowStore.summary,
description: $flowStore.description,
schema: $flowStore.schema
}
url.searchParams.append('flow', btoa(JSON.stringify(flowToMode(openFlow, $mode))))
window.open(url, '_blank')?.focus()
}}
>
<Icon data={faGlobe} scale={0.6} class="inline mr-2" />
Publish to Hub
</DropdownItem>
</Dropdown>
</div>
</FlowBoxHeader>
<div class="p-6">
<Path
bind:error={pathError}
bind:path={$flowStore.path}
{initialPath}
namePlaceholder="my_flow"
kind="flow"
>
<div slot="ownerToolkit">
Flow permissions depend on their path. Select the group <span class="font-mono">all</span>
to share your flow, and <span class="font-mono">user</span> to keep it private.
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
</div>
</Path>
<label class="block mt-4">
<span class="text-gray-700">Summary <Required required={false} /></span>
<textarea
bind:value={$flowStore.summary}
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
placeholder="A very short summary of the flow displayed when the flow is listed"
rows="1"
/>
</label>
</div>
<RadioButtonV2
options={[
[
{
title: 'UI or webhook triggered',
desc: 'Trigger this flow through the generated UI, a manual schedule or by calling the associated webhook'
},
'push'
],
[
{
title: 'Watching changes regularly',
desc: 'The first module of this flow is a trigger script whose purpose is to pull data from an external source and return all new items since last run. This flow is meant to be scheduled very regularly to reduce latency to react to new events. It will trigger the rest of the flow once per item. If no new items, the flow will be skipped.'
},
'pull'
]
]}
bind:value={$mode}
/>
{#if $mode == 'pull'}
<div class="p-4">
<CollapseLink text="set primary schedule" open={true}>
<Tooltip>
The primary schedule of a flow is simply a schedule that has the same name as a flow. It
can be set and enabled directly within the flow editor. "Watching for new changes" flows
are meant to be watching regularly for new items in an external systems. The primary
schedule purpose is there to set the periodicity at which you want this watcher to
operate.
</Tooltip>
<Toggle
bind:checked={scheduleEnabled}
options={{
left: { label: 'disabled', value: false },
right: { label: 'enabled', value: true }
}}
/>
<div class="p-2 mt-2 rounded" class:bg-gray-300={!scheduleEnabled}>
{#if !scheduleEnabled}
<span class="font-black">No next scheduled run when disabled</span>
{/if}
<CronInput bind:schedule={scheduleCron} />
</div>
<SchemaForm schema={$flowStore.schema} bind:args={scheduleArgs} />
</CollapseLink>
</div>
{/if}
<div class="p-6">
<FlowPreview
mode={$mode}
flow={$flowStore}
i={$flowStore?.value.modules.length}
bind:args={scheduleArgs}
/>
</div>
</FlowBox>
+30 -8
View File
@@ -1,9 +1,15 @@
import type { Schema } from '$lib/common'
import { type InputTransform, RawScript, ScriptService, type Flow, type FlowModule } from '$lib/gen'
import { RawScript, ScriptService, type Flow, type FlowModule } from '$lib/gen'
import { initialCode } from '$lib/script_helpers'
import { userStore, workspaceStore } from '$lib/stores'
import { emptySchema } from '$lib/utils'
import { derived, get, writable } from 'svelte/store'
import { createInlineScriptModuleFromPath, getFirstStepSchema, loadSchemaFromModule, scrollIntoView } from './utils'
import {
createInlineScriptModuleFromPath,
getFirstStepSchema,
loadSchemaFromModule,
scrollIntoView
} from './utils'
export type FlowMode = 'push' | 'pull'
@@ -17,7 +23,8 @@ export function initFlow(flow: Flow) {
flow = flattenForloopFlows(flow)
flow.value.modules.forEach((mod) => {
Object.values(mod.input_transform).forEach((inp) => {
if (inp.type == 'javascript') { // for now we use the value for dynamic expression when done in the static editor so we have to resort to this
if (inp.type == 'javascript') {
// for now we use the value for dynamic expression when done in the static editor so we have to resort to this
//@ts-ignore
inp.value = codeToStaticTemplate(inp.expr)
@@ -77,7 +84,6 @@ export function addModule(i?: number) {
input_transform: {}
}
flowStore.update((flow: Flow) => {
const insertAt = i ?? flow.value.modules.length
@@ -87,6 +93,19 @@ export function addModule(i?: number) {
return flow
})
schemasStore.update((schemas: Schema[]) => {
if (typeof i !== 'undefined') {
const previousSchema = schemas[i]
const hadSchema = Boolean(previousSchema)
if (hadSchema) {
schemas[i] = emptySchema()
schemas[i + 1] = previousSchema
}
}
return schemas
})
}
export async function pickScript(path: string, step: number) {
@@ -101,13 +120,17 @@ export async function pickScript(path: string, step: number) {
await loadSchema(step)
}
export async function createInlineScriptModule(language: RawScript.language, step: number, mode: FlowMode) {
const code = initialCode(language, (mode === 'pull' && step == 0))
export async function createInlineScriptModule(
language: RawScript.language,
step: number,
mode: FlowMode
) {
const code = initialCode(language, mode === 'pull' && step == 0)
flowStore.update((flow: Flow) => {
flow.value.modules[step].value = {
type: 'rawscript',
content: code,
language,
language
}
return flow
@@ -152,7 +175,6 @@ export async function createScriptFromInlineScript(step: number) {
const schemas = get(schemasStore)
const user = get(userStore)
const flowModuleValue = flow.value.modules[step].value
if (flowModuleValue.type != 'rawscript') {
+10 -2
View File
@@ -1,5 +1,8 @@
const config = {
content: ['./src/**/*.{html,js,svelte,ts}'],
content: [
'./src/**/*.{html,js,svelte,ts}',
"./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}",
],
theme: {
extend: {
@@ -26,7 +29,12 @@ const config = {
}
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')]
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('flowbite/plugin')
],
darkMode: 'class',
}
module.exports = config