Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch re-run jobs UI #4811

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions frontend/src/lib/components/runs/BatchRerunPanel.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<script lang="ts">
import { base } from '$lib/base'
import { FlowService, JobService, ScriptService, type Job, type ScriptArgs } from '../../gen'
import { Tab, Tabs } from '../common'
import { workspaceStore } from '$lib/stores'
import type { Schema } from '$lib/common'
import SchemaForm from '../SchemaForm.svelte'

export let selectedIds: string[]
export let jobs: Job[] | undefined
export let blankLink = false
export let workspace: string | undefined

let viewTab = selectedIds[0] || ''
$: currentViewJob =
viewTab === 'common_args' ? undefined : jobs?.find((job) => job.id === viewTab)

export let args: { [jobId: string]: ScriptArgs }[] = []
let schemas: { [jobId: string]: Schema }[] = []

$: {
if (currentViewJob && !schemas[currentViewJob.id]) {
loadScript(currentViewJob)
}
}

async function loadScript(job: Job): Promise<void> {
if (job.job_kind === 'script') {
if (job.script_hash) {
schemas[job.id] = (
await ScriptService.getScriptByHash({
workspace: $workspaceStore!,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using non-null assertion operator (!) with $workspaceStore. Consider handling the case where $workspaceStore might be null or undefined to prevent potential runtime errors.

hash: job.script_hash
})
).schema
} else if (job.script_path) {
schemas[job.id] = (
await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: job.script_path
})
).schema
}
} else if (job.job_kind === 'flow' && job.script_path) {
schemas[job.id] = (
await FlowService.getFlowByPath({
workspace: $workspaceStore!,
path: job.script_path
})
).schema
}
if (schemas[job.id]?.properties && Object.keys(schemas[job.id]).length > 0) {
args[job.id] = await JobService.getJobArgs({
workspace: $workspaceStore ?? '',
id: job.id
})
}
}

$: if (viewTab !== 'common_args' && !selectedIds.includes(viewTab)) {
viewTab = selectedIds[0] || '' // when you are viewing a job's tab and you deselect it
}
</script>

<div class="p-4 flex flex-col gap-2 items-start h-full">
{#if selectedIds.length > 0}
<div class=" w-full h-full">
<Tabs bind:selected={viewTab}>
<!-- <Tab size="xs" value="common_args">Common Input</Tab> -->
{#each selectedIds as selectedJobId}
<Tab size="xs" value={selectedJobId}
>{jobs?.find((job) => job.id === selectedJobId)?.script_path || 'Job'}</Tab
>
{/each}

<svelte:fragment slot="content">
<div class="flex flex-col flex-1 h-full">
{#if viewTab === 'common_args'}
<p>Not currently implemented</p>
{:else}
{@const currentViewJobId = viewTab}
{@const currentViewSchema = schemas[currentViewJobId]}
<a
href="{base}/run/{currentViewJobId}?workspace={workspace ?? $workspaceStore}"
class="flex flex-row gap-1 items-center"
target={blankLink ? '_blank' : undefined}
>
<span class="font-semibold text-sm leading-6">ID:</span>
<span class="text-sm">{currentViewJobId ?? ''}</span>
</a>

<span class="font-semibold text-xs leading-6">Arguments</span>

{#if currentViewSchema}
<div class="my-2" />
{#if !currentViewSchema.properties || Object.keys(currentViewSchema.properties).length === 0}
<div class="text-sm py-4 italic">No arguments</div>
{:else if args[currentViewJobId]}
<span class=" text-xs leading-6"
>Jobs will be re-ran using the old arguments, you can override them below</span
>
<SchemaForm
prettifyHeader
autofocus
schema={currentViewSchema}
bind:args={args[currentViewJobId]}
/>
{:else}
<div class="text-xs text-tertiary">Loading...</div>
{/if}
{:else}
<div class="text-xs text-tertiary">Loading...</div>
{/if}
{/if}
</div>
</svelte:fragment>
</Tabs>
</div>
{/if}
</div>
19 changes: 14 additions & 5 deletions frontend/src/lib/components/runs/RunRow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 === '-'

</script>

<Portal name="run-row">
Expand All @@ -54,13 +61,16 @@
)}
style="width: {containerWidth}px"
on:click={() => {
if (!isSelectingJobsToCancel || isJobCancelable(job)) {
if (
(!isSelectingJobsToCancel || isJobCancelable(job)) &&
(!isSelectingJobsToRerun || isJobRerunnable(job))
) {
dispatch('select')
}
}}
>
<div class="w-1/12 flex justify-center">
{#if isSelectingJobsToCancel && isJobCancelable(job)}
{#if (isSelectingJobsToCancel && isJobCancelable(job)) || (isSelectingJobsToRerun && isJobRerunnable(job))}
<div class="px-2">
<input type="checkbox" checked={selected} />
</div>
Expand Down Expand Up @@ -123,7 +133,6 @@
Scheduled for {displayDate(job.scheduled_for)}
{:else if job.canceled}
Cancelling job... (created <TimeAgo agoOnlyIfRecent date={job.created_at || ''} />)

{:else}
Waiting for executor (created <TimeAgo agoOnlyIfRecent date={job.created_at || ''} />)
{/if}
Expand Down
17 changes: 13 additions & 4 deletions frontend/src/lib/components/runs/RunsTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
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
export let externalJobs: Job[] = []
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
Expand Down Expand Up @@ -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 ''
Expand Down Expand Up @@ -195,7 +203,7 @@
bind:clientWidth={containerWidth}
>
<div bind:clientHeight={header}>
{#if isSelectingJobsToCancel && cancelableJobCount != 0}
{#if (isSelectingJobsToCancel && cancelableJobCount != 0) || (isSelectingJobsToRerun && rerunableJobCount != 0)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
Expand Down Expand Up @@ -279,9 +287,10 @@
job={jobOrDate.job}
selected={jobOrDate.job.id !== '-' && selectedIds.includes(jobOrDate.job.id)}
{isSelectingJobsToCancel}
{isSelectingJobsToRerun}
on:select={() => {
const jobId = jobOrDate.job.id
if (isSelectingJobsToCancel) {
if (isSelectingJobsToCancel || isSelectingJobsToRerun) {
if (selectedIds.includes(jobOrDate.job.id)) {
selectedIds = selectedIds.filter((id) => id != jobId)
} else {
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand All @@ -615,7 +619,7 @@ export function debounce(func: (...args: any[]) => any, wait: number) {

export function throttle<T>(func: (...args: any[]) => T, wait: number) {
let timeout: any
return function (...args: any[]) {
return function(...args: any[]) {
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
Expand Down Expand Up @@ -831,7 +835,7 @@ export async function tryEvery({
try {
await tryCode()
break
} catch (err) {}
} catch (err) { }
i++
}
if (i >= times) {
Expand Down
Loading
Loading