mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 00:01:34 +00:00
fix(frontend): Improve flow detail page (#7647)
* improve flow detail page * Do not display seconds for last edit * expand graph when possible * nit * make flowGraph min height reactive * Add flow graph tab when chat mode enabled * improve script detail layout * nit * nit * Update frontend/src/lib/components/TimeAgo.svelte Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Update frontend/src/lib/components/TimeAgo.svelte Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Update frontend/src/lib/components/TimeAgo.svelte Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Update frontend/src/lib/components/TimeAgo.svelte Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * nit --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
@@ -24,6 +24,8 @@
|
||||
export let triggerNode = false
|
||||
export let stepDetail: FlowModule | string | undefined = undefined
|
||||
export let workspace: string | undefined = $workspaceStore
|
||||
export let minHeight = 400
|
||||
export let noBorder = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
@@ -31,8 +33,9 @@
|
||||
<div class="grid grid-cols-3 w-full h-full">
|
||||
{#if !noGraph}
|
||||
<div
|
||||
class="{noSide ? 'col-span-3' : 'sm:col-span-2 col-span-3'} w-full border max-h-full"
|
||||
class="{noSide ? 'col-span-3' : 'sm:col-span-2 col-span-3'} w-full max-h-full"
|
||||
class:overflow-auto={overflowAuto}
|
||||
class:border={!noBorder}
|
||||
>
|
||||
<FlowGraphV2
|
||||
{triggerNode}
|
||||
@@ -40,7 +43,7 @@
|
||||
cache={flow.value.cache_ttl !== undefined}
|
||||
path={flow?.path}
|
||||
{download}
|
||||
minHeight={400}
|
||||
{minHeight}
|
||||
{workspace}
|
||||
modules={flow?.value?.modules}
|
||||
failureModule={flow?.value?.failure_module}
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
schedulable?: boolean
|
||||
detailed?: boolean
|
||||
autofocus?: boolean
|
||||
topButton?: boolean
|
||||
loading?: boolean
|
||||
noVariablePicker?: boolean
|
||||
viewKeybinding?: boolean
|
||||
@@ -82,7 +81,6 @@
|
||||
schedulable = true,
|
||||
detailed = true,
|
||||
autofocus = false,
|
||||
topButton = false,
|
||||
loading = false,
|
||||
noVariablePicker = false,
|
||||
viewKeybinding = false,
|
||||
@@ -226,17 +224,7 @@
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if topButton}
|
||||
<Button
|
||||
btnClasses="!px-6 !py-1 w-full"
|
||||
disabled={!isValid && !jsonView}
|
||||
on:click={() => runAction(undefined, args ?? {}, invisible_to_owner, overrideTag)}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if runnable?.schema}
|
||||
<div class="my-2"></div>
|
||||
{#if jsonView}
|
||||
<div
|
||||
class="py-2"
|
||||
@@ -255,7 +243,7 @@
|
||||
/>
|
||||
</div>
|
||||
{:else if !runnable.schema.properties || Object.keys(runnable.schema.properties).length === 0}
|
||||
<div class="text-sm py-4 italic">No arguments</div>
|
||||
<div class="text-sm italic">{`This ${runnable.kind ?? 'runnable'} takes no arguments`}</div>
|
||||
{:else}
|
||||
{#key reloadArgs}
|
||||
<div bind:clientHeight={schemaHeight}>
|
||||
@@ -279,13 +267,13 @@
|
||||
<div class="text-xs text-primary">No arguments</div>
|
||||
{/if}
|
||||
{#if schedulable}
|
||||
<div class="mt-10"></div>
|
||||
<div class="flex gap-2 items-start flex-wrap justify-between mt-2 md:mt-6 mb-6">
|
||||
<div class="flex gap-2 items-start flex-wrap justify-between mt-2 md:mt-6">
|
||||
<div class="flex-row-reverse flex-wrap flex w-full gap-4">
|
||||
<Button
|
||||
{loading}
|
||||
variant="accent"
|
||||
btnClasses="!px-6 !py-1 !h-8 inline-flex gap-2"
|
||||
unifiedSize="md"
|
||||
btnClasses="!inline-flex"
|
||||
disabled={!isValid && !jsonView}
|
||||
on:click={() => runAction(scheduledForStr, args ?? {}, invisible_to_owner, overrideTag)}
|
||||
shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }}
|
||||
@@ -295,7 +283,7 @@
|
||||
<div>
|
||||
<Popover placement="bottom" closeButton usePointerDownOutside>
|
||||
{#snippet trigger()}
|
||||
<Button nonCaptureEvent startIcon={{ icon: Calendar }} size="xs" color="light">
|
||||
<Button nonCaptureEvent startIcon={{ icon: Calendar }} unifiedSize="md" color="light">
|
||||
Advanced
|
||||
</Button>
|
||||
{/snippet}
|
||||
@@ -321,9 +309,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if !topButton}
|
||||
{:else}
|
||||
<Button
|
||||
btnClasses="!px-6 !py-1 w-full"
|
||||
variant="accent"
|
||||
disabled={!isValid && !jsonView}
|
||||
on:click={() => runAction(undefined, args ?? {}, invisible_to_owner, overrideTag)}
|
||||
shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }}
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="w-full flex flex-col gap-1 h-full overflow-y-auto">
|
||||
<div class="w-full flex flex-col gap-1 h-full overflow-y-auto pb-4">
|
||||
{#if !noButton}
|
||||
<div>
|
||||
<Popover class="w-full" placement="bottom" disablePopup={runnableId && previewArgs}>
|
||||
|
||||
@@ -81,41 +81,7 @@
|
||||
|
||||
<div class="min-w-[300px] h-full flex flex-col">
|
||||
<Splitpanes horizontal={true}>
|
||||
<Pane class="px-4 py-2 h-full">
|
||||
<Section
|
||||
label="Saved Inputs"
|
||||
tooltip="Shared inputs are available to anyone with access to the script"
|
||||
wrapperClass="h-full"
|
||||
small={true}
|
||||
>
|
||||
{#snippet action()}
|
||||
<SaveInputsButton
|
||||
{args}
|
||||
disabled={!isValid || jsonView}
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
on:update={() => {
|
||||
savedInputsPicker?.refresh()
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<SavedInputsPicker
|
||||
bind:this={savedInputsPicker}
|
||||
previewArgs={args}
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
{isValid}
|
||||
on:select={(e) => {
|
||||
if (e.detail) historicInputs?.resetSelected()
|
||||
selectArgs(e.detail, e.detail ? 'saved' : undefined)
|
||||
}}
|
||||
noButton={true}
|
||||
/>
|
||||
</Section>
|
||||
</Pane>
|
||||
|
||||
<Pane class="px-4 py-4 h-full">
|
||||
<Pane class="px-4 py-4 h-full" size={65}>
|
||||
<Section label="History" wrapperClass="h-full" small={true}>
|
||||
{#snippet action()}
|
||||
<div class="flex space-x-2">
|
||||
@@ -143,7 +109,7 @@
|
||||
{/snippet}
|
||||
|
||||
{#snippet content()}
|
||||
<div class="p-2 overflow-auto max-h-[400px] min-h-[300px] w-[400px]">
|
||||
<div class="p-4 overflow-auto max-h-[400px] min-h-[300px] w-[400px]">
|
||||
<div class="flex items-center flex-wrap gap-x-2 justify-between">
|
||||
<div class="text-sm text-secondary">Search by args</div>
|
||||
<div class="flex flex-wrap gap-x-2">
|
||||
@@ -216,5 +182,38 @@
|
||||
/>
|
||||
</Section>
|
||||
</Pane>
|
||||
<Pane>
|
||||
<Section
|
||||
label="Saved Inputs"
|
||||
tooltip="Shared inputs are available to anyone with access to the script"
|
||||
wrapperClass="h-full px-4 py-2 overflow-y-auto"
|
||||
small={true}
|
||||
>
|
||||
{#snippet action()}
|
||||
<SaveInputsButton
|
||||
{args}
|
||||
disabled={!isValid || jsonView}
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
on:update={() => {
|
||||
savedInputsPicker?.refresh()
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<SavedInputsPicker
|
||||
bind:this={savedInputsPicker}
|
||||
previewArgs={args}
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
{isValid}
|
||||
on:select={(e) => {
|
||||
if (e.detail) historicInputs?.resetSelected()
|
||||
selectArgs(e.detail, e.detail ? 'saved' : undefined)
|
||||
}}
|
||||
noButton={true}
|
||||
/>
|
||||
</Section>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
{#if description}
|
||||
<div class="text-xs text-primary mt-1">{@html description}</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-6 h-full mt-6">
|
||||
<div class="flex flex-col gap-6 grow min-h-0 mt-6">
|
||||
<div class={twMerge('grow min-h-0', clazz)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { getDbClockNow } from '$lib/forLater'
|
||||
import { displayDate } from '$lib/utils'
|
||||
import { onDestroy, onMount, untrack } from 'svelte'
|
||||
import { onMount, untrack } from 'svelte'
|
||||
|
||||
interface Props {
|
||||
date: string
|
||||
agoOnlyIfRecent?: boolean
|
||||
noDate?: boolean
|
||||
isRecent?: boolean
|
||||
noSeconds?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
date,
|
||||
agoOnlyIfRecent = false,
|
||||
noDate = false,
|
||||
isRecent = $bindable(true)
|
||||
isRecent = $bindable(true),
|
||||
noSeconds = false
|
||||
}: Props = $props()
|
||||
|
||||
let computedTimeAgo: string | undefined = $state(undefined)
|
||||
@@ -22,17 +24,20 @@
|
||||
let interval
|
||||
|
||||
onMount(() => {
|
||||
// Update every minute for noSeconds mode, every second for regular mode
|
||||
const intervalMs = noSeconds ? 60000 : 1000
|
||||
interval = setInterval(() => {
|
||||
computeDate()
|
||||
if (!isRecent) {
|
||||
clearInterval(interval)
|
||||
interval = undefined
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
}, intervalMs)
|
||||
|
||||
onDestroy(() => {
|
||||
interval && clearInterval(interval)
|
||||
// Add explicit cleanup
|
||||
return () => {
|
||||
interval && clearInterval(interval)
|
||||
}
|
||||
})
|
||||
|
||||
async function computeDate() {
|
||||
@@ -57,8 +62,53 @@
|
||||
return Math.max(0, Math.floor((getDbClockNow().getTime() - date.getTime()) / 1000))
|
||||
}
|
||||
|
||||
function minutesAgo(date: Date) {
|
||||
return Math.floor(secondsAgo(date) / 60)
|
||||
}
|
||||
|
||||
function hoursAgo(date: Date) {
|
||||
return Math.floor(minutesAgo(date) / 60)
|
||||
}
|
||||
|
||||
function isLessThanMonthAgo(date: Date): boolean {
|
||||
const today = getDbClockNow()
|
||||
const monthAgo = new Date(today)
|
||||
monthAgo.setMonth(monthAgo.getMonth() - 1)
|
||||
return date.getTime() >= monthAgo.getTime()
|
||||
}
|
||||
|
||||
async function displayDaysAgo(dateString: string): Promise<string> {
|
||||
const date = await new Date(dateString)
|
||||
const date = new Date(dateString)
|
||||
|
||||
// New noSeconds mode
|
||||
if (noSeconds) {
|
||||
const mins = minutesAgo(date)
|
||||
const hours = hoursAgo(date)
|
||||
const dAgo = daysAgo(date)
|
||||
|
||||
// If less than an hour ago -> "x min ago"
|
||||
if (mins < 60) {
|
||||
return `${mins} min${mins !== 1 ? 's' : ''} ago`
|
||||
}
|
||||
// If less than a day ago -> "x hours ago"
|
||||
else if (hours < 24) {
|
||||
isRecent = false
|
||||
return `${hours} hour${hours !== 1 ? 's' : ''} ago`
|
||||
}
|
||||
// If less than a month ago -> "x days ago"
|
||||
else if (isLessThanMonthAgo(date)) {
|
||||
return `${dAgo} day${dAgo !== 1 ? 's' : ''} ago`
|
||||
}
|
||||
// If more than a month ago -> "the mm/dd/year"
|
||||
else {
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const year = date.getFullYear()
|
||||
return `the ${month}/${day}/${year}`
|
||||
}
|
||||
}
|
||||
|
||||
// Original mode
|
||||
const nbSecondsAgo = secondsAgo(date)
|
||||
if (nbSecondsAgo < 600) {
|
||||
return `${nbSecondsAgo}s ago`
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { createEventDispatcher, getContext, tick } from 'svelte'
|
||||
import type { TriggerContext } from '../triggers'
|
||||
import { Calendar } from 'lucide-svelte'
|
||||
import { emptyString } from '$lib/utils'
|
||||
|
||||
type MainButton = {
|
||||
label: string
|
||||
@@ -28,7 +29,8 @@
|
||||
interface Props {
|
||||
mainButtons?: MainButton[]
|
||||
menuItems?: MenuItemButton[]
|
||||
title: string
|
||||
summary?: string
|
||||
path?: string
|
||||
tag: string | undefined
|
||||
errorHandlerKind: 'flow' | 'script'
|
||||
scriptOrFlowPath: string
|
||||
@@ -40,7 +42,8 @@
|
||||
let {
|
||||
mainButtons = [],
|
||||
menuItems = [],
|
||||
title,
|
||||
summary,
|
||||
path,
|
||||
tag,
|
||||
errorHandlerKind,
|
||||
scriptOrFlowPath,
|
||||
@@ -52,18 +55,31 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="border-b p-2 shadow-md">
|
||||
<div class="border-b p-1">
|
||||
<div class="mx-auto">
|
||||
<div class="flex w-full flex-wrap md:flex-nowrap justify-end gap-x-2 gap-y-4 items-center">
|
||||
<div class="grow px-2 inline-flex items-center gap-4 min-w-0">
|
||||
<div
|
||||
class="flex w-full flex-wrap md:flex-nowrap justify-end gap-x-2 gap-y-4 items-center min-h-10"
|
||||
>
|
||||
<div class="grow px-4 inline-flex items-center gap-4 min-w-0">
|
||||
<div
|
||||
class={twMerge(
|
||||
'text-lg min-w-24 font-semibold text-emphasis truncate',
|
||||
'min-w-24 text-emphasis truncate flex flex-col gap-0',
|
||||
$userStore?.operator ? 'pl-10' : ''
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</div>{#if tag}
|
||||
<span
|
||||
class={twMerge(
|
||||
'text-sm min-w-24 text-emphasis font-semibold truncate',
|
||||
$userStore?.operator ? 'pl-10' : ''
|
||||
)}
|
||||
>
|
||||
{emptyString(summary) ? (path ?? '') : summary}
|
||||
</span>
|
||||
{#if !emptyString(summary)}
|
||||
<span class="text-2xs text-secondary">{path}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if tag}
|
||||
<Badge>tag: {tag}</Badge>
|
||||
{/if}
|
||||
{@render children?.()}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
save_inputs?: import('svelte').Snippet
|
||||
flow_step?: import('svelte').Snippet
|
||||
triggers?: import('svelte').Snippet
|
||||
flow_graph?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -29,7 +30,8 @@
|
||||
scriptRender: script,
|
||||
save_inputs,
|
||||
flow_step,
|
||||
triggers
|
||||
triggers,
|
||||
flow_graph
|
||||
}: Props = $props()
|
||||
|
||||
let mobileTab: 'form' | 'detail' = $state('form')
|
||||
@@ -40,6 +42,7 @@
|
||||
const save_inputs_render = $derived(save_inputs)
|
||||
const flow_step_render = $derived(flow_step)
|
||||
const triggers_render = $derived(triggers)
|
||||
const flow_graph_render = $derived(flow_graph)
|
||||
|
||||
const useDesktopLayout = $derived(clientWidth >= 768 && !forceSmallScreen)
|
||||
</script>
|
||||
@@ -81,6 +84,9 @@
|
||||
{#if !isChatMode}
|
||||
<Tab value="saved_inputs" label="Inputs" />
|
||||
{/if}
|
||||
{#if isChatMode && flow_json}
|
||||
<Tab value="flow" label="Flow graph" />
|
||||
{/if}
|
||||
{#if !isOperator}
|
||||
<Tab value="triggers" label="Triggers" />
|
||||
{/if}
|
||||
@@ -102,6 +108,11 @@
|
||||
<TabContent value="triggers" class="flex flex-col flex-1 h-full mt-[-2px]">
|
||||
{@render triggers?.()}
|
||||
</TabContent>
|
||||
{#if isChatMode && flow_json}
|
||||
<TabContent value="flow" class="flex flex-col flex-1 h-full">
|
||||
{@render flow_graph_render?.()}
|
||||
</TabContent>
|
||||
{/if}
|
||||
{#if flow_json}
|
||||
<TabContent value="raw" class="flex flex-col flex-1 h-full overflow-auto p-2">
|
||||
<FlowViewerInner flow={flow_json} />
|
||||
|
||||
@@ -256,7 +256,6 @@
|
||||
|
||||
// Function to calculate extra gap needed for notes below the lowest flow nodes
|
||||
function calculateNoteGap(notes: FlowNote[] | undefined): number {
|
||||
console.log('calculateNoteGap', notes)
|
||||
if (!notes || notes.length === 0) {
|
||||
return 0
|
||||
}
|
||||
@@ -650,6 +649,10 @@
|
||||
]
|
||||
|
||||
await tick()
|
||||
updateHeight()
|
||||
}
|
||||
|
||||
function updateHeight() {
|
||||
if (nodes.length === 0) {
|
||||
height = minHeight
|
||||
} else {
|
||||
@@ -659,6 +662,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
minHeight
|
||||
untrack(() => updateHeight())
|
||||
})
|
||||
|
||||
const nodeTypes = {
|
||||
input2: InputNode,
|
||||
module: ModuleNode,
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
} from '$lib/components/flows/FlowAssetsHandler.svelte'
|
||||
import { page } from '$app/state'
|
||||
import FlowChat from '$lib/components/flows/conversations/FlowChat.svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
|
||||
let flow: Flow | undefined = $state()
|
||||
let can_write = false
|
||||
@@ -202,7 +203,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function runFlowForChat(userMessage: string, conversationId: string, additionalInputs?: Record<string, any>): Promise<string> {
|
||||
async function runFlowForChat(
|
||||
userMessage: string,
|
||||
conversationId: string,
|
||||
additionalInputs?: Record<string, any>
|
||||
): Promise<string> {
|
||||
const run = await JobService.runFlowByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path,
|
||||
@@ -400,6 +405,16 @@
|
||||
let flowHistory: FlowHistory | undefined = $state(undefined)
|
||||
let path = $derived(page.params.path ?? '')
|
||||
|
||||
let topSectionHeight = $state(0)
|
||||
let paneHeight = $state(0)
|
||||
let flowGraphHeight = $state(0)
|
||||
|
||||
let graphMinHeight = $derived.by(() => {
|
||||
if (!topSectionHeight || !paneHeight) return 400
|
||||
const availableHeight = paneHeight - topSectionHeight - 1 // Account for the separator between the top section and the graph
|
||||
return Math.max(400, availableHeight)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
const cliTrigger = triggersState.triggers.find((t) => t.type === 'cli')
|
||||
if (cliTrigger) {
|
||||
@@ -466,7 +481,6 @@
|
||||
}}
|
||||
{mainButtons}
|
||||
menuItems={getMenuItems(flow, deployUiSettings)}
|
||||
title={defaultIfEmptyString(flow?.summary, flow?.path ?? '')}
|
||||
bind:errorHandlerMuted={
|
||||
() => flow?.ws_error_handler_muted ?? false,
|
||||
(v) => {
|
||||
@@ -476,6 +490,8 @@
|
||||
scriptOrFlowPath={flow?.path ?? ''}
|
||||
errorHandlerKind="flow"
|
||||
tag={flow?.tag ?? ''}
|
||||
summary={flow?.summary}
|
||||
path={flow?.path}
|
||||
>
|
||||
<!-- @migration-task: migrate this slot by hand, `trigger-badges` is an invalid identifier -->
|
||||
{#snippet trigger_badges()}
|
||||
@@ -523,41 +539,47 @@
|
||||
{/snippet}
|
||||
{#snippet form()}
|
||||
{#if flow}
|
||||
<div class="flex flex-col h-full justify-between">
|
||||
<div class="flex flex-col h-full bg-surface divide-y" bind:clientHeight={paneHeight}>
|
||||
<div
|
||||
class="w-full {chatInputEnabled
|
||||
? 'p-3 flex flex-col h-full'
|
||||
: 'max-w-3xl p-8'} mx-auto gap-2 bg-surface"
|
||||
class="w-full {chatInputEnabled ? 'p-3 flex flex-col h-full' : 'max-w-3xl p-6'} mx-auto"
|
||||
bind:clientHeight={topSectionHeight}
|
||||
>
|
||||
{#if flow?.archived}
|
||||
<Alert type="error" title="Archived">This flow was archived</Alert>
|
||||
<div class="h-4"></div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-1">
|
||||
{#if !emptyString(flow?.description)}
|
||||
<GfmMarkdown md={defaultIfEmptyString(flow?.description, 'No description')} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if !emptyString(flow?.description)}
|
||||
<div class="p-4 rounded-md bg-surface-secondary">
|
||||
<GfmMarkdown
|
||||
md={defaultIfEmptyString(flow?.description, 'No description')}
|
||||
noPadding
|
||||
/>
|
||||
</div>
|
||||
<div class="h-4"></div>
|
||||
{/if}
|
||||
|
||||
{#if deploymentInProgress}
|
||||
<HeaderBadge color="yellow">
|
||||
<Loader2 size={12} class="inline animate-spin mr-1" />
|
||||
Deployment in progress
|
||||
{#if deploymentJobId}
|
||||
<a
|
||||
href="/run/{deploymentJobId}?workspace={$workspaceStore}"
|
||||
class="underline"
|
||||
target="_blank">view job</a
|
||||
>
|
||||
{/if}
|
||||
</HeaderBadge>
|
||||
<div class="pb-4" transition:slide={{ duration: 150 }}>
|
||||
<HeaderBadge color="yellow">
|
||||
<Loader2 size={12} class="inline animate-spin mr-1" />
|
||||
Deployment in progress
|
||||
{#if deploymentJobId}
|
||||
<a
|
||||
href="/run/{deploymentJobId}?workspace={$workspaceStore}"
|
||||
class="underline"
|
||||
target="_blank">view job</a
|
||||
>
|
||||
{/if}
|
||||
</HeaderBadge>
|
||||
</div>
|
||||
{/if}
|
||||
{#if flow.lock_error_logs && flow.lock_error_logs != ''}
|
||||
<div class="bg-red-100 dark:bg-red-700 border-l-4 border-red-500 p-4" role="alert">
|
||||
<p class="font-bold">Error deploying this flow</p>
|
||||
<Alert type="error" title="Deployment failed">
|
||||
<p> This flow has not been deployed successfully because of the following errors: </p>
|
||||
<LogViewer content={flow.lock_error_logs} isLoading={false} tag={undefined} />
|
||||
</div>
|
||||
</Alert>
|
||||
<div class="h-4"></div>
|
||||
{/if}
|
||||
|
||||
{#if chatInputEnabled}
|
||||
@@ -570,28 +592,37 @@
|
||||
inputSchema={flow?.schema}
|
||||
/>
|
||||
{:else}
|
||||
{@const hasSchema = flow.schema && Object.keys(flow.schema.properties ?? {}).length > 0}
|
||||
<!-- Normal Mode: Form Layout -->
|
||||
<div class="flex flex-col align-left">
|
||||
<div class="flex flex-row justify-between">
|
||||
<InputSelectedBadge
|
||||
onReject={() => {
|
||||
savedInputsV2?.resetSelected()
|
||||
}}
|
||||
{inputSelected}
|
||||
/>
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'JSON',
|
||||
rightTooltip: 'Fill args from JSON'
|
||||
}}
|
||||
lightMode
|
||||
on:change={(e) => {
|
||||
runForm?.setCode(JSON.stringify(args ?? {}, null, '\t'))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if hasSchema || inputSelected}
|
||||
<div
|
||||
class="flex flex-row justify-between min-h-12"
|
||||
transition:slide={{ duration: 150 }}
|
||||
>
|
||||
<InputSelectedBadge
|
||||
onReject={() => {
|
||||
savedInputsV2?.resetSelected()
|
||||
}}
|
||||
{inputSelected}
|
||||
/>
|
||||
|
||||
{#if hasSchema}
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'JSON',
|
||||
rightTooltip: 'Fill args from JSON'
|
||||
}}
|
||||
lightMode
|
||||
on:change={(e) => {
|
||||
runForm?.setCode(JSON.stringify(args ?? {}, null, '\t'))
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if flow.schema?.prompt_for_ai !== undefined}
|
||||
<AIFormAssistant
|
||||
@@ -620,26 +651,21 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="py-10"></div>
|
||||
|
||||
{#if !emptyString(flow.summary)}
|
||||
<div>
|
||||
<span class="text-primary text-xs">{flow.path}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="text-2xs text-secondary">
|
||||
Edited <TimeAgo date={flow.edited_at ?? ''} /> by {flow.edited_by}
|
||||
</span>
|
||||
<div class="pt-4 flex flex-col gap-1 w-full items-end">
|
||||
<span class="text-2xs text-secondary">
|
||||
Edited <TimeAgo date={flow.edited_at ?? ''} noSeconds /> by {flow.edited_by}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !chatInputEnabled}
|
||||
<div class="mt-8">
|
||||
<div class="grow min-h-0">
|
||||
<FlowGraphViewer
|
||||
triggerNode={true}
|
||||
download
|
||||
{flow}
|
||||
overflowAuto
|
||||
noSide={true}
|
||||
minHeight={graphMinHeight}
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
stepDetail = e.detail
|
||||
@@ -652,6 +678,7 @@
|
||||
on:triggerDetail={(e) => {
|
||||
rightPaneSelected = 'triggers'
|
||||
}}
|
||||
noBorder={true}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -701,6 +728,33 @@
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet flow_graph()}
|
||||
{#if flow}
|
||||
<div class="h-full overflow-auto" bind:clientHeight={flowGraphHeight}>
|
||||
<FlowGraphViewer
|
||||
triggerNode={true}
|
||||
download
|
||||
{flow}
|
||||
noSide={false}
|
||||
noBorder
|
||||
minHeight={flowGraphHeight}
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
stepDetail = e.detail
|
||||
rightPaneSelected = 'flow_step'
|
||||
} else {
|
||||
stepDetail = undefined
|
||||
rightPaneSelected = 'saved_inputs'
|
||||
}
|
||||
}}
|
||||
on:triggerDetail={(e) => {
|
||||
rightPaneSelected = 'triggers'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</DetailPageLayout>
|
||||
|
||||
<FlowAssetsHandler
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import InputSelectedBadge from '$lib/components/schema/InputSelectedBadge.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import { slide } from 'svelte/transition'
|
||||
import TriggersBadge from '$lib/components/graph/renderers/triggers/TriggersBadge.svelte'
|
||||
import TriggersEditor from '$lib/components/triggers/TriggersEditor.svelte'
|
||||
import { Triggers } from '$lib/components/triggers/triggers.svelte'
|
||||
@@ -567,7 +568,6 @@
|
||||
<DetailPageHeader
|
||||
{mainButtons}
|
||||
menuItems={getMenuItems(script, deployUiSettings)}
|
||||
title={defaultIfEmptyString(script?.summary, script?.path ?? '')}
|
||||
bind:errorHandlerMuted={
|
||||
() => script?.ws_error_handler_muted ?? false,
|
||||
(v) => {
|
||||
@@ -580,6 +580,8 @@
|
||||
on:seeTriggers={() => {
|
||||
rightPaneSelected = 'triggers'
|
||||
}}
|
||||
summary={script?.summary}
|
||||
path={script?.path}
|
||||
>
|
||||
{#snippet trigger_badges()}
|
||||
<TriggersBadge
|
||||
@@ -646,17 +648,13 @@
|
||||
{#if script.lock_error_logs || topHash || script.archived || script.deleted}
|
||||
<div class="flex flex-col gap-2 my-2">
|
||||
{#if script.lock_error_logs}
|
||||
<div
|
||||
class="bg-red-100 dark:bg-red-700 border-l-4 border-red-500 p-4"
|
||||
role="alert"
|
||||
>
|
||||
<p class="font-bold">Error deploying this script</p>
|
||||
<Alert type="error" title="Deployment failed">
|
||||
<p>
|
||||
This script has not been deployed successfully because of the following
|
||||
errors:
|
||||
</p>
|
||||
<LogViewer content={script.lock_error_logs} isLoading={false} tag={undefined} />
|
||||
</div>
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if topHash}
|
||||
<div class="mt-2"></div>
|
||||
@@ -671,57 +669,70 @@
|
||||
<Alert type="error" title="Archived">This path was archived</Alert>
|
||||
{/if}
|
||||
{#if script.deleted}
|
||||
<div
|
||||
class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4"
|
||||
role="alert"
|
||||
>
|
||||
<p class="font-bold">Deleted</p>
|
||||
<Alert type="error" title="Deleted">
|
||||
<p>The content of this script was deleted (by an admin, no less)</p>
|
||||
</div>
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !emptyString(script.description)}
|
||||
<GfmMarkdown md={defaultIfEmptyString(script?.description, 'No description')} />
|
||||
<div class="p-4 rounded-md bg-surface-secondary">
|
||||
<GfmMarkdown
|
||||
md={defaultIfEmptyString(script?.description, 'No description')}
|
||||
noPadding
|
||||
/>
|
||||
</div>
|
||||
<div class="h-4"></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if deploymentInProgress}
|
||||
<Badge color="yellow">
|
||||
<Loader2 size={12} class="inline animate-spin mr-1" />
|
||||
Deployment in progress
|
||||
{#if deploymentJobId}
|
||||
<a
|
||||
href="/run/{deploymentJobId}?workspace={$workspaceStore}"
|
||||
class="underline"
|
||||
target="_blank">view job</a
|
||||
>
|
||||
{/if}
|
||||
</Badge>
|
||||
<div class="pb-4" transition:slide={{ duration: 150 }}>
|
||||
<Badge color="yellow">
|
||||
<Loader2 size={12} class="inline animate-spin mr-1" />
|
||||
Deployment in progress
|
||||
{#if deploymentJobId}
|
||||
<a
|
||||
href="/run/{deploymentJobId}?workspace={$workspaceStore}"
|
||||
class="underline"
|
||||
target="_blank">view job</a
|
||||
>
|
||||
{/if}
|
||||
</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col align-left">
|
||||
<div class="flex flex-row justify-between">
|
||||
<InputSelectedBadge
|
||||
onReject={() => {
|
||||
savedInputsV2?.resetSelected()
|
||||
}}
|
||||
{inputSelected}
|
||||
/>
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'JSON',
|
||||
rightTooltip: 'Fill args from JSON'
|
||||
}}
|
||||
lightMode
|
||||
on:change={(e) => {
|
||||
runForm?.setCode(JSON.stringify(args ?? {}, null, '\t'))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if (script.schema && Object.keys(script.schema.properties ?? {}).length > 0) || inputSelected}
|
||||
{@const hasSchema =
|
||||
script.schema && Object.keys(script.schema.properties ?? {}).length > 0}
|
||||
<div
|
||||
class="flex flex-row justify-between min-h-12"
|
||||
transition:slide={{ duration: 150 }}
|
||||
>
|
||||
<InputSelectedBadge
|
||||
onReject={() => {
|
||||
savedInputsV2?.resetSelected()
|
||||
}}
|
||||
{inputSelected}
|
||||
/>
|
||||
{#if hasSchema}
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'JSON',
|
||||
rightTooltip: 'Fill args from JSON'
|
||||
}}
|
||||
lightMode
|
||||
on:change={(e) => {
|
||||
runForm?.setCode(JSON.stringify(args ?? {}, null, '\t'))
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if script?.schema?.prompt_for_ai !== undefined}
|
||||
<AIFormAssistant
|
||||
@@ -751,29 +762,25 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="py-10"></div>
|
||||
{#if !emptyString(script.summary)}
|
||||
<div>
|
||||
<span class="text-primary">{script.path}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-row gap-x-2 flex-wrap items-center">
|
||||
<div class="pt-4 flex flex-row gap-1 w-full justify-end items-center">
|
||||
<span class="text-2xs text-secondary">
|
||||
Edited <TimeAgo date={script.created_at || ''} /> by {script.created_by || 'unknown'}
|
||||
</span>
|
||||
<Badge small color="gray">
|
||||
{truncateHash(script?.hash ?? '')}
|
||||
</Badge>
|
||||
{#if script?.is_template}
|
||||
<Badge color="blue">Template</Badge>
|
||||
{/if}
|
||||
{#if script && script.kind !== 'script'}
|
||||
<Badge color="blue">
|
||||
{script?.kind}
|
||||
<div class="flex flex-row gap-x-2 flex-wrap items-center">
|
||||
<Badge small color="gray">
|
||||
{truncateHash(script?.hash ?? '')}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if script?.is_template}
|
||||
<Badge color="blue">Template</Badge>
|
||||
{/if}
|
||||
{#if script && script.kind !== 'script'}
|
||||
<Badge color="blue">
|
||||
{script?.kind}
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
<SharedBadge canWrite={can_write} extraPerms={script?.extra_perms ?? {}} />
|
||||
<SharedBadge canWrite={can_write} extraPerms={script?.extra_perms ?? {}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user