diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0730eb96b7cf4..542e1a1579455 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,7 +24,7 @@ "@redocly/json-to-json-schema": "^0.0.1", "@scalar/openapi-parser": "^0.15.0", "@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1", - "@tutorlatin/svelte-tiny-virtual-list": "^3.0.2", + "@tutorlatin/svelte-tiny-virtual-list": "^3.0.16", "@windmill-labs/svelte-dnd-action": "^0.9.44", "@xterm/addon-fit": "^0.10.0", "@xyflow/svelte": "^1.0.0", @@ -3020,9 +3020,9 @@ } }, "node_modules/@tutorlatin/svelte-tiny-virtual-list": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.15.tgz", - "integrity": "sha512-ew61aZNXGf0b5X+UjbOAhiNwzI21vijhB/mtBs8bpNOVYQ50TG6Qx00t+fR5C72eGnmdzguewZ2WP6QPNOTQJg==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.16.tgz", + "integrity": "sha512-JQSmhRDAFZbq2rTlzn+kFXJayi5VPLxeGjD01ZVyV2ti7PlQE/ov6rQFR1c8s7Y3B1OiTcv3oEWGi3ib69V8eQ==", "license": "MIT", "engines": { "node": ">=20.17.0" diff --git a/frontend/package.json b/frontend/package.json index 70cf96e75451c..fa0dc042be7d1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -89,7 +89,7 @@ "@redocly/json-to-json-schema": "^0.0.1", "@scalar/openapi-parser": "^0.15.0", "@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1", - "@tutorlatin/svelte-tiny-virtual-list": "^3.0.2", + "@tutorlatin/svelte-tiny-virtual-list": "^3.0.16", "@windmill-labs/svelte-dnd-action": "^0.9.44", "@xterm/addon-fit": "^0.10.0", "@xyflow/svelte": "^1.0.0", diff --git a/frontend/src/lib/cancelable-promise-utils.ts b/frontend/src/lib/cancelable-promise-utils.ts new file mode 100644 index 0000000000000..17e8be5a93e64 --- /dev/null +++ b/frontend/src/lib/cancelable-promise-utils.ts @@ -0,0 +1,84 @@ +import { CancelablePromise } from './gen' + +export namespace CancelablePromiseUtils { + export function then( + promise: CancelablePromise, + f: (value: T) => CancelablePromise + ): CancelablePromise { + let promiseToBeCanceled: CancelablePromise = promise + let p = new CancelablePromise((resolve, reject) => { + promise + .then((value1) => { + let promise2 = f(value1) + promiseToBeCanceled = promise2 + promise2.then((value2) => resolve(value2)).catch((err) => reject(err)) + }) + .catch((err) => reject(err)) + }) + p.cancel = () => promiseToBeCanceled.cancel() + return p + } + + export function pure(value: T): CancelablePromise { + return new CancelablePromise((resolve) => resolve(value)) + } + + export function err(error: any): CancelablePromise { + return new CancelablePromise((_, reject) => reject(error)) + } + + export function map( + promise: CancelablePromise, + f: (value: T) => U + ): CancelablePromise { + return then(promise, (value) => pure(f(value))) + } + + export function pipe( + promise: CancelablePromise, + f: (value: T) => void + ): CancelablePromise { + promise.then((value) => { + f(value) + }) + return promise + } + + export function catchErr( + promise: CancelablePromise, + f: (error: any) => CancelablePromise + ): CancelablePromise { + let promiseToBeCanceled: CancelablePromise = promise + let p = new CancelablePromise((resolve, reject) => { + promise + .then((value) => resolve(value)) + .catch((err) => { + let promise2 = f(err) + promiseToBeCanceled = promise2 + return promise2.then((value2) => resolve(value2)).catch((err2) => reject(err2)) + }) + .catch((err) => reject(err)) + }) + p.cancel = () => promiseToBeCanceled.cancel() + return p + } + + export function finallyDo(promise: CancelablePromise, f: () => void): CancelablePromise { + promise = map(promise, (value) => (f(), value)) + promise = catchErr(promise, (e) => (f(), err(e))) + return promise + } + + // Calls onTimeout if the promise does not settle within timeoutMs milliseconds + export function onTimeout( + promise: CancelablePromise, + timeoutMs: number, + onTimeout: () => void + ): CancelablePromise { + let timeoutId: number | undefined = setTimeout(onTimeout, timeoutMs) + promise = finallyDo(promise, () => { + if (timeoutId !== undefined) clearTimeout(timeoutId) + }) + return promise + } +} diff --git a/frontend/src/lib/components/RunChart.svelte b/frontend/src/lib/components/RunChart.svelte index d40cddc8080e2..78873a3537941 100644 --- a/frontend/src/lib/components/RunChart.svelte +++ b/frontend/src/lib/components/RunChart.svelte @@ -28,6 +28,7 @@ selectedIds?: string[] canSelect?: boolean lastFetchWentToEnd?: boolean + totalRowsFetched: number onPointClicked: (ids: string[]) => void onLoadExtra: () => void onZoom: (zoom: { min: Date; max: Date }) => void @@ -41,6 +42,7 @@ selectedIds = $bindable([]), canSelect = true, lastFetchWentToEnd = false, + totalRowsFetched, onPointClicked, onLoadExtra, onZoom @@ -301,13 +303,14 @@
{#if !lastFetchWentToEnd} -
+
+ +
{/if}
diff --git a/frontend/src/lib/components/RunsPage.svelte b/frontend/src/lib/components/RunsPage.svelte new file mode 100644 index 0000000000000..75d97f2d6f745 --- /dev/null +++ b/frontend/src/lib/components/RunsPage.svelte @@ -0,0 +1,1366 @@ + + + + + + + { + const func = askingForConfirmation?.onConfirm + await func?.(forceCancelInPopup) + askingForConfirmation = undefined + }} + type={askingForConfirmation?.type} + loading={askingForConfirmation?.loading} + on:canceled={() => { + askingForConfirmation = undefined + }} +> + {#if askingForConfirmation?.preContent} +
{askingForConfirmation.preContent}
+ + {#if forceCancelInPopup} +
+

+ Force cancel is enabled. This is dangerous, only do this if you have no alternatives. + Instead of being gracefully cancelled, all jobs will be immediately sent to the completed + job table regardless of them being processed or not or part of running flows. You may end + up in an inconsistent state. +

+
+ {/if} + {/if} +
+ + + + {#if selectedIds.length === 1} + {#if selectedIds[0] === '-'} +
There is no information available for this job
+ {:else} + + {/if} + {/if} +
+
+ + { + reset() + loadFromQuery() + }} +/> + +{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.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. + +
+ + + { + jobsFilter('waiting') + }} + onJobsSuspended={() => { + jobsFilter('suspended') + }} + small={innerWidth < smallScreenWidth} + /> +
+ +
+ +
+ + {#if minTs || maxTs} + + {/if} + { + 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) + }} + /> + + + + {#if maxTs || minTs} + + {/if} + { + 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) + }} + /> + + + {#if minTs || maxTs} + + + + {/if} +
+ + +
+ { + if (e.detail == 'running' && maxTs != undefined) { + maxTs = undefined + } + }} + {usernames} + {folders} + {paths} + mobile={innerWidth < verySmallScreenWidth} + small={innerWidth < smallScreenWidth} + calendarSmall={!minTs && !maxTs} + /> +
+
+
+ + +
+
+
+ { + 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 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} + numberOfLastJobsToFetch={perPage} + /> + { + localStorage.setItem('auto_refresh_in_runs', autoRefresh ? 'true' : 'false') + }} + options={{ right: 'Auto-refresh' }} + textClass="whitespace-nowrap" + /> +
+
+
+ + +
+ {#if jobs} + + {:else} +
+ {#each new Array(8) as _} + + {/each} +
+ {/if} +
+
+ Per page: + - {#if usernames} - {#if $userStore?.is_admin || $userStore?.is_super_admin} - - {/if} - {#each usernames as e} - {#if e == username || $userStore?.is_admin || $userStore?.is_super_admin} - - {:else} - - {/if} - {/each} - {/if} - + (resources.value?.push(r), (resource = r))} createText="Press enter to use this value" bind:value={resource} @@ -423,6 +401,7 @@ Operation - - {#each ['Create', 'Update', 'Delete', 'Execute'] as e} - - {/each} - + - {/if} - { - 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) - }} - /> - - - - {#if maxTs || minTs} - - {/if} - { - 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) - }} - /> - - - {#if minTs || maxTs} - - - - {/if} -
- - -
- { - if (e.detail == 'running' && maxTs != undefined) { - maxTs = undefined - } - }} - {usernames} - {folders} - {paths} - mobile={innerWidth < verySmallScreenWidth} - small={innerWidth < smallScreenWidth} - calendarSmall={!minTs && !maxTs} - /> -
-
-
- - -
-
-
- { - 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 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 jobs} - - {:else} -
- {#each new Array(8) as _} - - {/each} -
- {/if} -
-
-
- 0}> - {#if selectionMode === 're-run'} - - {:else if selectedIds.length === 1} - {#if selectedIds[0] === '-'} -
There is no information available for this job
- {:else} - - {/if} - {:else if selectedIds.length > 1} -
There are {selectedIds.length} jobs selected. Choose 1 to see detailed information
- {/if} -
-
-
-
-{/if} +{#key perPage} + +{/key}