mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
460 lines
12 KiB
Svelte
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>
|