- {#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
)
{/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}
+