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:
Guilhem
2026-01-22 15:55:18 +00:00
committed by GitHub
parent 0f39ceabc0
commit 5ed4c8dd26
11 changed files with 332 additions and 195 deletions
@@ -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}
+7 -18
View File
@@ -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>
+1 -1
View File
@@ -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>
+57 -7
View File
@@ -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}