mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 16:02:28 +00:00
feat(frontend): run test flow from graph (#6122)
* migrate FlowPreviewContent to svelte 5 * run background preview from input panel * share local run test * Show approval in graph is testing in graph * use component and props instead of portal for approval in graph * Add a toggle to show module status in graph * open module result after each run * Fix module reactivity issue * Add test flow button * Extract preview run logic from flowPreviewContent * Revert "Extract preview run logic from flowPreviewContent" This reverts commita39c70a920. * nit * lazy load preview content * create component for flow preview button * open preview v0 * open preview v1 * connect open preview button * improve graph run display * enable cancel preview * Run test flow from input panel * nit * wip * Use global context instead of module context for moduleTestState * nit * fix flow preview rendering * Add testJob to modulesTest context * update module status based on individual test data * fix: clear job status on run preview * detatch run buttons from input node * move preview job in FlowEditorContext * move outputPickerOpenFns to FlowEditorContext * add result panel * Add result output picker * add status to loops and branch * add open detail button to result panel * fix test up to * clean unnecessary binding * clean * Make iteration annotation smaller in editmode * detatch test button to and aproval from node * prevent flow edition during execution * Prevent step test run during flow run * Show approval in graph edges * prevent opening output popover if node is outside the graph * fix pointerdownOutside action * fix test up to dropdown not closing * fix test up to * nit * change job status badge display * fix running status * Enable test flow in Dev * fix darkmode * fix node panel display in Dev * fix test flow button positionning * fix suspend in subflows * improve lazy load of preview * prevent preview data unmount on close drawer * clean code * move flowjob into flow context * Revert "move flowjob into flow context" This reverts commit939e9dbaaf. * clean context * nit * fix dark mode status view * fix test button alignment * clean job status on deleted step * fix retry bad status display * Detect flow change * Update frontend/src/lib/components/flows/header/FlowPreviewButtons.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
@@ -27,7 +27,7 @@
|
||||
import FlowModuleSchemaMap from './flows/map/FlowModuleSchemaMap.svelte'
|
||||
import FlowEditorPanel from './flows/content/FlowEditorPanel.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { writable } from 'svelte/store'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import type { FlowState } from './flows/flowState'
|
||||
import { initHistory } from '$lib/history'
|
||||
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
|
||||
@@ -49,6 +49,9 @@
|
||||
import type { PickableProperties } from './flows/previousResults'
|
||||
import { Triggers } from './triggers/triggers.svelte'
|
||||
import { TestSteps } from './flows/testSteps.svelte'
|
||||
import { ModulesTestStates } from './modulesTest.svelte'
|
||||
import type { DurationStatus, GraphModuleState } from './graph'
|
||||
import { updateDerivedModuleStatesFromTestJobs } from './flows/utils'
|
||||
|
||||
let flowCopilotContext: FlowCopilotContext = {
|
||||
shouldUpdatePropertyType: writable<{
|
||||
@@ -108,6 +111,12 @@
|
||||
let pastPreviews: CompletedJob[] = $state([])
|
||||
let validCode = $state(true)
|
||||
|
||||
// Flow preview
|
||||
let flowPreviewButtons: FlowPreviewButtons | undefined = $state()
|
||||
const job: Job | undefined = $derived(flowPreviewButtons?.getJob())
|
||||
let showJobStatus = $state(false)
|
||||
let testModuleId: string | undefined = $state(undefined)
|
||||
|
||||
type LastEditScript = {
|
||||
content: string
|
||||
path: string
|
||||
@@ -436,11 +445,17 @@
|
||||
const scriptEditorDrawer = writable(undefined)
|
||||
const moving = writable<{ id: string } | undefined>(undefined)
|
||||
const history = initHistory(flowStore.val)
|
||||
|
||||
const testSteps = new TestSteps()
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
const modulesTestStates = new ModulesTestStates((moduleId) => {
|
||||
// Update the derived store with test job states
|
||||
delete $derivedModuleStates[moduleId]
|
||||
testModuleId = moduleId
|
||||
showJobStatus = false
|
||||
})
|
||||
const outputPickerOpenFns: Record<string, () => void> = $state({})
|
||||
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
triggersCount: triggersCount,
|
||||
simplifiedPoll: writable(false),
|
||||
@@ -469,7 +484,9 @@
|
||||
editPanelSize: undefined,
|
||||
payloadData: undefined
|
||||
}),
|
||||
currentEditor: writable(undefined)
|
||||
currentEditor: writable(undefined),
|
||||
modulesTestStates,
|
||||
outputPickerOpenFns
|
||||
})
|
||||
setContext<PropPickerContext>('PropPickerContext', {
|
||||
flowPropPickerConfig: writable<FlowPropPickerConfig | undefined>(undefined),
|
||||
@@ -487,7 +504,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
let flowPreviewButtons: FlowPreviewButtons | undefined = $state()
|
||||
let reload = $state(0)
|
||||
|
||||
async function inferModuleArgs(selectedIdStore: string) {
|
||||
@@ -554,6 +570,69 @@
|
||||
$effect(() => {
|
||||
$selectedIdStore && untrack(() => inferModuleArgs($selectedIdStore))
|
||||
})
|
||||
|
||||
const localModuleStates: Writable<Record<string, GraphModuleState>> = $derived(
|
||||
flowPreviewButtons?.getLocalModuleStates() ?? writable({})
|
||||
)
|
||||
const localDurationStatuses: Writable<Record<string, DurationStatus>> = $derived(
|
||||
flowPreviewButtons?.getLocalDurationStatuses() ?? writable({})
|
||||
)
|
||||
const suspendStatus: Writable<Record<string, { job: Job; nb: number }>> = $derived(
|
||||
flowPreviewButtons?.getSuspendStatus() ?? writable({})
|
||||
)
|
||||
|
||||
// Create a derived store that only shows the module states when showModuleStatus is true
|
||||
// this store can also be updated
|
||||
let derivedModuleStates = writable<Record<string, GraphModuleState>>({})
|
||||
$effect(() => {
|
||||
derivedModuleStates.update((currentStates) => {
|
||||
return showJobStatus ? $localModuleStates : currentStates
|
||||
})
|
||||
})
|
||||
$effect(() => {
|
||||
updateDerivedModuleStatesFromTestJobs(testModuleId, modulesTestStates, derivedModuleStates)
|
||||
})
|
||||
|
||||
let flowModuleSchemaMap: FlowModuleSchemaMap | undefined = $state()
|
||||
function onJobDone() {
|
||||
if (!job) {
|
||||
return
|
||||
}
|
||||
// job was running and is now stopped
|
||||
if (!flowPreviewButtons?.getPreviewOpen()) {
|
||||
if (
|
||||
job.type === 'CompletedJob' &&
|
||||
job.success &&
|
||||
flowPreviewButtons?.getPreviewMode() === 'whole'
|
||||
) {
|
||||
if (flowModuleSchemaMap?.isNodeVisible('result') && $selectedIdStore !== 'Result') {
|
||||
outputPickerOpenFns['Result']?.()
|
||||
}
|
||||
} else {
|
||||
// Find last module with a job in flow_status
|
||||
const lastModuleWithJob = job.flow_status?.modules
|
||||
?.slice()
|
||||
.reverse()
|
||||
.find((module) => 'job' in module)
|
||||
if (
|
||||
lastModuleWithJob &&
|
||||
lastModuleWithJob.id &&
|
||||
flowModuleSchemaMap?.isNodeVisible(lastModuleWithJob.id)
|
||||
) {
|
||||
outputPickerOpenFns[lastModuleWithJob.id]?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetModulesStates() {
|
||||
derivedModuleStates.set({})
|
||||
showJobStatus = false
|
||||
}
|
||||
|
||||
const individualStepTests = $derived(
|
||||
!(showJobStatus && job) && Object.keys($derivedModuleStates).length > 0
|
||||
)
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
@@ -691,18 +770,40 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center pt-1 z-50 absolute right-2 top-2 gap-2">
|
||||
<FlowPreviewButtons bind:this={flowPreviewButtons} />
|
||||
<div class="flex justify-center pt-1 z-50 absolute -translate-x-[100%] right-2 top-2 gap-2">
|
||||
<FlowPreviewButtons
|
||||
bind:this={flowPreviewButtons}
|
||||
{onJobDone}
|
||||
onRunPreview={() => {
|
||||
localModuleStates.set({})
|
||||
showJobStatus = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Splitpanes horizontal class="h-full max-h-screen grow">
|
||||
<Pane size={67}>
|
||||
{#if flowStore.val?.value?.modules}
|
||||
<div id="flow-editor"></div>
|
||||
<FlowModuleSchemaMap
|
||||
bind:this={flowModuleSchemaMap}
|
||||
disableAi
|
||||
disableTutorials
|
||||
smallErrorHandler={true}
|
||||
disableStaticInputs
|
||||
localModuleStates={derivedModuleStates}
|
||||
onTestUpTo={flowPreviewButtons?.testUpTo}
|
||||
isOwner={flowPreviewButtons?.getIsOwner()}
|
||||
onTestFlow={flowPreviewButtons?.runPreview}
|
||||
isRunning={flowPreviewButtons?.getIsRunning()}
|
||||
onCancelTestFlow={flowPreviewButtons?.cancelTest}
|
||||
onOpenPreview={flowPreviewButtons?.openPreview}
|
||||
onHideJobStatus={resetModulesStates}
|
||||
{individualStepTests}
|
||||
flowJob={job}
|
||||
{showJobStatus}
|
||||
onDelete={(id) => {
|
||||
delete $derivedModuleStates[id]
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class="text-red-400 mt-20">Missing flow modules</div>
|
||||
@@ -722,6 +823,12 @@
|
||||
flowPreviewButtons?.openPreview()
|
||||
}
|
||||
}}
|
||||
onTestFlow={flowPreviewButtons?.runPreview}
|
||||
{job}
|
||||
isOwner={flowPreviewButtons?.getIsOwner()}
|
||||
{localDurationStatuses}
|
||||
{suspendStatus}
|
||||
onOpenDetails={flowPreviewButtons?.openPreview}
|
||||
/>
|
||||
{/key}
|
||||
</Pane>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
type OpenFlow,
|
||||
type InputTransform,
|
||||
type TriggersCount,
|
||||
CaptureService
|
||||
CaptureService,
|
||||
type Job
|
||||
} from '$lib/gen'
|
||||
import { initHistory, redo, undo } from '$lib/history'
|
||||
import {
|
||||
@@ -32,7 +33,7 @@
|
||||
import AIChangesWarningModal from '$lib/components/copilot/chat/flow/AIChangesWarningModal.svelte'
|
||||
|
||||
import { onMount, setContext, untrack, type ComponentType } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import CenteredPage from './CenteredPage.svelte'
|
||||
import { Badge, Button, UndoRedo } from './common'
|
||||
import FlowEditor from './flows/FlowEditor.svelte'
|
||||
@@ -41,7 +42,7 @@
|
||||
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
|
||||
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
|
||||
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
|
||||
import { cleanInputs } from './flows/utils'
|
||||
import { cleanInputs, updateDerivedModuleStatesFromTestJobs } from './flows/utils'
|
||||
import {
|
||||
Calendar,
|
||||
Pen,
|
||||
@@ -77,12 +78,14 @@
|
||||
import { Triggers } from './triggers/triggers.svelte'
|
||||
import { TestSteps } from './flows/testSteps.svelte'
|
||||
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
|
||||
import type { DurationStatus, GraphModuleState } from './graph'
|
||||
import {
|
||||
setStepHistoryLoaderContext,
|
||||
StepHistoryLoader,
|
||||
type stepState
|
||||
} from './stepHistoryLoader.svelte'
|
||||
import type { FlowBuilderProps } from './flow_builder'
|
||||
import { ModulesTestStates } from './modulesTest.svelte'
|
||||
|
||||
let {
|
||||
initialPath = $bindable(''),
|
||||
@@ -141,6 +144,12 @@
|
||||
let aiChangesWarningOpen = $state(false)
|
||||
let aiChangesConfirmCallback = $state<() => void>(() => {})
|
||||
|
||||
// Flow preview
|
||||
let flowPreviewButtons: FlowPreviewButtons | undefined = $state()
|
||||
const flowPreviewContent = $derived(flowPreviewButtons?.getFlowPreviewContent())
|
||||
const job: Job | undefined = $derived(flowPreviewContent?.getJob())
|
||||
let showJobStatus = $state(false)
|
||||
|
||||
async function handleDraftTriggersConfirmed(event: CustomEvent<{ selectedTriggers: Trigger[] }>) {
|
||||
const { selectedTriggers } = event.detail
|
||||
// Continue with saving the flow
|
||||
@@ -567,6 +576,15 @@
|
||||
}
|
||||
|
||||
let insertButtonOpen = writable<boolean>(false)
|
||||
let testModuleId: string | undefined = $state(undefined)
|
||||
let modulesTestStates = new ModulesTestStates((moduleId) => {
|
||||
// Update the derived store with test job states
|
||||
delete $derivedModuleStates[moduleId]
|
||||
testModuleId = moduleId
|
||||
showJobStatus = false
|
||||
})
|
||||
let outputPickerOpenFns: Record<string, () => void> = $state({})
|
||||
let flowEditor: FlowEditor | undefined = $state(undefined)
|
||||
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
@@ -586,7 +604,9 @@
|
||||
customUi,
|
||||
insertButtonOpen,
|
||||
executionCount: writable(0),
|
||||
flowInputEditorState: flowInputEditorStateStore
|
||||
flowInputEditorState: flowInputEditorStateStore,
|
||||
modulesTestStates,
|
||||
outputPickerOpenFns
|
||||
})
|
||||
|
||||
// Add triggers context store
|
||||
@@ -813,8 +833,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
let flowPreviewButtons: FlowPreviewButtons | undefined = $state()
|
||||
|
||||
let forceTestTab: Record<string, boolean> = $state({})
|
||||
let highlightArg: Record<string, string | undefined> = $state({})
|
||||
|
||||
@@ -877,6 +895,70 @@
|
||||
stepHistoryLoader.setFlowJobInitial(loadedFromHistoryUrl.flowJobInitial)
|
||||
stepHistoryLoader.stepStates = loadedFromHistoryUrl.stepsState
|
||||
}
|
||||
|
||||
function onJobDone() {
|
||||
if (!job) {
|
||||
return
|
||||
}
|
||||
// job was running and is now stopped
|
||||
if (!flowPreviewButtons?.getPreviewOpen()) {
|
||||
if (
|
||||
job.type === 'CompletedJob' &&
|
||||
job.success &&
|
||||
flowPreviewButtons?.getPreviewMode() === 'whole'
|
||||
) {
|
||||
if (flowEditor?.isNodeVisible('result') && $selectedIdStore !== 'Result') {
|
||||
outputPickerOpenFns['Result']?.()
|
||||
}
|
||||
} else {
|
||||
// Find last module with a job in flow_status
|
||||
const lastModuleWithJob = job.flow_status?.modules
|
||||
?.slice()
|
||||
.reverse()
|
||||
.find((module) => 'job' in module)
|
||||
if (
|
||||
lastModuleWithJob &&
|
||||
lastModuleWithJob.id &&
|
||||
flowEditor?.isNodeVisible(lastModuleWithJob.id)
|
||||
) {
|
||||
outputPickerOpenFns[lastModuleWithJob.id]?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const localModuleStates: Writable<Record<string, GraphModuleState>> = $derived(
|
||||
flowPreviewContent?.getLocalModuleStates() ?? writable({})
|
||||
)
|
||||
const localDurationStatuses: Writable<Record<string, DurationStatus>> = $derived(
|
||||
flowPreviewContent?.getLocalDurationStatuses() ?? writable({})
|
||||
)
|
||||
const suspendStatus: Writable<Record<string, { job: Job; nb: number }>> = $derived(
|
||||
flowPreviewContent?.getSuspendStatus() ?? writable({})
|
||||
)
|
||||
|
||||
// Create a derived store that only shows the module states when showModuleStatus is true
|
||||
// this store can also be updated
|
||||
let derivedModuleStates = writable<Record<string, GraphModuleState>>({})
|
||||
$effect(() => {
|
||||
derivedModuleStates.update((currentStates) => {
|
||||
return showJobStatus ? $localModuleStates : currentStates
|
||||
})
|
||||
})
|
||||
$effect(() => {
|
||||
updateDerivedModuleStatesFromTestJobs(testModuleId, modulesTestStates, derivedModuleStates)
|
||||
})
|
||||
|
||||
function resetModulesStates() {
|
||||
derivedModuleStates.set({})
|
||||
showJobStatus = false
|
||||
}
|
||||
|
||||
const individualStepTests = $derived(
|
||||
!(showJobStatus && job) && Object.keys($derivedModuleStates).length > 0
|
||||
)
|
||||
|
||||
const flowHasChanged = $derived(flowPreviewContent?.flowHasChanged())
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
@@ -1069,8 +1151,13 @@
|
||||
captureOn.set(true)
|
||||
showCaptureHint.set(true)
|
||||
}}
|
||||
{onJobDone}
|
||||
bind:this={flowPreviewButtons}
|
||||
{loading}
|
||||
onRunPreview={() => {
|
||||
localModuleStates.set({})
|
||||
showJobStatus = true
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
loading={loadingDraft}
|
||||
@@ -1097,6 +1184,7 @@
|
||||
<!-- metadata -->
|
||||
{#if $flowStateStore}
|
||||
<FlowEditor
|
||||
bind:this={flowEditor}
|
||||
{disabledFlowInputs}
|
||||
disableAi={disableAi || customUi?.stepInputs?.ai == false}
|
||||
disableSettings={customUi?.settingsPanel === false}
|
||||
@@ -1115,8 +1203,8 @@
|
||||
previewArgsStore.val = JSON.parse(JSON.stringify(e.detail))
|
||||
flowPreviewButtons?.openPreview(true)
|
||||
}}
|
||||
onTestUpTo={() => {
|
||||
flowPreviewButtons?.testUpTo()
|
||||
onTestUpTo={(id) => {
|
||||
flowPreviewButtons?.testUpTo(id)
|
||||
}}
|
||||
{savedFlow}
|
||||
onDeployTrigger={handleDeployTrigger}
|
||||
@@ -1136,9 +1224,22 @@
|
||||
aiChatOpen={aiChatManager.open}
|
||||
showFlowAiButton={!disableAi && customUi?.topBar?.aiBuilder != false}
|
||||
toggleAiChat={() => aiChatManager.toggleOpen()}
|
||||
onRunPreview={() => {
|
||||
flowPreviewButtons?.openPreview(true)
|
||||
onOpenPreview={flowPreviewButtons?.openPreview}
|
||||
localModuleStates={derivedModuleStates}
|
||||
isOwner={flowPreviewContent?.getIsOwner()}
|
||||
onTestFlow={flowPreviewButtons?.runPreview}
|
||||
isRunning={flowPreviewContent?.getIsRunning()}
|
||||
onCancelTestFlow={flowPreviewContent?.cancelTest}
|
||||
onHideJobStatus={resetModulesStates}
|
||||
{individualStepTests}
|
||||
{job}
|
||||
{localDurationStatuses}
|
||||
{suspendStatus}
|
||||
{showJobStatus}
|
||||
onDelete={(id) => {
|
||||
delete $derivedModuleStates[id]
|
||||
}}
|
||||
{flowHasChanged}
|
||||
/>
|
||||
{:else}
|
||||
<CenteredPage>Loading...</CenteredPage>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { run, stopPropagation } from 'svelte/legacy'
|
||||
|
||||
import { type Job, JobService, type RestartedFrom, type OpenFlow } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Badge, Button } from './common'
|
||||
@@ -24,33 +26,56 @@
|
||||
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
|
||||
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
|
||||
|
||||
export let previewMode: 'upTo' | 'whole'
|
||||
export let open: boolean
|
||||
export let preventEscape = false
|
||||
interface Props {
|
||||
previewMode: 'upTo' | 'whole'
|
||||
open: boolean
|
||||
preventEscape?: boolean
|
||||
jobId?: string | undefined
|
||||
job?: Job | undefined
|
||||
initial?: boolean
|
||||
selectedJobStep?: string | undefined
|
||||
selectedJobStepIsTopLevel?: boolean | undefined
|
||||
selectedJobStepType?: 'single' | 'forloop' | 'branchall'
|
||||
rightColumnSelect?: 'timeline' | 'node_status' | 'node_definition' | 'user_states'
|
||||
branchOrIterationN?: number
|
||||
scrollTop?: number
|
||||
localModuleStates?: Writable<Record<string, GraphModuleState>>
|
||||
localDurationStatuses?: Writable<Record<string, DurationStatus>>
|
||||
onRunPreview?: () => void
|
||||
render?: boolean
|
||||
onJobDone?: () => void
|
||||
upToId?: string | undefined
|
||||
}
|
||||
|
||||
export let jobId: string | undefined = undefined
|
||||
export let job: Job | undefined = undefined
|
||||
|
||||
export let selectedJobStep: string | undefined = undefined
|
||||
export let selectedJobStepIsTopLevel: boolean | undefined = undefined
|
||||
export let selectedJobStepType: 'single' | 'forloop' | 'branchall' = 'single'
|
||||
export let rightColumnSelect: 'timeline' | 'node_status' | 'node_definition' | 'user_states' =
|
||||
'timeline'
|
||||
|
||||
export let branchOrIterationN: number = 0
|
||||
export let scrollTop: number = 0
|
||||
|
||||
export let localModuleStates: Writable<Record<string, GraphModuleState>> = writable({})
|
||||
export let localDurationStatuses: Writable<Record<string, DurationStatus>> = writable({})
|
||||
let {
|
||||
previewMode = $bindable(),
|
||||
open,
|
||||
preventEscape = $bindable(false),
|
||||
jobId = $bindable(undefined),
|
||||
job = $bindable(undefined),
|
||||
initial = $bindable(false),
|
||||
selectedJobStep = $bindable(undefined),
|
||||
selectedJobStepIsTopLevel = $bindable(undefined),
|
||||
selectedJobStepType = $bindable('single'),
|
||||
rightColumnSelect = $bindable('timeline'),
|
||||
branchOrIterationN = $bindable(0),
|
||||
scrollTop = $bindable(0),
|
||||
localModuleStates = $bindable(writable({})),
|
||||
localDurationStatuses = $bindable(writable({})),
|
||||
onRunPreview,
|
||||
render = false,
|
||||
onJobDone,
|
||||
upToId = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let restartBranchNames: [number, string][] = []
|
||||
|
||||
let isRunning: boolean = false
|
||||
let jobProgressReset: () => void
|
||||
let jsonView: boolean = false
|
||||
let jsonEditor: JsonInputs
|
||||
let schemaHeight = 0
|
||||
let isValid: boolean = true
|
||||
let isRunning: boolean = $state(false)
|
||||
let jsonView: boolean = $state(false)
|
||||
let jsonEditor: JsonInputs | undefined = $state(undefined)
|
||||
let schemaHeight = $state(0)
|
||||
let isValid: boolean = $state(true)
|
||||
let suspendStatus: Writable<Record<string, { job: Job; nb: number }>> = $state(writable({}))
|
||||
let isOwner: boolean = $state(false)
|
||||
|
||||
export function test() {
|
||||
renderCount++
|
||||
@@ -67,14 +92,14 @@
|
||||
fakeInitialPath,
|
||||
customUi,
|
||||
executionCount
|
||||
} = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
} = $state(getContext<FlowEditorContext>('FlowEditorContext'))
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let renderCount: number = $state(0)
|
||||
let schemaFormWithArgPicker: SchemaFormWithArgPicker | undefined = $state(undefined)
|
||||
let currentJobId: string | undefined = $state(undefined)
|
||||
let stepHistoryLoader = getStepHistoryLoaderContext()
|
||||
|
||||
let renderCount: number = 0
|
||||
let schemaFormWithArgPicker: SchemaFormWithArgPicker | undefined = undefined
|
||||
let currentJobId: string | undefined = undefined
|
||||
let flowProgressBar: FlowProgressBar | undefined = $state(undefined)
|
||||
|
||||
function extractFlow(previewMode: 'upTo' | 'whole'): OpenFlow {
|
||||
const previewFlow = aiChatManager.flowAiChatHelpers?.getPreviewFlow()
|
||||
@@ -83,7 +108,7 @@
|
||||
} else {
|
||||
const flow = previewFlow ?? stateSnapshot(flowStore).val
|
||||
const idOrders = dfs(flow.value.modules, (x) => x.id)
|
||||
let upToIndex = idOrders.indexOf($selectedId)
|
||||
let upToIndex = idOrders.indexOf(upToId ?? $selectedId)
|
||||
|
||||
if (upToIndex != -1) {
|
||||
flow.value.modules = sliceModules(flow.value.modules, upToIndex, idOrders)
|
||||
@@ -92,7 +117,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let lastPreviewFlow: undefined | string = undefined
|
||||
let lastPreviewFlow: undefined | string = $state(undefined)
|
||||
export async function runPreview(
|
||||
args: Record<string, any>,
|
||||
restartedFrom: RestartedFrom | undefined
|
||||
@@ -102,7 +127,7 @@
|
||||
}
|
||||
try {
|
||||
lastPreviewFlow = JSON.stringify(flowStore.val)
|
||||
jobProgressReset()
|
||||
flowProgressBar?.reset()
|
||||
const newFlow = extractFlow(previewMode)
|
||||
jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
|
||||
isRunning = true
|
||||
@@ -110,6 +135,7 @@
|
||||
savedArgs = previewArgs.val
|
||||
inputSelected = undefined
|
||||
}
|
||||
onRunPreview?.()
|
||||
} catch (e) {
|
||||
sendUserToast('Could not run preview', true, undefined, e.toString())
|
||||
isRunning = false
|
||||
@@ -160,8 +186,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
let savedArgs = previewArgs.val
|
||||
let inputSelected: 'captures' | 'history' | 'saved' | undefined = undefined
|
||||
let savedArgs = $state(previewArgs.val)
|
||||
let inputSelected: 'captures' | 'history' | 'saved' | undefined = $state(undefined)
|
||||
async function selectInput(input, type?: 'captures' | 'history' | 'saved' | undefined) {
|
||||
if (!input) {
|
||||
previewArgs.val = savedArgs
|
||||
@@ -181,297 +207,335 @@
|
||||
renderCount++
|
||||
}
|
||||
|
||||
$: if (job?.type === 'CompletedJob') {
|
||||
isRunning = false
|
||||
}
|
||||
|
||||
$: selectedJobStep !== undefined && onSelectedJobStepChange()
|
||||
|
||||
let scrollableDiv: HTMLDivElement | undefined = undefined
|
||||
function handleScroll() {
|
||||
scrollTop = scrollableDiv?.scrollTop ?? 0
|
||||
}
|
||||
|
||||
$: scrollableDiv && onScrollableDivChange()
|
||||
|
||||
function onScrollableDivChange() {
|
||||
if (scrollTop != 0 && scrollableDiv) {
|
||||
scrollableDiv.scrollTop = scrollTop
|
||||
}
|
||||
}
|
||||
run(() => {
|
||||
selectedJobStep !== undefined && onSelectedJobStepChange()
|
||||
})
|
||||
run(() => {
|
||||
scrollableDiv && onScrollableDivChange()
|
||||
})
|
||||
|
||||
export async function cancelTest() {
|
||||
isRunning = false
|
||||
try {
|
||||
jobId &&
|
||||
(await JobService.cancelQueuedJob({
|
||||
workspace: $workspaceStore ?? '',
|
||||
id: jobId,
|
||||
requestBody: {}
|
||||
}))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function getLocalModuleStates() {
|
||||
return localModuleStates
|
||||
}
|
||||
|
||||
export function getLocalDurationStatuses() {
|
||||
return localDurationStatuses
|
||||
}
|
||||
|
||||
export function getSuspendStatus() {
|
||||
return suspendStatus
|
||||
}
|
||||
|
||||
export function getIsRunning() {
|
||||
return isRunning
|
||||
}
|
||||
|
||||
export function getIsOwner() {
|
||||
return isOwner
|
||||
}
|
||||
|
||||
export function getJob() {
|
||||
return job
|
||||
}
|
||||
|
||||
export function flowHasChanged() {
|
||||
return !!lastPreviewFlow && JSON.stringify(flowStore.val) != lastPreviewFlow
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
|
||||
<div class="flex flex-col space-y-2 h-screen bg-surface px-4 py-2 w-full" id="flow-preview-content">
|
||||
<div class="flex flex-row w-full items-center gap-x-2">
|
||||
<div class="w-8">
|
||||
<Button
|
||||
on:click={() => dispatch('close')}
|
||||
startIcon={{ icon: X }}
|
||||
iconOnly
|
||||
size="sm"
|
||||
color="light"
|
||||
btnClasses="hover:bg-surface-hover bg-surface-secondaryw-8 h-8 rounded-full p-0"
|
||||
/>
|
||||
{#if render}
|
||||
<div class="flex flex-row w-full items-center gap-x-2">
|
||||
<div class="w-8">
|
||||
<Button
|
||||
on:click={() => dispatch('close')}
|
||||
startIcon={{ icon: X }}
|
||||
iconOnly
|
||||
size="sm"
|
||||
color="light"
|
||||
btnClasses="hover:bg-surface-hover bg-surface-secondaryw-8 h-8 rounded-full p-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if isRunning}
|
||||
<div class="mx-auto">
|
||||
<Button
|
||||
color="red"
|
||||
on:click={async () => {
|
||||
cancelTest()
|
||||
}}
|
||||
size="sm"
|
||||
btnClasses="w-full max-w-lg"
|
||||
loading={true}
|
||||
clickableWhileLoading
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grow justify-center flex flex-row gap-4">
|
||||
{#if jobId !== undefined && selectedJobStep !== undefined && selectedJobStepIsTopLevel && aiChatManager.flowAiChatHelpers?.getModuleAction(selectedJobStep) !== 'removed'}
|
||||
{#if selectedJobStepType == 'single'}
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
title={`Re-start this flow from step ${selectedJobStep} (included).`}
|
||||
on:click={() => {
|
||||
runPreview(previewArgs.val, {
|
||||
flow_job_id: jobId,
|
||||
step_id: selectedJobStep,
|
||||
branch_or_iteration_n: 0
|
||||
})
|
||||
}}
|
||||
startIcon={{ icon: Play }}
|
||||
>
|
||||
Re-start from
|
||||
<Badge baseClass="ml-1" color="indigo">
|
||||
{selectedJobStep}
|
||||
</Badge>
|
||||
</Button>
|
||||
{:else}
|
||||
<Popover
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-start' }}
|
||||
contentClasses="p-4"
|
||||
>
|
||||
{#snippet button()}
|
||||
<Button
|
||||
title={`Re-start this flow from step ${selectedJobStep} (included).`}
|
||||
variant="border"
|
||||
color="blue"
|
||||
startIcon={{ icon: RefreshCw }}
|
||||
on:click={() => {
|
||||
runPreview(previewArgs.val, {
|
||||
flow_job_id: jobId,
|
||||
step_id: selectedJobStep,
|
||||
branch_or_iteration_n: 0
|
||||
})
|
||||
}}
|
||||
nonCaptureEvent={true}
|
||||
>
|
||||
Re-start from
|
||||
<Badge baseClass="ml-1" color="indigo">
|
||||
{selectedJobStep}
|
||||
</Badge>
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<label class="block text-primary p-4">
|
||||
<div class="pb-1 text-sm text-secondary"
|
||||
>{selectedJobStepType == 'forloop'
|
||||
? 'From iteration #:'
|
||||
: 'From branch:'}</div
|
||||
>
|
||||
<div class="flex w-full">
|
||||
{#if selectedJobStepType === 'forloop'}
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
bind:value={branchOrIterationN}
|
||||
class="!w-32 grow"
|
||||
onclick={stopPropagation(() => {})}
|
||||
/>
|
||||
{:else}
|
||||
<select
|
||||
bind:value={branchOrIterationN}
|
||||
class="!w-32 grow"
|
||||
onclick={stopPropagation(() => {})}
|
||||
>
|
||||
{#each restartBranchNames as [branchIdx, branchName]}
|
||||
<option value={branchIdx}>{branchName}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
buttonType="button"
|
||||
btnClasses="!p-1 !w-[34px] !ml-1"
|
||||
aria-label="Restart flow"
|
||||
on:click|once={() => {
|
||||
runPreview(previewArgs.val, {
|
||||
flow_job_id: jobId,
|
||||
step_id: selectedJobStep,
|
||||
branch_or_iteration_n: branchOrIterationN
|
||||
})
|
||||
}}
|
||||
>
|
||||
<ArrowRight size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
</label>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={{ icon: isRunning ? RefreshCw : Play }}
|
||||
color="dark"
|
||||
size="sm"
|
||||
btnClasses="w-full max-w-lg"
|
||||
on:click={() => runPreview(previewArgs.val, undefined)}
|
||||
id="flow-editor-test-flow-drawer"
|
||||
shortCut={{ Icon: CornerDownLeft }}
|
||||
>
|
||||
{#if previewMode == 'upTo'}
|
||||
Test up to
|
||||
<Badge baseClass="ml-1" color="indigo">
|
||||
{$selectedId}
|
||||
</Badge>
|
||||
{:else}
|
||||
Test flow
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isRunning}
|
||||
<div class="mx-auto">
|
||||
<Button
|
||||
color="red"
|
||||
on:click={async () => {
|
||||
isRunning = false
|
||||
try {
|
||||
jobId &&
|
||||
(await JobService.cancelQueuedJob({
|
||||
workspace: $workspaceStore ?? '',
|
||||
id: jobId,
|
||||
requestBody: {}
|
||||
}))
|
||||
} catch {}
|
||||
}}
|
||||
size="sm"
|
||||
btnClasses="w-full max-w-lg"
|
||||
loading={true}
|
||||
clickableWhileLoading
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grow justify-center flex flex-row gap-4">
|
||||
{#if jobId !== undefined && selectedJobStep !== undefined && selectedJobStepIsTopLevel && aiChatManager.flowAiChatHelpers?.getModuleAction(selectedJobStep) !== 'removed'}
|
||||
{#if selectedJobStepType == 'single'}
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
title={`Re-start this flow from step ${selectedJobStep} (included).`}
|
||||
on:click={() => {
|
||||
runPreview(previewArgs.val, {
|
||||
flow_job_id: jobId,
|
||||
step_id: selectedJobStep,
|
||||
branch_or_iteration_n: 0
|
||||
})
|
||||
}}
|
||||
startIcon={{ icon: Play }}
|
||||
>
|
||||
Re-start from
|
||||
<Badge baseClass="ml-1" color="indigo">
|
||||
{selectedJobStep}
|
||||
</Badge>
|
||||
</Button>
|
||||
{:else}
|
||||
<Popover
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-start' }}
|
||||
contentClasses="p-4"
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button
|
||||
title={`Re-start this flow from step ${selectedJobStep} (included).`}
|
||||
variant="border"
|
||||
color="blue"
|
||||
startIcon={{ icon: RefreshCw }}
|
||||
on:click={() => {
|
||||
runPreview(previewArgs.val, {
|
||||
flow_job_id: jobId,
|
||||
step_id: selectedJobStep,
|
||||
branch_or_iteration_n: 0
|
||||
})
|
||||
}}
|
||||
nonCaptureEvent={true}
|
||||
>
|
||||
Re-start from
|
||||
<Badge baseClass="ml-1" color="indigo">
|
||||
{selectedJobStep}
|
||||
</Badge>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<label class="block text-primary p-4">
|
||||
<div class="pb-1 text-sm text-secondary"
|
||||
>{selectedJobStepType == 'forloop' ? 'From iteration #:' : 'From branch:'}</div
|
||||
>
|
||||
<div class="flex w-full">
|
||||
{#if selectedJobStepType === 'forloop'}
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
bind:value={branchOrIterationN}
|
||||
class="!w-32 grow"
|
||||
on:click|stopPropagation={() => {}}
|
||||
/>
|
||||
{:else}
|
||||
<select
|
||||
bind:value={branchOrIterationN}
|
||||
class="!w-32 grow"
|
||||
on:click|stopPropagation={() => {}}
|
||||
>
|
||||
{#each restartBranchNames as [branchIdx, branchName]}
|
||||
<option value={branchIdx}>{branchName}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
buttonType="button"
|
||||
btnClasses="!p-1 !w-[34px] !ml-1"
|
||||
aria-label="Restart flow"
|
||||
on:click|once={() => {
|
||||
runPreview(previewArgs.val, {
|
||||
flow_job_id: jobId,
|
||||
step_id: selectedJobStep,
|
||||
branch_or_iteration_n: branchOrIterationN
|
||||
})
|
||||
}}
|
||||
>
|
||||
<ArrowRight size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
</label>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={{ icon: isRunning ? RefreshCw : Play }}
|
||||
color="dark"
|
||||
size="sm"
|
||||
btnClasses="w-full max-w-lg"
|
||||
on:click={() => runPreview(previewArgs.val, undefined)}
|
||||
id="flow-editor-test-flow-drawer"
|
||||
shortCut={{ Icon: CornerDownLeft }}
|
||||
>
|
||||
{#if previewMode == 'upTo'}
|
||||
Test up to
|
||||
<Badge baseClass="ml-1" color="indigo">
|
||||
{$selectedId}
|
||||
</Badge>
|
||||
{:else}
|
||||
Test flow
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-full flex flex-col gap-y-1">
|
||||
{#if lastPreviewFlow && JSON.stringify(flowStore.val) != lastPreviewFlow}
|
||||
<div class="pt-1">
|
||||
<div
|
||||
class="bg-orange-200 text-orange-600 border border-orange-600 p-2 flex items-center gap-2 rounded"
|
||||
>
|
||||
<AlertTriangle size={14} /> Flow changed since last preview
|
||||
<div class="flex"></div>
|
||||
<div class="w-full flex flex-col gap-y-1">
|
||||
{#if flowHasChanged()}
|
||||
<div class="pt-1">
|
||||
<div
|
||||
class="bg-orange-200 text-orange-600 border border-orange-600 p-2 flex items-center gap-2 rounded"
|
||||
>
|
||||
<AlertTriangle size={14} /> Flow changed since last preview
|
||||
<div class="flex"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<FlowProgressBar {job} bind:reset={jobProgressReset} />
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
<FlowProgressBar {job} bind:this={flowProgressBar} />
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
bind:this={scrollableDiv}
|
||||
class="overflow-y-auto grow flex flex-col pt-4"
|
||||
on:scroll={(e) => handleScroll()}
|
||||
onscroll={(e) => handleScroll()}
|
||||
>
|
||||
<div class="border-b">
|
||||
<SchemaFormWithArgPicker
|
||||
bind:this={schemaFormWithArgPicker}
|
||||
runnableId={$initialPathStore}
|
||||
stablePathForCaptures={$initialPathStore || fakeInitialPath}
|
||||
runnableType={'FlowPath'}
|
||||
previewArgs={previewArgs.val}
|
||||
on:openTriggers
|
||||
on:select={(e) => {
|
||||
selectInput(e.detail.payload, e.detail?.type)
|
||||
}}
|
||||
{isValid}
|
||||
{jsonView}
|
||||
>
|
||||
<div class="w-full flex flex-row justify-between">
|
||||
<InputSelectedBadge
|
||||
onReject={() => schemaFormWithArgPicker?.resetSelected()}
|
||||
{inputSelected}
|
||||
/>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
label="JSON View"
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'JSON',
|
||||
rightTooltip: 'Fill args from JSON'
|
||||
}}
|
||||
lightMode
|
||||
on:change={(e) => {
|
||||
jsonEditor?.setCode(JSON.stringify(previewArgs.val ?? {}, null, '\t'))
|
||||
refresh()
|
||||
}}
|
||||
{#if render}
|
||||
<div class="border-b">
|
||||
<SchemaFormWithArgPicker
|
||||
bind:this={schemaFormWithArgPicker}
|
||||
runnableId={$initialPathStore}
|
||||
stablePathForCaptures={$initialPathStore || fakeInitialPath}
|
||||
runnableType={'FlowPath'}
|
||||
previewArgs={previewArgs.val}
|
||||
on:openTriggers
|
||||
on:select={(e) => {
|
||||
selectInput(e.detail.payload, e.detail?.type)
|
||||
}}
|
||||
{isValid}
|
||||
{jsonView}
|
||||
>
|
||||
<div class="w-full flex flex-row justify-between">
|
||||
<InputSelectedBadge
|
||||
onReject={() => schemaFormWithArgPicker?.resetSelected()}
|
||||
{inputSelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if jsonView}
|
||||
<div class="py-2" style="height: {Math.max(schemaHeight, 100)}px" data-schema-picker>
|
||||
<JsonInputs
|
||||
bind:this={jsonEditor}
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
previewArgs.val = e.detail
|
||||
}
|
||||
}}
|
||||
updateOnBlur={false}
|
||||
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}`}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
{#key renderCount}
|
||||
<div bind:clientHeight={schemaHeight} class="min-h-[40vh]">
|
||||
<SchemaForm
|
||||
noVariablePicker
|
||||
compact
|
||||
schema={flowStore.val.schema}
|
||||
bind:args={previewArgs.val}
|
||||
on:change={() => {
|
||||
savedArgs = previewArgs.val
|
||||
<div class="flex flex-row gap-2">
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
label="JSON View"
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'JSON',
|
||||
rightTooltip: 'Fill args from JSON'
|
||||
}}
|
||||
lightMode
|
||||
on:change={(e) => {
|
||||
jsonEditor?.setCode(JSON.stringify(previewArgs.val ?? {}, null, '\t'))
|
||||
refresh()
|
||||
}}
|
||||
bind:isValid
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
</SchemaFormWithArgPicker>
|
||||
</div>
|
||||
</div>
|
||||
{#if jsonView}
|
||||
<div class="py-2" style="height: {Math.max(schemaHeight, 100)}px" data-schema-picker>
|
||||
<JsonInputs
|
||||
bind:this={jsonEditor}
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
previewArgs.val = e.detail
|
||||
}
|
||||
}}
|
||||
updateOnBlur={false}
|
||||
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}`}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
{#key renderCount}
|
||||
<div bind:clientHeight={schemaHeight} class="min-h-[40vh]">
|
||||
<SchemaForm
|
||||
noVariablePicker
|
||||
compact
|
||||
schema={flowStore.val.schema}
|
||||
bind:args={previewArgs.val}
|
||||
on:change={() => {
|
||||
savedArgs = previewArgs.val
|
||||
}}
|
||||
bind:isValid
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
</SchemaFormWithArgPicker>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="pt-4 flex flex-col grow relative">
|
||||
<div
|
||||
class="absolute top-[22px] right-2 border p-1.5 hover:bg-surface-hover rounded-md center-center"
|
||||
>
|
||||
<FlowHistoryJobPicker
|
||||
selectInitial={jobId == undefined}
|
||||
on:select={(e) => {
|
||||
if (!currentJobId) {
|
||||
currentJobId = jobId
|
||||
}
|
||||
const detail = e.detail
|
||||
jobId = detail.jobId
|
||||
if (detail.initial && stepHistoryLoader?.flowJobInitial === undefined) {
|
||||
stepHistoryLoader?.setFlowJobInitial(detail.initial)
|
||||
}
|
||||
}}
|
||||
on:unselect={() => {
|
||||
jobId = currentJobId
|
||||
currentJobId = undefined
|
||||
}}
|
||||
path={$initialPathStore == '' ? $pathStore : $initialPathStore}
|
||||
/>
|
||||
{#if render}
|
||||
<FlowHistoryJobPicker
|
||||
selectInitial={jobId == undefined}
|
||||
on:select={(e) => {
|
||||
if (!currentJobId) {
|
||||
currentJobId = jobId
|
||||
}
|
||||
const detail = e.detail
|
||||
jobId = detail.jobId
|
||||
if (detail.initial && stepHistoryLoader?.flowJobInitial === undefined) {
|
||||
stepHistoryLoader?.setFlowJobInitial(detail.initial)
|
||||
}
|
||||
}}
|
||||
on:unselect={() => {
|
||||
jobId = currentJobId
|
||||
currentJobId = undefined
|
||||
}}
|
||||
path={$initialPathStore == '' ? $pathStore : $initialPathStore}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
{#if jobId}
|
||||
{#if stepHistoryLoader?.flowJobInitial}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
stepHistoryLoader?.setFlowJobInitial(false)
|
||||
}}
|
||||
class="cursor-pointer h-full hover:bg-gray-500/20 dark:hover:bg-gray-500/20 dark:bg-gray-500/80 rounded bg-gray-500/40 absolute top-0 left-0 w-full z-50"
|
||||
@@ -487,15 +551,20 @@
|
||||
bind:job
|
||||
bind:localModuleStates
|
||||
bind:localDurationStatuses
|
||||
bind:suspendStatus
|
||||
hideDownloadInGraph={customUi?.downloadLogs === false}
|
||||
wideResults
|
||||
{flowStateStore}
|
||||
{jobId}
|
||||
on:done={() => {
|
||||
isRunning = false
|
||||
$executionCount = $executionCount + 1
|
||||
onJobDone?.()
|
||||
}}
|
||||
bind:selectedJobStep
|
||||
bind:rightColumnSelect
|
||||
bind:isOwner
|
||||
{render}
|
||||
/>
|
||||
{:else}
|
||||
<div class="italic text-tertiary h-full grow"> Flow status will be displayed here </div>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import { Badge, Loader2 } from 'lucide-svelte'
|
||||
import FlowJobResult from './FlowJobResult.svelte'
|
||||
import FlowPreviewStatus from './preview/FlowPreviewStatus.svelte'
|
||||
import FlowStatusWaitingForEvents from './FlowStatusWaitingForEvents.svelte'
|
||||
import type { FlowStatusModule, Job } from '$lib/gen'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import type { DurationStatus } from './graph'
|
||||
import type { Writable } from 'svelte/store'
|
||||
|
||||
interface Props {
|
||||
job: Job
|
||||
workspaceId: string | undefined
|
||||
isOwner: boolean
|
||||
hideFlowResult: boolean
|
||||
hideDownloadLogs: boolean
|
||||
localDurationStatuses: Writable<Record<string, DurationStatus>>
|
||||
innerModules: FlowStatusModule[]
|
||||
suspendStatus: Writable<Record<string, { job: Job; nb: number }>>
|
||||
hideJobId?: boolean
|
||||
extra?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
let {
|
||||
job,
|
||||
workspaceId,
|
||||
isOwner,
|
||||
hideFlowResult,
|
||||
hideDownloadLogs,
|
||||
localDurationStatuses,
|
||||
innerModules,
|
||||
suspendStatus,
|
||||
hideJobId,
|
||||
extra
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
<FlowPreviewStatus {job} {hideJobId} {extra} />
|
||||
|
||||
{#if !job}
|
||||
<div>
|
||||
<Loader2 class="animate-spin" />
|
||||
</div>
|
||||
{:else if `result` in job}
|
||||
{#if !hideFlowResult}
|
||||
<div class="w-full h-full">
|
||||
<FlowJobResult
|
||||
workspaceId={job?.workspace_id}
|
||||
jobId={job?.id}
|
||||
tag={job?.tag}
|
||||
loading={job['running'] == true}
|
||||
result={job.result}
|
||||
logs={job.logs}
|
||||
durationStates={localDurationStatuses}
|
||||
downloadLogs={!hideDownloadLogs}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if job.flow_status?.modules?.[job?.flow_status?.step]?.type === 'WaitingForEvents'}
|
||||
<FlowStatusWaitingForEvents {workspaceId} {job} {isOwner} />
|
||||
{:else if $suspendStatus && Object.keys($suspendStatus).length > 0}
|
||||
<div class="flex gap-2 flex-col">
|
||||
{#each Object.values($suspendStatus) as suspendCount (suspendCount.job.id)}
|
||||
<div>
|
||||
<div class="text-sm">
|
||||
Flow suspended, waiting for {suspendCount.nb} events
|
||||
</div>
|
||||
<FlowStatusWaitingForEvents job={suspendCount.job} {workspaceId} {isOwner} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if job.logs}
|
||||
<div
|
||||
class="text-xs p-4 bg-surface-secondary overflow-auto max-h-80 border border-tertiary-inverse"
|
||||
>
|
||||
<pre class="w-full">{job.logs}</pre>
|
||||
</div>
|
||||
{:else if innerModules?.length > 0}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each innerModules as mod, i (mod.id)}
|
||||
{#if mod.type == 'InProgress'}
|
||||
{@const rawMod = job.raw_flow?.modules[i]}
|
||||
|
||||
<div
|
||||
><span class="inline-flex gap-1"
|
||||
><Badge color="indigo">{mod.id}</Badge>
|
||||
<span class="font-medium text-primary">
|
||||
{#if !emptyString(rawMod?.summary)}
|
||||
{rawMod?.summary ?? ''}
|
||||
{:else if rawMod?.value.type == 'script'}
|
||||
{rawMod.value.path ?? ''}
|
||||
{:else if rawMod?.value.type}
|
||||
{rawMod?.value.type}
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<Loader2 class="animate-spin" /></span
|
||||
></div
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -26,11 +26,12 @@
|
||||
export let localModuleStates: Writable<Record<string, GraphModuleState>> = writable({})
|
||||
export let localDurationStatuses: Writable<Record<string, DurationStatus>> = writable({})
|
||||
export let job: Job | undefined = undefined
|
||||
export let render = true
|
||||
export let suspendStatus = writable({})
|
||||
|
||||
let lastJobId: string = jobId
|
||||
|
||||
let retryStatus = writable({})
|
||||
let suspendStatus = writable({})
|
||||
setContext<FlowStatusViewerContext>('FlowStatusViewer', {
|
||||
flowStateStore,
|
||||
suspendStatus,
|
||||
@@ -85,4 +86,5 @@
|
||||
{isOwner}
|
||||
{wideResults}
|
||||
bind:rightColumnSelect
|
||||
{render}
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { base } from '$lib/base'
|
||||
import FlowJobResult from './FlowJobResult.svelte'
|
||||
import FlowPreviewStatus from './preview/FlowPreviewStatus.svelte'
|
||||
import DisplayResult from './DisplayResult.svelte'
|
||||
|
||||
import { createEventDispatcher, getContext, tick } from 'svelte'
|
||||
@@ -19,10 +18,9 @@
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
import { type DurationStatus, type FlowStatusViewerContext, type GraphModuleState } from './graph'
|
||||
import ModuleStatus from './ModuleStatus.svelte'
|
||||
import { emptyString, isScriptPreview, msToSec, truncateRev } from '$lib/utils'
|
||||
import { isScriptPreview, msToSec, truncateRev } from '$lib/utils'
|
||||
import JobArgs from './JobArgs.svelte'
|
||||
import { ChevronDown, Hourglass, Loader2 } from 'lucide-svelte'
|
||||
import FlowStatusWaitingForEvents from './FlowStatusWaitingForEvents.svelte'
|
||||
import { ChevronDown, Hourglass } from 'lucide-svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import FlowTimeline from './FlowTimeline.svelte'
|
||||
import { dfs } from './flows/dfs'
|
||||
@@ -31,6 +29,7 @@
|
||||
import FlowGraphViewerStep from './FlowGraphViewerStep.svelte'
|
||||
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
|
||||
import { buildPrefix } from './graph/graphBuilder.svelte'
|
||||
import FlowPreviewResult from './FlowPreviewResult.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -41,7 +40,8 @@
|
||||
hideDownloadInGraph,
|
||||
hideTimeline,
|
||||
hideNodeDefinition,
|
||||
hideDownloadLogs
|
||||
hideDownloadLogs,
|
||||
hideJobId
|
||||
} = getContext<FlowStatusViewerContext>('FlowStatusViewer')
|
||||
|
||||
export let jobId: string
|
||||
@@ -916,71 +916,17 @@
|
||||
{/if}
|
||||
{:else if render}
|
||||
<div class={'border rounded-md shadow p-2'}>
|
||||
<FlowPreviewStatus {job} />
|
||||
{#if !job}
|
||||
<div>
|
||||
<Loader2 class="animate-spin" />
|
||||
</div>
|
||||
{:else if `result` in job}
|
||||
{#if !hideFlowResult}
|
||||
<div class="w-full h-full">
|
||||
<FlowJobResult
|
||||
workspaceId={job?.workspace_id}
|
||||
jobId={job?.id}
|
||||
tag={job?.tag}
|
||||
loading={job['running'] == true}
|
||||
result={job.result}
|
||||
logs={job.logs}
|
||||
durationStates={localDurationStatuses}
|
||||
downloadLogs={!hideDownloadLogs}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if job.flow_status?.modules?.[job?.flow_status?.step]?.type === 'WaitingForEvents'}
|
||||
<FlowStatusWaitingForEvents {workspaceId} {job} {isOwner} />
|
||||
{:else if $suspendStatus && Object.keys($suspendStatus).length > 0}
|
||||
<div class="flex gap-2 flex-col">
|
||||
{#each Object.values($suspendStatus) as suspendCount (suspendCount.job.id)}
|
||||
<div>
|
||||
<div class="text-sm">
|
||||
Flow suspended, waiting for {suspendCount.nb} events
|
||||
</div>
|
||||
<FlowStatusWaitingForEvents job={suspendCount.job} {workspaceId} {isOwner} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if job.logs}
|
||||
<div
|
||||
class="text-xs p-4 bg-surface-secondary overflow-auto max-h-80 border border-tertiary-inverse"
|
||||
>
|
||||
<pre class="w-full">{job.logs}</pre>
|
||||
</div>
|
||||
{:else if innerModules?.length > 0}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each innerModules as mod, i (mod.id)}
|
||||
{#if mod.type == 'InProgress'}
|
||||
{@const rawMod = job.raw_flow?.modules[i]}
|
||||
|
||||
<div
|
||||
><span class="inline-flex gap-1"
|
||||
><Badge color="indigo">{mod.id}</Badge>
|
||||
<span class="font-medium text-primary">
|
||||
{#if !emptyString(rawMod?.summary)}
|
||||
{rawMod?.summary ?? ''}
|
||||
{:else if rawMod?.value.type == 'script'}
|
||||
{rawMod.value.path ?? ''}
|
||||
{:else if rawMod?.value.type}
|
||||
{rawMod?.value.type}
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<Loader2 class="animate-spin" /></span
|
||||
></div
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<FlowPreviewResult
|
||||
{job}
|
||||
{workspaceId}
|
||||
{isOwner}
|
||||
{hideFlowResult}
|
||||
{hideDownloadLogs}
|
||||
{localDurationStatuses}
|
||||
{innerModules}
|
||||
{suspendStatus}
|
||||
{hideJobId}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if render}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
export let isOwner: boolean
|
||||
export let workspaceId: string | undefined
|
||||
export let job: Job
|
||||
export let light: boolean = false
|
||||
|
||||
let default_payload: object = {}
|
||||
let resumeUrl: string | undefined = undefined
|
||||
@@ -115,9 +116,11 @@
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full mt-2 text-sm text-tertiary">
|
||||
<p>Waiting to be resumed</p>
|
||||
{#if !light}
|
||||
<p>Waiting to be resumed</p>
|
||||
{/if}
|
||||
{#if description != undefined}
|
||||
<DisplayResult {workspaceId} noControls result={description} language={job?.language}/>
|
||||
<DisplayResult {workspaceId} noControls result={description} language={job?.language} />
|
||||
{/if}
|
||||
<div>
|
||||
{#if isOwner || resumeUrl}
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
<script lang="ts" module>
|
||||
type testModuleState = {
|
||||
loading: boolean
|
||||
cancel?: () => Promise<void>
|
||||
}
|
||||
|
||||
let testModulesState = $state<Record<string, testModuleState>>({})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { ScriptService, type FlowModule, type Job } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getScriptByPath } from '$lib/scripts'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import TestJobLoader from './TestJobLoader.svelte'
|
||||
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
|
||||
@@ -32,7 +23,7 @@
|
||||
scriptProgress = $bindable(undefined)
|
||||
}: Props = $props()
|
||||
|
||||
const { flowStore, flowStateStore, pathStore, testSteps, previewArgs } =
|
||||
const { flowStore, flowStateStore, pathStore, testSteps, previewArgs, modulesTestStates } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let testJobLoader: TestJobLoader | undefined = $state(undefined)
|
||||
@@ -52,9 +43,12 @@
|
||||
// Not defined if JobProgressBar not loaded
|
||||
if (jobProgressReset) jobProgressReset()
|
||||
|
||||
testModulesState[mod.id].cancel = async () => {
|
||||
await testJobLoader?.cancelJob()
|
||||
testJob = undefined
|
||||
if (modulesTestStates.states[mod.id]) {
|
||||
modulesTestStates.states[mod.id].cancel = async () => {
|
||||
await testJobLoader?.cancelJob()
|
||||
modulesTestStates.states[mod.id].testJob = undefined
|
||||
}
|
||||
modulesTestStates.runTestCb?.(mod.id)
|
||||
}
|
||||
|
||||
const val = mod.value
|
||||
@@ -98,24 +92,28 @@
|
||||
}
|
||||
stepHistoryLoader?.resetInitial(mod.id)
|
||||
}
|
||||
testJob = undefined
|
||||
modulesTestStates.states[mod.id].testJob = undefined
|
||||
}
|
||||
|
||||
export function cancelJob() {
|
||||
testModulesState[mod.id]?.cancel?.()
|
||||
modulesTestStates.states[mod.id]?.cancel?.()
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
testIsLoading = testModulesState[mod.id]?.loading ?? false
|
||||
// Update testIsLoading to read the state from parent components
|
||||
testIsLoading = modulesTestStates.states[mod.id]?.loading ?? false
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const modId = mod.id
|
||||
testModulesState[modId] = {
|
||||
...(testModulesState[modId] ?? { loading: false, instances: 0 }),
|
||||
loading: testIsLoading
|
||||
}
|
||||
$effect(() => {
|
||||
// Update testJob to read the state from parent components
|
||||
testJob = modulesTestStates.states[mod.id]?.testJob
|
||||
})
|
||||
|
||||
modulesTestStates.states[mod.id] = {
|
||||
...(modulesTestStates.states[mod.id] ?? { loading: false }),
|
||||
loading: testIsLoading,
|
||||
testJob: testJob
|
||||
}
|
||||
</script>
|
||||
|
||||
<TestJobLoader
|
||||
@@ -124,16 +122,16 @@
|
||||
bind:scriptProgress
|
||||
bind:this={testJobLoader}
|
||||
bind:isLoading={
|
||||
() => testModulesState[mod.id]?.loading ?? false,
|
||||
() => modulesTestStates.states[mod.id]?.loading ?? false,
|
||||
(v) => {
|
||||
let newLoading = v ?? false
|
||||
if (testModulesState[mod.id]?.loading !== newLoading) {
|
||||
testModulesState[mod.id] = {
|
||||
...(testModulesState[mod.id] ?? {}),
|
||||
if (modulesTestStates.states[mod.id]?.loading !== newLoading) {
|
||||
modulesTestStates.states[mod.id] = {
|
||||
...(modulesTestStates.states[mod.id] ?? {}),
|
||||
loading: newLoading
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bind:job={testJob}
|
||||
bind:job={modulesTestStates.states[mod.id].testJob}
|
||||
/>
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
children?: import('svelte').Snippet
|
||||
tooltip?: import('svelte').Snippet
|
||||
[key: string]: any
|
||||
dropdownOpen?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -97,6 +98,7 @@
|
||||
hideDropdown = false,
|
||||
children,
|
||||
tooltip,
|
||||
dropdownOpen = $bindable(false),
|
||||
...rest
|
||||
}: Props = $props()
|
||||
|
||||
@@ -343,6 +345,7 @@
|
||||
usePointerDownOutside
|
||||
on:open={() => dispatch('dropdownOpen', true)}
|
||||
on:close={() => dispatch('dropdownOpen', false)}
|
||||
bind:open={dropdownOpen}
|
||||
>
|
||||
{#snippet buttonReplacement()}
|
||||
<div
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
import { getContext, onDestroy, onMount, setContext } from 'svelte'
|
||||
import type { FlowEditorContext } from './types'
|
||||
|
||||
import { writable } from 'svelte/store'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import type { PropPickerContext, FlowPropPickerConfig } from '$lib/components/prop_picker'
|
||||
import type { PickableProperties } from '$lib/components/flows/previousResults'
|
||||
import type { Flow } from '$lib/gen'
|
||||
import type { Flow, Job } from '$lib/gen'
|
||||
import type { Trigger } from '$lib/components/triggers/utils'
|
||||
import FlowAIChat from '../copilot/chat/flow/FlowAIChat.svelte'
|
||||
import { aiChatManager, AIMode } from '../copilot/chat/AIChatManager.svelte'
|
||||
import type { DurationStatus, GraphModuleState } from '../graph'
|
||||
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
@@ -39,7 +40,20 @@
|
||||
aiChatOpen?: boolean
|
||||
showFlowAiButton?: boolean
|
||||
toggleAiChat?: () => void
|
||||
onRunPreview?: () => void
|
||||
localModuleStates?: Writable<Record<string, GraphModuleState>>
|
||||
isOwner?: boolean
|
||||
onTestFlow?: () => void
|
||||
isRunning?: boolean
|
||||
onCancelTestFlow?: () => void
|
||||
onOpenPreview?: () => void
|
||||
onHideJobStatus?: () => void
|
||||
individualStepTests?: boolean
|
||||
job?: Job
|
||||
localDurationStatuses?: Writable<Record<string, DurationStatus>>
|
||||
suspendStatus?: Writable<Record<string, { job: Job; nb: number }>>
|
||||
showJobStatus?: boolean
|
||||
onDelete?: (id: string) => void
|
||||
flowHasChanged?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -57,14 +71,31 @@
|
||||
onEditInput = undefined,
|
||||
forceTestTab,
|
||||
highlightArg,
|
||||
localModuleStates = writable({}),
|
||||
aiChatOpen,
|
||||
showFlowAiButton,
|
||||
toggleAiChat,
|
||||
onRunPreview = () => {}
|
||||
isOwner,
|
||||
onTestFlow,
|
||||
isRunning,
|
||||
onCancelTestFlow,
|
||||
onOpenPreview,
|
||||
onHideJobStatus,
|
||||
individualStepTests = false,
|
||||
job,
|
||||
localDurationStatuses,
|
||||
suspendStatus,
|
||||
showJobStatus,
|
||||
onDelete,
|
||||
flowHasChanged
|
||||
}: Props = $props()
|
||||
|
||||
let flowModuleSchemaMap: FlowModuleSchemaMap | undefined = $state()
|
||||
|
||||
export function isNodeVisible(nodeId: string): boolean {
|
||||
return flowModuleSchemaMap?.isNodeVisible(nodeId) ?? false
|
||||
}
|
||||
|
||||
setContext<PropPickerContext>('PropPickerContext', {
|
||||
flowPropPickerConfig: writable<FlowPropPickerConfig | undefined>(undefined),
|
||||
pickablePropertiesFiltered: writable<PickableProperties | undefined>(undefined)
|
||||
@@ -114,9 +145,22 @@
|
||||
}}
|
||||
{onTestUpTo}
|
||||
{onEditInput}
|
||||
{localModuleStates}
|
||||
{aiChatOpen}
|
||||
{showFlowAiButton}
|
||||
{toggleAiChat}
|
||||
{isOwner}
|
||||
{onTestFlow}
|
||||
{isRunning}
|
||||
{onCancelTestFlow}
|
||||
{onOpenPreview}
|
||||
{onHideJobStatus}
|
||||
{individualStepTests}
|
||||
flowJob={job}
|
||||
{showJobStatus}
|
||||
{suspendStatus}
|
||||
{onDelete}
|
||||
{flowHasChanged}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -139,7 +183,12 @@
|
||||
{onDeployTrigger}
|
||||
{forceTestTab}
|
||||
{highlightArg}
|
||||
{onRunPreview}
|
||||
{onTestFlow}
|
||||
{job}
|
||||
{isOwner}
|
||||
{localDurationStatuses}
|
||||
{suspendStatus}
|
||||
onOpenDetails={onOpenPreview}
|
||||
/>
|
||||
{/if}
|
||||
</Pane>
|
||||
|
||||
@@ -6,13 +6,16 @@
|
||||
import FlowInput from './FlowInput.svelte'
|
||||
import FlowFailureModule from './FlowFailureModule.svelte'
|
||||
import FlowConstants from './FlowConstants.svelte'
|
||||
import type { FlowModule, Flow } from '$lib/gen'
|
||||
import type { FlowModule, Flow, Job } from '$lib/gen'
|
||||
import FlowPreprocessorModule from './FlowPreprocessorModule.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import { insertNewPreprocessorModule } from '../flowStateUtils.svelte'
|
||||
import TriggersEditor from '../../triggers/TriggersEditor.svelte'
|
||||
import { handleSelectTriggerFromKind, type Trigger } from '$lib/components/triggers/utils'
|
||||
import { computeMissingInputWarnings } from '../missingInputWarnings'
|
||||
import FlowResult from './FlowResult.svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { DurationStatus } from '$lib/components/graph'
|
||||
|
||||
interface Props {
|
||||
noEditor?: boolean
|
||||
@@ -27,7 +30,12 @@
|
||||
onDeployTrigger?: (trigger: Trigger) => void
|
||||
forceTestTab?: Record<string, boolean>
|
||||
highlightArg?: Record<string, string | undefined>
|
||||
onRunPreview?: () => void
|
||||
onTestFlow?: () => void
|
||||
job?: Job
|
||||
isOwner?: boolean
|
||||
localDurationStatuses?: Writable<Record<string, DurationStatus>>
|
||||
suspendStatus?: Writable<Record<string, { job: Job; nb: number }>>
|
||||
onOpenDetails?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -39,7 +47,12 @@
|
||||
onDeployTrigger = () => {},
|
||||
forceTestTab,
|
||||
highlightArg,
|
||||
onRunPreview
|
||||
onTestFlow,
|
||||
job,
|
||||
isOwner,
|
||||
localDurationStatuses,
|
||||
suspendStatus,
|
||||
onOpenDetails
|
||||
}: Props = $props()
|
||||
|
||||
const {
|
||||
@@ -84,10 +97,10 @@
|
||||
showCaptureHint.set(true)
|
||||
}}
|
||||
on:applyArgs
|
||||
{onRunPreview}
|
||||
{onTestFlow}
|
||||
/>
|
||||
{:else if $selectedId === 'Result'}
|
||||
<p class="p-4 text-secondary">The result of the flow will be the result of the last node.</p>
|
||||
<FlowResult {noEditor} {job} {isOwner} {localDurationStatuses} {suspendStatus} {onOpenDetails} />
|
||||
{:else if $selectedId === 'constants'}
|
||||
<FlowConstants {noEditor} />
|
||||
{:else if $selectedId === 'failure'}
|
||||
|
||||
@@ -46,10 +46,10 @@
|
||||
interface Props {
|
||||
noEditor: boolean
|
||||
disabled: boolean
|
||||
onRunPreview?: () => void
|
||||
onTestFlow?: () => void
|
||||
}
|
||||
|
||||
let { noEditor, disabled, onRunPreview }: Props = $props()
|
||||
let { noEditor, disabled, onTestFlow }: Props = $props()
|
||||
|
||||
const {
|
||||
flowStore,
|
||||
@@ -206,7 +206,7 @@
|
||||
if (previewArguments) {
|
||||
previewArgs.val = structuredClone($state.snapshot(previewArguments))
|
||||
}
|
||||
onRunPreview?.()
|
||||
onTestFlow?.()
|
||||
}
|
||||
|
||||
function updatePreviewSchemaAndArgs(payload: any) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import FlowPreviewResult from '$lib/components/FlowPreviewResult.svelte'
|
||||
import type { DurationStatus } from '$lib/components/graph'
|
||||
import type { Job } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
|
||||
interface Props {
|
||||
job?: Job
|
||||
isOwner?: boolean
|
||||
localDurationStatuses?: Writable<Record<string, DurationStatus>>
|
||||
suspendStatus?: Writable<Record<string, { job: Job; nb: number }>>
|
||||
noEditor: boolean
|
||||
onOpenDetails?: () => void
|
||||
}
|
||||
|
||||
let { job, isOwner, localDurationStatuses, suspendStatus, noEditor, onOpenDetails }: Props =
|
||||
$props()
|
||||
</script>
|
||||
|
||||
<FlowCard {noEditor} title="Flow result">
|
||||
{#if job && isOwner !== undefined && localDurationStatuses && suspendStatus}
|
||||
<div class="px-4 py-2">
|
||||
<FlowPreviewResult
|
||||
{job}
|
||||
workspaceId={$workspaceStore}
|
||||
{isOwner}
|
||||
hideFlowResult={false}
|
||||
hideDownloadLogs={false}
|
||||
{localDurationStatuses}
|
||||
innerModules={[]}
|
||||
{suspendStatus}
|
||||
{extra}
|
||||
hideJobId
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="p-4 text-secondary">The result of the flow will be the result of the last node.</p>
|
||||
{/if}
|
||||
</FlowCard>
|
||||
|
||||
{#snippet extra()}
|
||||
<div class="flex justify-end">
|
||||
<Button variant="border" color="light" size="xs" on:click={() => onOpenDetails?.()}
|
||||
>Open details</Button
|
||||
>
|
||||
</div>
|
||||
{/snippet}
|
||||
@@ -3,25 +3,34 @@
|
||||
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import FlowPreviewContent from '$lib/components/FlowPreviewContent.svelte'
|
||||
import type { Job } from '$lib/gen'
|
||||
import { createEventDispatcher, tick } from 'svelte'
|
||||
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { Play } from 'lucide-svelte'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import type { DurationStatus, GraphModuleState } from '$lib/components/graph'
|
||||
import { aiChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte'
|
||||
|
||||
interface Props {
|
||||
loading?: boolean
|
||||
onRunPreview?: () => void
|
||||
onJobDone?: () => void
|
||||
}
|
||||
|
||||
let { loading = false }: Props = $props()
|
||||
let { loading = false, onRunPreview, onJobDone }: Props = $props()
|
||||
|
||||
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let previewOpen = $state(false)
|
||||
|
||||
let flowPreviewContent: FlowPreviewContent | undefined = $state(undefined)
|
||||
let preventEscape = $state(false)
|
||||
let selectedJobStep: string | undefined = $state(undefined)
|
||||
let selectedJobStepIsTopLevel: boolean | undefined = $state(undefined)
|
||||
let selectedJobStepType: 'single' | 'forloop' | 'branchall' = $state('single')
|
||||
let branchOrIterationN: number = $state(0)
|
||||
let scrollTop: number = $state(0)
|
||||
let previewMode: 'upTo' | 'whole' = $state('whole')
|
||||
let upToId: string | undefined = $state(undefined)
|
||||
let previewOpen = $state(false)
|
||||
let deferContent = $state(false)
|
||||
|
||||
export async function openPreview(test: boolean = false) {
|
||||
if (!previewOpen) {
|
||||
@@ -30,29 +39,33 @@
|
||||
flowPreviewContent?.refresh()
|
||||
if (!test) return
|
||||
}
|
||||
previewMode = 'whole'
|
||||
flowPreviewContent?.test()
|
||||
}
|
||||
|
||||
export async function runPreview() {
|
||||
if (!previewOpen) {
|
||||
deferContent = true
|
||||
await tick()
|
||||
}
|
||||
previewMode = 'whole'
|
||||
flowPreviewContent?.refresh()
|
||||
flowPreviewContent?.test()
|
||||
}
|
||||
|
||||
export function cancelTest() {
|
||||
flowPreviewContent?.cancelTest()
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let flowPreviewContent: FlowPreviewContent | undefined = $state(undefined)
|
||||
let jobId: string | undefined = $state(undefined)
|
||||
let job: Job | undefined = $state(undefined)
|
||||
let preventEscape = $state(false)
|
||||
let selectedJobStep: string | undefined = $state(undefined)
|
||||
let selectedJobStepIsTopLevel: boolean | undefined = $state(undefined)
|
||||
let selectedJobStepType: 'single' | 'forloop' | 'branchall' = $state('single')
|
||||
let branchOrIterationN: number = $state(0)
|
||||
let scrollTop: number = $state(0)
|
||||
|
||||
let rightColumnSelect: 'timeline' | 'node_status' | 'node_definition' | 'user_states' =
|
||||
$state('timeline')
|
||||
|
||||
let localModuleStates: Writable<Record<string, GraphModuleState>> = $state(writable({}))
|
||||
let localDurationStatuses: Writable<Record<string, DurationStatus>> = $state(writable({}))
|
||||
|
||||
let upToDisabled = $derived(
|
||||
$selectedId == undefined ||
|
||||
let upToDisabled = $derived.by(() => {
|
||||
const upToSelected = upToId ?? $selectedId
|
||||
return (
|
||||
upToSelected == undefined ||
|
||||
[
|
||||
'settings',
|
||||
'settings-metadata',
|
||||
@@ -73,15 +86,38 @@
|
||||
'Result',
|
||||
'Input',
|
||||
'triggers'
|
||||
].includes($selectedId) ||
|
||||
$selectedId?.includes('branch') ||
|
||||
aiChatManager.flowAiChatHelpers?.getModuleAction($selectedId) === 'removed'
|
||||
)
|
||||
].includes(upToSelected) ||
|
||||
upToSelected?.includes('branch') ||
|
||||
aiChatManager.flowAiChatHelpers?.getModuleAction(upToSelected) === 'removed'
|
||||
)
|
||||
})
|
||||
|
||||
export function testUpTo() {
|
||||
export async function testUpTo(id: string | undefined, openPreview: boolean = false) {
|
||||
upToId = id
|
||||
if (upToDisabled) return
|
||||
if (openPreview) {
|
||||
previewOpen = true
|
||||
} else if (!previewOpen) {
|
||||
deferContent = true
|
||||
await tick()
|
||||
}
|
||||
previewMode = 'upTo'
|
||||
previewOpen = true
|
||||
flowPreviewContent?.refresh()
|
||||
if (!openPreview) {
|
||||
flowPreviewContent?.test()
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreviewMode() {
|
||||
return previewMode
|
||||
}
|
||||
|
||||
export function getPreviewOpen() {
|
||||
return previewOpen
|
||||
}
|
||||
|
||||
export function getFlowPreviewContent() {
|
||||
return flowPreviewContent
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -101,7 +137,7 @@
|
||||
? [
|
||||
{
|
||||
label: 'Test up to ' + $selectedId,
|
||||
onClick: testUpTo
|
||||
onClick: () => testUpTo($selectedId, true)
|
||||
}
|
||||
]
|
||||
: undefined}
|
||||
@@ -110,22 +146,20 @@
|
||||
</Button>
|
||||
|
||||
{#if !loading}
|
||||
<Drawer bind:open={previewOpen} size="75%" {preventEscape}>
|
||||
<Drawer bind:open={previewOpen} size="75%" {preventEscape} alwaysOpen={deferContent}>
|
||||
<FlowPreviewContent
|
||||
bind:this={flowPreviewContent}
|
||||
bind:localModuleStates
|
||||
bind:localDurationStatuses
|
||||
open={previewOpen}
|
||||
bind:scrollTop
|
||||
bind:previewMode
|
||||
bind:job
|
||||
bind:jobId
|
||||
bind:selectedJobStep
|
||||
bind:selectedJobStepIsTopLevel
|
||||
bind:selectedJobStepType
|
||||
bind:branchOrIterationN
|
||||
bind:rightColumnSelect
|
||||
on:close={() => {
|
||||
// keep the data in the preview content
|
||||
deferContent = true
|
||||
previewOpen = false
|
||||
}}
|
||||
on:openTriggers={(e) => {
|
||||
@@ -133,6 +167,10 @@
|
||||
dispatch('openTriggers', e.detail)
|
||||
}}
|
||||
bind:preventEscape
|
||||
{onRunPreview}
|
||||
render={previewOpen}
|
||||
{onJobDone}
|
||||
{upToId}
|
||||
/>
|
||||
</Drawer>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Loader2, Play, X } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import type { Job } from '$lib/gen'
|
||||
|
||||
interface Props {
|
||||
isRunning?: boolean
|
||||
hover?: boolean
|
||||
selected?: boolean
|
||||
individualStepTests?: boolean
|
||||
showJobStatus?: boolean
|
||||
job?: Job
|
||||
flowHasChanged?: boolean
|
||||
onTestFlow?: () => void
|
||||
onCancelTestFlow?: () => void
|
||||
onOpenPreview?: () => void
|
||||
onHideJobStatus?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
isRunning,
|
||||
hover,
|
||||
selected,
|
||||
individualStepTests,
|
||||
job,
|
||||
showJobStatus,
|
||||
flowHasChanged,
|
||||
onTestFlow,
|
||||
onCancelTestFlow,
|
||||
onOpenPreview,
|
||||
onHideJobStatus
|
||||
}: Props = $props()
|
||||
|
||||
const flowPreviewJob = $derived(showJobStatus ? job : undefined)
|
||||
const wide = $derived(hover || selected || flowPreviewJob || individualStepTests)
|
||||
</script>
|
||||
|
||||
{#if !isRunning}
|
||||
<Button
|
||||
size="sm"
|
||||
color="dark"
|
||||
btnClasses={twMerge(
|
||||
'relative p-1.5 h-[36px] transition-all duration-200',
|
||||
wide ? 'w-[120px]' : 'w-[44.5px]'
|
||||
)}
|
||||
on:click={() => {
|
||||
onTestFlow?.()
|
||||
}}
|
||||
>
|
||||
{#if isRunning}
|
||||
<Loader2 size={16} class="animate-spin" />
|
||||
{:else}
|
||||
<Play size={16} />
|
||||
{/if}
|
||||
{#if wide}
|
||||
<span transition:fade={{ duration: 100 }} class="text-xs">Test flow</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{#if wide && (flowPreviewJob || individualStepTests)}
|
||||
<div
|
||||
class="flex flex-row items-center shadow-sm rounded-md mt-1"
|
||||
in:fade={{ duration: 100, delay: 200 }}
|
||||
style={`width: ${wide ? '120px' : '44.5px'}`}
|
||||
>
|
||||
<Popover class="grow min-w-0">
|
||||
<button
|
||||
class={twMerge(
|
||||
'text-xs rounded-md rounded-r-none px-1.5 h-[24px] w-full bg-surface flex flex-row items-center gap-2 justify-center transition-all duration-200 ',
|
||||
'hover:bg-surface-hover text-gray-400 hover:text-primary'
|
||||
)}
|
||||
onclick={onOpenPreview}
|
||||
>
|
||||
{#if flowPreviewJob && !flowHasChanged}
|
||||
<div
|
||||
class={twMerge(
|
||||
'rounded-full h-2 w-2',
|
||||
'success' in flowPreviewJob && flowPreviewJob.success
|
||||
? 'bg-green-400'
|
||||
: 'bg-red-400'
|
||||
)}
|
||||
title={'success' in flowPreviewJob && flowPreviewJob.success ? 'Success' : 'Failed'}
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<span class={twMerge('text-xs truncate', flowHasChanged && 'text-orange-600')} dir="rtl">
|
||||
{!individualStepTests && !flowHasChanged && flowPreviewJob
|
||||
? flowPreviewJob.id.slice(-5)
|
||||
: '~'}
|
||||
</span>
|
||||
</button>
|
||||
{#snippet text()}
|
||||
{#if !individualStepTests && !flowHasChanged}
|
||||
See run details
|
||||
{:else}
|
||||
Open preview
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Popover>
|
||||
<button
|
||||
class="h-[24px] px-1.5 bg-surface rounded-md rounded-l-none text-gray-400 hover:bg-red-500 hover:text-white"
|
||||
title="Hide jobs status"
|
||||
onclick={onHideJobStatus}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Button
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="contained"
|
||||
btnClasses="h-[34px] w-[120px] p-1.5"
|
||||
on:click={async () => {
|
||||
onCancelTestFlow?.()
|
||||
}}
|
||||
>
|
||||
<Loader2 size={16} class="animate-spin" />
|
||||
<span transition:fade={{ duration: 100 }} class="text-xs">Cancel</span>
|
||||
</Button>
|
||||
{/if}
|
||||
@@ -41,6 +41,7 @@
|
||||
import ModuleTest from '$lib/components/ModuleTest.svelte'
|
||||
import { getStepHistoryLoaderContext } from '$lib/components/stepHistoryLoader.svelte'
|
||||
import { aiModuleActionToBgColor } from '$lib/components/copilot/chat/flow/utils'
|
||||
import type { FlowStatusModule, Job } from '$lib/gen'
|
||||
|
||||
interface Props {
|
||||
selected?: boolean
|
||||
@@ -76,7 +77,12 @@
|
||||
inputTransform?: Record<string, any> | undefined
|
||||
onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
flowJob?: Job | undefined
|
||||
isOwner?: boolean
|
||||
enableTestRun?: boolean
|
||||
type?: FlowStatusModule['type'] | undefined
|
||||
darkMode?: boolean
|
||||
skipped?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -108,7 +114,11 @@
|
||||
inputTransform,
|
||||
onUpdateMock,
|
||||
onEditInput,
|
||||
enableTestRun = false
|
||||
flowJob,
|
||||
enableTestRun = false,
|
||||
type,
|
||||
darkMode,
|
||||
skipped
|
||||
}: Props = $props()
|
||||
|
||||
let pickableIds: Record<string, any> | undefined = $state(undefined)
|
||||
@@ -193,6 +203,8 @@
|
||||
const icon_render = $derived(icon)
|
||||
|
||||
const action = $derived(getAiModuleAction(id))
|
||||
|
||||
let testRunDropdownOpen = $state(false)
|
||||
</script>
|
||||
|
||||
{#if deletable && id && editId}
|
||||
@@ -261,310 +273,320 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full module flex rounded-sm cursor-pointer max-w-full ',
|
||||
'flex relative',
|
||||
deletable ? aiModuleActionToBgColor(action) : ''
|
||||
)}
|
||||
style="width: 275px; height: 34px; background-color: {hover && bgHoverColor
|
||||
? bgHoverColor
|
||||
: bgColor};"
|
||||
onmouseenter={() => (hover = true)}
|
||||
onmouseleave={() => (hover = false)}
|
||||
onpointerdown={stopPropagation(preventDefault(() => dispatch('pointerdown')))}
|
||||
>
|
||||
{#if deletable}
|
||||
<ModuleAcceptReject {action} {id} />
|
||||
{/if}
|
||||
<div class="relative">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class={classNames(
|
||||
'absolute rounded-sm outline-offset-0 outline-slate-500 dark:outline-gray-400',
|
||||
selected ? 'outline outline-2' : 'active:outline active:outline-2'
|
||||
'w-full module flex rounded-sm cursor-pointer max-w-full',
|
||||
deletable ? aiModuleActionToBgColor(action) : ''
|
||||
)}
|
||||
style={`width: 275px; height: ${outputPickerVisible ? '51px' : '34px'};`}
|
||||
></div>
|
||||
<div
|
||||
class="absolute text-sm right-2 flex flex-row gap-1 z-10 transition-all duration-100"
|
||||
style={`bottom: ${outputPickerBarOpen ? '-38px' : '-12px'}`}
|
||||
style="width: 275px; height: 34px; background-color: {hover && bgHoverColor
|
||||
? bgHoverColor
|
||||
: bgColor};"
|
||||
onmouseenter={() => (hover = true)}
|
||||
onmouseleave={() => (hover = false)}
|
||||
onpointerdown={stopPropagation(preventDefault(() => dispatch('pointerdown')))}
|
||||
>
|
||||
{#if retry}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center rounded border bg-surface border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
{#if retries}<span class="text-red-400 mr-2">{retries}</span>{/if}
|
||||
<Repeat size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
Retries
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{#if deletable}
|
||||
<ModuleAcceptReject {action} {id} />
|
||||
{/if}
|
||||
|
||||
{#if concurrency}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center rounded border bg-surface border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
<Gauge size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
Concurrency Limits
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if cache}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center rounded border bg-surface border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
<Database size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
Cached
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if earlyStop}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
<Square size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
{isTrigger ? 'Stop early if there are no new events' : 'Early stop/break'}
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if skip}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
<SkipForward size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
Skip
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if suspend}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
<PhoneIncoming size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
Suspend
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if sleep}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
<Bed size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
Sleep
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if mock?.enabled}
|
||||
<Popover notClickable>
|
||||
<button
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
||||
onclick={() => {
|
||||
outputPicker?.toggleOpen()
|
||||
}}
|
||||
data-popover
|
||||
>
|
||||
<Pin size={12} />
|
||||
</button>
|
||||
{#snippet text()}
|
||||
Pinned
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={twMerge('flex flex-col w-full', deletable && action === 'removed' ? 'opacity-50' : '')}
|
||||
>
|
||||
<FlowModuleSchemaItemViewer
|
||||
{label}
|
||||
{path}
|
||||
{id}
|
||||
deletable={deletable && !action}
|
||||
{bold}
|
||||
bind:editId
|
||||
{hover}
|
||||
<div
|
||||
class={classNames(
|
||||
'absolute rounded-sm outline-offset-0 outline-slate-500 dark:outline-gray-400',
|
||||
selected ? 'outline outline-2' : 'active:outline active:outline-2'
|
||||
)}
|
||||
style={`width: 275px; height: ${outputPickerVisible ? '51px' : '34px'};`}
|
||||
></div>
|
||||
<div
|
||||
class="absolute text-sm right-2 flex flex-row gap-1 z-10 transition-all duration-100"
|
||||
style={`bottom: ${outputPickerBarOpen ? '-38px' : '-12px'}`}
|
||||
>
|
||||
{#snippet icon()}
|
||||
{@render icon_render?.()}
|
||||
{/snippet}
|
||||
</FlowModuleSchemaItemViewer>
|
||||
|
||||
{#if outputPickerVisible}
|
||||
<OutputPicker
|
||||
bind:this={outputPicker}
|
||||
{selected}
|
||||
{hover}
|
||||
{isConnectingCandidate}
|
||||
{historyOpen}
|
||||
{inputTransform}
|
||||
id={id ?? ''}
|
||||
bind:bottomBarOpen={outputPickerBarOpen}
|
||||
{loopStatus}
|
||||
{onEditInput}
|
||||
>
|
||||
{#snippet children({ allowCopy, isConnecting, selectConnection })}
|
||||
<OutputPickerInner
|
||||
{allowCopy}
|
||||
prefix={'results'}
|
||||
connectingData={isConnecting ? connectingData : undefined}
|
||||
{mock}
|
||||
{lastJob}
|
||||
{testJob}
|
||||
moduleId={id}
|
||||
onSelect={selectConnection}
|
||||
{onUpdateMock}
|
||||
{path}
|
||||
{loopStatus}
|
||||
rightMargin
|
||||
bind:derivedHistoryOpen={historyOpen}
|
||||
historyOffset={{ mainAxis: 12, crossAxis: -9 }}
|
||||
clazz="p-1"
|
||||
isLoading={testIsLoading ||
|
||||
(id ? stepHistoryLoader?.stepStates[id]?.loadingJobs : false)}
|
||||
initial={id ? stepHistoryLoader?.stepStates[id]?.initial : undefined}
|
||||
/>
|
||||
{/snippet}
|
||||
</OutputPicker>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if deletable && !action}
|
||||
{#if enableTestRun}
|
||||
<div
|
||||
class="absolute top-1/2 -translate-y-1/2 -translate-x-[100%] -left-[0] flex items-center w-fit px-2 h-9 min-w-14"
|
||||
>
|
||||
{#if (hover || selected) && outputPickerVisible}
|
||||
<div transition:fade={{ duration: 100 }}>
|
||||
{#if !testIsLoading}
|
||||
<Button
|
||||
size="sm"
|
||||
color="dark"
|
||||
title="Run"
|
||||
btnClasses="p-1.5"
|
||||
on:click={() => {
|
||||
outputPicker?.toggleOpen(true)
|
||||
moduleTest?.loadArgsAndRunTest()
|
||||
}}
|
||||
dropdownItems={[
|
||||
{
|
||||
label: 'Test up to here',
|
||||
onClick: () => {
|
||||
if (id) {
|
||||
onTestUpTo?.(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
dropdownBtnClasses="!w-4 px-1"
|
||||
>
|
||||
{#if testIsLoading}
|
||||
<Loader2 size={12} class="animate-spin" />
|
||||
{:else}
|
||||
<Play size={12} />
|
||||
{/if}
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="contained"
|
||||
btnClasses="!h-[25.5px] !w-[44.5px] !p-1.5 gap-0.5"
|
||||
on:click={async () => {
|
||||
moduleTest?.cancelJob()
|
||||
}}
|
||||
>
|
||||
<Loader2 size={10} class="animate-spin mr-0.5" />
|
||||
<X size={14} />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if retry}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center rounded border bg-surface border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
{#if retries}<span class="text-red-400 mr-2">{retries}</span>{/if}
|
||||
<Repeat size={12} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
class="absolute -top-[10px] -right-[10px] rounded-full h-[20px] w-[20px] trash center-center text-secondary
|
||||
{#snippet text()}
|
||||
Retries
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
{#if concurrency}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center rounded border bg-surface border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
<Gauge size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
Concurrency Limits
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if cache}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center rounded border bg-surface border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
<Database size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
Cached
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if earlyStop}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
<Square size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
{isTrigger ? 'Stop early if there are no new events' : 'Early stop/break'}
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if skip}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
<SkipForward size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
Skip
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if suspend}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
<PhoneIncoming size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
Suspend
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if sleep}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
||||
>
|
||||
<Bed size={12} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
Sleep
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if mock?.enabled}
|
||||
<Popover notClickable>
|
||||
<button
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-surface rounded border border-gray-400 text-secondary px-1 py-0.5"
|
||||
onclick={() => {
|
||||
outputPicker?.toggleOpen()
|
||||
}}
|
||||
data-popover
|
||||
>
|
||||
<Pin size={12} />
|
||||
</button>
|
||||
{#snippet text()}
|
||||
Pinned
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={twMerge('flex flex-col w-full', deletable && action === 'removed' ? 'opacity-50' : '')}
|
||||
>
|
||||
<FlowModuleSchemaItemViewer
|
||||
{label}
|
||||
{path}
|
||||
{id}
|
||||
deletable={deletable && !action}
|
||||
{bold}
|
||||
bind:editId
|
||||
{hover}
|
||||
>
|
||||
{#snippet icon()}
|
||||
{@render icon_render?.()}
|
||||
{/snippet}
|
||||
</FlowModuleSchemaItemViewer>
|
||||
|
||||
{#if outputPickerVisible}
|
||||
<OutputPicker
|
||||
bind:this={outputPicker}
|
||||
{selected}
|
||||
{hover}
|
||||
{isConnectingCandidate}
|
||||
{historyOpen}
|
||||
{inputTransform}
|
||||
id={id ?? ''}
|
||||
bind:bottomBarOpen={outputPickerBarOpen}
|
||||
{loopStatus}
|
||||
{onEditInput}
|
||||
{type}
|
||||
{darkMode}
|
||||
{skipped}
|
||||
>
|
||||
{#snippet children({ allowCopy, isConnecting, selectConnection })}
|
||||
<OutputPickerInner
|
||||
{allowCopy}
|
||||
prefix={'results'}
|
||||
connectingData={isConnecting ? connectingData : undefined}
|
||||
{mock}
|
||||
{lastJob}
|
||||
{testJob}
|
||||
moduleId={id}
|
||||
onSelect={selectConnection}
|
||||
{onUpdateMock}
|
||||
{path}
|
||||
{loopStatus}
|
||||
rightMargin
|
||||
bind:derivedHistoryOpen={historyOpen}
|
||||
historyOffset={{ mainAxis: 12, crossAxis: -9 }}
|
||||
clazz="p-1"
|
||||
isLoading={testIsLoading ||
|
||||
(id ? stepHistoryLoader?.stepStates[id]?.loadingJobs : false)}
|
||||
initial={id ? stepHistoryLoader?.stepStates[id]?.initial : undefined}
|
||||
/>
|
||||
{/snippet}
|
||||
</OutputPicker>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if deletable && !action}
|
||||
<button
|
||||
class="absolute -top-[10px] -right-[10px] rounded-full h-[20px] w-[20px] trash center-center text-secondary
|
||||
outline-[1px] outline dark:outline-gray-500 outline-gray-300 bg-surface duration-0 hover:bg-red-400 hover:text-white
|
||||
{hover || selected ? '' : '!hidden'}"
|
||||
title="Delete"
|
||||
onclick={stopPropagation(
|
||||
preventDefault((event) => dispatch('delete', { id, type: modType }))
|
||||
)}
|
||||
>
|
||||
<X class="mx-[3px]" size={12} strokeWidth={2} />
|
||||
</button>
|
||||
title="Delete"
|
||||
onclick={stopPropagation(
|
||||
preventDefault((event) => dispatch('delete', { id, type: modType }))
|
||||
)}
|
||||
>
|
||||
<X class="mx-[3px]" size={12} strokeWidth={2} />
|
||||
</button>
|
||||
|
||||
{#if id !== 'preprocessor'}
|
||||
<button
|
||||
class="absolute -top-[10px] right-[60px] rounded-full h-[20px] w-[20px] trash center-center text-secondary
|
||||
{#if id !== 'preprocessor'}
|
||||
<button
|
||||
class="absolute -top-[10px] right-[60px] rounded-full h-[20px] w-[20px] trash center-center text-secondary
|
||||
outline-[1px] outline dark:outline-gray-500 outline-gray-300 bg-surface duration-0 hover:bg-blue-400 hover:text-white
|
||||
{hover ? '' : '!hidden'}"
|
||||
onclick={stopPropagation(preventDefault((event) => dispatch('move')))}
|
||||
title="Move"
|
||||
>
|
||||
<Move class="mx-[3px]" size={12} strokeWidth={2} />
|
||||
</button>
|
||||
{/if}
|
||||
onclick={stopPropagation(preventDefault((event) => dispatch('move')))}
|
||||
title="Move"
|
||||
>
|
||||
<Move class="mx-[3px]" size={12} strokeWidth={2} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if (id && Object.values($flowInputsStore?.[id]?.flowStepWarnings || {}).length > 0) || Boolean(warningMessage)}
|
||||
<div class="absolute -top-[10px] -left-[10px]">
|
||||
<Popover>
|
||||
{#snippet text()}
|
||||
<ul class="list-disc px-2">
|
||||
{#if id}
|
||||
{#each Object.values($flowInputsStore?.[id]?.flowStepWarnings || {}) as m}
|
||||
<li>
|
||||
{m.message}
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
{/snippet}
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex items-center justify-center h-full w-full rounded-md p-0.5 border duration-0 ',
|
||||
id &&
|
||||
Object.values($flowInputsStore?.[id]?.flowStepWarnings || {})?.some(
|
||||
(x) => x.type === 'error'
|
||||
)
|
||||
? 'border-red-600 text-red-600 bg-red-100 hover:bg-red-300'
|
||||
: 'border-yellow-600 text-yellow-600 bg-yellow-100 hover:bg-yellow-300'
|
||||
)}
|
||||
>
|
||||
<AlertTriangle size={14} strokeWidth={2} />
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
{#if (id && Object.values($flowInputsStore?.[id]?.flowStepWarnings || {}).length > 0) || Boolean(warningMessage)}
|
||||
<div class="absolute -top-[10px] -left-[10px]">
|
||||
<Popover>
|
||||
{#snippet text()}
|
||||
<ul class="list-disc px-2">
|
||||
{#if id}
|
||||
{#each Object.values($flowInputsStore?.[id]?.flowStepWarnings || {}) as m}
|
||||
<li>
|
||||
{m.message}
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
{/snippet}
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex items-center justify-center h-full w-full rounded-md p-0.5 border duration-0 ',
|
||||
id &&
|
||||
Object.values($flowInputsStore?.[id]?.flowStepWarnings || {})?.some(
|
||||
(x) => x.type === 'error'
|
||||
)
|
||||
? 'border-red-600 text-red-600 bg-red-100 hover:bg-red-300'
|
||||
: 'border-yellow-600 text-yellow-600 bg-yellow-100 hover:bg-yellow-300'
|
||||
)}
|
||||
>
|
||||
<AlertTriangle size={14} strokeWidth={2} />
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if editMode && enableTestRun && flowJob?.type !== 'QueuedJob'}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="absolute top-1/2 -translate-y-1/2 -translate-x-[100%] -left-[0] flex items-center w-fit px-2 h-9 min-w-14"
|
||||
onmouseenter={() => (hover = true)}
|
||||
onmouseleave={() => (hover = false)}
|
||||
>
|
||||
{#if (hover || selected || testRunDropdownOpen) && outputPickerVisible}
|
||||
<div transition:fade={{ duration: 100 }}>
|
||||
{#if !testIsLoading}
|
||||
<Button
|
||||
size="sm"
|
||||
color="light"
|
||||
title="Run"
|
||||
variant="border"
|
||||
btnClasses="p-1.5"
|
||||
on:click={() => {
|
||||
outputPicker?.toggleOpen(true)
|
||||
moduleTest?.loadArgsAndRunTest()
|
||||
}}
|
||||
dropdownItems={[
|
||||
{
|
||||
label: 'Test up to here',
|
||||
onClick: () => {
|
||||
if (id) {
|
||||
onTestUpTo?.(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
dropdownBtnClasses="!w-4 px-1"
|
||||
bind:dropdownOpen={testRunDropdownOpen}
|
||||
>
|
||||
{#if testIsLoading}
|
||||
<Loader2 size={12} class="animate-spin" />
|
||||
{:else}
|
||||
<Play size={12} />
|
||||
{/if}
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="contained"
|
||||
btnClasses="!h-[25.5px] !w-[44.5px] !p-1.5 gap-0.5"
|
||||
on:click={async () => {
|
||||
moduleTest?.cancelJob()
|
||||
}}
|
||||
>
|
||||
<Loader2 size={10} class="animate-spin mr-0.5" />
|
||||
<X size={14} />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
pickFlow,
|
||||
insertNewPreprocessorModule
|
||||
} from '$lib/components/flows/flowStateUtils.svelte'
|
||||
import type { FlowModule, ScriptLang } from '$lib/gen'
|
||||
import type { FlowModule, Job, ScriptLang } from '$lib/gen'
|
||||
import { emptyFlowModuleState } from '../utils'
|
||||
|
||||
import { dfs } from '../dfs'
|
||||
@@ -35,6 +35,8 @@
|
||||
import { dfsByModule } from '../previousResults'
|
||||
import type { InlineScript, InsertKind } from '$lib/components/graph/graphBuilder.svelte'
|
||||
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
|
||||
import type { GraphModuleState } from '$lib/components/graph'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import FlowStickyNode from './FlowStickyNode.svelte'
|
||||
import { getStepHistoryLoaderContext } from '$lib/components/stepHistoryLoader.svelte'
|
||||
|
||||
@@ -49,9 +51,22 @@
|
||||
workspace?: string | undefined
|
||||
onTestUpTo?: ((id: string) => void) | undefined
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
localModuleStates?: Writable<Record<string, GraphModuleState>>
|
||||
aiChatOpen?: boolean
|
||||
showFlowAiButton?: boolean
|
||||
toggleAiChat?: () => void
|
||||
isOwner?: boolean
|
||||
onTestFlow?: () => void
|
||||
isRunning?: boolean
|
||||
onCancelTestFlow?: () => void
|
||||
onOpenPreview?: () => void
|
||||
onHideJobStatus?: () => void
|
||||
individualStepTests?: boolean
|
||||
flowJob?: Job | undefined
|
||||
showJobStatus?: boolean
|
||||
suspendStatus?: Writable<Record<string, { job: Job; nb: number }>>
|
||||
onDelete?: (id: string) => void
|
||||
flowHasChanged?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -65,9 +80,22 @@
|
||||
workspace = $workspaceStore,
|
||||
onTestUpTo,
|
||||
onEditInput,
|
||||
localModuleStates = writable({}),
|
||||
aiChatOpen,
|
||||
showFlowAiButton,
|
||||
toggleAiChat
|
||||
toggleAiChat,
|
||||
isOwner,
|
||||
onTestFlow,
|
||||
isRunning,
|
||||
onCancelTestFlow,
|
||||
onOpenPreview,
|
||||
onHideJobStatus,
|
||||
individualStepTests = false,
|
||||
flowJob = undefined,
|
||||
showJobStatus = false,
|
||||
suspendStatus = writable({}),
|
||||
onDelete,
|
||||
flowHasChanged
|
||||
}: Props = $props()
|
||||
|
||||
let flowTutorials: FlowTutorials | undefined = $state(undefined)
|
||||
@@ -77,6 +105,7 @@
|
||||
const { triggersCount, triggersState } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
const { flowPropPickerConfig } = getContext<PropPickerContext>('PropPickerContext')
|
||||
|
||||
export async function insertNewModuleAtIndex(
|
||||
modules: FlowModule[],
|
||||
index: number,
|
||||
@@ -223,6 +252,11 @@
|
||||
let deleteCallback: (() => void) | undefined = $state(undefined)
|
||||
let dependents: Record<string, string[]> = $state({})
|
||||
|
||||
let graph: FlowGraphV2 | undefined = $state(undefined)
|
||||
export function isNodeVisible(nodeId: string): boolean {
|
||||
return graph?.isNodeVisible(nodeId) ?? false
|
||||
}
|
||||
|
||||
function shouldRunTutorial(tutorialName: string, name: string, index: number) {
|
||||
return (
|
||||
$tutorialsToDo.includes(index) &&
|
||||
@@ -335,6 +369,7 @@
|
||||
|
||||
<div class="z-10 flex-auto grow bg-surface-secondary" bind:clientHeight={minHeight}>
|
||||
<FlowGraphV2
|
||||
bind:this={graph}
|
||||
earlyStop={flowStore.val.value?.skip_expr !== undefined}
|
||||
cache={flowStore.val.value?.cache_ttl !== undefined}
|
||||
triggerNode={customUi?.triggers != false}
|
||||
@@ -353,6 +388,13 @@
|
||||
editMode
|
||||
{onTestUpTo}
|
||||
{onEditInput}
|
||||
flowModuleStates={$localModuleStates}
|
||||
{isOwner}
|
||||
{individualStepTests}
|
||||
{flowJob}
|
||||
{showJobStatus}
|
||||
{suspendStatus}
|
||||
{flowHasChanged}
|
||||
onDelete={(id) => {
|
||||
dependents = getDependentComponents(id, flowStore.val)
|
||||
const cb = () => {
|
||||
@@ -365,6 +407,7 @@
|
||||
removeAtId(flowStore.val.value.modules, id)
|
||||
}
|
||||
refreshStateStore(flowStore)
|
||||
onDelete?.(id)
|
||||
}
|
||||
|
||||
if (Object.keys(dependents).length > 0) {
|
||||
@@ -547,6 +590,11 @@
|
||||
module.mock = $state.snapshot(detail.mock)
|
||||
refreshStateStore(flowStore)
|
||||
}}
|
||||
{onTestFlow}
|
||||
{isRunning}
|
||||
{onCancelTestFlow}
|
||||
{onOpenPreview}
|
||||
{onHideJobStatus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Button } from '$lib/components/common'
|
||||
import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte'
|
||||
import IconedResourceType from '$lib/components/IconedResourceType.svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import type { FlowModule, FlowStatusModule, Job } from '$lib/gen'
|
||||
import { Building, Repeat, Square, ArrowDown, GitBranch } from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
@@ -17,6 +17,7 @@
|
||||
} from '$lib/components/graph/graphBuilder.svelte'
|
||||
import { checkIfParentLoop } from '$lib/components/flows/utils'
|
||||
import type { FlowEditorContext } from '$lib/components/flows/types'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
interface Props {
|
||||
moduleId: string
|
||||
@@ -45,6 +46,11 @@
|
||||
mock: { enabled: boolean; return_value?: unknown }
|
||||
}) => void
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
flowJob?: Job | undefined
|
||||
isOwner?: boolean
|
||||
type?: FlowStatusModule['type'] | undefined
|
||||
darkMode?: boolean
|
||||
skipped?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -63,7 +69,12 @@
|
||||
onSelect,
|
||||
onTestUpTo,
|
||||
onUpdateMock,
|
||||
onEditInput
|
||||
onEditInput,
|
||||
flowJob,
|
||||
isOwner = false,
|
||||
type,
|
||||
darkMode,
|
||||
skipped
|
||||
}: Props = $props()
|
||||
|
||||
const { selectedId } = getContext<{
|
||||
@@ -107,12 +118,22 @@
|
||||
{/if}
|
||||
|
||||
{#if duration_ms}
|
||||
<div class="absolute z-10 right-0 -top-4 center-center text-tertiary text-2xs">
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute z-10 right-0 -top-4 center-center text-tertiary text-2xs',
|
||||
editMode ? 'text-gray-400 dark:text-gray-500 text-2xs font-normal mr-2' : ''
|
||||
)}
|
||||
>
|
||||
{msToSec(duration_ms)}s
|
||||
</div>
|
||||
{/if}
|
||||
{#if annotation && annotation != ''}
|
||||
<div class="absolute z-10 left-0 -top-5 center-center text-tertiary">
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute z-10 left-0 -top-5 center-center text-tertiary',
|
||||
editMode ? '-top-4 text-gray-400 dark:text-gray-500 text-xs font-normal' : ''
|
||||
)}
|
||||
>
|
||||
{annotation}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -160,6 +181,8 @@
|
||||
alwaysShowOutputPicker={!mod.id.startsWith('subflow:')}
|
||||
loopStatus={{ type: 'self', flow: mod.value.type }}
|
||||
{onTestUpTo}
|
||||
{type}
|
||||
{darkMode}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
@@ -181,6 +204,8 @@
|
||||
{bgColor}
|
||||
{bgHoverColor}
|
||||
{onTestUpTo}
|
||||
{type}
|
||||
{darkMode}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
@@ -202,6 +227,8 @@
|
||||
{bgColor}
|
||||
{bgHoverColor}
|
||||
{onTestUpTo}
|
||||
{type}
|
||||
{darkMode}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
@@ -246,7 +273,12 @@
|
||||
inputTransform={mod.value.type !== 'identity' ? mod.value.input_transforms : undefined}
|
||||
{onTestUpTo}
|
||||
{onEditInput}
|
||||
{flowJob}
|
||||
{isOwner}
|
||||
enableTestRun
|
||||
{type}
|
||||
{darkMode}
|
||||
{skipped}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
getAiModuleAction
|
||||
} from '$lib/components/copilot/chat/flow/ModuleAcceptReject.svelte'
|
||||
import { aiModuleActionToBgColor } from '$lib/components/copilot/chat/flow/utils'
|
||||
import FlowGraphPreviewButton from './FlowGraphPreviewButton.svelte'
|
||||
import type { Job } from '$lib/gen'
|
||||
|
||||
interface Props {
|
||||
label?: string | undefined
|
||||
@@ -25,13 +27,24 @@
|
||||
preLabel?: string | undefined
|
||||
inputJson?: Object | undefined
|
||||
prefix?: string
|
||||
alwaysPluggable?: boolean
|
||||
cache?: boolean
|
||||
earlyStop?: boolean
|
||||
editMode?: boolean
|
||||
icon?: import('svelte').Snippet
|
||||
onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
onTestFlow?: () => void
|
||||
isRunning?: boolean
|
||||
onCancelTestFlow?: () => void
|
||||
onOpenPreview?: () => void
|
||||
onHideJobStatus?: () => void
|
||||
individualStepTests?: boolean
|
||||
nodeKind?: 'input' | 'result'
|
||||
job?: Job
|
||||
type?: string
|
||||
showJobStatus?: boolean
|
||||
darkMode?: boolean
|
||||
flowHasChanged?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -47,20 +60,43 @@
|
||||
preLabel = undefined,
|
||||
inputJson = undefined,
|
||||
prefix = '',
|
||||
alwaysPluggable = false,
|
||||
nodeKind,
|
||||
cache = false,
|
||||
earlyStop = false,
|
||||
editMode = false,
|
||||
icon,
|
||||
onUpdateMock,
|
||||
onEditInput
|
||||
onEditInput,
|
||||
onTestFlow,
|
||||
isRunning,
|
||||
onCancelTestFlow,
|
||||
onOpenPreview,
|
||||
onHideJobStatus,
|
||||
individualStepTests = false,
|
||||
job,
|
||||
showJobStatus = false,
|
||||
darkMode = false,
|
||||
flowHasChanged = false
|
||||
}: Props = $props()
|
||||
|
||||
const outputPickerVisible = $derived(
|
||||
(alwaysPluggable || (inputJson && Object.keys(inputJson).length > 0)) && editMode
|
||||
(nodeKind || (inputJson && Object.keys(inputJson).length > 0)) && editMode
|
||||
)
|
||||
|
||||
let action = $derived(label === 'Input' ? getAiModuleAction(label) : undefined)
|
||||
let hoverButton = $state(false)
|
||||
|
||||
const outputType = $derived(
|
||||
showJobStatus
|
||||
? job?.type === 'QueuedJob'
|
||||
? 'InProgress'
|
||||
: job?.type === 'CompletedJob'
|
||||
? job.success
|
||||
? 'Success'
|
||||
: 'Failure'
|
||||
: undefined
|
||||
: undefined
|
||||
)
|
||||
</script>
|
||||
|
||||
<VirtualItemWrapper
|
||||
@@ -112,6 +148,8 @@
|
||||
id={id ?? ''}
|
||||
isConnectingCandidate={true}
|
||||
variant="virtual"
|
||||
type={outputType}
|
||||
{darkMode}
|
||||
>
|
||||
{#snippet children({ allowCopy, isConnecting, selectConnection })}
|
||||
<OutputPickerInner
|
||||
@@ -121,18 +159,25 @@
|
||||
onSelect={selectConnection}
|
||||
moduleId={''}
|
||||
{onUpdateMock}
|
||||
hideHeaderBar
|
||||
hideHeaderBar={nodeKind !== 'result'}
|
||||
simpleViewer={inputJson}
|
||||
rightMargin
|
||||
historyOffset={{ mainAxis: 12, crossAxis: -9 }}
|
||||
clazz="p-1"
|
||||
{onEditInput}
|
||||
selectionId={id ?? label ?? ''}
|
||||
testJob={job}
|
||||
disableMock
|
||||
disableHistory
|
||||
customEmptyJobMessage={nodeKind === 'result'
|
||||
? 'Test the flow to see results'
|
||||
: undefined}
|
||||
/>
|
||||
{/snippet}
|
||||
</OutputPicker>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="absolute text-sm right-12 -bottom-3 flex flex-row gap-1 z-10">
|
||||
{#if cache}
|
||||
<Popover notClickable>
|
||||
@@ -162,4 +207,36 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet previewButton()}
|
||||
{#if nodeKind === 'input'}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="absolute top-1/2 -translate-y-[35px] -translate-x-[100%] -left-[0] flex py-4 justify-end w-fit px-2 min-w-32"
|
||||
onmouseenter={() => {
|
||||
hoverButton = true
|
||||
}}
|
||||
onmouseleave={() => {
|
||||
hoverButton = false
|
||||
}}
|
||||
>
|
||||
{#if outputPickerVisible}
|
||||
<div transition:fade={{ duration: 100 }}>
|
||||
<FlowGraphPreviewButton
|
||||
{isRunning}
|
||||
hover={hoverButton}
|
||||
{selected}
|
||||
{onTestFlow}
|
||||
{onCancelTestFlow}
|
||||
{onOpenPreview}
|
||||
{onHideJobStatus}
|
||||
{individualStepTests}
|
||||
{job}
|
||||
{showJobStatus}
|
||||
{flowHasChanged}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</VirtualItemWrapper>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
children?: import('svelte').Snippet<[any]>
|
||||
outputPickerVisible?: boolean
|
||||
className?: string
|
||||
previewButton?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -27,7 +28,8 @@
|
||||
bgHoverColor = '',
|
||||
children,
|
||||
outputPickerVisible = false,
|
||||
className
|
||||
className,
|
||||
previewButton
|
||||
}: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
@@ -43,35 +45,39 @@
|
||||
let hover: boolean = $state(false)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class={classNames('w-full flex relative rounded-sm', onTop ? 'z-[901]' : '', className)}
|
||||
style="width: 275px; max-height: 34px; background-color: {hover && bgHoverColor && selectable
|
||||
? bgHoverColor
|
||||
: bgColor};"
|
||||
onpointerdown={() => {
|
||||
if (selectable) {
|
||||
dispatch('select', id || label || '')
|
||||
}
|
||||
}}
|
||||
onmouseenter={() => {
|
||||
hover = true
|
||||
}}
|
||||
onmouseleave={() => {
|
||||
hover = false
|
||||
}}
|
||||
title={label ? label + ' ' : ''}
|
||||
id={`flow-editor-virtual-${encodeURIComponent(label || label || '')}`}
|
||||
>
|
||||
<div class="relative">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute outline-gray-600 dark:outline-gray-400 rounded-sm',
|
||||
selected ? 'outline outline-2' : '',
|
||||
selectable ? 'cursor-pointer active:outline active:outline-2' : ''
|
||||
)}
|
||||
style={`width: 275px; height: ${outputPickerVisible ? '50px' : '34px'};`}
|
||||
class={classNames('w-full flex relative rounded-sm', onTop ? 'z-[901]' : '', className)}
|
||||
style="width: 275px; max-height: 34px; background-color: {hover && bgHoverColor && selectable
|
||||
? bgHoverColor
|
||||
: bgColor};"
|
||||
onpointerdown={() => {
|
||||
if (selectable) {
|
||||
dispatch('select', id || label || '')
|
||||
}
|
||||
}}
|
||||
onmouseenter={() => {
|
||||
hover = true
|
||||
}}
|
||||
onmouseleave={() => {
|
||||
hover = false
|
||||
}}
|
||||
title={label ? label + ' ' : ''}
|
||||
id={`flow-editor-virtual-${encodeURIComponent(label || label || '')}`}
|
||||
>
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute outline-gray-600 dark:outline-gray-400 rounded-sm',
|
||||
selected ? 'outline outline-2' : '',
|
||||
selectable ? 'cursor-pointer active:outline active:outline-2' : ''
|
||||
)}
|
||||
style={`width: 275px; height: ${outputPickerVisible ? '50px' : '34px'};`}
|
||||
>
|
||||
</div>
|
||||
{@render children?.({ hover })}
|
||||
</div>
|
||||
{@render children?.({ hover })}
|
||||
|
||||
{@render previewButton?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<script lang="ts">
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { getContext } from 'svelte'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { PropPickerContext } from '$lib/components/prop_picker'
|
||||
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
|
||||
import InputPickerInner from './InputPickerInner.svelte'
|
||||
import { ChevronDown, Plug } from 'lucide-svelte'
|
||||
import { useSvelteFlow } from '@xyflow/svelte'
|
||||
import { getStateColor } from '$lib/components/graph/util'
|
||||
import type { FlowStatusModule } from '$lib/gen'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
|
||||
interface Props {
|
||||
selected?: boolean
|
||||
@@ -22,6 +25,9 @@
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
initial?: boolean
|
||||
onResetInitial?: () => void
|
||||
type: FlowStatusModule['type'] | undefined
|
||||
darkMode?: boolean
|
||||
skipped?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -35,7 +41,10 @@
|
||||
id,
|
||||
bottomBarOpen = $bindable(false),
|
||||
loopStatus,
|
||||
onEditInput
|
||||
onEditInput,
|
||||
type,
|
||||
darkMode,
|
||||
skipped
|
||||
}: Props = $props()
|
||||
|
||||
const context = getContext<PropPickerContext>('PropPickerContext')
|
||||
@@ -104,6 +113,18 @@
|
||||
$effect(() => {
|
||||
updatePositioning(historyOpen, zoom)
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
let { outputPickerOpenFns } = getContext<FlowEditorContext>('FlowEditorContext') || {}
|
||||
if (outputPickerOpenFns) {
|
||||
outputPickerOpenFns[id] = () => {
|
||||
outputOpen = true
|
||||
}
|
||||
return () => {
|
||||
delete outputPickerOpenFns[id]
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -127,6 +148,9 @@
|
||||
'h-1 hover:h-[20px]',
|
||||
bottomBarOpen && 'h-[20px]'
|
||||
)}
|
||||
style:background-color={type && type !== 'WaitingForEvents'
|
||||
? getStateColor(type, !!darkMode, true, skipped)
|
||||
: undefined}
|
||||
data-prop-picker
|
||||
>
|
||||
<div class="flex flex-row items-center justify-center w-full h-full">
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
selectionId?: string
|
||||
initial?: boolean
|
||||
onResetInitial?: () => void
|
||||
customEmptyJobMessage?: string
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -101,7 +102,8 @@
|
||||
onUpdateMock,
|
||||
onEditInput,
|
||||
selectionId,
|
||||
initial
|
||||
initial,
|
||||
customEmptyJobMessage
|
||||
}: Props = $props()
|
||||
|
||||
type SelectedJob =
|
||||
@@ -701,7 +703,7 @@
|
||||
{:else if !job}
|
||||
<div class="flex flex-col items-center justify-center h-full">
|
||||
<p class="text-xs text-secondary">
|
||||
Test this step to see results{#if !disableMock}
|
||||
{customEmptyJobMessage ?? 'Test this step to see results'}{#if !disableMock}
|
||||
{' or'}
|
||||
<button
|
||||
class="text-blue-500 hover:text-blue-700 underline"
|
||||
|
||||
@@ -8,6 +8,7 @@ import type Editor from '../Editor.svelte'
|
||||
import type SimpleEditor from '../SimpleEditor.svelte'
|
||||
import type { StateStore } from '$lib/utils'
|
||||
import type { TestSteps } from './testSteps.svelte'
|
||||
import type { ModulesTestStates } from '../modulesTest.svelte'
|
||||
|
||||
export type FlowInput = Record<
|
||||
string,
|
||||
@@ -78,4 +79,6 @@ export type FlowEditorContext = {
|
||||
customUi: FlowBuilderWhitelabelCustomUi
|
||||
insertButtonOpen: Writable<boolean>
|
||||
executionCount: Writable<number>
|
||||
modulesTestStates: ModulesTestStates
|
||||
outputPickerOpenFns: Record<string, () => void>
|
||||
}
|
||||
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { cleanExpr, emptySchema } from '$lib/utils'
|
||||
import { get } from 'svelte/store'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
import type { FlowModuleState } from './flowState'
|
||||
import { type PickableProperties, dfs } from './previousResults'
|
||||
import { NEVER_TESTED_THIS_FAR } from './models'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { ExtendedOpenFlow } from './types'
|
||||
import type { GraphModuleState } from '../graph'
|
||||
import type { ModulesTestStates } from '../modulesTest.svelte'
|
||||
|
||||
function create_context_function_template(eval_string: string, context: Record<string, any>) {
|
||||
return `
|
||||
@@ -195,3 +197,50 @@ export function checkIfParentLoop(
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates moduleStates based on test job data from modulesTestStates
|
||||
* Extracts job information and converts it to GraphModuleState format
|
||||
* for graph rendering
|
||||
*/
|
||||
export function updateDerivedModuleStatesFromTestJobs(
|
||||
moduleId: string | undefined,
|
||||
moduleTestStates: ModulesTestStates | undefined,
|
||||
moduleStates: Writable<Record<string, GraphModuleState>> | undefined
|
||||
) {
|
||||
if (!moduleId || !moduleTestStates || !moduleStates) {
|
||||
return
|
||||
}
|
||||
const newStates: Record<string, GraphModuleState> = {}
|
||||
|
||||
const testState = moduleTestStates?.states[moduleId]
|
||||
if (testState) {
|
||||
if (testState.testJob) {
|
||||
const job = testState.testJob
|
||||
|
||||
// Create GraphModuleState from job data
|
||||
const moduleState: GraphModuleState = {
|
||||
args: job.args,
|
||||
type: job.type === 'QueuedJob' ? 'InProgress' : job['success'] ? 'Success' : 'Failure',
|
||||
job_id: job.id,
|
||||
tag: job.tag,
|
||||
duration_ms: job['duration_ms'],
|
||||
started_at: job.started_at ? new Date(job.started_at).getTime() : undefined
|
||||
}
|
||||
|
||||
newStates[moduleId] = moduleState
|
||||
} else if (testState.loading) {
|
||||
// If test is loading, show as InProgress
|
||||
newStates[moduleId] = {
|
||||
type: 'InProgress',
|
||||
args: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the store with test job states
|
||||
moduleStates.update((currentStates) => ({
|
||||
...currentStates,
|
||||
...newStates
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { FlowService, type FlowModule } from '../../gen'
|
||||
import { FlowService, type FlowModule, type Job } from '../../gen'
|
||||
import { NODE, type GraphModuleState } from '.'
|
||||
import { getContext, onDestroy, setContext, tick, untrack } from 'svelte'
|
||||
|
||||
@@ -86,6 +86,12 @@
|
||||
editMode?: boolean
|
||||
allowSimplifiedPoll?: boolean
|
||||
expandedSubflows?: Record<string, FlowModule[]>
|
||||
isOwner?: boolean
|
||||
isRunning?: boolean
|
||||
individualStepTests?: boolean
|
||||
flowJob?: Job | undefined
|
||||
showJobStatus?: boolean
|
||||
suspendStatus?: Writable<Record<string, { job: Job; nb: number }>>
|
||||
onDelete?: (id: string) => void
|
||||
onInsert?: (detail: {
|
||||
sourceId?: string
|
||||
@@ -108,6 +114,11 @@
|
||||
onTestUpTo?: ((id: string) => void) | undefined
|
||||
onSelectedIteration?: onSelectedIteration
|
||||
onEditInput?: (moduleId: string, key: string) => void
|
||||
onTestFlow?: () => void
|
||||
onCancelTestFlow?: () => void
|
||||
onOpenPreview?: () => void
|
||||
onHideJobStatus?: () => void
|
||||
flowHasChanged?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -146,7 +157,18 @@
|
||||
allowSimplifiedPoll = true,
|
||||
expandedSubflows = $bindable({}),
|
||||
onTestUpTo = undefined,
|
||||
onEditInput = undefined
|
||||
onEditInput = undefined,
|
||||
isOwner = false,
|
||||
onTestFlow = undefined,
|
||||
isRunning = false,
|
||||
onCancelTestFlow = undefined,
|
||||
onOpenPreview = undefined,
|
||||
onHideJobStatus = undefined,
|
||||
individualStepTests = false,
|
||||
flowJob = undefined,
|
||||
showJobStatus = false,
|
||||
suspendStatus = writable({}),
|
||||
flowHasChanged = false
|
||||
}: Props = $props()
|
||||
|
||||
setContext<{
|
||||
@@ -308,6 +330,18 @@
|
||||
},
|
||||
editInput: (moduleId: string, key: string) => {
|
||||
onEditInput?.(moduleId, key)
|
||||
},
|
||||
testFlow: () => {
|
||||
onTestFlow?.()
|
||||
},
|
||||
cancelTestFlow: () => {
|
||||
onCancelTestFlow?.()
|
||||
},
|
||||
openPreview: () => {
|
||||
onOpenPreview?.()
|
||||
},
|
||||
hideJobStatus: () => {
|
||||
onHideJobStatus?.()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,7 +434,14 @@
|
||||
newFlow,
|
||||
cache,
|
||||
earlyStop,
|
||||
editMode
|
||||
editMode,
|
||||
isOwner,
|
||||
isRunning,
|
||||
individualStepTests,
|
||||
flowJob,
|
||||
showJobStatus,
|
||||
suspendStatus,
|
||||
flowHasChanged
|
||||
},
|
||||
failureModule,
|
||||
preprocessorModule,
|
||||
@@ -445,6 +486,11 @@
|
||||
}
|
||||
}, 10)
|
||||
})
|
||||
|
||||
let viewportResizer: ViewportResizer | undefined = $state(undefined)
|
||||
export function isNodeVisible(nodeId: string): boolean {
|
||||
return viewportResizer?.isNodeVisible(nodeId) ?? false
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if insertable}
|
||||
@@ -467,7 +513,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<SvelteFlowProvider>
|
||||
<ViewportResizer {width} />
|
||||
<ViewportResizer {height} {width} {nodes} bind:this={viewportResizer} />
|
||||
<SvelteFlow
|
||||
onpaneclick={(e) => {
|
||||
document.dispatchEvent(new Event('focus'))
|
||||
@@ -489,7 +535,7 @@
|
||||
nodesDraggable={false}
|
||||
--background-color={false}
|
||||
>
|
||||
<div class="absolute inset-0 !bg-surface-secondary"></div>
|
||||
<div class="absolute inset-0 !bg-surface-secondary h-full"></div>
|
||||
<Controls position="top-right" orientation="horizontal" showLock={false}>
|
||||
{#if download}
|
||||
<ControlButton
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { useSvelteFlow } from '@xyflow/svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { NODE } from './util'
|
||||
|
||||
let { width } = $props()
|
||||
let { width, nodes, height } = $props()
|
||||
const { setViewport, getViewport } = useSvelteFlow()
|
||||
|
||||
$effect(() => {
|
||||
@@ -21,4 +22,42 @@
|
||||
x: viewport.x + diff / 2
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node is fully visible in the current viewport with margin
|
||||
* @param nodeId - The ID of the node to check
|
||||
* @returns boolean - true if node is fully visible with 20px margin, false otherwise
|
||||
*/
|
||||
export function isNodeVisible(nodeId: string): boolean {
|
||||
const node = nodes.find((n) => n.id === nodeId)
|
||||
const viewport = getViewport()
|
||||
if (!node || !viewport) return false
|
||||
|
||||
const { x, y, zoom } = viewport
|
||||
const nodeX = node.position.x
|
||||
const nodeY = node.position.y
|
||||
|
||||
// 20px margin scaled by zoom level
|
||||
const margin = 20 / zoom
|
||||
|
||||
// Calculate viewport bounds with margin
|
||||
const viewportLeft = -x / zoom + margin
|
||||
const viewportTop = -y / zoom + margin
|
||||
const viewportRight = viewportLeft + width / zoom - 2 * margin
|
||||
const viewportBottom = viewportTop + height / zoom - 2 * margin
|
||||
|
||||
// Calculate node bounds
|
||||
const nodeLeft = nodeX
|
||||
const nodeTop = nodeY
|
||||
const nodeRight = nodeX + NODE.width
|
||||
const nodeBottom = nodeY + NODE.height
|
||||
|
||||
// Node is visible only if it's completely within viewport bounds with margin
|
||||
return (
|
||||
nodeLeft > viewportLeft &&
|
||||
nodeRight < viewportRight &&
|
||||
nodeTop > viewportTop &&
|
||||
nodeBottom < viewportBottom
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { FlowModule, RawScript, Script } from '$lib/gen'
|
||||
import type { FlowModule, Job, RawScript, Script } from '$lib/gen'
|
||||
import { type Edge } from '@xyflow/svelte'
|
||||
import { getDependeeAndDependentComponents } from '../flows/flowExplorer'
|
||||
import { dfsByModule } from '../flows/previousResults'
|
||||
import { defaultIfEmptyString } from '$lib/utils'
|
||||
import type { GraphModuleState } from './model'
|
||||
import type { Writable } from 'svelte/store'
|
||||
|
||||
export type InsertKind =
|
||||
| 'script'
|
||||
@@ -54,6 +55,10 @@ export type GraphEventHandlers = {
|
||||
updateMock: (detail: { mock: FlowModule['mock']; id: string }) => void
|
||||
testUpTo: (id: string) => void
|
||||
editInput: (moduleId: string, key: string) => void
|
||||
testFlow: () => void
|
||||
cancelTestFlow: () => void
|
||||
openPreview: () => void
|
||||
hideJobStatus: () => void
|
||||
}
|
||||
|
||||
export type SimplifiableFlow = { simplifiedFlow: boolean }
|
||||
@@ -102,6 +107,11 @@ export type InputN = {
|
||||
cache: boolean
|
||||
earlyStop: boolean
|
||||
editMode: boolean
|
||||
isRunning: boolean
|
||||
individualStepTests: boolean
|
||||
flowJob: Job | undefined
|
||||
showJobStatus: boolean
|
||||
flowHasChanged: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +127,8 @@ export type ModuleN = {
|
||||
flowModuleStates: Record<string, GraphModuleState> | undefined
|
||||
insertable: boolean
|
||||
editMode: boolean
|
||||
flowJob: Job | undefined
|
||||
isOwner: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +186,9 @@ export type ResultN = {
|
||||
data: {
|
||||
success: boolean | undefined
|
||||
eventHandlers: GraphEventHandlers
|
||||
editMode: boolean
|
||||
job: Job | undefined
|
||||
showJobStatus: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +298,13 @@ export function graphBuilder(
|
||||
cache: boolean
|
||||
earlyStop: boolean
|
||||
editMode: boolean
|
||||
isOwner: boolean
|
||||
isRunning: boolean
|
||||
individualStepTests: boolean
|
||||
flowJob: Job | undefined
|
||||
showJobStatus: boolean
|
||||
suspendStatus: Writable<Record<string, { job: Job; nb: number }>>
|
||||
flowHasChanged: boolean
|
||||
},
|
||||
failureModule: FlowModule | undefined,
|
||||
preprocessorModule: FlowModule | undefined,
|
||||
@@ -333,7 +355,9 @@ export function graphBuilder(
|
||||
moving: moving,
|
||||
flowModuleStates: extra.flowModuleStates,
|
||||
insertable: extra.insertable,
|
||||
editMode: extra.editMode
|
||||
editMode: extra.editMode,
|
||||
isOwner: extra.isOwner,
|
||||
flowJob: extra.flowJob
|
||||
},
|
||||
type: 'module'
|
||||
})
|
||||
@@ -433,7 +457,12 @@ export function graphBuilder(
|
||||
disableAi: extra.disableAi,
|
||||
cache: extra.cache,
|
||||
earlyStop: extra.earlyStop,
|
||||
editMode: extra.editMode
|
||||
editMode: extra.editMode,
|
||||
isRunning: extra.isRunning,
|
||||
individualStepTests: extra.individualStepTests,
|
||||
flowJob: extra.flowJob,
|
||||
showJobStatus: extra.showJobStatus,
|
||||
flowHasChanged: extra.flowHasChanged
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +496,10 @@ export function graphBuilder(
|
||||
id: 'result',
|
||||
data: {
|
||||
eventHandlers: eventHandlers,
|
||||
success: success
|
||||
success: success,
|
||||
editMode: extra.editMode,
|
||||
job: extra.flowJob,
|
||||
showJobStatus: extra.showJobStatus
|
||||
},
|
||||
type: 'result'
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<script lang="ts">
|
||||
import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte'
|
||||
import { getBezierPath, BaseEdge, type EdgeProps, EdgeLabel } from '@xyflow/svelte'
|
||||
import { ClipboardCopy } from 'lucide-svelte'
|
||||
import { ClipboardCopy, Hourglass } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { GraphEventHandlers } from '../../graphBuilder.svelte'
|
||||
import { getStraightLinePath } from '../utils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import FlowStatusWaitingForEvents from '$lib/components/FlowStatusWaitingForEvents.svelte'
|
||||
import type { Job } from '$lib/gen'
|
||||
import type { GraphModuleState } from '../../model'
|
||||
|
||||
const { useDataflow } = getContext<{
|
||||
useDataflow: Writable<boolean | undefined>
|
||||
@@ -35,6 +39,10 @@
|
||||
enableTrigger: boolean
|
||||
disableAi: boolean
|
||||
disableMoveIds: string[]
|
||||
flowModuleStates: Record<string, GraphModuleState> | undefined
|
||||
isOwner: boolean
|
||||
flowJob: Job | undefined
|
||||
suspendStatus?: Writable<Record<string, { job: Job; nb: number }>>
|
||||
}
|
||||
} = $props()
|
||||
|
||||
@@ -55,10 +63,21 @@
|
||||
? `${edgePath} ${getStraightLinePath({ sourceX, sourceY, targetY })}`
|
||||
: edgePath
|
||||
)
|
||||
|
||||
// TODO: this is a hack to show the waiting for events indicator on the edge a proper way would be to have a edge state
|
||||
// and handle the edge state in the graph builder
|
||||
let waitingForEvents = $derived(
|
||||
data?.flowModuleStates?.[data.targetId]?.type === 'WaitingForEvents' ||
|
||||
data?.flowModuleStates?.[`${data.sourceId}-v`]?.type === 'WaitingForEvents'
|
||||
)
|
||||
|
||||
const suspendStatus: Writable<Record<string, { job: Job; nb: number }>> | undefined = $derived(
|
||||
data?.suspendStatus
|
||||
)
|
||||
</script>
|
||||
|
||||
<EdgeLabel x={sourceX} y={sourceY + 28} class="base-edge" style="">
|
||||
{#if data?.insertable && !$useDataflow && !data?.moving}
|
||||
{#if data?.insertable && !$useDataflow && !data?.moving && !waitingForEvents}
|
||||
<div
|
||||
class={twMerge('edgeButtonContainer nodrag nopan top-0')}
|
||||
style:transform="translate(-50%, -50%)"
|
||||
@@ -128,6 +147,42 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if waitingForEvents && data.flowJob && data.flowJob.type === 'QueuedJob'}
|
||||
<div
|
||||
class="px-2 py-0.5 rounded-md bg-surface shadow-md text-violet-700 dark:text-violet-400 text-xs flex items-center gap-1"
|
||||
>
|
||||
<Hourglass size={12} />
|
||||
<div class="flex">
|
||||
<span class="dot">.</span>
|
||||
<span class="dot">.</span>
|
||||
<span class="dot">.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class={'fixed top-1/2 -translate-y-1/2 left-[170px] h-fit w-fit rounded-md bg-surface flex items-center justify-center p-2 ml-2 shadow-md'}
|
||||
>
|
||||
{#if data?.flowJob && data.flowJob.flow_status?.modules?.[data.flowJob.flow_status?.step]?.type === 'WaitingForEvents'}
|
||||
<FlowStatusWaitingForEvents
|
||||
job={data.flowJob}
|
||||
workspaceId={$workspaceStore!}
|
||||
isOwner={data.isOwner}
|
||||
light
|
||||
/>
|
||||
{:else if $suspendStatus && Object.keys($suspendStatus).length > 0}
|
||||
<div class="flex gap-2 flex-col">
|
||||
{#each Object.values($suspendStatus) as suspendCount (suspendCount.job.id)}
|
||||
<FlowStatusWaitingForEvents
|
||||
job={suspendCount.job}
|
||||
workspaceId={$workspaceStore!}
|
||||
isOwner={data.isOwner}
|
||||
light
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</EdgeLabel>
|
||||
|
||||
<BaseEdge path={completeEdge} {markerEnd} class={$useDataflow ? 'hidden' : ''} />
|
||||
@@ -140,4 +195,28 @@
|
||||
/* if you have an interactive element, set pointer-events: all */
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.dot {
|
||||
opacity: 0;
|
||||
animation: dotFade 1.5s infinite;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.dot:nth-child(2) {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.dot:nth-child(3) {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes dotFade {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -72,11 +72,29 @@
|
||||
}}
|
||||
inputJson={topFlowInput}
|
||||
prefix="flow_input"
|
||||
alwaysPluggable
|
||||
nodeKind="input"
|
||||
cache={data.cache}
|
||||
earlyStop={data.earlyStop}
|
||||
editMode={data.editMode}
|
||||
onEditInput={data.eventHandlers.editInput}
|
||||
onTestFlow={() => {
|
||||
data.eventHandlers.testFlow()
|
||||
}}
|
||||
isRunning={data.isRunning}
|
||||
onCancelTestFlow={() => {
|
||||
data.eventHandlers.cancelTestFlow()
|
||||
}}
|
||||
onOpenPreview={() => {
|
||||
data.eventHandlers.openPreview()
|
||||
}}
|
||||
onHideJobStatus={() => {
|
||||
data.eventHandlers.hideJobStatus()
|
||||
}}
|
||||
individualStepTests={data.individualStepTests}
|
||||
job={data.flowJob}
|
||||
showJobStatus={data.showJobStatus}
|
||||
flowHasChanged={data.flowHasChanged}
|
||||
{darkMode}
|
||||
/>
|
||||
{/snippet}
|
||||
</NodeWrapper>
|
||||
|
||||
@@ -65,8 +65,18 @@
|
||||
'/' +
|
||||
(moduleState?.iteration_total ?? '?')
|
||||
: ''}
|
||||
bgColor={getStateColor(type, darkMode, true, moduleState?.skipped)}
|
||||
bgHoverColor={getStateHoverColor(type, darkMode, true, moduleState?.skipped)}
|
||||
bgColor={getStateColor(
|
||||
data.editMode ? undefined : type,
|
||||
darkMode,
|
||||
true,
|
||||
moduleState?.skipped
|
||||
)}
|
||||
bgHoverColor={getStateHoverColor(
|
||||
data.editMode ? undefined : type,
|
||||
darkMode,
|
||||
true,
|
||||
moduleState?.skipped
|
||||
)}
|
||||
moving={data.moving}
|
||||
duration_ms={moduleState?.duration_ms}
|
||||
retries={moduleState?.retries}
|
||||
@@ -94,6 +104,11 @@
|
||||
data.eventHandlers.updateMock(detail)
|
||||
}}
|
||||
onEditInput={data.eventHandlers.editInput}
|
||||
flowJob={data.flowJob}
|
||||
isOwner={data.isOwner}
|
||||
{type}
|
||||
{darkMode}
|
||||
skipped={moduleState?.skipped}
|
||||
/>
|
||||
|
||||
<div class="absolute -bottom-10 left-1/2 transform -translate-x-1/2 z-10">
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
const { selectedId } = getContext<{
|
||||
selectedId: Writable<string | undefined>
|
||||
}>('FlowGraphContext')
|
||||
|
||||
const type = $derived(
|
||||
data.success == undefined ? undefined : data.success ? 'Success' : 'Failure'
|
||||
)
|
||||
</script>
|
||||
|
||||
<NodeWrapper enableSourceHandle={false}>
|
||||
@@ -25,17 +29,17 @@
|
||||
selectable={true}
|
||||
selected={$selectedId === 'Result'}
|
||||
hideId={true}
|
||||
bgColor={getStateColor(
|
||||
data.success == undefined ? undefined : data.success ? 'Success' : 'Failure',
|
||||
darkMode
|
||||
)}
|
||||
bgHoverColor={getStateHoverColor(
|
||||
data.success == undefined ? undefined : data.success ? 'Success' : 'Failure',
|
||||
darkMode
|
||||
)}
|
||||
bgColor={getStateColor(type, darkMode)}
|
||||
bgHoverColor={getStateHoverColor(type, darkMode)}
|
||||
on:select={(e) => {
|
||||
setTimeout(() => data?.eventHandlers?.select(e.detail))
|
||||
}}
|
||||
nodeKind="result"
|
||||
editMode={data.editMode}
|
||||
job={data.job}
|
||||
{type}
|
||||
showJobStatus={data.showJobStatus}
|
||||
{darkMode}
|
||||
/>
|
||||
{/snippet}
|
||||
</NodeWrapper>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Job } from '$lib/gen'
|
||||
|
||||
type moduleTestState = {
|
||||
loading: boolean
|
||||
cancel?: () => Promise<void>
|
||||
testJob?: Job
|
||||
}
|
||||
|
||||
export class ModulesTestStates {
|
||||
states: Record<string, moduleTestState> = $state({})
|
||||
runTestCb?: (moduleId: string) => void
|
||||
|
||||
constructor(runTestCb?: (moduleId: string) => void) {
|
||||
this.states = {}
|
||||
this.runTestCb = runTestCb
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,24 @@
|
||||
|
||||
import JobStatus from '../JobStatus.svelte'
|
||||
import { ExternalLinkIcon } from 'lucide-svelte'
|
||||
import type { FlowStatusViewerContext } from '../graph'
|
||||
import { getContext } from 'svelte'
|
||||
import { truncateRev } from '$lib/utils'
|
||||
export let job: QueuedJob | CompletedJob
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
let { hideJobId } = getContext<FlowStatusViewerContext>('FlowStatusViewer')
|
||||
interface Props {
|
||||
job: QueuedJob | CompletedJob
|
||||
hideJobId?: boolean
|
||||
extra?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
let { job, hideJobId = false, extra }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-1 text-tertiary dark:text-gray-400">
|
||||
<div
|
||||
class={twMerge(
|
||||
'grid grid-cols-2 gap-4 mb-1 text-tertiary dark:text-gray-400',
|
||||
extra && job && !hideJobId ? 'grid-cols-3' : 'grid-cols-2'
|
||||
)}
|
||||
>
|
||||
<JobStatus {job} />
|
||||
{#if job && !hideJobId}
|
||||
<div>
|
||||
@@ -29,4 +38,5 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{@render extra?.()}
|
||||
</div>
|
||||
|
||||
@@ -379,6 +379,7 @@ export function pointerDownOutside(
|
||||
event.stopPropagation()
|
||||
}
|
||||
node.dispatchEvent(new CustomEvent<PointerEvent>('pointerdown_outside', { detail: event }))
|
||||
if (typeof options === 'object') options.onClickOutside?.(event)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import type { PickableProperties } from '$lib/components/flows/previousResults'
|
||||
import { Triggers } from '$lib/components/triggers/triggers.svelte'
|
||||
import { TestSteps } from '$lib/components/flows/testSteps.svelte'
|
||||
import { ModulesTestStates } from '$lib/components/modulesTest.svelte'
|
||||
|
||||
let token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
let workspace = $page.url.searchParams.get('workspace') ?? undefined
|
||||
@@ -106,7 +107,9 @@
|
||||
editPanelSize: undefined,
|
||||
payloadData: undefined
|
||||
}),
|
||||
currentEditor: writable(undefined)
|
||||
currentEditor: writable(undefined),
|
||||
modulesTestStates: new ModulesTestStates(),
|
||||
outputPickerOpenFns: {}
|
||||
})
|
||||
setContext<PropPickerContext>('PropPickerContext', {
|
||||
flowPropPickerConfig: writable<FlowPropPickerConfig | undefined>(undefined),
|
||||
|
||||
Reference in New Issue
Block a user