diff --git a/frontend/src/lib/components/runs/BatchRerunPanel.svelte b/frontend/src/lib/components/runs/BatchRerunPanel.svelte new file mode 100644 index 0000000000000..94beff19340d1 --- /dev/null +++ b/frontend/src/lib/components/runs/BatchRerunPanel.svelte @@ -0,0 +1,120 @@ + + +
+ {#if selectedIds.length > 0} +
+ + + {#each selectedIds as selectedJobId} + {jobs?.find((job) => job.id === selectedJobId)?.script_path || 'Job'} + {/each} + + +
+ {#if viewTab === 'common_args'} +

Not currently implemented

+ {:else} + {@const currentViewJobId = viewTab} + {@const currentViewSchema = schemas[currentViewJobId]} + + ID: + {currentViewJobId ?? ''} + + + Arguments + + {#if currentViewSchema} +
+ {#if !currentViewSchema.properties || Object.keys(currentViewSchema.properties).length === 0} +
No arguments
+ {:else if args[currentViewJobId]} + Jobs will be re-ran using the old arguments, you can override them below + + {:else} +
Loading...
+ {/if} + {:else} +
Loading...
+ {/if} + {/if} +
+ + +
+ {/if} +
diff --git a/frontend/src/lib/components/runs/RunRow.svelte b/frontend/src/lib/components/runs/RunRow.svelte index f79b1b7307e65..facdaf7b7d75a 100644 --- a/frontend/src/lib/components/runs/RunRow.svelte +++ b/frontend/src/lib/components/runs/RunRow.svelte @@ -2,7 +2,14 @@ import { base } from '$lib/base' import { goto } from '$lib/navigation' import type { Job } from '$lib/gen' - import { displayDate, msToReadableTime, truncateHash, truncateRev, isJobCancelable } from '$lib/utils' + import { + displayDate, + msToReadableTime, + truncateHash, + truncateRev, + isJobCancelable, + isJobRerunnable + } from '$lib/utils' import { Badge, Button } from '../common' import ScheduleEditor from '../ScheduleEditor.svelte' import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte' @@ -34,11 +41,11 @@ export let containsLabel: boolean = false export let activeLabel: string | null export let isSelectingJobsToCancel: boolean = false + export let isSelectingJobsToRerun: boolean = false let scheduleEditor: ScheduleEditor $: isExternal = job && job.id === '-' - @@ -54,13 +61,16 @@ )} style="width: {containerWidth}px" on:click={() => { - if (!isSelectingJobsToCancel || isJobCancelable(job)) { + if ( + (!isSelectingJobsToCancel || isJobCancelable(job)) && + (!isSelectingJobsToRerun || isJobRerunnable(job)) + ) { dispatch('select') } }} >
- {#if isSelectingJobsToCancel && isJobCancelable(job)} + {#if (isSelectingJobsToCancel && isJobCancelable(job)) || (isSelectingJobsToRerun && isJobRerunnable(job))}
@@ -123,7 +133,6 @@ Scheduled for {displayDate(job.scheduled_for)} {:else if job.canceled} Cancelling job... (created ) - {:else} Waiting for executor (created ) {/if} diff --git a/frontend/src/lib/components/runs/RunsTable.svelte b/frontend/src/lib/components/runs/RunsTable.svelte index 340eaeba9b7ef..cd88bfb7f4fa5 100644 --- a/frontend/src/lib/components/runs/RunsTable.svelte +++ b/frontend/src/lib/components/runs/RunsTable.svelte @@ -8,7 +8,7 @@ import Popover from '../Popover.svelte' import { workspaceStore } from '$lib/stores' import { twMerge } from 'tailwind-merge' - import { isJobCancelable } from '$lib/utils' + import { isJobCancelable, isJobRerunnable } from '$lib/utils' //import InfiniteLoading from 'svelte-infinite-loading' export let jobs: Job[] | undefined = undefined @@ -16,6 +16,7 @@ export let omittedObscuredJobs: boolean export let showExternalJobs: boolean = false export let isSelectingJobsToCancel: boolean = false + export let isSelectingJobsToRerun: boolean = false export let selectedIds: string[] = [] export let selectedWorkspace: string | undefined = undefined export let activeLabel: string | null = null @@ -148,13 +149,20 @@ selectedIds = [] } else { allSelected = true - selectedIds = jobs?.filter(isJobCancelable).map((j) => j.id) ?? [] + selectedIds = + (isSelectingJobsToCancel + ? jobs?.filter(isJobCancelable).map((j) => j.id) + : jobs?.filter(isJobRerunnable).map((j) => j.id)) ?? [] } } let cancelableJobCount: number = 0 $: isSelectingJobsToCancel && (allSelected = selectedIds.length === cancelableJobCount) $: isSelectingJobsToCancel && (cancelableJobCount = jobs?.filter(isJobCancelable).length ?? 0) + let rerunableJobCount: number = 0 + $: isSelectingJobsToRerun && (allSelected = selectedIds.length === rerunableJobCount) + $: isSelectingJobsToRerun && (rerunableJobCount = jobs?.filter(isJobRerunnable).length ?? 0) + function jobCountString(jobCount: number | undefined, lastFetchWentToEnd: boolean): string { if (jobCount === undefined) { return '' @@ -195,7 +203,7 @@ bind:clientWidth={containerWidth} >
- {#if isSelectingJobsToCancel && cancelableJobCount != 0} + {#if (isSelectingJobsToCancel && cancelableJobCount != 0) || (isSelectingJobsToRerun && rerunableJobCount != 0)}
{ const jobId = jobOrDate.job.id - if (isSelectingJobsToCancel) { + if (isSelectingJobsToCancel || isSelectingJobsToRerun) { if (selectedIds.includes(jobOrDate.job.id)) { selectedIds = selectedIds.filter((id) => id != jobId) } else { diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index dbc600c715963..f4b3dfb3dd3a7 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -20,6 +20,10 @@ export function isJobCancelable(j: Job): boolean { return j.type === 'QueuedJob' && !j.schedule_path && !j.canceled } +export function isJobRerunnable(j: Job): boolean { + return j.type === 'CompletedJob' && (j.job_kind === "script" || j.job_kind === "flow") +} + export function validateUsername(username: string): string { if (username != '' && !/^[a-zA-Z]\w+$/.test(username)) { return 'username can only contain letters and numbers and must start with a letter' @@ -605,7 +609,7 @@ export function isObject(obj: any) { export function debounce(func: (...args: any[]) => any, wait: number) { let timeout: any - return function (...args: any[]) { + return function(...args: any[]) { // @ts-ignore const context = this clearTimeout(timeout) @@ -615,7 +619,7 @@ export function debounce(func: (...args: any[]) => any, wait: number) { export function throttle(func: (...args: any[]) => T, wait: number) { let timeout: any - return function (...args: any[]) { + return function(...args: any[]) { if (!timeout) { timeout = setTimeout(() => { timeout = null @@ -831,7 +835,7 @@ export async function tryEvery({ try { await tryCode() break - } catch (err) {} + } catch (err) { } i++ } if (i >= times) { diff --git a/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte index 120699e183739..744980fddc9a1 100644 --- a/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte @@ -7,7 +7,8 @@ FolderService, ScriptService, FlowService, - type ExtendedJobs + type ExtendedJobs, + type ScriptArgs } from '$lib/gen' import { page } from '$app/stores' @@ -38,7 +39,8 @@ import DropdownV2 from '$lib/components/DropdownV2.svelte' import { goto } from '$app/navigation' import { base } from '$app/paths' - import { isJobCancelable } from '$lib/utils' + import { isJobCancelable, isJobRerunnable } from '$lib/utils' + import BatchRerunPanel from '$lib/components/runs/BatchRerunPanel.svelte' let jobs: Job[] | undefined let selectedIds: string[] = [] @@ -156,6 +158,8 @@ let runDrawer: Drawer let isCancelingVisibleJobs = false let isCancelingFilteredJobs = false + let isRerunningVisibleJobs = false + let isRerunningFilteredJobs = false let lookback: number = 1 let innerWidth = window.innerWidth @@ -326,6 +330,8 @@ selectedIds = [] jobIdsToCancel = [] isSelectingJobsToCancel = false + jobIdsToRerun = [] + isSelectingJobsToRerun = false selectedWorkspace = undefined jobLoader?.loadJobs(minTs, maxTs, true) } @@ -437,10 +443,15 @@ let jobIdsToCancel: string[] = [] let isSelectingJobsToCancel = false + let jobIdsToRerun: string[] = [] + let isSelectingJobsToRerun = false + let changedRerunArgs: { [jobId: string]: ScriptArgs }[] = [] let fetchingFilteredJobs = false let selectedFiltersString: string | undefined = undefined async function cancelVisibleJobs() { + isSelectingJobsToRerun = false + selectedIds = [] isSelectingJobsToCancel = true selectedIds = jobs?.filter(isJobCancelable).map((j) => j.id) ?? [] if (selectedIds.length === 0) { @@ -448,6 +459,8 @@ } } async function cancelFilteredJobs() { + isSelectingJobsToRerun = false + selectedIds = [] isCancelingFilteredJobs = true fetchingFilteredJobs = true const selectedFilters = { @@ -497,6 +510,66 @@ isCancelingVisibleJobs = true } + async function rerunSelectedJobs() { + jobIdsToRerun = selectedIds + isRerunningVisibleJobs = true + } + + async function rerunVisibleJobs() { + isSelectingJobsToCancel = false + selectedIds = [] + isSelectingJobsToRerun = true + selectedIds = jobs?.filter(isJobRerunnable).map((j) => j.id) ?? [] + if (selectedIds.length === 0) { + sendUserToast('There are no visible jobs that can be re-ran', true) + } + } + async function rerunFilteredJobs() { + isSelectingJobsToCancel = false + selectedIds = [] + isRerunningFilteredJobs = true + fetchingFilteredJobs = true + const selectedFilters = { + workspace: $workspaceStore ?? '', + startedBefore: maxTs, + startedAfter: minTs, + schedulePath, + scriptPathExact: path === null || path === '' ? undefined : path, + createdBy: user === null || user === '' ? undefined : user, + scriptPathStart: folder === null || folder === '' ? undefined : `f/${folder}/`, + jobKinds, + success: success == 'success' ? true : success == 'failure' ? false : undefined, + running: + success == 'running' || success == 'suspended' + ? true + : success == 'waiting' + ? false + : undefined, + isSkipped: isSkipped ? undefined : false, + // isFlowStep: jobKindsCat != 'all' ? false : undefined, + hasNullParent: + path != undefined || path != undefined || jobKindsCat != 'all' ? true : undefined, + label: label === null || label === '' ? undefined : label, + tag: tag === null || tag === '' ? undefined : tag, + isNotSchedule: showSchedules == false ? true : undefined, + suspended: success == 'waiting' ? false : success == 'suspended' ? true : undefined, + scheduledForBeforeNow: + showFutureJobs == false || success == 'waiting' || success == 'suspended' + ? true + : undefined, + args: + argFilter && argFilter != '{}' && argFilter != '' && argError == '' ? argFilter : undefined, + result: + resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == '' + ? resultFilter + : undefined, + allWorkspaces: allWorkspaces ? true : undefined + } + + selectedFiltersString = JSON.stringify(selectedFilters, null, 4) + jobIdsToRerun = await JobService.listFilteredUuids(selectedFilters) // TODO + fetchingFilteredJobs = false + } function jobCountString(count: number) { return `${count} ${count == 1 ? 'job' : 'jobs'}` } @@ -617,6 +690,49 @@ }} /> + { + isRerunningVisibleJobs = false + let jobArgsPromises = jobIdsToRerun.map( + (jobId) => + changedRerunArgs[jobId] ?? + JobService.getJobArgs({ + workspace: $workspaceStore ?? '', + id: jobId + }) + ) + + const jobArgs = await Promise.all(jobArgsPromises) + const selectedJobs = jobIdsToRerun.map((jobId) => jobs?.find((job) => job.id === jobId)) + + const jobsToRerun = selectedJobs.map((job, i) => + job?.job_kind === 'script' + ? JobService.runScriptByHash({ + workspace: $workspaceStore ?? '', + hash: job?.script_hash, + requestBody: jobArgs[i] + }) + : JobService.runFlowByPath({ + workspace: $workspaceStore ?? '', + path: job?.script_path, + requestBody: jobArgs[i] + }) + ) + await Promise.all(jobsToRerun) + jobIdsToRerun = [] + selectedIds = [] + jobLoader?.loadJobs(minTs, maxTs, true, true) + sendUserToast(`Re-ran ${jobArgs.length} jobs`) + isSelectingJobsToRerun = false + }} + on:canceled={() => { + isRerunningVisibleJobs = false + }} +/> + {#if selectedIds.length === 1} @@ -856,6 +972,70 @@ {/if}
+ +
+ {#if isSelectingJobsToRerun} +
+ +
+ {:else if !$userStore?.is_admin && !$superadmin} + + +
+ Batch re-run jobs + +
+
+
+ {:else} + + +
+ Batch Re-run jobs + +
+
+
+ {/if} +
- {#if selectedIds.length === 1} + {#if isSelectingJobsToRerun} + + {:else if selectedIds.length === 1} {#if selectedIds[0] === '-'}
There is no information available for this job
{:else} @@ -1230,6 +1418,70 @@ {/if}
+ +
+ {#if isSelectingJobsToRerun} +
+ +
+ {:else if !$userStore?.is_admin && !$superadmin} + + +
+ Batch Re-run jobs + +
+
+
+ {:else} + + +
+ Batch Re-run Jobs + +
+
+
+ {/if} +