diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 40533b896a..4fe32e4b28 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -49,7 +49,7 @@ "highlight.js": "^11.8.0", "idb": "^8.0.2", "lru-cache": "^11.1.0", - "lucide-svelte": "^0.399.0", + "lucide-svelte": "^0.540.0", "minimatch": "^10.0.1", "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~20.2.1", "monaco-editor-wrapper": "6.12.0", @@ -7848,9 +7848,10 @@ "optional": true }, "node_modules/lucide-svelte": { - "version": "0.399.0", - "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.399.0.tgz", - "integrity": "sha512-NQ8AxNMKbIJsx7HV//gnAsIY1wJfb3rbXSK2S/ZDjIldvAEdzGngpUT8T8Q8zHYUuii0bavAmVARN8giR4vvpA==", + "version": "0.540.0", + "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.540.0.tgz", + "integrity": "sha512-jedJgKrrsT1B6mAMHGMmk6dIZKhorLq4KO7v+j7hYfeGR0LRe3W+SvwpWF1E6WT2/AY9IAm8KT/G+Nzf+0lW3g==", + "license": "ISC", "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } diff --git a/frontend/package.json b/frontend/package.json index e5d37b1747..a283adc5fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -116,7 +116,7 @@ "highlight.js": "^11.8.0", "idb": "^8.0.2", "lru-cache": "^11.1.0", - "lucide-svelte": "^0.399.0", + "lucide-svelte": "^0.540.0", "minimatch": "^10.0.1", "monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~20.2.1", "monaco-editor-wrapper": "6.12.0", diff --git a/frontend/src/lib/components/ArgInfo.svelte b/frontend/src/lib/components/ArgInfo.svelte index feadd419cd..be042b95e5 100644 --- a/frontend/src/lib/components/ArgInfo.svelte +++ b/frontend/src/lib/components/ArgInfo.svelte @@ -95,14 +95,14 @@
{#if JSON.stringify(value).length > 120} {/if} -
+
diff --git a/frontend/src/lib/components/ConcurrentJobsChart.svelte b/frontend/src/lib/components/ConcurrentJobsChart.svelte index 351a4caa25..c401df4be6 100644 --- a/frontend/src/lib/components/ConcurrentJobsChart.svelte +++ b/frontend/src/lib/components/ConcurrentJobsChart.svelte @@ -13,16 +13,24 @@ Tooltip } from 'chart.js' import type { CompletedJob, ExtendedJobs } from '$lib/gen' - import { createEventDispatcher } from 'svelte' import { getDbClockNow } from '$lib/forLater' import { Line } from '$lib/components/chartjs-wrappers/chartJs' - export let extendedJobs: ExtendedJobs | undefined = undefined - export let maxIsNow: boolean = false - export let minTimeSet: string | undefined = undefined - export let maxTimeSet: string | undefined = undefined + interface Props { + extendedJobs?: ExtendedJobs | undefined + maxIsNow?: boolean + minTimeSet?: string | undefined + maxTimeSet?: string | undefined + onZoom: (zoom: { min: Date; max: Date }) => void + } - const dispatch = createEventDispatcher() + let { + extendedJobs = undefined, + maxIsNow = false, + minTimeSet = undefined, + maxTimeSet = undefined, + onZoom + }: Props = $props() function calculateTimeSeries(extendedJobs: ExtendedJobs): AggregatedInterval[] { const timeline = new Map() @@ -57,7 +65,7 @@ } const s = timeline.get(startTime)! s.count += 1 - s.id_started.push('unknoww') + s.id_started.push('unknown') if (j.duration_ms != undefined) { const jc = j as CompletedJob const endTime = startTime + jc.duration_ms @@ -103,9 +111,6 @@ } type AggregatedInterval = { time: Date; count: number; msg?: string } - let intervals: AggregatedInterval[] | undefined = undefined - $: intervals = extendedJobs ? calculateTimeSeries(extendedJobs) : undefined - ChartJS.register( Title, Tooltip, @@ -118,31 +123,12 @@ TimeScale ) - $: data = { - datasets: [ - { - borderColor: '#4ade80', - backgroundColor: '#f8717100', - pointRadius: 0, - label: 'running', - showLine: true, - stepped: true, - data: - intervals?.map((job) => ({ - x: job.time as any, - y: job.count, - id: job.msg - })) ?? [] - } - ] - } - const zoomOptions = { pan: { enabled: true, modifierKey: 'ctrl' as 'ctrl', onPanComplete: ({ chart }) => { - dispatch('zoom', { + onZoom({ min: addSeconds(new Date(chart.scales.x.min), -1), max: addSeconds(new Date(chart.scales.x.max), 1) }) @@ -154,17 +140,13 @@ }, mode: 'x' as 'x', onZoom: ({ chart }) => { - dispatch('zoom', { + onZoom({ min: addSeconds(new Date(chart.scales.x.min), -1), max: addSeconds(new Date(chart.scales.x.max), 1) }) } } } - let minTime = addSeconds(new Date(), -300) - let maxTime = getDbClockNow() - - $: computeMinMaxTime(intervals, minTimeSet, maxTimeSet) function minJobTime(intervals: AggregatedInterval[]): Date { return intervals[0].time @@ -181,15 +163,13 @@ let minTimeSetDate = minTimeSet ? new Date(minTimeSet) : undefined let maxTimeSetDate = maxTimeSet ? new Date(maxTimeSet) : undefined if (minTimeSetDate && maxTimeSetDate) { - minTime = minTimeSetDate - maxTime = maxTimeSetDate - return + return { min: minTimeSetDate, max: maxTimeSetDate } } if (intervals == undefined || intervals?.length == 0) { - minTime = minTimeSetDate ?? addSeconds(new Date(), -300) - maxTime = maxTimeSetDate ?? getDbClockNow() - return + const minTime = minTimeSetDate ?? addSeconds(new Date(), -300) + const maxTime = maxTimeSetDate ?? getDbClockNow() + return { min: minTime, max: maxTime } } const maxJob = maxIsNow ? getDbClockNow() : maxJobTime(intervals) @@ -197,12 +177,11 @@ const diff = (maxJob.getTime() - minJob.getTime()) / 20000 - minTime = minTimeSetDate ?? addSeconds(minJob, -diff) - if (maxIsNow) { - maxTime = maxTimeSetDate ?? maxJob - } else { - maxTime = maxTimeSetDate ?? addSeconds(maxJob, diff) - } + const minTime = minTimeSetDate ?? addSeconds(minJob, -diff) + const maxTime = maxIsNow + ? (maxTimeSetDate ?? maxJob) + : (maxTimeSetDate ?? addSeconds(maxJob, diff)) + return { min: minTime, max: maxTime } } function addSeconds(date: Date, seconds: number): Date { @@ -210,7 +189,30 @@ return date } - $: options = { + const intervals = $derived(extendedJobs ? calculateTimeSeries(extendedJobs) : undefined) + + let data = $derived({ + datasets: [ + { + borderColor: '#4ade80', + backgroundColor: '#f8717100', + pointRadius: 0, + label: 'running', + showLine: true, + stepped: true, + data: + intervals?.map((job) => ({ + x: job.time as any, + y: job.count, + id: job.msg + })) ?? [] + } + ] + }) + + const minMaxTimes = $derived(computeMinMaxTime(intervals, minTimeSet, maxTimeSet)) + + let options = $derived({ responsive: true, maintainAspectRatio: false, plugins: { @@ -232,8 +234,8 @@ grid: { display: false }, - min: minTime, - max: maxTime + min: minMaxTimes.min, + max: minMaxTimes.max }, y: { grid: { @@ -254,7 +256,7 @@ intersect: false, mode: 'index' } - } as any + } as any)
diff --git a/frontend/src/lib/components/DropdownSelect.svelte b/frontend/src/lib/components/DropdownSelect.svelte new file mode 100644 index 0000000000..7a31f3435b --- /dev/null +++ b/frontend/src/lib/components/DropdownSelect.svelte @@ -0,0 +1,38 @@ + + + + {#snippet buttonReplacement()} +
+
+ + {selectedDisplayName ?? items.find((item) => item.id === selected)?.displayName ?? ''} + + + {@render extraLabel?.()} +
+ +
+ {/snippet} +
diff --git a/frontend/src/lib/components/DropdownV2Inner.svelte b/frontend/src/lib/components/DropdownV2Inner.svelte index c8236eed5f..15ec3b5ada 100644 --- a/frontend/src/lib/components/DropdownV2Inner.svelte +++ b/frontend/src/lib/components/DropdownV2Inner.svelte @@ -47,7 +47,7 @@ aiDescription={item.displayName} > {#if item.icon} - + {/if}

{item.displayName} diff --git a/frontend/src/lib/components/FlowStatusViewerInner.svelte b/frontend/src/lib/components/FlowStatusViewerInner.svelte index a2012f73c3..493ace3201 100644 --- a/frontend/src/lib/components/FlowStatusViewerInner.svelte +++ b/frontend/src/lib/components/FlowStatusViewerInner.svelte @@ -50,6 +50,7 @@ AI_TOOL_MESSAGE_PREFIX, getToolCallId } from './graph/renderers/nodes/AIToolNode.svelte' + import JobAssetsViewer from './assets/JobAssetsViewer.svelte' let { flowStateStore, @@ -1048,7 +1049,11 @@ } } }) - let selected = $derived(isListJob ? 'sequence' : 'graph') as 'sequence' | 'graph' | 'logs' + let selected = $derived(isListJob ? 'sequence' : 'graph') as + | 'sequence' + | 'graph' + | 'logs' + | 'assets' let animateLogsTab = $state(false) @@ -1076,6 +1081,23 @@ return '' } + + // Set all tabs content to the same height to prevent layout jumps + let tabsHeight = $state({ + sequenceHeight: 0, + logsHeight: 0, + assetsHeight: 0, + graphHeight: 0 + }) + + let minTabHeight = $derived( + Math.max( + tabsHeight.sequenceHeight, + tabsHeight.logsHeight, + tabsHeight.assetsHeight, + tabsHeight.graphHeight + ) + ) @@ -1154,12 +1176,17 @@ : ''}>Logs Details + Assets {:else}

{/if} {/if} -
+
{#if isListJob} {@const sliceFrom = globalIterationBounds[buildSubflowKey(flowJobIds?.moduleId ?? '', prefix)] @@ -1508,7 +1535,11 @@
Empty flow
{/if}
-
+
+ {#if selected == 'assets' && render} +
+ +
+ {/if}
{#if render} {#if job.raw_flow && !isListJob} -
+
diff --git a/frontend/src/lib/components/JobArgs.svelte b/frontend/src/lib/components/JobArgs.svelte index 52603575a7..cd0c121a93 100644 --- a/frontend/src/lib/components/JobArgs.svelte +++ b/frontend/src/lib/components/JobArgs.svelte @@ -85,7 +85,7 @@ ${Object.entries(args) {#each Object.entries(args).sort((a, b) => a[0].localeCompare(b[0])) as [arg, value]} {arg} - + {/each} {:else if args} diff --git a/frontend/src/lib/components/RunChart.svelte b/frontend/src/lib/components/RunChart.svelte index 819437f82b..0ea14fec29 100644 --- a/frontend/src/lib/components/RunChart.svelte +++ b/frontend/src/lib/components/RunChart.svelte @@ -15,28 +15,42 @@ LogarithmicScale } from 'chart.js' import type { CompletedJob } from '$lib/gen' - import { createEventDispatcher } from 'svelte' import { getDbClockNow } from '$lib/forLater' import Button from './common/button/Button.svelte' import { Scatter } from '$lib/components/chartjs-wrappers/chartJs' + import DarkModeObserver from './DarkModeObserver.svelte' - export let jobs: CompletedJob[] | undefined = [] - export let maxIsNow: boolean = false - export let minTimeSet: string | undefined = undefined - export let maxTimeSet: string | undefined = undefined - export let selectedIds: string[] = [] - export let canSelect: boolean = true - export let lastFetchWentToEnd: boolean = false + interface Props { + jobs?: CompletedJob[] | undefined + maxIsNow?: boolean + minTimeSet?: string | undefined + maxTimeSet?: string | undefined + selectedIds?: string[] + canSelect?: boolean + lastFetchWentToEnd?: boolean + onPointClicked: (ids: string[]) => void + onLoadExtra: () => void + onZoom: (zoom: { min: Date; max: Date }) => void + } + + let { + jobs = [], + maxIsNow = false, + minTimeSet = undefined, + maxTimeSet = undefined, + selectedIds = $bindable([]), + canSelect = true, + lastFetchWentToEnd = false, + onPointClicked, + onLoadExtra, + onZoom + }: Props = $props() - const dispatch = createEventDispatcher() const SUCCESS_COLOR = '#4ade80' // const SUCCESS_COLOR_TRANSPARENT = '#c9b638' - const SUCCESS_COLOR_TRANSPARENT = mergeColors(SUCCESS_COLOR, getBackgorundColor(), 0.8) + const SUCCESS_COLOR_TRANSPARENT = $derived(mergeColors(SUCCESS_COLOR, getBackgorundColor(), 0.8)) const FAIL_COLOR = '#f87171' - const FAIL_COLOR_TRANSPARENT = mergeColors(FAIL_COLOR, getBackgorundColor(), 0.8) - - $: success = jobs?.filter((x) => x.success) - $: failed = jobs?.filter((x) => !x.success) + const FAIL_COLOR_TRANSPARENT = $derived(mergeColors(FAIL_COLOR, getBackgorundColor(), 0.8)) ChartJS.register( Title, @@ -51,43 +65,12 @@ TimeScale ) - $: data = { - datasets: [ - { - borderColor: 'rgba(99,0,125, 0)', - backgroundColor: FAIL_COLOR as string | string[], - radius: 3, - label: 'Failed', - data: - failed?.map((job) => ({ - x: job.started_at as any, - y: job.duration_ms, - id: job.id, - path: job.script_path - })) ?? [] - }, - { - borderColor: 'rgba(99,0,125, 0)', - backgroundColor: SUCCESS_COLOR as string | string[], - radius: 3, - label: 'Successful', - data: - success?.map((job) => ({ - x: job.started_at as any, - y: job.duration_ms, - id: job.id, - path: job.script_path - })) ?? [] - } - ] - } - const zoomOptions = { pan: { enabled: true, modifierKey: 'ctrl' as 'ctrl', onPanComplete: ({ chart }) => { - dispatch('zoom', { + onZoom({ min: addSeconds(new Date(chart.scales.x.min), -1), max: addSeconds(new Date(chart.scales.x.max), 1) }) @@ -99,7 +82,7 @@ }, mode: 'x' as 'x', onZoom: ({ chart }) => { - dispatch('zoom', { + onZoom({ min: addSeconds(new Date(chart.scales.x.min), -1), max: addSeconds(new Date(chart.scales.x.max), 1) }) @@ -107,15 +90,14 @@ } } - function isDark(): boolean { - return document.documentElement.classList.contains('dark') - } - - ChartJS.defaults.color = isDark() ? '#ccc' : '#666' - ChartJS.defaults.borderColor = isDark() ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)' + let darkMode = $state(false) + $effect(() => { + ChartJS.defaults.color = darkMode ? '#ccc' : '#666' + ChartJS.defaults.borderColor = darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)' + }) function getBackgorundColor(): string { - return isDark() ? '#2e3440' : '#ffffff' + return darkMode ? '#2e3440' : '#ffffff' } function hexToRgb(hexColor: string): number[] { hexColor = hexColor.replace(/^#/, '') @@ -150,29 +132,10 @@ return rgbToHex(blendedRgb) } - function highlightSelectedPoints(ids: string[]) { - if (!canSelect || ids.length === 0) { - data.datasets[0].backgroundColor = FAIL_COLOR - data.datasets[1].backgroundColor = SUCCESS_COLOR - } else { - data.datasets[0].backgroundColor = data.datasets[0].data.map((p) => - ids.includes(p.id) ? FAIL_COLOR : FAIL_COLOR_TRANSPARENT - ) - data.datasets[1].backgroundColor = data.datasets[1].data.map((p) => - ids.includes(p.id) ? SUCCESS_COLOR : SUCCESS_COLOR_TRANSPARENT - ) - } - } - function getPath(x: any): string { return x.path } - let minTime = addSeconds(new Date(), -300) - let maxTime = getDbClockNow() - - $: computeMinMaxTime(jobs, minTimeSet, maxTimeSet) - function minJobTime(jobs: CompletedJob[]): Date { let min: Date = new Date(jobs[0].started_at) for (const job of jobs) { @@ -195,6 +158,7 @@ } return max } + function computeMinMaxTime( jobs: CompletedJob[] | undefined, minTimeSet: string | undefined, @@ -203,15 +167,13 @@ let minTimeSetDate = minTimeSet ? new Date(minTimeSet) : undefined let maxTimeSetDate = maxTimeSet ? new Date(maxTimeSet) : undefined if (minTimeSetDate && maxTimeSetDate) { - minTime = minTimeSetDate - maxTime = maxTimeSetDate - return + return { minTime: minTimeSetDate, maxTime: maxTimeSetDate } } if (jobs == undefined || jobs?.length == 0) { - minTime = minTimeSetDate ?? addSeconds(new Date(), -300) - maxTime = maxTimeSetDate ?? getDbClockNow() - return + const computedMinTime = minTimeSetDate ?? addSeconds(new Date(), -300) + const computedMaxTime = maxTimeSetDate ?? getDbClockNow() + return { minTime: computedMinTime, maxTime: computedMaxTime } } const maxJob = maxIsNow ? getDbClockNow() : maxJobTime(jobs) @@ -219,12 +181,11 @@ const diff = (maxJob.getTime() - minJob.getTime()) / 20000 - minTime = minTimeSetDate ?? addSeconds(minJob, -diff) - if (maxIsNow) { - maxTime = maxTimeSetDate ?? maxJob - } else { - maxTime = maxTimeSetDate ?? addSeconds(maxJob, diff) - } + let computedMinTime = minTimeSetDate ?? addSeconds(minJob, -diff) + let computedMaxTime = maxIsNow + ? (maxTimeSetDate ?? maxJob) + : (maxTimeSetDate ?? addSeconds(maxJob, diff)) + return { minTime: computedMinTime, maxTime: computedMaxTime } } function addSeconds(date: Date, seconds: number): Date { @@ -232,7 +193,56 @@ return date } - $: scatterOptions = { + let success = $derived(jobs?.filter((x) => x.success)) + let failed = $derived(jobs?.filter((x) => !x.success)) + let data = $derived.by(() => { + const data = { + datasets: [ + { + borderColor: 'rgba(99,0,125, 0)', + backgroundColor: FAIL_COLOR as string | string[], + radius: 3, + label: 'Failed', + data: + failed?.map((job) => ({ + x: job.started_at as any, + y: job.duration_ms, + id: job.id, + path: job.script_path + })) ?? [] + }, + { + borderColor: 'rgba(99,0,125, 0)', + backgroundColor: SUCCESS_COLOR as string | string[], + radius: 3, + label: 'Successful', + data: + success?.map((job) => ({ + x: job.started_at as any, + y: job.duration_ms, + id: job.id, + path: job.script_path + })) ?? [] + } + ] + } + if (!canSelect || selectedIds.length === 0) { + data.datasets[0].backgroundColor = FAIL_COLOR + data.datasets[1].backgroundColor = SUCCESS_COLOR + } else { + data.datasets[0].backgroundColor = data.datasets[0].data.map((p) => + selectedIds.includes(p.id) ? FAIL_COLOR : FAIL_COLOR_TRANSPARENT + ) + data.datasets[1].backgroundColor = data.datasets[1].data.map((p) => + selectedIds.includes(p.id) ? SUCCESS_COLOR : SUCCESS_COLOR_TRANSPARENT + ) + } + return data + }) + + const minMaxTime = $derived.by(() => computeMinMaxTime(jobs, minTimeSet, maxTimeSet)) + + let scatterOptions = $derived({ responsive: true, maintainAspectRatio: false, plugins: { @@ -242,17 +252,17 @@ }, tooltip: { callbacks: { - label: function (context) { + label: function (context: any) { return getPath(context.raw) } } } }, - onClick: (e, u) => { + onClick: (_e: any, u: any) => { if (canSelect) { - const ids = u.map((j) => data.datasets[j.datasetIndex].data[j.index].id) + const ids = u.map((j: any) => data.datasets[j.datasetIndex].data[j.index].id) selectedIds = ids - dispatch('pointClicked', ids) + onPointClicked(ids) } }, @@ -262,8 +272,8 @@ grid: { display: false }, - min: minTime, - max: maxTime + min: minMaxTime.minTime, + max: minMaxTime.maxTime }, y: { grid: { @@ -277,11 +287,11 @@ } }, animation: false - } as any - - $: data && scatterOptions && highlightSelectedPoints(selectedIds) + } as any) + +
{#if !lastFetchWentToEnd} -
{#if Boolean(options?.left)} -
    - {#each assets.value ?? [] as asset} -
  • -
    - {asset.path} - - {formatAssetKind({ - ...asset, - ...(asset.kind === 'resource' - ? { metadata: { resource_type: resourceDataCache[asset.path] } } - : {}) - })} - -
    - -
  • - {/each} -
+{#if assets.value && assets.value.length > 0} +
    + {#each assets.value ?? [] as asset} +
  • +
    + {asset.path} + + {formatAssetKind({ + ...asset, + ...(asset.kind === 'resource' + ? { metadata: { resource_type: resourceDataCache[asset.path] } } + : {}) + })} + +
    + +
  • + {/each} +
+{:else} +
No assets found
+{/if} diff --git a/frontend/src/lib/components/common/badge/Badge.svelte b/frontend/src/lib/components/common/badge/Badge.svelte index 94660d801e..57591212d6 100644 --- a/frontend/src/lib/components/common/badge/Badge.svelte +++ b/frontend/src/lib/components/common/badge/Badge.svelte @@ -13,6 +13,7 @@ export let baseClass = 'text-center' export let capitalize = false export let icon: BadgeIconProps | undefined = undefined + export let verySmall = false let hidden = false const colors: Record = { @@ -46,11 +47,18 @@ $: badgeClass = classNames( baseClass, - small ? 'text-xs' : large ? 'text-sm font-medium' : 'text-xs font-semibold', + small + ? 'text-xs' + : verySmall + ? 'text-2xs' + : large + ? 'text-sm font-medium' + : 'text-xs font-semibold', colors[color], href && (color.startsWith(ColorModifier) ? hovers[color.replace(ColorModifier, '')] : hovers[color]), rounded ? 'rounded-full px-2 py-1' : 'rounded px-2.5 py-0.5', + verySmall ? 'px-0.5 py-0.5' : '', 'flex flex-row gap-1 items-center', $$props.class ) diff --git a/frontend/src/lib/components/common/calendarPicker/CalendarPicker.svelte b/frontend/src/lib/components/common/calendarPicker/CalendarPicker.svelte index 834bb0865d..ee1d1b5032 100644 --- a/frontend/src/lib/components/common/calendarPicker/CalendarPicker.svelte +++ b/frontend/src/lib/components/common/calendarPicker/CalendarPicker.svelte @@ -4,6 +4,7 @@ import Popover from '$lib/components/meltComponents/Popover.svelte' import type { Placement } from '@floating-ui/core' import DateTimeInput from '$lib/components/DateTimeInput.svelte' + import { twMerge } from 'tailwind-merge' export let date: string | undefined export let label: string @@ -25,7 +26,10 @@ - {/snippet} - Concurrency: {truncateRev(concurrencyKey, 20)} - - {/if} - {#if job?.worker} - - {#snippet text()} - This job was run on worker: - - {/snippet} - Worker: {truncateRev(job.worker, 20)} - - {/if} -
- - ID: - {job?.id ?? ''} - - - Arguments - -
- -
- - {#if job?.type === 'CompletedJob'} - Results - {/if} - - {#if job && 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)} -
-
Job is scheduled for
-
{new Date(job?.['scheduled_for']).toLocaleString()}
-
- {/if} - -
- {#if job?.workflow_as_code_status} - - {/if} - - {#if job?.type === 'CompletedJob'} - - Result - Logs - Assets - {#if isScriptPreview(job?.job_kind)} - Code - {/if} - - - - {#if job} - {#if viewTab == 'result' && (job?.job_kind == 'flow' || isFlowPreview(job?.job_kind))} -
-
- -
-
- {:else if viewTab == 'assets'} - - {:else} -
- {#if viewTab == 'logs'} -
- -
- {:else if viewTab == 'code'} - {#if job && 'raw_code' in job && job.raw_code} -
- -
- {:else if job} - No code is available - {:else} - - {/if} - {:else if job !== undefined && (job.result_stream || (job.type == 'CompletedJob' && job.result !== undefined))} - - {:else if job} - No output is available yet - {/if} -
- {/if} +
+
+ {#if job} +
+ {#if job?.['priority']} + + priority: {job?.['priority']} + {/if} - {:else if job && `running` in job ? job.running : false} - {#if job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)} -
- - -
- {:else} -
Job is still running
- {/if} + {#if job?.['mem_peak']} + + Mem: {job?.['mem_peak'] ? `${(job['mem_peak'] / 1024).toPrecision(4)}MB` : 'N/A'} + + {/if} + {#if workspace && $workspaceStore != workspace} + + Workspace: {workspace} + + {/if} + {#if job.tag} + + Tag: {job.tag} + + {/if} + {#if job?.['labels'] && Array.isArray(job?.['labels']) && job?.['labels'].length > 0} + {#each job?.['labels'] as label} + Label: {label} + {/each} + {/if} + {#if concurrencyKey} + + {#snippet text()} + This job has concurrency limits enabled with the key: + + {/snippet} + Concurrency: {truncateRev(concurrencyKey, 20)} + + {/if} + {#if job?.worker} + + {#snippet text()} + This job was run on worker: + + {/snippet} + Worker: {truncateRev(job.worker, 20)} + + {/if} +
+ + ID: + {job?.id ?? ''} + + +
+ +
+ + {#if job && 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)} +
+
Job is scheduled for
+
{new Date(job?.['scheduled_for']).toLocaleString()}
+
{/if} -
- {/if} + +
+ {#if job?.workflow_as_code_status} + + {/if} + + {#if job?.type === 'CompletedJob'} + {#if job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)} +
+ +
+ {:else} + + Results + Logs + Assets + {#if isScriptPreview(job?.job_kind)} + Code + {/if} + + + + {#if job} +
+ {#if viewTab == 'logs'} +
+ +
+ {:else if viewTab == 'assets'} +
+ +
+ {:else if viewTab == 'code'} +
+ {#if job && 'raw_code' in job && job.raw_code} +
+ +
+ {:else if job} + No code available + {:else} + + {/if} +
+ {:else if job !== undefined && (job.result_stream || (job.type == 'CompletedJob' && job.result !== undefined))} +
+ +
+ {:else if job} + No output is available yet + {/if} +
+ {/if} + {/if} + {:else if job && `running` in job ? job.running : false} + {#if job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)} +
+ + +
+ {:else} +
Job is still running
+ + {/if} + {/if} +
+ {:else if jobIsLoading} +
+ +
+ {/if} +
+ import Tooltip from '$lib/components/meltComponents/Tooltip.svelte' + import PreprocessedArgsDisplay from '$lib/components/runs/PreprocessedArgsDisplay.svelte' + import { truncateHash } from '$lib/utils' + import { base } from '$app/paths' + import { truncateRev } from '$lib/utils' + import WorkerHostname from '$lib/components/WorkerHostname.svelte' + import { workspaceStore } from '$lib/stores' + import Badge from '$lib/components/common/badge/Badge.svelte' + import type { Job } from '$lib/gen' + + interface Props { + job: Job + displayPersistentScriptDefinition?: boolean + openPersistentScriptDrawer?: () => void + concurrencyKey?: string + showScriptHash?: boolean + verySmall?: boolean + } + + let { + job, + displayPersistentScriptDefinition, + openPersistentScriptDrawer, + concurrencyKey, + showScriptHash = true, + verySmall = false + }: Props = $props() + + +{#if job.script_hash && showScriptHash && job.job_kind !== 'aiagent'} + {#if job.job_kind == 'script'} + {truncateHash(job.script_hash)} + {:else} +
+ {truncateHash(job.script_hash)} +
+ {/if} +{/if} +{#if job && 'job_kind' in job} +
+ {job.job_kind} +
+{/if} +{#if job && job.flow_status && job.job_kind === 'script'} + +{/if} +{#if displayPersistentScriptDefinition} + +{/if} +{#if job && 'priority' in job} +
+ priority: {job.priority} +
+{/if} +{#if job.tag} + +
+ Tag: {job.tag} +
+{/if} +{#if !job.visible_to_owner} +
+ + only visible to you + + {#snippet text()} + The option to hide this run from the owner of this script or flow was activated + {/snippet} + + +
+{/if} +{#if job?.['labels'] && Array.isArray(job?.['labels']) && job?.['labels'].length > 0} + {#each job?.['labels'] as label} +
+ Label: {label} +
+ {/each} +{/if} +{#if concurrencyKey} +
+ + {#snippet text()} + This job has concurrency limits enabled with the key + + {concurrencyKey} + + {/snippet} + + Concurrency: {truncateRev(concurrencyKey, 20)} + +
+{/if} +{#if job?.worker} +
+ + {#snippet text()} + worker: + + {job?.worker} +
+ + {/snippet} + + Worker: {truncateRev(job?.worker, 20)} +
+
+{/if} diff --git a/frontend/src/lib/components/runs/RunLabels.svelte b/frontend/src/lib/components/runs/RunLabels.svelte new file mode 100644 index 0000000000..dfe5f09c1b --- /dev/null +++ b/frontend/src/lib/components/runs/RunLabels.svelte @@ -0,0 +1,112 @@ + + +{#if labels && labels.length > 0} +
+ {#each visibleLabels as label} + + + {#snippet text()} + {`Filter by label: ${label}`} + {/snippet} + + {/each} + + {#if hiddenLabels.length > 0} + + {#snippet buttonReplacement()} + + {/snippet} + + {/if} +
+{:else} + - +{/if} diff --git a/frontend/src/lib/components/runs/RunOption.svelte b/frontend/src/lib/components/runs/RunOption.svelte new file mode 100644 index 0000000000..fd70dc58dc --- /dev/null +++ b/frontend/src/lib/components/runs/RunOption.svelte @@ -0,0 +1,30 @@ + + +
+
+ {#if forAttr} + + {:else} + {label} + {/if} + {#if tooltip} + {@render tooltip()} + {/if} +
+ +
+ {@render children()} +
+
diff --git a/frontend/src/lib/components/runs/RunRow.svelte b/frontend/src/lib/components/runs/RunRow.svelte index 6cc9912ca0..0e805bde19 100644 --- a/frontend/src/lib/components/runs/RunRow.svelte +++ b/frontend/src/lib/components/runs/RunRow.svelte @@ -4,24 +4,28 @@ import type { Job } from '$lib/gen' import { displayDate, - msToReadableTime, truncateHash, truncateRev, - isFlowPreview, isScriptPreview, - isJobSelectable + isJobSelectable, + msToReadableTime, + isFlowPreview } from '$lib/utils' import { Badge, Button } from '../common' import ScheduleEditor from '$lib/components/triggers/schedules/ScheduleEditor.svelte' import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte' import { + Bot, Calendar, Check, + Clock, + Code, + ExternalLink, FastForward, - Folder, Hourglass, - ListFilter, + ListFilterPlus, + Package, Play, ShieldQuestion, X @@ -34,45 +38,104 @@ import WaitTimeWarning from '../common/waitTimeWarning/WaitTimeWarning.svelte' import type { RunsSelectionMode } from './RunsBatchActionsDropdown.svelte' + import DropdownV2 from '../DropdownV2.svelte' + import { Tooltip } from '../meltComponents' + import { GitIcon } from '../icons' + import RunLabels from './RunLabels.svelte' + import './runs-grid.css' const dispatch = createEventDispatcher() - export let job: Job - export let selected: boolean = false - export let containerWidth: number = 0 - export let containsLabel: boolean = false - export let activeLabel: string | null - export let selectionMode: RunsSelectionMode | false = false + interface Props { + job: Job + selected?: boolean + containerWidth?: number + containsLabel?: boolean + showTag?: boolean + activeLabel: string | null + selectionMode?: RunsSelectionMode | false + } - let scheduleEditor: ScheduleEditor + let { + job, + selected = false, + containerWidth = 0, + containsLabel = false, + showTag = true, + activeLabel, + selectionMode = false + }: Props = $props() - $: isExternal = job && job.id === '-' + let scheduleEditor: ScheduleEditor | undefined = $state(undefined) + + let isExternal = $derived(job && job.id === '-') + + function getJobKindIcon(jobKind: Job['job_kind']) { + if (jobKind === 'flow' || isFlowPreview(jobKind)) { + return BarsStaggered + } else if (jobKind === 'deploymentcallback') { + return GitIcon + } else if ( + jobKind === 'dependencies' || + jobKind === 'appdependencies' || + jobKind === 'flowdependencies' + ) { + return Package + } else if ( + jobKind === 'script' || + isScriptPreview(jobKind) || + jobKind === 'script_hub' || + jobKind === 'singlescriptflow' + ) { + return Code + } else if (jobKind === 'aiagent') { + return Bot + } + return Code + } + + let labelWidth = $state(0) + + let isJobRecent = $state(true) goto('/schedules')} bind:this={scheduleEditor} /> - - + +
{ + onclick={() => { if (!selectionMode || isJobSelectable(selectionMode)(job)) { dispatch('select') } }} > -
- {#if selectionMode && isJobSelectable(selectionMode)(job)} -
- + + {#if selectionMode} +
+
+
- {/if} +
+ {/if} + + +
{#if isExternal} @@ -114,16 +177,13 @@ {/if}
-
-
+ +
+
{#if job} {#if 'started_at' in job && job.started_at} - Started - {#if job && 'duration_ms' in job && job.duration_ms != undefined} - (Ran in {msToReadableTime( - job.duration_ms - )}{#if job.job_kind == 'flow' || isFlowPreview(job.job_kind)} total{/if}) - {/if} + {isJobRecent ? 'Started' : ''} + {#if job && (job.self_wait_time_ms || job.aggregate_wait_time_ms)} {/if} {:else if `scheduled_for` in job && job.scheduled_for && forLater(job.scheduled_for)} - Scheduled for {displayDate(job.scheduled_for)} + {displayDate(job.scheduled_for)} {:else if job.canceled} {#if job.type == 'CompletedJob'} Cancelled @@ -151,154 +211,207 @@
-
-
- {#if job === undefined} - No job found - {:else} -
-
- {#if job.script_path} -
- {#if isExternal} - - - {:else} - - {job.script_path} - - - {/if} - {#if job.script_path?.startsWith('f/')} - - {/if} -
- {:else if 'job_kind' in job && isScriptPreview(job.job_kind)} - Preview without path - {:else if 'job_kind' in job && job.job_kind == 'dependencies'} - - lock deps of {truncateHash(job.script_hash ?? '')} - - {:else if 'job_kind' in job && job.job_kind == 'identity'} - no op - {/if} -
-
- {/if} -
- - {#if job && job.parent_job} - {#if job.is_flow_step} - - {:else} - - {/if} + +
+ {#if job && 'duration_ms' in job && job.duration_ms != undefined} + {msToReadableTime(job.duration_ms, 2)} + {:else} + - {/if}
- {#if containsLabel} -
- {#if job && job?.['labels']} -
- {#if Array.isArray(job?.['labels'])} - {#each job?.['labels'] as label} - - {/each} + + +
+ {#if job === undefined} + No job found + {:else} + {@const JobKindIcon = getJobKindIcon(job.job_kind)} +
+ +
+ {#if job && job.parent_job} + * + {/if} + +
+ {#snippet text()} + + {#if job && job.job_kind} + {job.job_kind} + {/if} + {#if job && job.is_flow_step && job.parent_job} +
Step of flow + + {truncateRev(job.parent_job, 10)} + + {:else if job && job.parent_job} +
Parent + + {truncateRev(job.parent_job, 10)} + + {/if} +
+ {/snippet} +
+ +
+ {#if job.script_path} +
+ {#if isExternal} + - + {:else} + + {job.script_path} + + {/if} + {#if !isExternal || job.script_path?.startsWith('f/')} + {@const isFolder = job.script_path?.startsWith('f/')} + { + const items = isExternal + ? [] + : [ + { + displayName: `Filter by path: ${job.script_path}`, + action: () => dispatch('filterByPath', job.script_path), + disabled: isExternal + } + ] + if (isFolder) { + const folder = job.script_path?.split('/')[1] + return [ + { + displayName: `Filter by folder: ${folder}`, + action: () => dispatch('filterByFolder', folder) + }, + ...items + ] + } + return items + }} + class="w-fit" + > + {#snippet buttonReplacement()} +
+ +
+ {/snippet} +
+ {/if} +
+ {:else if 'job_kind' in job && isScriptPreview(job.job_kind)} + Preview without path + {:else if 'job_kind' in job && job.job_kind == 'dependencies'} + lock deps of {truncateHash(job.script_hash ?? '')} + {:else if 'job_kind' in job && job.job_kind == 'identity'} + no op + {:else if 'job_kind' in job && isFlowPreview(job.job_kind)} + Preview without path {/if}
- {/if} +
+ {/if} +
+ + {#if containsLabel} +
+ dispatch('filterByLabel', label)} + {labelWidth} + />
{/if} -
+ +
{#if job && job.schedule_path} -
- +
- + {#snippet buttonReplacement()} +
+ +
+ {/snippet} +
{:else} -
-
- {truncateRev(job.created_by ?? '', 20)} +
+
+ {job.created_by ?? ''}
{#if !isExternal} - + {#snippet buttonReplacement()} +
+ +
+ {/snippet} + {/if}
{/if}
+ + + {#if showTag} +
+ {#if job.tag} + {job.tag} + {/if} +
+ {/if} + + + {#if !isExternal} +
+ + + +
+ {/if}
diff --git a/frontend/src/lib/components/runs/RunsBatchActionsDropdown.svelte b/frontend/src/lib/components/runs/RunsBatchActionsDropdown.svelte index 1f682c4fa5..9274aea02e 100644 --- a/frontend/src/lib/components/runs/RunsBatchActionsDropdown.svelte +++ b/frontend/src/lib/components/runs/RunsBatchActionsDropdown.svelte @@ -1,21 +1,36 @@ - -
- {#if !mobile} -
- {#if $workspaceStore == 'admins'} -
- Workspaces - (allWorkspaces = detail === 'all')} - > - {#snippet children({ item })} - - - {/snippet} - -
- {/if} +{#snippet runsTooltip()} + + {#snippet text()} + 'Runs are jobs that have no parent jobs (flows are jobs that are parent of the jobs they + start), they have been triggered through the UI, a schedule or webhook' + {/snippet} + +{/snippet} +{#snippet previewsTooltip()} + + {#snippet text()} + 'Previews are jobs that have been started in the editor as "Tests"' + {/snippet} + +{/snippet} +{#snippet dependenciesTooltip()} + + {#snippet text()} + 'Deploying a script, flow or an app launch a dependency job that create and then attach the + lockfile to the deployed item. This mechanism ensure that logic is always executed with the + exact same direct and indirect dependencies.' + {/snippet} + +{/snippet} +{#snippet syncTooltip()} + + {#snippet text()} + 'Sync jobs that are triggered on every script deployment to sync the workspace with the Git + repository configured in the the workspace settings' + {/snippet} + +{/snippet} -
- Filter by - { - if (e.detail != filterBy) { - path = null - user = null - folder = null - label = null - concurrencyKey = null - tag = null - schedulePath = undefined +{#if !mobile} + {#if $workspaceStore == 'admins'} + + (allWorkspaces = detail === 'all')} + > + {#snippet children({ item })} + + + {/snippet} + + + {/if} + +
+ + { + if (e.detail != filterBy) { + resetFilter() + } + }} + > + {#snippet children({ item })} + + + + filterBy, + (v) => { + resetFilter() + filterBy = v + } } - }} - > - {#snippet children({ item })} - - - - - {/snippet} - -
+ /> + {/snippet} +
+ - {#if filterBy == 'user'} - {#key user} -
- User - user ?? undefined, (v) => (user = v ?? null)} + clearable + onClear={() => ((user = null), dispatch('reset'))} + inputClass="!h-[32px] min-w-36" + onCreateItem={(item) => (usernames.push(item), (user = item))} + createText="Press enter to use this value" + id="user" + /> + + {/key} + {:else if filterBy == 'folder'} + {#key folder} -
- Folder - - folder ?? undefined, (v) => (folder = v ?? null)} + clearable + onClear={() => ((folder = null), dispatch('reset'))} + inputClass="!h-[32px] min-w-36" + id="folder" + /> {/key} - {:else if filterBy === 'path'} + + {:else if filterBy === 'path'} + {#key path} -
- Path - path ?? undefined, (v) => (path = v ?? null)} + clearable + onClear={() => ((path = null), dispatch('reset'))} + inputClass="!h-[32px] min-w-36" + onCreateItem={(item) => (paths.push(item), (path = item))} + createText="Press enter to use this value" + id="path" + /> {/key} - {:else if filterBy === 'label'} + + {:else if filterBy === 'label'} + + {#snippet tooltip()} + Job Labels are string values in the array at the result field 'wm_labels' to easily filter them. + {/snippet} {#key label}
{#if label} @@ -229,20 +283,11 @@ {/if} - Label Job Labels are string values in the array at the result field 'wm_labels' to easily filter them. - { if (labelTimeout) { @@ -254,58 +299,59 @@ }, 1000) }} /> -
+
{/key} - {:else if filterBy === 'concurrencyKey'} + + {:else if filterBy === 'concurrencyKey'} + + {#snippet tooltip()} + For concurrency limited jobs, the concurrency key defines a group of jobs that share the + same limits. + {#if !$enterpriseLicense} + Concurrency limits are an EE feature. + {/if} + {/snippet} {#key concurrencyKey} -
- {#if concurrencyKey} - - {/if} - Concurrency Key - For concurrency limited jobs, the concurrency key defines a group of jobs that share - the same limits. - {#if !$enterpriseLicense} - Concurrency limits are an EE feature. - {/if} - - - - { - if (concurrencyKeyTimeout) { - clearTimeout(concurrencyKeyTimeout) - } - - concurrencyKeyTimeout = setTimeout(() => { - concurrencyKey = displayedConcurrencyKey - }, 1000) + {#if concurrencyKey} +
+ > + + + {/if} + + + { + if (concurrencyKeyTimeout) { + clearTimeout(concurrencyKeyTimeout) + } + + concurrencyKeyTimeout = setTimeout(() => { + concurrencyKey = displayedConcurrencyKey + }, 1000) + }} + id="concurrencyKey" + /> {/key} - {:else if filterBy === 'tag'} +
+ {:else if filterBy === 'tag'} + {#key tag}
{#if tag} @@ -319,13 +365,12 @@ {/if} - Tag { if (tagTimeout) { @@ -336,17 +381,20 @@ tag = displayedTag }, 1000) }} + id="tag" /> -
+
{/key} - {:else if filterBy === 'schedulePath'} + + {:else if filterBy === 'schedulePath'} + {#key tag}
{#if tag} @@ -360,13 +408,12 @@ {/if} - Schedule Path { if (tagTimeout) { @@ -377,10 +424,13 @@ schedulePath = displayedSchedule }, 1000) }} + id="schedulePath" />
{/key} - {:else if filterBy === 'worker'} +
+ {:else if filterBy === 'worker'} + {#key worker}
{#if worker} @@ -394,13 +444,12 @@ {/if} - Worker { if (workerTimeout) { @@ -411,20 +460,70 @@ worker = displayedWorker }, 1000) }} + id="worker" /> -
+
{/key} - {/if} -
-
- Kind + + {/if} +
+ + + + {#if small && !calendarSmall} + { + jobKindsCat = 'all' + }, + id: 'all' + }, + { + displayName: 'Runs', + action: () => { + jobKindsCat = 'runs' + }, + id: 'runs', + extra: runsTooltip + }, + { + displayName: 'Previews', + action: () => { + jobKindsCat = 'previews' + }, + id: 'previews', + extra: previewsTooltip + }, + { + displayName: 'Deps', + action: () => { + jobKindsCat = 'dependencies' + }, + id: 'dependencies', + extra: dependenciesTooltip + }, + { + displayName: 'Sync', + action: () => { + jobKindsCat = 'deploymentcallbacks' + }, + id: 'deploymentcallbacks', + extra: syncTooltip + } + ]} + selected={jobKindsCat} + /> + {:else} {#snippet children({ item })} @@ -435,13 +534,6 @@ tooltip="Runs are jobs that have no parent jobs (flows are jobs that are parent of the jobs they start), they have been triggered through the UI, a schedule or webhook" {item} /> - - jobKindsCat, + (v) => { + resetFilter() + jobKindsCat = v + } + } /> {/snippet} -
-
- Status - { - success = detail === 'all' ? undefined : detail - dispatch('successChange', success) - }} - > - {#snippet children({ item })} - + {/if} + + + + { + success = detail === 'all' ? undefined : detail + dispatch('successChange', success) + }} + id="status" + > + {#snippet children({ item })} + + + + + {#if success == 'waiting'} + {:else if success == 'suspended'} - - {#if success == 'waiting'} - - {:else if success == 'suspended'} - - {/if} - {/snippet} - -
- {/if} + {/if} + {/snippet} + +
+{/if} + {#snippet trigger()} - + {/snippet} {#snippet content()}
- {#if mobile || true} + {#if mobile} + {#if $workspaceStore == 'admins'} + + {/if}
- - {`Filter by a json being a subset of the args/result. Try '\{"foo": "bar"\}'`} - - - +
+ + {`Filter by a json being a subset of the args/result. Try '\{"foo": "bar"\}'`} + + + +
+ diff --git a/frontend/src/lib/components/runs/RunsQueue.svelte b/frontend/src/lib/components/runs/RunsQueue.svelte index 8c5090135b..77c4743767 100644 --- a/frontend/src/lib/components/runs/RunsQueue.svelte +++ b/frontend/src/lib/components/runs/RunsQueue.svelte @@ -1,36 +1,122 @@ - - -
-
Waiting for workers Jobs waiting for a worker being available to be executed
-
{queue_count ? ($queue_count ?? 0).toFixed(0) : '...'}
-
- {#if queue_count && ($queue_count ?? 0) > 0} - - {/if} +
+ {#if small} + + {#snippet trigger()} +
+ + {#if queue_count && ($queue_count ?? 0) > 0} +
+ {queue_count ? ($queue_count ?? 0).toFixed(0) : '...'} +
+ {/if} +
+ {/snippet} -
+ {#snippet content()} + {@render queuedContent()} + {/snippet} + + {:else} + {@render queuedContent()} + {/if} + + {#if small} + + {#snippet trigger()} +
+ +
+ {suspended_count ? ($suspended_count ?? 0).toFixed(0) : '...'} +
+
+ {/snippet} + {#snippet content()} + {@render suspendedContent()} + {/snippet} +
+ {:else} + {@render suspendedContent()} + {/if}
-{#if suspended_count && ($suspended_count ?? 0) > 0} -
-
Suspended Jobs waiting for an event or approval before being resumed
-
{suspended_count ? ($suspended_count ?? 0).toFixed(0) : '...'}
+{#snippet queuedContent()} + + {#snippet tooltip()} + Jobs waiting for a worker being available to be executed + {/snippet} +
0 + ? 'bg-yellow-500 text-white rounded-full w-6 h-6 flex center-center' + : ''}>{queue_count ? ($queue_count ?? 0).toFixed(0) : '...'}
- +
-
-{/if} + +{/snippet} + +{#snippet suspendedContent()} + {#if suspended_count && ($suspended_count ?? 0) > 0} + + {#snippet tooltip()} + Jobs waiting for an event or approval before being resumed + {/snippet} +
0 + ? 'bg-surface-secondary-inverse text-primary-inverse rounded-full w-6 h-6 flex center-center' + : ''}>{suspended_count ? ($suspended_count ?? 0).toFixed(0) : '...'}
+
+ +
+
+ {/if} +{/snippet} diff --git a/frontend/src/lib/components/runs/RunsTable.svelte b/frontend/src/lib/components/runs/RunsTable.svelte index 753f62129b..0c639b23aa 100644 --- a/frontend/src/lib/components/runs/RunsTable.svelte +++ b/frontend/src/lib/components/runs/RunsTable.svelte @@ -1,7 +1,4 @@ computeHeight()} />
- {#if selectionMode && selectableJobCount} - - -
-
- -
- Select all -
- {/if} -
- {#if showExternalJobs && externalJobs.length > 0} -
+
+ {#if selectionMode} +
+ {/if} +
+ {#if showExternalJobs && externalJobs.length > 0}
{jobs ? jobCountString(jobs.length + externalJobs.length, lastFetchWentToEnd) : ''}{externalJobs.length} jobs obscured
-
- {:else if $workspaceStore !== 'admins' && omittedObscuredJobs} -
+ {:else if $workspaceStore !== 'admins' && omittedObscuredJobs} +
+ {jobs ? jobCountString(jobs.length, lastFetchWentToEnd) : ''} + + + {#snippet text()} + Too specific filtering may have caused the omission of obscured jobs. This is done + for security reasons. To see obscured jobs, try removing some filters. + {/snippet} + +
+ {:else} {jobs ? jobCountString(jobs.length, lastFetchWentToEnd) : ''} - - - {#snippet text()} - Too specific filtering may have caused the omission of obscured jobs. This is done for - security reasons. To see obscured jobs, try removing some filters. - {/snippet} - -
- {:else} -
{jobs ? jobCountString(jobs.length, lastFetchWentToEnd) : ''}
- {/if} -
-
Path
+ {/if} +
+
Started
+
Duration
+
Path
{#if containsLabel} -
Label
+
Label
{/if} -
Triggered by
+
Triggered by
+ {#if showTag} +
Tag
+ {/if} +
{#if jobs?.length == 0 && (!showExternalJobs || externalJobs?.length == 0)} @@ -294,13 +274,16 @@ {#if jobOrDate} {#if jobOrDate?.type === 'date'} -
+
{jobOrDate.date}
{:else}
string sortBy?: (a: Item, b: Item) => number onFocus?: () => void @@ -137,6 +139,7 @@ autocomplete="off" onpointerdown={() => (open = true)} bind:this={inputEl} + {id} /> 0) { return `${minutes}m ${seconds % 60}s` } else { - return `${msToSec(ms)}s` + return `${msToSec(ms, maximumFractionDigits)}s` } } @@ -1307,6 +1307,7 @@ export type Item = { type?: 'action' | 'delete' hide?: boolean | undefined extra?: Snippet + id?: string } export function isObjectTooBig(obj: any): boolean { diff --git a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte index cb42658b10..e1b69d283d 100644 --- a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte @@ -22,9 +22,7 @@ encodeState, isFlowPreview, isNotFlow, - isScriptPreview, - truncateHash, - truncateRev + isScriptPreview } from '$lib/utils' import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte' @@ -82,20 +80,21 @@ import { json } from 'svelte-highlight/languages' import Toggle from '$lib/components/Toggle.svelte' import WorkflowTimeline from '$lib/components/WorkflowTimeline.svelte' - import Tooltip from '$lib/components/meltComponents/Tooltip.svelte' + import HighlightTheme from '$lib/components/HighlightTheme.svelte' - import PreprocessedArgsDisplay from '$lib/components/runs/PreprocessedArgsDisplay.svelte' + import ExecutionDuration from '$lib/components/ExecutionDuration.svelte' import CustomPopover from '$lib/components/CustomPopover.svelte' import { isWindmillTooBigObject } from '$lib/components/job_args' import ScheduleEditor from '$lib/components/triggers/schedules/ScheduleEditor.svelte' import { setContext, untrack } from 'svelte' - import WorkerHostname from '$lib/components/WorkerHostname.svelte' + import FlowAssetsHandler, { initFlowGraphAssetsCtx } from '$lib/components/flows/FlowAssetsHandler.svelte' import JobAssetsViewer from '$lib/components/assets/JobAssetsViewer.svelte' import { page } from '$app/state' + import RunBadges from '$lib/components/runs/RunBadges.svelte' let job: (Job & { result?: any; result_stream?: string }) | undefined = $state() let jobUpdateLastFetch: Date | undefined = $state() @@ -764,96 +763,14 @@ {/if} {job.script_path ?? (job.job_kind == 'dependencies' ? 'lock dependencies' : 'No path')}
- {#if job.script_hash && job.job_kind !== 'aiagent'} - {#if job.job_kind == 'script'} - {truncateHash(job.script_hash)} - {:else} -
- {truncateHash(job.script_hash)} -
- {/if} - {/if} - {#if job && 'job_kind' in job} -
- {job.job_kind} -
- {/if} - {#if job && job.flow_status && job.job_kind === 'script'} - - {/if} - {#if persistentScriptDefinition} - - {/if} - {#if job && 'priority' in job} -
- priority: {job.priority} -
- {/if} - {#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'snowflake', 'mssql', 'graphql', 'oracledb', 'nativets', 'bash', 'powershell', 'php', 'rust', 'other', 'ansible', 'csharp', 'nu', 'java', 'duckdb', 'dependency', 'ruby'].includes(job.tag)} - -
- Tag: {job.tag} -
- {/if} - {#if !job.visible_to_owner} -
- - only visible to you - - {#snippet text()} - The option to hide this run from the owner of this script or flow was - activated - {/snippet} - - -
- {/if} - {#if job?.['labels'] && Array.isArray(job?.['labels']) && job?.['labels'].length > 0} - {#each job?.['labels'] as label} -
- Label: {label} -
- {/each} - {/if} - {#if concurrencyKey} -
- - {#snippet text()} - This job has concurrency limits enabled with the key - - {concurrencyKey} - - {/snippet} - - Concurrency: {truncateRev(concurrencyKey, 20)} - -
- {/if} - {#if job?.worker} -
- - {#snippet text()} - worker: - - {job?.worker} -
- - {/snippet} - - Worker: {truncateRev(job?.worker, 20)} -
-
- {/if} + { + persistentScriptDrawer?.open?.(persistentScriptDefinition) + }} + {concurrencyKey} + />
{/if}
@@ -973,7 +890,7 @@
{:else if job} - No code is available + No code available {:else} {/if} @@ -981,7 +898,7 @@
- {:else if job !== undefined && (job.result || job.result_stream)} + {:else if job !== undefined && (job.result_stream || (job.type == 'CompletedJob' && job.result !== undefined))} { loadingSelectedIds && selectedIds.length && setTimeout(() => (loadingSelectedIds = false), 250) }) @@ -770,6 +771,33 @@ extended.jobs.length + extended.obscured_jobs.length >= 1000 ) }) + + const bubble = createBubbler() + + function selectAll() { + if (!selectionMode) return + if (allSelected) { + allSelected = false + selectedIds = [] + } else { + allSelected = true + selectedIds = jobs?.filter(isJobSelectable(selectionMode)).map((j) => j.id) ?? [] + } + } + + let allSelected = $derived.by(() => { + return selectionMode && selectedIds.length === selectableJobCount + }) + + const selectableJobCount = $derived.by(() => { + if (!selectionMode) return 0 + return jobs?.filter(isJobSelectable(selectionMode)).length ?? 0 + }) + + let tableTopBarWidth = $state(0) + + const smallScreenWidth = 1920 + const verySmallScreenWidth = 1300 { reset() loadFromQuery() @@ -853,220 +880,61 @@

Unauthorized

Page not available for operators

-{:else if innerWidth > 900} -
-
-
-
-
-

- Runs -

+{:else} +
+ +
+
+
+

+ Runs +

- - All past and schedule executions of scripts and flows, including previews. You only - see your own runs or runs of groups you belong to unless you are an admin. - -
+ + All past and schedule executions of scripts and flows, including previews. You only see + your own runs or runs of groups you belong to unless you are an admin. +
- { - if (e.detail == 'running' && maxTs != undefined) { - maxTs = undefined - } - }} - {usernames} - {folders} - {paths} - /> -
-
-
-
-
-
- { - graph = detail - graphIsRunsChart = graph === 'RunChart' - }} - > - {#snippet children({ item })} - - - {/snippet} - -
- {#if !graphIsRunsChart} - setLookback(0) - }, - { - displayName: '1 day', - action: () => setLookback(1) - }, - { - displayName: '3 days', - action: () => setLookback(3) - }, - { - displayName: '7 days', - action: () => setLookback(7) - } - ]} - > - {#snippet buttonReplacement()} -
- - {lookback} days lookback - - How far behind the min datetime to start considering jobs for the concurrency - graph. Change this value to include jobs started before the set time window for - the computation of the graph - -
- {/snippet} -
- {/if} -
-
- {#if graph === 'RunChart'} - { - minTs = e.detail.min.toISOString() - maxTs = e.detail.max.toISOString() - manualDatePicker?.resetChoice() - jobsLoader?.loadJobs(minTs, maxTs, true) - }} - on:pointClicked={(e) => { - runsTable?.scrollToRun(e.detail) - }} - /> - {:else if graph === 'ConcurrencyChart'} - { - minTs = e.detail.min.toISOString() - maxTs = e.detail.max.toISOString() - jobsLoader?.loadJobs(minTs, maxTs, true) - }} - /> - {/if} -
-
-
+ { + onJobsWaiting={() => { jobsFilter('waiting') }} - on:jobs_suspended={() => { + onJobsSuspended={() => { jobsFilter('suspended') }} - /> -
-
- { - localStorage.setItem('show_schedules_in_run', showSchedules ? 'true' : 'false') - }} - /> - - 110 ? 'inline' : 'hidden'}>CRON Schedules - - -
-
- Planned later - { - localStorage.setItem('show_future_jobs', showFutureJobs ? 'true' : 'false') - }} - /> - -
-
-
-
- Min datetime - - +
+ +
+ + {#if minTs || maxTs} + + {/if} { minTs = new Date(detail).toISOString() calendarChangeTimeout && clearTimeout(calendarChangeTimeout) @@ -1082,20 +950,22 @@ }, 1000) }} /> -
-
-
-
- Max - + + + + {#if maxTs || minTs} + + {/if} { maxTs = new Date(detail).toISOString() calendarChangeTimeout && clearTimeout(calendarChangeTimeout) @@ -1111,68 +981,292 @@ }, 1000) }} /> -
+ + + {#if minTs || maxTs} + + + + {/if} +
+ + +
+ { + if (e.detail == 'running' && maxTs != undefined) { + maxTs = undefined + } + }} + {usernames} + {folders} + {paths} + mobile={innerWidth < verySmallScreenWidth} + small={innerWidth < smallScreenWidth} + calendarSmall={!minTs && !maxTs} + />
-
-
- - { - lastFetchWentToEnd = false - jobsLoader?.loadJobs(minTs, maxTs, true, true) - }} - bind:minTs - bind:maxTs - bind:selectedManualDate - {loading} - bind:this={manualDatePicker} - /> - { - localStorage.setItem('auto_refresh_in_runs', autoRefresh ? 'true' : 'false') - }} - options={{ right: 'Auto-refresh' }} - textClass="whitespace-nowrap" - />
- + +
+
+
+ { + graph = detail + graphIsRunsChart = graph === 'RunChart' + }} + > + {#snippet children({ item })} + + + {/snippet} + + + {#if !graphIsRunsChart} + setLookback(0), + id: '0' + }, + { + displayName: '1 day', + action: () => setLookback(1), + id: '1' + }, + { + displayName: '3 days', + action: () => setLookback(3), + id: '3' + }, + { + displayName: '7 days', + action: () => setLookback(7), + id: '7' + } + ]} + selected={lookback.toString()} + selectedDisplayName={`${lookback} days lookback`} + > + {#snippet extraLabel()} + + {#snippet text()} + How far behind the min datetime to start considering jobs for the concurrency + graph. Change this value to include jobs started before the set time window for + the computation of the graph + {/snippet} + + {/snippet} + + {/if} +
+
+ {#if graph === 'RunChart'} + { + minTs = zoom.min.toISOString() + maxTs = zoom.max.toISOString() + manualDatePicker?.resetChoice() + jobsLoader?.loadJobs(minTs, maxTs, true) + }} + onPointClicked={(ids) => { + runsTable?.scrollToRun(ids) + }} + /> + {:else if graph === 'ConcurrencyChart'} + { + minTs = zoom.min.toISOString() + maxTs = zoom.max.toISOString() + jobsLoader?.loadJobs(minTs, maxTs, true) + }} + /> + {/if} +
+ +
- {#if jobs} - - {:else} -
- {#each new Array(8) as _} - - {/each} +
+ +
+
+ {#if selectionMode && selectableJobCount} +
+
+ +
+ +
+ {/if} + + +
+ +
+
+ { + localStorage.setItem( + 'show_schedules_in_run', + showSchedules ? 'true' : 'false' + ) + }} + options={tableTopBarWidth < 800 || selectionMode + ? {} + : { right: 'CRON Schedules' }} + /> + + + +
+ +
+ { + localStorage.setItem('show_future_jobs', showFutureJobs ? 'true' : 'false') + }} + id="planned-later" + options={tableTopBarWidth < 800 || selectionMode + ? {} + : { right: 'Planned later' }} + /> + + + +
+
+ { + lastFetchWentToEnd = false + jobsLoader?.loadJobs(minTs, maxTs, true) + }} + bind:minTs + bind:maxTs + bind:selectedManualDate + {loading} + bind:this={manualDatePicker} + /> + { + localStorage.setItem('auto_refresh_in_runs', autoRefresh ? 'true' : 'false') + }} + options={{ right: 'Auto-refresh' }} + textClass="whitespace-nowrap" + /> +
+
- {/if} + + +
+ {#if jobs} + + {:else} +
+ {#each new Array(8) as _} + + {/each} +
+ {/if} +
+
- + {#if selectionMode === 're-run'} {:else if selectedIds.length === 1} @@ -1195,309 +1289,6 @@ {/if} - -
-{:else} -
-
-
-
-

Runs

- - - All past and schedule executions of scripts and flows, including previews. You only see - your own runs or runs of groups you belong to unless you are an admin. - -
- { - if (e.detail == 'running' && maxTs != undefined) { - maxTs = undefined - } - }} - /> -
-
-
-
-
- { - graph = detail - graphIsRunsChart = graph == 'RunChart' - }} - > - {#snippet children({ item })} - - - {/snippet} - - {#if !graphIsRunsChart} - setLookback(0) - }, - { - displayName: '1 day', - action: () => setLookback(1) - }, - { - displayName: '3 days', - action: () => setLookback(3) - }, - { - displayName: '7 days', - action: () => setLookback(7) - } - ]} - > - {#snippet buttonReplacement()} -
- - {lookback} days lookback - - How far behind the min datetime to start considering jobs for the concurrency - graph. Change this value to include jobs started before the set time window for - the computation of the graph - -
- {/snippet} -
- {/if} -
-
- {#if graph === 'RunChart'} - { - minTs = e.detail.min.toISOString() - maxTs = e.detail.max.toISOString() - manualDatePicker?.resetChoice() - jobsLoader?.loadJobs(minTs, maxTs, true) - }} - on:pointClicked={(e) => { - runsTable?.scrollToRun(e.detail) - }} - /> - {:else if graph === 'ConcurrencyChart'} - { - minTs = e.detail.min.toISOString() - maxTs = e.detail.max.toISOString() - jobsLoader?.loadJobs(minTs, maxTs, true) - }} - /> - {/if} -
-
-
- {#if queue_count} - { - jobsFilter('waiting') - }} - on:jobs_suspended={() => { - jobsFilter('suspended') - }} - /> - {/if} -
- -
-
-
-
- { - localStorage.setItem('show_schedules_in_run', showSchedules ? 'true' : 'false') - }} - /> - Schedules - - -
-
- Planned later - { - localStorage.setItem('show_future_jobs', showFutureJobs ? 'true' : 'false') - }} - /> - -
-
-
-
-
- Min - - - - { - minTs = new Date(detail).toISOString() - calendarChangeTimeout && clearTimeout(calendarChangeTimeout) - calendarChangeTimeout = setTimeout(() => { - jobsLoader?.loadJobs(minTs, maxTs, true) - }, 1000) - }} - on:clear={async () => { - minTs = undefined - calendarChangeTimeout && clearTimeout(calendarChangeTimeout) - calendarChangeTimeout = setTimeout(() => { - jobsLoader?.loadJobs(minTs, maxTs, true) - }, 1000) - }} - /> -
-
-
-
- Max - - { - maxTs = new Date(detail).toISOString() - calendarChangeTimeout && clearTimeout(calendarChangeTimeout) - calendarChangeTimeout = setTimeout(() => { - jobsLoader?.loadJobs(minTs, maxTs, true) - }, 1000) - }} - on:clear={async () => { - maxTs = undefined - calendarChangeTimeout && clearTimeout(calendarChangeTimeout) - calendarChangeTimeout = setTimeout(() => { - jobsLoader?.loadJobs(minTs, maxTs, true) - }, 1000) - }} - /> -
-
-
-
- - { - lastFetchWentToEnd = false - jobsLoader?.loadJobs(minTs, maxTs, true, true) - }} - bind:this={manualDatePicker} - bind:minTs - bind:maxTs - bind:selectedManualDate - {loading} - /> - - { - localStorage.setItem('auto_refresh_in_runs', autoRefresh ? 'true' : 'false') - }} - options={{ right: 'Auto-refresh' }} - textClass="whitespace-nowrap" - /> -
-
-
- { - if (!selectionMode) runDrawer?.openDrawer() - }} - on:filterByPath={filterByPath} - on:filterByUser={filterByUser} - on:filterByFolder={filterByFolder} - on:filterByLabel={filterByLabel} - on:filterByConcurrencyKey={filterByConcurrencyKey} - on:filterByWorker={filterByWorker} - on:filterByTag={filterByTag} - bind:this={runsTable} - />
{/if}