Files
windmill/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte
T

460 lines
12 KiB
Svelte

<script lang="ts">
import { createBubbler, stopPropagation } from 'svelte/legacy'
const bubble = createBubbler()
import { Button, Drawer, DrawerContent } from '$lib/components/common'
import { base } from '$lib/base'
import FlowGraphViewer from '$lib/components/FlowGraphViewer.svelte'
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
import FlowModuleScript from '$lib/components/flows/content/FlowModuleScript.svelte'
import FlowPathViewer from '$lib/components/flows/content/FlowPathViewer.svelte'
import { emptySchema, getHubFlowIdFromPath, isHubFlowPath, sendUserToast } from '$lib/utils'
import { getContext, tick, untrack } from 'svelte'
import type {
ConnectedAppInput,
RowAppInput,
RunnableByPath,
StaticAppInput,
UserAppInput,
CtxAppInput
} from '../../inputType'
import type { AppViewerContext } from '../../types'
import { createEventDispatcher } from 'svelte'
import { deepEqual } from 'fast-equals'
import { computeFields } from './utils'
import { inferArgs, loadSchema } from '$lib/infer'
import AppRunButton from './AppRunButton.svelte'
import { getScriptByPath } from '$lib/scripts'
import { autoPlacement } from '@floating-ui/core'
import { ExternalLink, Eye, GitFork, Pen, RefreshCw, Trash } from 'lucide-svelte'
import { get } from 'svelte/store'
import RunButton from '$lib/components/RunButton.svelte'
import Popover from '$lib/components/meltComponents/Popover.svelte'
import ScriptEditorDrawer from '$lib/components/flows/content/ScriptEditorDrawer.svelte'
import FlowEditorDrawer from '$lib/components/flows/content/FlowEditorDrawer.svelte'
import { FlowService, ScriptService, type OpenFlow } from '$lib/gen'
import { replaceScriptPlaceholderWithItsValues } from '$lib/hub'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
runnable: RunnableByPath
fields:
| Record<
string,
StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput | CtxAppInput
>
| undefined
id: string
rawApps?: boolean
isLoading?: boolean
onRun?: any
onCancel?: any
hubFlowPreview?: OpenFlow | undefined
}
let {
runnable = $bindable(),
fields = $bindable(),
id,
rawApps = false,
isLoading = false,
onRun = async () => {},
onCancel = async () => {},
hubFlowPreview = $bindable(undefined)
}: Props = $props()
const viewerContext = getContext<AppViewerContext>('AppViewerContext')
let drawerFlowViewer: Drawer | undefined = $state(undefined)
let flowPath: string = $state('')
let drawerShowsHubFlow = $state(false)
let notFound = $state(false)
let hubFlowId = $derived(getHubFlowIdFromPath(runnable.path))
// Key to force re-mounting of viewer components (bypasses FlowModuleScript cache)
let refreshKey = $state(0)
let scriptEditorDrawer: ScriptEditorDrawer | undefined = $state(undefined)
let flowEditorDrawer: FlowEditorDrawer | undefined = $state(undefined)
const dispatch = createEventDispatcher()
async function refreshScript(runnable: RunnableByPath) {
hubFlowPreview = undefined
try {
const loaded = await getScriptByPath(runnable.path)
const schema = loaded.schema ?? emptySchema()
if (!deepEqual(runnable.schema, schema)) {
runnable.schema = schema
if (!runnable.schema.order) {
runnable.schema.order = Object.keys(runnable.schema.properties ?? {})
}
fields = computeFields(schema, false, fields ?? {})
}
} catch (e) {
notFound = true
console.error(e)
}
}
async function refreshFlow(runnable: RunnableByPath) {
hubFlowPreview = undefined
try {
const hubFlowId = getHubFlowIdFromPath(runnable.path)
if (hubFlowId !== undefined) {
const hub = await FlowService.getHubFlowById({ id: hubFlowId })
const flow = hub.flow ? structuredClone(hub.flow) : undefined
if (flow?.value.preprocessor_module?.value.type === 'rawscript') {
flow.value.preprocessor_module.value.content = replaceScriptPlaceholderWithItsValues(
String(hubFlowId),
flow.value.preprocessor_module.value.content
)
}
if (!flow) {
notFound = true
return
}
hubFlowPreview = flow
const schema =
flow.schema && typeof flow.schema === 'object' && Object.keys(flow.schema).length > 0
? (flow.schema as any)
: emptySchema()
if (!deepEqual(runnable.schema, schema)) {
runnable.schema = schema
if (!runnable.schema.order) {
runnable.schema.order = Object.keys(runnable.schema.properties ?? {})
}
fields = computeFields(schema, false, fields ?? {})
}
return
}
const loaded = await loadSchema($operatingWorkspace ?? '', runnable.path, 'flow')
const schema = loaded?.schema ?? emptySchema()
if (!deepEqual(runnable.schema, schema)) {
runnable.schema = schema
if (!runnable.schema.order) {
runnable.schema.order = Object.keys(runnable.schema.properties ?? {})
}
fields = computeFields(schema, false, fields ?? {})
}
} catch (e) {
notFound = true
console.error(e)
}
}
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: 'inline',
name: path,
inlineScript: {
content,
language,
schema,
path
}
})
}
async function openScriptEditor(path: string) {
try {
const script = await ScriptService.getScriptByPath({
workspace: $operatingWorkspace!,
path
})
scriptEditorDrawer?.openDrawer(script.hash, () => {
// Increment refreshKey to force re-mounting of FlowModuleScript (bypasses cache)
refreshKey++
// Refresh the schema
lastRunnable = undefined
refresh(runnable)
})
} catch (e) {
sendUserToast(`Failed to load script: ${e}`, true)
}
}
function openFlowEditor(path: string) {
flowEditorDrawer?.openDrawer(path, () => {
// Increment refreshKey to force re-mounting of FlowPathViewer (bypasses cache)
refreshKey++
// Refresh the schema
lastRunnable = undefined
refresh(runnable)
})
}
let lastRunnable: RunnableByPath | undefined = undefined
function refresh(runnable) {
if (deepEqual(runnable, lastRunnable)) {
return
}
notFound = false
if (runnable.runType == 'script') {
refreshScript(runnable)
} else if (runnable.runType == 'flow') {
refreshFlow(runnable)
} else {
hubFlowPreview = undefined
}
lastRunnable = runnable
}
$effect(() => {
runnable.path
untrack(() => {
refresh(runnable)
})
})
</script>
<Drawer bind:this={drawerFlowViewer} size="1200px">
<DrawerContent
title="Flow {flowPath}"
on:close={() => {
flowPath = ''
drawerShowsHubFlow = false
drawerFlowViewer?.closeDrawer()
}}
>
{#if drawerShowsHubFlow}
<div class="flex flex-col flex-1 h-full min-h-0 overflow-auto">
{#if hubFlowPreview}
<FlowGraphViewer
triggerNode
provideTriggerContext
fillAvailableHeight
flow={{ ...hubFlowPreview, path: flowPath }}
/>
{:else if notFound}
<div class="p-4 text-red-400">Hub flow not found at {flowPath}</div>
{:else}
<div class="p-4">
<Skeleton layout={[[40]]} />
</div>
{/if}
</div>
{:else if flowPath}
<FlowPathViewer path={flowPath} fillAvailableHeight />
{/if}
</DrawerContent>
</Drawer>
<ScriptEditorDrawer
bind:this={scriptEditorDrawer}
on:save={() => {
// Increment refreshKey to force re-mounting of FlowModuleScript (bypasses cache)
refreshKey++
// Refresh the schema
lastRunnable = undefined
refresh(runnable)
}}
/>
<FlowEditorDrawer
bind:this={flowEditorDrawer}
on:save={() => {
// Increment refreshKey to force re-mounting of FlowPathViewer (bypasses cache)
refreshKey++
// Refresh the schema
lastRunnable = undefined
refresh(runnable)
}}
/>
<div class="p-2 h-full flex flex-col gap-2">
<div class="flex flex-row-reverse w-full gap-2">
{#if !rawApps}
<AppRunButton hideShortcut {id} />
{:else}
<RunButton {isLoading} {onRun} {onCancel} />
{/if}
<Button
variant="default"
size="xs"
startIcon={{ icon: RefreshCw }}
on:click={async () => {
sendUserToast('Getting latest runnable version at that path')
// Increment refreshKey to force re-mounting of viewer components (bypasses cache)
refreshKey++
lastRunnable = undefined
refresh(runnable)
if (viewerContext) {
viewerContext.stateId.update((x) => x + 1)
}
await tick()
}}
/>
<Button
size="xs"
variant="default"
startIcon={{ icon: Trash }}
on:click={() => {
dispatch('delete')
}}
>
Clear
</Button>
{#if runnable.runType == 'flow'}
<Button
variant="default"
size="xs"
startIcon={{ icon: Eye }}
on:click={() => {
flowPath = runnable.path
drawerShowsHubFlow = isHubFlowPath(runnable.path)
drawerFlowViewer?.openDrawer()
}}
>
Expand
</Button>
{#if hubFlowId}
<Button
variant="default"
size="xs"
startIcon={{ icon: GitFork }}
endIcon={{ icon: ExternalLink }}
target="_blank"
href="{base}/flows/add?hub={hubFlowId}"
>
Fork
</Button>
{:else}
<Button
variant="default"
size="xs"
startIcon={{ icon: Pen }}
on:click={() => {
openFlowEditor(runnable.path)
}}
>
Edit
</Button>
<Button
variant="default"
size="xs"
startIcon={{ icon: Eye }}
endIcon={{ icon: ExternalLink }}
target="_blank"
href="{base}/flows/get/{runnable.path}?workspace={$operatingWorkspace}"
>
Details
</Button>
{/if}
{:else}
<Button
size="xs"
variant="default"
startIcon={{ icon: Pen }}
on:click={() => {
openScriptEditor(runnable.path)
}}
>
Edit
</Button>
<Button
size="xs"
variant="default"
startIcon={{ icon: GitFork }}
on:click={() => {
fork(runnable.path)
}}
>
Fork
</Button>
{/if}
<Popover
floatingConfig={{
middleware: [
autoPlacement({
allowedPlacements: [
'bottom-start',
'bottom-end',
'top-start',
'top-end',
'top',
'bottom'
]
})
]
}}
closeButton
contentClasses="block text-primary text-xs p-4 w-[20vh]"
>
{#snippet trigger()}
<Button
nonCaptureEvent={true}
btnClasses={'bg-surface text-primay hover:bg-hover'}
variant="default"
size="xs"
>
Cache
</Button>
{/snippet}
{#snippet content()}
{#if runnable.runType == 'flow' && isHubFlowPath(runnable.path)}
Since this is a reference to a hub flow, cache settings are managed from the flow after
you fork it into your workspace.
{:else}
Since this is a reference to a workspace {runnable.runType}, set the cache in the
{runnable.runType} settings directly by editing it. The cache will be shared by any app or
flow that uses this {runnable.runType}.
{/if}
{/snippet}
</Popover>
<input
onkeydown={stopPropagation(bubble('keydown'))}
bind:value={runnable.name}
placeholder="Background runnable name"
class="!text-xs !rounded-xs"
/>
</div>
<div class="w-full grow min-h-0 overflow-y-auto">
{#key `${viewerContext?.stateId ? get(viewerContext.stateId) : 0}-${refreshKey}`}
{#if notFound}
<div class="text-red-400">
{#if runnable.runType == 'flow' && isHubFlowPath(runnable.path)}
Hub flow not found at {runnable.path}
{:else}
{runnable.runType} not found at {runnable.path} in workspace {$operatingWorkspace}
{/if}
</div>
{:else if runnable.runType == 'script' || runnable.runType == 'hubscript'}
<div class="border">
<FlowModuleScript path={runnable.path} />
</div>
{:else if runnable.runType == 'flow'}
{#if isHubFlowPath(runnable.path)}
{#if hubFlowPreview}
<div class="flex flex-col flex-1 h-full min-h-0 overflow-auto">
<FlowGraphViewer
triggerNode
provideTriggerContext
fillAvailableHeight
flow={{ ...hubFlowPreview, path: runnable.path }}
/>
</div>
{:else}
<Skeleton layout={[[40]]} />
{/if}
{:else}
<FlowPathViewer path={runnable.path} fillAvailableHeight />
{/if}
{:else}
Unrecognized runType {runnable.runType}
{/if}
{/key}
</div>
</div>