feat(frontend): background script can run script and flows (#1515)

* all

* progress

* all

* all
This commit is contained in:
Ruben Fiszel
2023-05-03 00:10:41 +02:00
committed by GitHub
parent 629f5a0961
commit e4c60aaf4a
26 changed files with 573 additions and 448 deletions
@@ -1,27 +1,17 @@
<script lang="ts">
import { getContext, onMount } from 'svelte'
import { initOutput } from '../../editor/appUtils'
import type {
ConnectedAppInput,
RowAppInput,
StaticAppInput,
UserAppInput
} from '../../inputType'
import type { AppViewerContext, InlineScript } from '../../types'
import type { AppViewerContext, HiddenRunnable } from '../../types'
import RunnableComponent from './RunnableComponent.svelte'
import InitializeComponent from './InitializeComponent.svelte'
export let id: string
export let name: string
export let inlineScript: InlineScript | undefined
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
export let autoRefresh: boolean
export let noBackendValue: any = undefined
export let recomputeOnInputChanged: boolean
export let runnable: HiddenRunnable
const { worldStore, staticExporter, noBackend } = getContext<AppViewerContext>('AppViewerContext')
let result: any = noBackend ? noBackendValue : undefined
let result: any = noBackend ? runnable.noBackendValue : undefined
onMount(() => {
$staticExporter[id] = () => {
@@ -35,21 +25,18 @@
})
</script>
{#if inlineScript}
{#if runnable && (runnable.type == 'runnableByPath' || (runnable.type == 'runnableByName' && runnable.inlineScript != undefined))}
<RunnableComponent
render={false}
{id}
{fields}
{autoRefresh}
fields={runnable.fields}
autoRefresh={true}
bind:result
transformer={undefined}
{recomputeOnInputChanged}
runnable={{
name,
inlineScript,
type: 'runnableByName'
}}
recomputeOnInputChanged={runnable.recomputeOnInputChanged ?? true}
{runnable}
wrapperClass="hidden"
recomputableByRefreshButton={runnable.autoRefresh ?? true}
{outputs}
>
<slot />
@@ -35,6 +35,7 @@
export let recomputeOnInputChanged: boolean = true
export let loading = false
export let refreshOnStart: boolean = false
export let recomputableByRefreshButton: boolean
const {
worldStore,
@@ -346,7 +347,7 @@
}
$runnableComponents[id] = {
autoRefresh: autoRefresh,
autoRefresh: autoRefresh && recomputableByRefreshButton,
refreshOnStart: refreshOnStart,
cb: cancellableRun
}
@@ -124,9 +124,8 @@
bind:result
runnable={componentInput.runnable}
transformer={componentInput.transformer}
autoRefresh={componentInput.autoRefresh === undefined
? autoRefresh
: componentInput.autoRefresh}
{autoRefresh}
recomputableByRefreshButton={componentInput.autoRefresh ?? true}
bind:recomputeOnInputChanged={componentInput.recomputeOnInputChanged}
{id}
{extraQueryParams}
@@ -44,6 +44,7 @@
import SettingsPanel from './SettingsPanel.svelte'
import { secondaryMenu, SecondaryMenu } from './settingsPanel/secondaryMenu'
import Popover from '../../Popover.svelte'
import { migrateApp } from '../utils'
export let app: App
export let path: string
@@ -52,6 +53,8 @@
export let summary: string
export let fromHub: boolean = false
migrateApp(app)
const appStore = writable<App>(app)
const selectedComponent = writable<string[] | undefined>(undefined)
const mode = writable<EditorMode>(initialMode)
@@ -34,6 +34,7 @@
AppInput,
ConnectedAppInput,
RowAppInput,
Runnable,
StaticAppInput,
UserAppInput
} from '../inputType'
@@ -120,7 +121,7 @@
)
}
async function computeTriggerables() {
const allTriggers = await Promise.all(
const allTriggers = (await Promise.all(
allItems($app.grid, $app.subgrids)
.flatMap((x) => {
let c = x.data as AppComponent
@@ -132,27 +133,32 @@
})
.map(async (input) => {
if (input?.type == 'runnable') {
const staticInputs = collectStaticFields(input.fields)
if (input.runnable?.type == 'runnableByName') {
let hex = await hash(input.runnable.inlineScript?.content)
return [`rawscript/${hex}`, staticInputs]
} else if (input.runnable?.type == 'runnableByPath') {
let prefix =
input.runnable.runType !== 'hubscript' ? input.runnable.runType : 'script'
return [`${prefix}/${input.runnable.path}`, staticInputs]
}
return await processRunnable(input.runnable, input.fields)
}
return []
})
.concat(
Object.values($app.hiddenInlineScripts ?? {}).map(async (v) => {
let hex = await hash(v.inlineScript?.content)
const staticInputs = collectStaticFields(v.fields)
return [`rawscript/${hex}`, staticInputs]
return await processRunnable(v, v.fields)
})
)
)) as ([string, Record<string, any>] | undefined)[]
policy.triggerables = Object.fromEntries(
allTriggers.filter(Boolean) as [string, Record<string, any>][]
)
policy.triggerables = Object.fromEntries(allTriggers.filter((x) => x.length > 0))
}
async function processRunnable(
runnable: Runnable,
fields: Record<string, any>
): Promise<[string, Record<string, any>] | undefined> {
const staticInputs = collectStaticFields(fields)
if (runnable?.type == 'runnableByName') {
let hex = await hash(runnable.inlineScript?.content)
return [`rawscript/${hex}`, staticInputs]
} else if (runnable?.type == 'runnableByPath') {
let prefix = runnable.runType !== 'hubscript' ? runnable.runType : 'script'
return [`${prefix}/${runnable.path}`, staticInputs]
}
}
async function createApp(path: string) {
await computeTriggerables()
@@ -496,7 +502,13 @@
</Drawer>
<Drawer bind:open={jobsDrawerOpen} size="900px">
<DrawerContent noPadding title="Debug Runs" on:close={() => (jobsDrawerOpen = false)} tooltip="Look at latests runs to spot potential bugs." documentationLink="https://docs.windmill.dev/docs/apps/app_toolbar#debug-runs">
<DrawerContent
noPadding
title="Debug Runs"
on:close={() => (jobsDrawerOpen = false)}
tooltip="Look at latests runs to spot potential bugs."
documentationLink="https://docs.windmill.dev/docs/apps/app_toolbar#debug-runs"
>
<Splitpanes class="!overflow-visible">
<Pane size={25}>
<PanelSection title="Past Runs">
@@ -21,6 +21,7 @@
import { HiddenComponent } from '../components'
import { deepEqual } from 'fast-equals'
import { dfs } from './appUtils'
import { migrateApp } from '../utils'
export let app: App
export let appPath: string
@@ -33,6 +34,8 @@
export let noBackend: boolean = false
export let isLocked = false
migrateApp(app)
const appStore = writable<App>(app)
const selectedComponent = writable<string[] | undefined>(undefined)
const mode = writable<EditorMode>('preview')
@@ -163,17 +166,9 @@
</div>
{#if app.hiddenInlineScripts}
{#each app.hiddenInlineScripts as script, index}
{#if script}
<HiddenComponent
id={`bg_${index}`}
inlineScript={script.inlineScript}
name={script.name}
fields={script.fields}
recomputeOnInputChanged={script.recomputeOnInputChanged ?? true}
autoRefresh={script.autoRefresh ?? false}
noBackendValue={script.noBackendValue}
/>
{#each app.hiddenInlineScripts as runnable, index}
{#if runnable}
<HiddenComponent id={`bg_${index}`} {runnable} />
{/if}
{/each}
{/if}
@@ -127,16 +127,9 @@
</div>
{#if $app.hiddenInlineScripts}
{#each $app.hiddenInlineScripts as script, index}
{#if script}
<HiddenComponent
id={`bg_${index}`}
inlineScript={script.inlineScript}
name={script.name}
fields={script.fields}
recomputeOnInputChanged={script.recomputeOnInputChanged ?? true}
autoRefresh={script.autoRefresh ?? false}
/>
{#each $app.hiddenInlineScripts as runnable, index}
{#if runnable}
<HiddenComponent id={`bg_${index}`} {runnable} />
{/if}
{/each}
{/if}
@@ -52,22 +52,6 @@
return undefined
}
$: {
if (hiddenInlineScript && hiddenInlineScript?.script.recomputeOnInputChanged === undefined) {
hiddenInlineScript.script.recomputeOnInputChanged = true
}
//TODO: remove after migration is done
if (
hiddenInlineScript &&
hiddenInlineScript?.script.doNotRecomputeOnInputChanged != undefined
) {
hiddenInlineScript.script.recomputeOnInputChanged =
!hiddenInlineScript.script.doNotRecomputeOnInputChanged
hiddenInlineScript.script.doNotRecomputeOnInputChanged = undefined
}
}
</script>
{#if componentSettings}
@@ -97,7 +81,7 @@
{/key}
{:else if hiddenInlineScript}
<BackgroundScriptSettings
bind:script={hiddenInlineScript.script}
bind:runnable={hiddenInlineScript.script}
id={`bg_${hiddenInlineScript.index}`}
/>
@@ -7,5 +7,7 @@
</script>
{#each $app.hiddenInlineScripts as action, index}
<BackgroundScriptOutput id={`bg_${index}`} name={action.name} first={index === 0} />
{#if !action.hidden}
<BackgroundScriptOutput id={`bg_${index}`} name={action.name} first={index === 0} />
{/if}
{/each}
@@ -28,7 +28,8 @@
$search != '' &&
($hasResult[id] ||
Object.entries($hasResult).some(([key, value]) => value && subids.includes(key)))
$: open = $allIdsInPath.includes(id) || $manuallyOpened[id] || inSearch
$: open =
$allIdsInPath.includes(id) || id == $selectedComponent?.[0] || $manuallyOpened[id] || inSearch
const hoverColor = {
blue: 'hover:bg-blue-100 hover:text-blue-500',
@@ -76,12 +77,7 @@
})
$app.hiddenInlineScripts.forEach((x) => {
console.log('process', x.name, id)
processRunnable(from, to, {
name: x.name,
inlineScript: x.inlineScript,
type: 'runnableByName'
})
processRunnable(from, to, x)
})
}
propagateRename(id, newId)
@@ -16,9 +16,11 @@
import { defaultCode } from '../component'
import InlineScriptList from '../settingsPanel/mainInput/InlineScriptList.svelte'
import WorkspaceScriptList from '../settingsPanel/mainInput/WorkspaceScriptList.svelte'
import RunnableSelector from '../settingsPanel/mainInput/RunnableSelector.svelte'
export let name: string
export let componentType: string | undefined = undefined
export let showScriptPicker = false
let tab = 'inlinescripts'
let filter: string = ''
@@ -140,9 +142,12 @@
</Drawer>
<div class="flex flex-col px-4 gap-2 text-sm" in:fly={{ duration: 50 }}>
<div class="mt-4 flex justify-between gap-8 mb-2">
<div class="font-bold items-baseline">Choose a language:</div>
<div class="mt-2 flex justify-between gap-4">
<div class="font-bold items-baseline truncate">Choose a language</div>
<div class="flex gap-2">
{#if showScriptPicker}
<RunnableSelector on:pick hideCreateScript />
{/if}
<Button
on:click={() => picker?.openDrawer()}
size="xs"
@@ -151,8 +156,9 @@
startIcon={{ icon: faCodeBranch }}
btnClasses="truncate"
>
Fork a workspace or hub script
Fork a script
</Button>
<Button
on:click={() => dispatch('delete')}
size="xs"
@@ -160,7 +166,7 @@
variant="border"
btnClasses="truncate"
>
Delete
Clear
</Button>
</div>
</div>
@@ -181,7 +187,7 @@
{/each}
</div>
<div class="mt-4 mb-2 text-sm">Typescript templates</div>
<div class="mt-2 mb-2 text-sm">Typescript templates</div>
<div class="flex gap-2 flex-row flex-wrap mb-4">
<FlowScriptPicker
@@ -2,8 +2,8 @@
import Button from '$lib/components/common/button/Button.svelte'
import type { Preview } from '$lib/gen'
import { createEventDispatcher, getContext, onMount } from 'svelte'
import type { AppViewerContext, CancelablePromise, InlineScript } from '../../types'
import { Loader2, Maximize2, Trash2 } from 'lucide-svelte'
import type { AppViewerContext, InlineScript } from '../../types'
import { Maximize2, Trash2 } from 'lucide-svelte'
import InlineScriptEditorDrawer from './InlineScriptEditorDrawer.svelte'
import { inferArgs } from '$lib/infer'
import type { Schema } from '$lib/common'
@@ -17,6 +17,7 @@
import Kbd from '$lib/components/common/kbd/Kbd.svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import { buildExtraLib } from '../../utils'
import RunButton from './RunButton.svelte'
let inlineScriptEditorDrawer: InlineScriptEditorDrawer
@@ -87,8 +88,6 @@
inlineScript?.language == 'frontend' && worldStore
? buildExtraLib($worldStore?.outputsById ?? {}, id, false, $state, true)
: undefined
let cancelable: CancelablePromise<void> | undefined = undefined
</script>
{#if inlineScript}
@@ -163,50 +162,7 @@
</div>
</div>
</Button>
{#if $runnableComponents[id] != undefined}
{#if !runLoading}
<Button
loading={runLoading}
size="xs"
color="dark"
variant="border"
btnClasses="!px-2 !py-1 !bg-gray-700 !text-white hover:!bg-gray-900"
on:click={async () => {
runLoading = true
try {
cancelable = $runnableComponents[id]?.cb?.(
!transformer ? inlineScript : undefined
)
await cancelable
} catch {}
runLoading = false
}}
>
<div class="flex flex-row gap-1 items-center">
Run
<div class="flex flex-row items-center">
<Kbd small>{getModifierKey()}</Kbd>
<Kbd small><span class="text-lg font-bold">⏎</span></Kbd>
</div>
</div>
</Button>
{:else}
<Button
size="xs"
color="red"
variant="border"
btnClasses="!px-2 !py-1.5"
on:click={async () => {
cancelable?.cancel()
runLoading = false
}}
>
<Loader2 size={14} class="animate-spin mr-2" />
Cancel
</Button>
{/if}
{/if}
<RunButton bind:runLoading {id} inlineScript={!transformer ? inlineScript : undefined} />
</div>
</div>
@@ -1,26 +1,12 @@
<script lang="ts">
import { Drawer, DrawerContent } from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import FlowModuleScript from '$lib/components/flows/content/FlowModuleScript.svelte'
import FlowPathViewer from '$lib/components/flows/content/FlowPathViewer.svelte'
import { inferArgs } from '$lib/infer'
import { workspaceStore } from '$lib/stores'
import { emptySchema, getScriptByPath, sendUserToast } from '$lib/utils'
import {
faCodeBranch,
faExternalLinkAlt,
faEye,
faPen,
faRefresh
} from '@fortawesome/free-solid-svg-icons'
import type { AppInput, RunnableByPath } from '../../inputType'
import { clearResultAppInput, loadSchema } from '../../utils'
import type { AppInput, Runnable } from '../../inputType'
import { clearResultAppInput } from '../../utils'
import EmptyInlineScript from './EmptyInlineScript.svelte'
import InlineScriptEditor from './InlineScriptEditor.svelte'
import { computeFields } from './utils'
import { deepEqual } from 'fast-equals'
import { getContext } from 'svelte'
import type { AppViewerContext } from '../../types'
import InlineScriptRunnableByPath from './InlineScriptRunnableByPath.svelte'
export let componentInput: AppInput | undefined
export let defaultUserInput = false
@@ -28,80 +14,20 @@
export let id: string
export let transformer: boolean
const { app, stateId } = getContext<AppViewerContext>('AppViewerContext')
async function fork(path: string) {
let { content, language, schema } = await getScriptByPath(path)
if (componentInput && componentInput.type == 'runnable') {
if (!schema || typeof schema == 'string' || Object.keys(schema).length == 0) {
schema = emptySchema()
await inferArgs(language, content, schema)
}
componentInput.runnable = {
type: 'runnableByName',
name: path,
inlineScript: {
content,
language,
schema,
path
}
}
} else {
console.error('componentInput is undefined')
}
}
let drawerFlowViewer: Drawer
let flowPath: string = ''
async function refreshScript(x: RunnableByPath) {
let { schema } = await getScriptByPath(x.path)
if (!deepEqual(x.schema, schema)) {
x.schema = schema
if (componentInput?.type == 'runnable') {
componentInput.fields = computeFields(schema, false, componentInput.fields)
}
componentInput = componentInput
}
}
async function refreshFlow(x: RunnableByPath) {
if (componentInput?.type == 'runnable') {
const schema = (await loadSchema($workspaceStore ?? '', x.path, 'flow')) ?? emptySchema()
if (!deepEqual(x.schema, schema)) {
x.schema = schema
const nfields = computeFields(schema, false, componentInput.fields)
componentInput.fields = nfields
componentInput = componentInput
}
}
}
$: if (
componentInput &&
componentInput.type == 'runnable' &&
componentInput?.runnable?.type === 'runnableByPath'
) {
if (componentInput.runnable.runType == 'script') {
refreshScript(componentInput.runnable)
} else if (componentInput.runnable.runType == 'flow') {
refreshFlow(componentInput.runnable)
}
}
const { app } = getContext<AppViewerContext>('AppViewerContext')
function deleteInlineScript() {
if (componentInput && componentInput.type == 'runnable') {
componentInput = clearResultAppInput(componentInput)
}
}
</script>
<Drawer bind:this={drawerFlowViewer} size="1200px">
<DrawerContent title="Flow {flowPath}" on:close={drawerFlowViewer.closeDrawer}>
<FlowPathViewer path={flowPath ?? ''} />
</DrawerContent>
</Drawer>
async function fork(runnable: Runnable) {
if (componentInput?.type == 'runnable') {
componentInput.runnable = runnable
}
}
</script>
{#if transformer}
{#if componentInput?.type == 'runnable' && componentInput.transformer}
@@ -147,97 +73,20 @@
componentInput?.runnable?.type === 'runnableByName'
) {
componentInput.runnable.inlineScript = e.detail
componentInput.autoRefresh = true
componentInput.recomputeOnInputChanged = true
$app = $app
}
}}
/>
{/if}
{:else if componentInput?.runnable?.type === 'runnableByPath' && componentInput?.runnable?.path}
<div class="p-2 h-full flex flex-col gap-2">
{#if componentInput.runnable.runType == 'script' || componentInput.runnable.runType == 'hubscript'}
<div class="flex flex-row">
<Button
size="xs"
variant="border"
startIcon={{ icon: faCodeBranch }}
on:click={() => {
if (
componentInput &&
componentInput.type == 'runnable' &&
componentInput.runnable?.type === 'runnableByPath'
) {
fork(componentInput.runnable.path)
$app = $app
}
}}
>
Fork
</Button>
</div>
{/if}
<div class="w-full">
{#key $stateId}
{#if componentInput.runnable.runType == 'script' || componentInput.runnable.runType == 'hubscript'}
<FlowModuleScript path={componentInput.runnable.path} />
{:else if componentInput.runnable.runType == 'flow'}
<div class="pb-2 flex gap-2 w-full flex-row-reverse">
<Button
variant="border"
size="xs"
startIcon={{ icon: faRefresh }}
on:click={() => {
if (
componentInput?.type == 'runnable' &&
componentInput.runnable?.type === 'runnableByPath'
) {
sendUserToast('Refreshing inputs')
if (componentInput.runnable.runType == 'script') {
refreshScript(componentInput.runnable)
} else if (componentInput.runnable.runType == 'flow') {
refreshFlow(componentInput.runnable)
}
$stateId = $stateId + 1
}
}}
>
Refresh
</Button>
<Button
variant="border"
size="xs"
startIcon={{ icon: faEye }}
on:click={() => {
flowPath = componentInput?.['runnable']?.path
drawerFlowViewer.openDrawer()
}}
>
Expand
</Button>
<Button
variant="border"
size="xs"
startIcon={{ icon: faPen }}
endIcon={{ icon: faExternalLinkAlt }}
target="_blank"
href="/flows/edit/{componentInput?.['runnable']?.path}?nodraft=true">Edit</Button
>
<Button
variant="border"
size="xs"
startIcon={{ icon: faEye }}
endIcon={{ icon: faExternalLinkAlt }}
target="_blank"
href="/flows/get/{componentInput?.['runnable']?.path}?workspace={$workspaceStore}"
>
Details page
</Button>
</div>
<FlowPathViewer path={componentInput.runnable.path} />
{:else}
Unrecognized runType {componentInput.runnable.runType}
{/if}
{/key}
</div>
</div>
<InlineScriptRunnableByPath
on:fork={(e) => fork(e.detail)}
bind:runnable={componentInput.runnable}
bind:fields={componentInput.fields}
on:delete
{id}
/>
{/if}
{/if}
@@ -0,0 +1,59 @@
<script lang="ts">
import type { HiddenRunnable } from '../../types'
import InlineScriptEditor from './InlineScriptEditor.svelte'
import EmptyInlineScript from './EmptyInlineScript.svelte'
import InlineScriptRunnableByPath from './InlineScriptRunnableByPath.svelte'
import type { Runnable, StaticAppInput } from '../../inputType'
export let runnable: HiddenRunnable
export let id: string
async function fork(nrunnable: Runnable) {
runnable = { ...runnable, ...nrunnable, autoRefresh: true, recomputeOnInputChanged: true }
}
function onPick(o: { runnable: Runnable; fields: Record<string, StaticAppInput> }) {
runnable = {
...runnable,
...o.runnable,
fields: o.fields,
autoRefresh: true,
recomputeOnInputChanged: true
}
}
</script>
{#if runnable?.type === 'runnableByName' && runnable.inlineScript}
<InlineScriptEditor
{id}
bind:inlineScript={runnable.inlineScript}
bind:name={runnable.name}
bind:fields={runnable.fields}
syncFields
on:delete
/>
{:else if runnable?.type == 'runnableByPath'}
<InlineScriptRunnableByPath
bind:runnable
bind:fields={runnable.fields}
on:fork={(e) => fork(e.detail)}
on:delete
{id}
/>
{:else}
<EmptyInlineScript
on:pick={(e) => onPick(e.detail)}
name={runnable.name}
on:delete
showScriptPicker
on:new={(e) => {
runnable = {
type: 'runnableByName',
inlineScript: e.detail,
name: runnable.name,
fields: {},
autoRefresh: true,
recomputeOnInputChanged: true
}
}}
/>
{/if}
@@ -0,0 +1,174 @@
<script lang="ts">
import { Button, Drawer, DrawerContent } from '$lib/components/common'
import FlowModuleScript from '$lib/components/flows/content/FlowModuleScript.svelte'
import FlowPathViewer from '$lib/components/flows/content/FlowPathViewer.svelte'
import { emptySchema, getScriptByPath, sendUserToast } from '$lib/utils'
import { getContext } from 'svelte'
import type {
ConnectedAppInput,
RowAppInput,
RunnableByPath,
StaticAppInput,
UserAppInput
} from '../../inputType'
import {
faCodeBranch,
faExternalLinkAlt,
faEye,
faPen,
faRefresh,
faTrashAlt
} from '@fortawesome/free-solid-svg-icons'
import type { AppViewerContext } from '../../types'
import { workspaceStore } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import { deepEqual } from 'fast-equals'
import { computeFields } from './utils'
import { loadSchema } from '../../utils'
import { inferArgs } from '$lib/infer'
import RunButton from './RunButton.svelte'
export let runnable: RunnableByPath
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
export let id: string
const { stateId } = getContext<AppViewerContext>('AppViewerContext')
let drawerFlowViewer: Drawer
let flowPath: string = ''
const dispatch = createEventDispatcher()
async function refreshScript(x: RunnableByPath) {
let { schema } = await getScriptByPath(x.path)
if (!deepEqual(x.schema, schema)) {
x.schema = schema
fields = computeFields(schema, false, fields)
}
}
async function refreshFlow(x: RunnableByPath) {
const schema = (await loadSchema($workspaceStore ?? '', x.path, 'flow')) ?? emptySchema()
if (!deepEqual(x.schema, schema)) {
x.schema = schema
fields = computeFields(schema, false, fields)
}
}
async function fork(path: string) {
let { content, language, schema } = await getScriptByPath(path)
if (!schema || typeof schema == 'string' || Object.keys(schema).length == 0) {
schema = emptySchema()
await inferArgs(language, content, schema)
}
dispatch('fork', {
type: 'runnableByName',
name: path,
inlineScript: {
content,
language,
schema,
path
}
})
}
function refresh() {
if (runnable.runType == 'script') {
refreshScript(runnable)
} else if (runnable.runType == 'flow') {
refreshFlow(runnable)
}
}
$: runnable.runType && refresh()
</script>
<Drawer bind:this={drawerFlowViewer} size="1200px">
<DrawerContent title="Flow {flowPath}" on:close={drawerFlowViewer.closeDrawer}>
<FlowPathViewer path={flowPath ?? ''} />
</DrawerContent>
</Drawer>
<div class="p-2 h-full flex flex-col gap-2">
<div class="flex flex-row-reverse w-full gap-2">
<RunButton hideShortcut {id} />
<Button
size="xs"
variant="border"
startIcon={{ icon: faCodeBranch }}
on:click={() => {
fork(runnable.path)
}}
>
Fork
</Button>
<Button
variant="border"
size="xs"
startIcon={{ icon: faRefresh }}
on:click={() => {
sendUserToast('Refreshing inputs')
refresh()
$stateId = $stateId + 1
}}
>
Refresh
</Button>
<Button
size="xs"
variant="border"
color="red"
startIcon={{ icon: faTrashAlt }}
on:click={() => {
dispatch('delete')
}}
>
Clear
</Button>
{#if runnable.runType == 'flow'}
<Button
variant="border"
size="xs"
startIcon={{ icon: faEye }}
on:click={() => {
flowPath = runnable.path
drawerFlowViewer.openDrawer()
}}
>
Expand
</Button>
<Button
variant="border"
size="xs"
startIcon={{ icon: faPen }}
endIcon={{ icon: faExternalLinkAlt }}
target="_blank"
href="/flows/edit/{runnable.path}?nodraft=true">Edit</Button
>
<Button
variant="border"
size="xs"
startIcon={{ icon: faEye }}
endIcon={{ icon: faExternalLinkAlt }}
target="_blank"
href="/flows/get/{runnable.path}?workspace={$workspaceStore}"
>
Details page
</Button>
{/if}
</div>
<div class="w-full">
{#key $stateId}
{#if runnable.runType == 'script' || runnable.runType == 'hubscript'}
<div class="border">
<FlowModuleScript path={runnable.path} />
</div>
{:else if runnable.runType == 'flow'}
<FlowPathViewer path={runnable.path} />
{:else}
Unrecognized runType {runnable.runType}
{/if}
{/key}
</div>
</div>
@@ -5,9 +5,9 @@
import { Pane, Splitpanes } from 'svelte-splitpanes'
import InlineScriptsPanelList from './InlineScriptsPanelList.svelte'
import InlineScriptEditor from './InlineScriptEditor.svelte'
import EmptyInlineScript from './EmptyInlineScript.svelte'
import InlineScriptsPanelWithTable from './InlineScriptsPanelWithTable.svelte'
import { findGridItem } from '../appUtils'
import InlineScriptHiddenRunnable from './InlineScriptHiddenRunnable.svelte'
const { app, runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
const { selectedComponentInEditor } = getContext<AppEditorContext>('AppEditorContext')
@@ -18,12 +18,17 @@
$app.hiddenInlineScripts.splice(index, 1)
$app.hiddenInlineScripts = [...$app.hiddenInlineScripts]
} else {
$app.hiddenInlineScripts[index].hidden = true
$app.hiddenInlineScripts[index].inlineScript = undefined
$app.hiddenInlineScripts[index].name = `Background Script ${index}`
$app.hiddenInlineScripts[index] = {
hidden: true,
inlineScript: undefined,
name: `Background Script ${index}`,
fields: {},
type: 'runnableByName'
}
$app.hiddenInlineScripts = $app.hiddenInlineScripts
}
$selectedComponentInEditor = undefined
delete $runnableComponents[`bg_${index}`]
}
@@ -70,27 +75,12 @@
{/key}
{:else if hiddenInlineScript > -1}
{#key hiddenInlineScript}
{#if $app.hiddenInlineScripts?.[hiddenInlineScript]?.inlineScript}
<InlineScriptEditor
{#if $app.hiddenInlineScripts?.[hiddenInlineScript]}
<InlineScriptHiddenRunnable
on:delete={() => deleteBackgroundScript(hiddenInlineScript)}
id={`bg_${hiddenInlineScript}`}
bind:inlineScript={$app.hiddenInlineScripts[hiddenInlineScript].inlineScript}
bind:name={$app.hiddenInlineScripts[hiddenInlineScript].name}
bind:fields={$app.hiddenInlineScripts[hiddenInlineScript].fields}
syncFields
on:delete={() => deleteBackgroundScript(hiddenInlineScript)}
/>
{:else}
<EmptyInlineScript
name={$app.hiddenInlineScripts[hiddenInlineScript].name}
on:delete={() => deleteBackgroundScript(hiddenInlineScript)}
on:new={(e) => {
if ($app.hiddenInlineScripts[hiddenInlineScript]) {
$app.hiddenInlineScripts[hiddenInlineScript].inlineScript = e.detail
}
}}
/>
{/if}
{/key}
bind:runnable={$app.hiddenInlineScripts[hiddenInlineScript]}
/>{/if}{/key}
{:else}
<div class="text-sm text-gray-500 text-center py-8 px-2">
No script found at id {$selectedComponentInEditor}
@@ -23,11 +23,15 @@
$: runnables = getAppScripts($app.grid, $app.subgrids)
// When selected component changes, update selectedScriptComponentId
$: if (
$selectedComponent != $selectedComponentInEditor &&
!$selectedComponentInEditor?.includes('_transformer')
) {
$selectedComponentInEditor = $selectedComponent?.[0]
$: $selectedComponent && handleSelectedComponent()
function handleSelectedComponent() {
if (
$selectedComponent != $selectedComponentInEditor &&
!$selectedComponentInEditor?.includes('_transformer')
) {
$selectedComponentInEditor = $selectedComponent?.[0]
}
}
function createBackgroundScript() {
@@ -57,7 +61,7 @@
name: newScriptPath,
inlineScript: undefined,
autoRefresh: true,
type: 'runnableByName',
fields: {}
})
$app.hiddenInlineScripts = $app.hiddenInlineScripts
@@ -0,0 +1,59 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppViewerContext, CancelablePromise, InlineScript } from '../../types'
import { Button, Kbd } from '$lib/components/common'
import { getModifierKey } from '$lib/utils'
import { Loader2 } from 'lucide-svelte'
export let id: string
export let inlineScript: InlineScript | undefined = undefined
export let runLoading = false
export let hideShortcut = false
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
let cancelable: CancelablePromise<void> | undefined = undefined
</script>
{#if $runnableComponents[id] != undefined}
{#if !runLoading}
<Button
loading={runLoading}
size="xs"
color="dark"
variant="border"
btnClasses="!px-2 !py-1 !bg-gray-700 !text-white hover:!bg-gray-900"
on:click={async () => {
runLoading = true
try {
cancelable = $runnableComponents[id]?.cb?.(inlineScript)
await cancelable
} catch {}
runLoading = false
}}
>
<div class="flex flex-row gap-1 items-center">
Run
{#if !hideShortcut}
<div class="flex flex-row items-center">
<Kbd small>{getModifierKey()}</Kbd>
<Kbd small><span class="text-lg font-bold">⏎</span></Kbd>
</div>
{/if}
</div>
</Button>
{:else}
<Button
size="xs"
color="red"
variant="border"
btnClasses="!px-2 !py-1.5"
on:click={async () => {
cancelable?.cancel()
runLoading = false
}}
>
<Loader2 size={14} class="animate-spin mr-2" />
Cancel
</Button>
{/if}
{/if}
@@ -6,18 +6,20 @@
export let id: string
export let inputSpecs: RichConfigurations
export let inputSpecsConfiguration: RichConfigurations = inputSpecs
export let inputSpecsConfiguration: RichConfigurations | undefined = undefined
export let userInputEnabled: boolean = false
export let shouldCapitalize: boolean = true
export let rowColumns = false
export let resourceOnly = false
export let displayType = false
$: finalInputSpecsConfiguration = inputSpecsConfiguration ?? inputSpecs
</script>
{#if inputSpecs}
<div class="w-full flex flex-col gap-4">
{#each Object.keys(inputSpecsConfiguration) as k}
{#if inputSpecsConfiguration[k]?.type == 'oneOf'}
{#each Object.keys(finalInputSpecsConfiguration) as k}
{#if finalInputSpecsConfiguration[k]?.type == 'oneOf'}
<OneOfInputSpecsEditor
key={k}
bind:oneOf={inputSpecs[k]}
@@ -25,12 +27,12 @@
{shouldCapitalize}
{resourceOnly}
{rowColumns}
inputSpecsConfiguration={inputSpecsConfiguration?.[k]?.['configuration']}
labels={inputSpecsConfiguration?.[k]?.['labels']}
tooltip={inputSpecsConfiguration?.[k]?.['tooltip']}
inputSpecsConfiguration={finalInputSpecsConfiguration?.[k]?.['configuration']}
labels={finalInputSpecsConfiguration?.[k]?.['labels']}
tooltip={finalInputSpecsConfiguration?.[k]?.['tooltip']}
/>
{:else}
{@const meta = inputSpecsConfiguration?.[k]}
{@const meta = finalInputSpecsConfiguration?.[k]}
<InputsSpecEditor
key={k}
bind:componentInput={inputSpecs[k]}
@@ -49,12 +49,16 @@
function getActions(hasScript: boolean): ActionType[] {
if (hasScript) {
return [
{
label: 'Detach',
icon: ExternalLink,
color: 'light',
callback: detach
},
...(appInput.runnable?.type === 'runnableByName' && appInput.runnable.inlineScript
? ([
{
label: 'Detach',
icon: ExternalLink,
color: 'light',
callback: detach
}
] as const)
: []),
{
label: 'Clear',
icon: X,
@@ -1,17 +1,36 @@
<script lang="ts">
import type { ResultAppInput } from '$lib/components/apps/inputType'
import type { ResultAppInput, Runnable, StaticAppInput } from '$lib/components/apps/inputType'
import { isScriptByNameDefined, isScriptByPathDefined } from '$lib/components/apps/utils'
import { getContext } from 'svelte'
import type { AppComponent } from '../../component'
import RunnableSelector from '../mainInput/RunnableSelector.svelte'
import SelectedRunnable from '../SelectedRunnable.svelte'
import type { AppEditorContext } from '$lib/components/apps/types'
export let appInput: ResultAppInput
export let defaultUserInput = false
export let appComponent: AppComponent
const { selectedComponentInEditor } = getContext<AppEditorContext>('AppEditorContext')
function onPick({
runnable,
fields
}: {
runnable: Runnable
fields: Record<string, StaticAppInput>
}) {
if (appInput.type === 'runnable') {
appInput = { ...appInput, runnable, fields }
$selectedComponentInEditor = appComponent.id
} else {
console.warn('Cannot pick runnable for non-runnable input')
}
}
</script>
{#if isScriptByPathDefined(appInput) || isScriptByNameDefined(appInput)}
<SelectedRunnable {appComponent} bind:appInput />
{:else if appInput !== undefined}
<RunnableSelector id={appComponent.id} {defaultUserInput} bind:appInput />
<RunnableSelector {defaultUserInput} on:pick={(e) => onPick(e.detail)} />
{/if}
@@ -4,27 +4,32 @@
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
import { Building, Globe2 } from 'lucide-svelte'
import InlineScriptList from './InlineScriptList.svelte'
import type { ResultAppInput } from '$lib/components/apps/inputType'
import type { Runnable, StaticAppInput } from '$lib/components/apps/inputType'
import WorkspaceScriptList from './WorkspaceScriptList.svelte'
import WorkspaceFlowList from './WorkspaceFlowList.svelte'
import type { AppEditorContext, AppViewerContext } from '$lib/components/apps/types'
import { getContext } from 'svelte'
import type { AppViewerContext } from '$lib/components/apps/types'
import { createEventDispatcher, getContext } from 'svelte'
import type { Schema } from '$lib/common'
import { getAllScriptNames, loadSchema, schemaToInputsSpec } from '$lib/components/apps/utils'
import { emptySchema } from '$lib/utils'
type Tab = 'hubscripts' | 'workspacescripts' | 'workspaceflows' | 'inlinescripts'
export let appInput: ResultAppInput
export let defaultUserInput = false
export let id: string
export let hideCreateScript = false
let tab: Tab = 'inlinescripts'
let filter: string = ''
let picker: Drawer
const { app, workspace } = getContext<AppViewerContext>('AppViewerContext')
const { selectedComponentInEditor } = getContext<AppEditorContext>('AppEditorContext')
const dispatch = createEventDispatcher<{
pick: {
runnable: Runnable
fields: Record<string, StaticAppInput>
}
}>()
async function loadSchemaFromTriggerable(
path: string,
@@ -34,48 +39,49 @@
}
async function pickScript(path: string) {
if (appInput.type === 'runnable') {
const schema = await loadSchemaFromTriggerable(path, 'script')
const fields = schemaToInputsSpec(schema, defaultUserInput)
appInput.runnable = {
type: 'runnableByPath',
path,
runType: 'script',
schema
}
appInput.fields = fields
$selectedComponentInEditor = id
}
const schema = await loadSchemaFromTriggerable(path, 'script')
const fields = schemaToInputsSpec(schema, defaultUserInput)
const runnable = {
type: 'runnableByPath',
path,
runType: 'script',
schema
} as const
dispatch('pick', {
runnable,
fields
})
}
async function pickFlow(path: string) {
if (appInput.type === 'runnable') {
const schema = await loadSchemaFromTriggerable(path, 'flow')
const fields = schemaToInputsSpec(schema, defaultUserInput)
appInput.runnable = {
type: 'runnableByPath',
path,
runType: 'flow',
schema
}
appInput.fields = fields
$selectedComponentInEditor = id
}
const schema = await loadSchemaFromTriggerable(path, 'flow')
const fields = schemaToInputsSpec(schema, defaultUserInput)
const runnable = {
type: 'runnableByPath',
path,
runType: 'flow',
schema
} as const
dispatch('pick', {
runnable,
fields
})
}
async function pickHubScript(path: string) {
if (appInput.type === 'runnable') {
const schema = await loadSchemaFromTriggerable(path, 'hubscript')
const fields = schemaToInputsSpec(schema, defaultUserInput)
appInput.runnable = {
type: 'runnableByPath',
path,
runType: 'hubscript',
schema
}
appInput.fields = fields
$selectedComponentInEditor = id
}
const schema = await loadSchemaFromTriggerable(path, 'hubscript')
const fields = schemaToInputsSpec(schema, defaultUserInput)
const runnable = {
type: 'runnableByPath',
path,
runType: 'hubscript',
schema
} as const
dispatch('pick', {
runnable,
fields
})
}
function pickInlineScript(name: string) {
@@ -83,20 +89,20 @@
(script) => script.name === name
)
const unusedInlineScript = $app.unusedInlineScripts?.[unusedInlineScriptIndex]
if (appInput.type === 'runnable' && unusedInlineScript?.inlineScript) {
appInput.runnable = {
dispatch('pick', {
runnable: {
type: 'runnableByName',
name,
inlineScript: unusedInlineScript.inlineScript
}
},
fields: {}
})
$app.unusedInlineScripts.splice(unusedInlineScriptIndex, 1)
}
$selectedComponentInEditor = id
$app = $app
$app.unusedInlineScripts.splice(unusedInlineScriptIndex, 1)
$app.unusedInlineScripts = $app.unusedInlineScripts
}
function createScript(): string {
function createScript() {
let index = 0
let newScriptPath = `Inline Script ${index}`
@@ -107,16 +113,14 @@
newScriptPath = `Inline Script ${++index}`
}
appInput.runnable = {
type: 'runnableByName',
name: newScriptPath,
inlineScript: undefined
}
appInput = appInput
$selectedComponentInEditor = id
return newScriptPath
dispatch('pick', {
runnable: {
type: 'runnableByName',
name: newScriptPath,
inlineScript: undefined
},
fields: {}
})
}
</script>
@@ -175,16 +179,18 @@
</Drawer>
<div class="flex flex-col gap-2">
<Button
on:click={createScript}
size="xs"
color="light"
variant="border"
startIcon={{ icon: faPlus }}
btnClasses="truncate w-full"
>
Create an inline script
</Button>
{#if !hideCreateScript}
<Button
on:click={createScript}
size="xs"
color="light"
variant="border"
startIcon={{ icon: faPlus }}
btnClasses="truncate w-full"
>
Create an inline script
</Button>
{/if}
<Button
on:click={() => picker?.openDrawer()}
size="xs"
@@ -1,18 +1,18 @@
<script lang="ts">
import ScriptSettingHeader from './shared/ScriptSettingHeader.svelte'
import type { AppViewerContext, HiddenInlineScript } from '$lib/components/apps/types'
import type { AppViewerContext, HiddenRunnable } from '$lib/components/apps/types'
import ScriptRunConfiguration from './shared/ScriptRunConfiguration.svelte'
import BackgroundScriptTriggerBy from './shared/BackgroundScriptTriggerBy.svelte'
import { getContext } from 'svelte'
import ScriptSettingsSection from './shared/ScriptSettingsSection.svelte'
export let script: HiddenInlineScript
export let runnable: HiddenRunnable
export let id: string
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
const { runnableComponents, app } = getContext<AppViewerContext>('AppViewerContext')
function updateAutoRefresh() {
const autoRefresh = script.autoRefresh
const autoRefresh = runnable.autoRefresh
if ($runnableComponents?.[id]?.autoRefresh !== autoRefresh && autoRefresh !== undefined) {
$runnableComponents[id] = {
...$runnableComponents[id],
@@ -20,21 +20,23 @@
}
}
}
$: runnable && ($app = $app)
</script>
<div class={'border-y border-gray-200 divide-y'}>
<ScriptSettingHeader name={script.name} />
{#if script.inlineScript}
<ScriptSettingHeader name={runnable.name} />
{#if runnable.type == 'runnableByPath' || runnable.inlineScript}
<ScriptRunConfiguration
bind:autoRefresh={script.autoRefresh}
bind:recomputeOnInputChanged={script.recomputeOnInputChanged}
canConfigureRecomputeOnInputChanged={script.inlineScript?.language !== 'frontend'}
bind:autoRefresh={runnable.autoRefresh}
bind:recomputeOnInputChanged={runnable.recomputeOnInputChanged}
canConfigureRecomputeOnInputChanged={runnable.type == 'runnableByPath' ||
runnable.inlineScript?.language !== 'frontend'}
on:updateAutoRefresh={updateAutoRefresh}
/>
<BackgroundScriptTriggerBy
bind:script
recomputeOnInputChanged={script.recomputeOnInputChanged}
bind:script={runnable}
recomputeOnInputChanged={runnable.recomputeOnInputChanged}
/>
{:else}
<ScriptSettingsSection title="Language selection">
@@ -1,17 +1,17 @@
<script lang="ts">
import type { HiddenInlineScript } from '$lib/components/apps/types'
import type { HiddenRunnable } from '$lib/components/apps/types'
import { getDependencies } from '../utils'
import ScriptTriggers from './ScriptTriggers.svelte'
export let script: HiddenInlineScript
export let script: HiddenRunnable
export let recomputeOnInputChanged: boolean | undefined = undefined
$: isFrontend = script.inlineScript?.language === 'frontend'
$: isFrontend = script.type == 'runnableByName' && script.inlineScript?.language === 'frontend'
$: triggerEvents = script.autoRefresh ? ['start', 'refresh'] : []
</script>
{#if script.inlineScript}
{#if script.type == 'runnableByName' && script.inlineScript}
<ScriptTriggers
bind:inlineScript={script.inlineScript}
{triggerEvents}
@@ -19,4 +19,11 @@
{isFrontend}
shoudlDisplayChangeEvents={recomputeOnInputChanged || isFrontend}
/>
{:else if script.type === 'runnableByName'}
<ScriptTriggers
dependencies={getDependencies(script.fields)}
{triggerEvents}
{isFrontend}
shoudlDisplayChangeEvents={recomputeOnInputChanged}
/>
{/if}
+7 -4
View File
@@ -12,6 +12,7 @@ import type {
InputConnection,
ResultAppInput,
RowAppInput,
Runnable,
StaticAppInput,
UploadAppInput,
UserAppInput
@@ -103,9 +104,10 @@ export type InlineScript = {
export type AppCssItemName = 'viewer' | 'grid' | AppComponent['type']
export type HiddenInlineScript = {
export type HiddenRunnable = {
name: string
inlineScript: InlineScript | undefined
// inlineScript?: InlineScript | undefined
// type?: 'runnableByName' | 'runnableByPath'
fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
autoRefresh?: boolean
//deprecated and to be removed after migration
@@ -113,7 +115,7 @@ export type HiddenInlineScript = {
recomputeOnInputChanged?: boolean
noBackendValue?: any
hidden?: boolean
}
} & Runnable
export type App = {
grid: GridItem[]
@@ -123,7 +125,8 @@ export type App = {
name: string
inlineScript: InlineScript
}>
hiddenInlineScripts: Array<HiddenInlineScript>
//TODO: should be called hidden runnables but migration tbd
hiddenInlineScripts: Array<HiddenRunnable>
css?: Partial<Record<AppCssItemName, Record<string, ComponentCssProperty>>>
subgrids?: Record<string, GridItem[]>
}
+13
View File
@@ -14,6 +14,19 @@ import type {
VerticalAlignment
} from './types'
export function migrateApp(app: App) {
app.hiddenInlineScripts.forEach((x) => {
if (x.type == undefined) {
//@ts-ignore
x.type = 'runnableByName'
}
//TODO: remove after migration is done
if (x.doNotRecomputeOnInputChanged != undefined) {
x.recomputeOnInputChanged = x.doNotRecomputeOnInputChanged
x.doNotRecomputeOnInputChanged = undefined
}
})
}
export function allItems(
grid: GridItem[],
subgrids: Record<string, GridItem[]> | undefined