This commit is contained in:
Clement Zhang
2024-11-18 17:47:50 +01:00
parent 259617ebe4
commit 91f167a28e
5 changed files with 179 additions and 170 deletions
@@ -2,7 +2,13 @@
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
} from '$lib/utils'
import { Badge, Button } from '../common'
import ScheduleEditor from '../ScheduleEditor.svelte'
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
@@ -34,11 +40,11 @@
export let containsLabel: boolean = false
export let activeLabel: string | null
export let isSelectingJobsToCancel: boolean = false
export let isBatch: boolean = false
let scheduleEditor: ScheduleEditor
$: isExternal = job && job.id === '-'
</script>
<Portal name="run-row">
@@ -60,7 +66,7 @@
}}
>
<div class="w-1/12 flex justify-center">
{#if isSelectingJobsToCancel && isJobCancelable(job)}
{#if isBatch || (isSelectingJobsToCancel && isJobCancelable(job))}
<div class="px-2">
<input type="checkbox" checked={selected} />
</div>
@@ -123,7 +129,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}
@@ -1,155 +0,0 @@
<script lang="ts">
import { JobService, type Job, type RunScriptByPathData, type ScriptArgs } from '$lib/gen'
import Modal from '../common/modal/Modal.svelte'
import Button from '../common/button/Button.svelte'
import Toggle from '../Toggle.svelte'
export let jobs: Job[] | undefined
export let modalIsOpen = false
type NewJob = {
prior: Job & { args: ScriptArgs }
newArgs: ScriptArgs
isSelected: boolean
}
// local state
let newJobs: NewJob[] = []
if (jobs) {
const fewJobs = jobs.slice(0, 10)
const promises = fewJobs.map((job) => JobService.getJobArgs({ id: job.id, workspace: 'demo' }))
Promise.allSettled(promises).then((results) => {
const priorArgs = results
.filter((result) => result.status === 'fulfilled')
.map((p) => p.value) as ScriptArgs[]
newJobs = fewJobs.map((job, i) => ({
prior: { ...job, args: priorArgs[i] },
newArgs: structuredClone(priorArgs[i]),
isSelected: false
}))
})
}
type RunnableNewJob = NewJob & {
prior: {
script_path: string
workspace_id: string
}
}
function isRunnableNewJob(newJob: NewJob): newJob is RunnableNewJob {
const { prior } = newJob
return prior.script_path !== undefined && prior.workspace_id !== undefined
}
async function executeBatch() {
console.log(newJobs)
const payloads: RunScriptByPathData[] = newJobs.reduce((payloadsArr, newJob) => {
if (!isRunnableNewJob(newJob)) {
console.error('this job is not runnable:', newJob) // TODO: rm line
return payloadsArr
}
payloadsArr.push({
path: newJob.prior.script_path,
requestBody: newJob.newArgs,
workspace: newJob.prior.workspace_id
})
return payloadsArr
}, [] as RunScriptByPathData[])
console.log(payloads)
const promises = payloads.map((payload) => JobService.runScriptByPath(payload))
Promise.allSettled(promises).then((results) => {
console.log(results)
})
}
</script>
<Modal
bind:open={modalIsOpen}
on:confirmed={() => {
modalIsOpen = false
}}
on:canceled
title="Runnable batch"
class="w-full max-w-full xl:max-w-7xl"
>
<div class="flex space-x-6">
<!-- Left Side: Job List -->
<div class="w-1/3 p-4 border-r">
<h3 class="text-lg font-semibold mb-4">Runnable Jobs</h3>
<ul class="space-y-2">
{#each newJobs as job}
<li>
<Toggle bind:checked={job.isSelected} />
<span>{job.prior.id}</span>
<span>{job.prior.script_path}</span>
<span>{job.prior.tag}</span>
</li>
{/each}
</ul>
</div>
<!-- Right Side: Job Configurations -->
<div class="w-2/3 p-4">
<h3 class="text-lg font-semibold mb-4">Job Configurations</h3>
<Button
on:click={() => {
newJobs = newJobs.map((job) => ({
...job,
isSelected: true
}))
}}
color="light"
size="sm">Select all</Button
>
<Button
on:click={() => {
newJobs = newJobs.map((job) => ({
...job,
newArgs: structuredClone(job.prior.args)
}))
}}
color="light"
size="sm">Reset job args to prior execution</Button
>
{#each newJobs as job}
{#if job.isSelected}
<div class="mb-6">
<h4 class="text-md font-semibold mb-2">{job.prior.id} Config</h4>
<div class="space-y-2">
{#if job.newArgs !== undefined}
{#each Object.keys(job.newArgs) as key}
<div class="flex items-center space-x-4">
<label class="w-20" for={key}>{key}</label>
<input type="text" class="p-2 border rounded" bind:value={job.newArgs[key]} />
</div>
{/each}
{/if}
</div>
</div>
{/if}
{/each}
</div>
</div>
<Button
slot="actions"
on:click={() => {
executeBatch()
// modalIsOpen = false
}}
color="light"
size="sm"
>
<span class="inline-flex gap-2">Execute</span>
</Button>
</Modal>
@@ -0,0 +1,135 @@
<script lang="ts">
import { JobService, type Job, type RunScriptByPathData, type ScriptArgs } from '$lib/gen'
import { RefreshCw } from 'lucide-svelte'
import Button from '../common/button/Button.svelte'
// export let ids: string[]
export let jobs: Job[] | undefined
export let selectedIds: string[]
type NewJob = {
prior: Job & { args: ScriptArgs }
newArgs: ScriptArgs
}
type RunnableNewJob = NewJob & {
prior: {
script_path: string
workspace_id: string
}
}
// local state.
let newJobs: NewJob[] = []
if (jobs) {
const fewJobs = jobs
.filter((job) => selectedIds.indexOf(job.id) > -1)
.filter((job): job is Job & { workspace_id: string } => job.workspace_id !== undefined)
const promises = fewJobs.map((job) =>
JobService.getJobArgs({ id: job.id, workspace: job.workspace_id })
)
Promise.allSettled(promises).then((results) => {
const priorArgs = results
.filter((result) => result.status === 'fulfilled')
.map((p) => p.value) as ScriptArgs[]
newJobs = fewJobs.map((job, i) => ({
prior: { ...job, args: priorArgs[i] },
newArgs: structuredClone(priorArgs[i])
}))
console.log({ newJobs })
})
}
function isRunnableNewJob(newJob: NewJob): newJob is RunnableNewJob {
const { prior } = newJob
return (
prior !== undefined && prior.script_path !== undefined && prior.workspace_id !== undefined
)
}
async function runBatchImmediately() {
console.log('clicked on run', newJobs)
const payloads: RunScriptByPathData[] = newJobs.reduce((payloadsArr, newJob) => {
if (!isRunnableNewJob(newJob)) {
console.error('this job is not runnable:', newJob) // TODO: rm line
return payloadsArr
}
payloadsArr.push({
path: newJob.prior.script_path,
requestBody: newJob.newArgs,
workspace: newJob.prior.workspace_id
})
return payloadsArr
}, [] as RunScriptByPathData[])
console.log('payloads', payloads)
const promises = payloads.map((payload) => JobService.runScriptByPath(payload))
const results = await Promise.allSettled(promises)
console.log('results', results)
return results
}
</script>
<div class="p-4 flex flex-col gap-2 items-start h-full">
<Button
on:click|once={() => {
runBatchImmediately()
}}
color="blue"
size="sm"
startIcon={{ icon: RefreshCw }}>Run immediately selected scripts with same args</Button
>
<!-- <div class="w-2/3 p-4">
<h3 class="text-lg font-semibold mb-4">Job Configurations</h3>
<Button
on:click={() => {
newJobs = newJobs.map((job) => ({
...job,
newArgs: structuredClone(job.prior.args)
}))
}}
color="light"
size="sm">Reset job args to prior execution</Button
>
<Button
on:click={() => {
newJobs = newJobs.sort((a, b) => {
if (a.prior.script_hash === undefined) return 1
if (b.prior.script_hash === undefined) return -1
return a.prior.script_hash.localeCompare(b.prior.script_hash)
})
console.log(newJobs.map((job) => job.prior.script_hash))
}}
color="light"
size="sm"
>Group jobs by script
</Button>
{#each newJobs as job}
<div class="mb-6">
<h4 class="text-md font-semibold mb-2">{job.prior.script_path} Config</h4>
<div class="space-y-2">
{#if job.newArgs !== undefined}
{#each Object.keys(job.newArgs) as key}
<div class="flex items-center space-x-4">
<label class="w-20" for={key}>{key}</label>
<input type="text" class="p-2 border rounded" bind:value={job.newArgs[key]} />
</div>
{/each}
{/if}
</div>
</div>
{/each}
</div> -->
</div>
@@ -9,7 +9,6 @@
import { workspaceStore } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import { isJobCancelable } from '$lib/utils'
import RunnableBatchModal from './RunnableBatchModal.svelte'
//import InfiniteLoading from 'svelte-infinite-loading'
export let jobs: Job[] | undefined = undefined
@@ -17,6 +16,7 @@
export let omittedObscuredJobs: boolean
export let showExternalJobs: boolean = false
export let isSelectingJobsToCancel: boolean = false
export let isBatch: boolean = false
export let selectedIds: string[] = []
export let selectedWorkspace: string | undefined = undefined
export let activeLabel: string | null = null
@@ -149,12 +149,27 @@
selectedIds = []
} else {
allSelected = true
if (isBatch && !isSelectingJobsToCancel) {
selectedIds = jobs?.map((j) => j.id) ?? []
return
}
selectedIds = jobs?.filter(isJobCancelable).map((j) => j.id) ?? []
}
}
let cancelableJobCount: number = 0
$: isSelectingJobsToCancel && (allSelected = selectedIds.length === cancelableJobCount)
$: isSelectingJobsToCancel && (cancelableJobCount = jobs?.filter(isJobCancelable).length ?? 0)
$: handleIsBatchToggle(isBatch)
function handleIsBatchToggle(newIsBatch: boolean) {
if (newIsBatch) {
allSelected = true
selectedIds = jobs?.map((j) => j.id) ?? []
} else {
allSelected = false
selectedIds = []
}
}
function jobCountString(jobCount: number | undefined, lastFetchWentToEnd: boolean): string {
if (jobCount === undefined) {
@@ -186,22 +201,19 @@
}
}
}
let batchModalIsOpen = false
</script>
<svelte:window on:resize={() => computeHeight()} />
<RunnableBatchModal {jobs} bind:modalIsOpen={batchModalIsOpen} />
<!-- <button on:click={() => console.log(isSelectingJobs)}>console.log</button> -->
<div
class="divide-y min-w-[640px] h-full"
id="runs-table-wrapper"
bind:clientWidth={containerWidth}
>
<button on:click={() => (batchModalIsOpen = !batchModalIsOpen)}>Batch execution</button>
<div bind:clientHeight={header}>
{#if isSelectingJobsToCancel && cancelableJobCount != 0}
{#if !!isBatch || (isSelectingJobsToCancel && cancelableJobCount != 0)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
@@ -285,9 +297,10 @@
job={jobOrDate.job}
selected={jobOrDate.job.id !== '-' && selectedIds.includes(jobOrDate.job.id)}
{isSelectingJobsToCancel}
{isBatch}
on:select={() => {
const jobId = jobOrDate.job.id
if (isSelectingJobsToCancel) {
if (isBatch || isSelectingJobsToCancel) {
if (selectedIds.includes(jobOrDate.job.id)) {
selectedIds = selectedIds.filter((id) => id != jobId)
} else {
@@ -39,6 +39,7 @@
import { goto } from '$app/navigation'
import { base } from '$app/paths'
import { isJobCancelable } from '$lib/utils'
import RunsBatch from '$lib/components/runs/RunsBatch.svelte'
let jobs: Job[] | undefined
let selectedIds: string[] = []
@@ -326,6 +327,7 @@
selectedIds = []
jobIdsToCancel = []
isSelectingJobsToCancel = false
isBatch = false
selectedWorkspace = undefined
jobLoader?.loadJobs(minTs, maxTs, true)
}
@@ -437,10 +439,12 @@
let jobIdsToCancel: string[] = []
let isSelectingJobsToCancel = false
let isBatch: boolean = false
let fetchingFilteredJobs = false
let selectedFiltersString: string | undefined = undefined
async function cancelVisibleJobs() {
isBatch = false
isSelectingJobsToCancel = true
selectedIds = jobs?.filter(isJobCancelable).map((j) => j.id) ?? []
if (selectedIds.length === 0) {
@@ -573,7 +577,7 @@
/>
<ConfirmationModal
title={`Confirm cancelling all jobs correspoding to the selected filters (${jobIdsToCancel.length} jobs)`}
title={`Confirm cancelling all jobs corresponding to the selected filters (${jobIdsToCancel.length} jobs)`}
confirmationText={`Cancel ${jobIdsToCancel.length} jobs that matched the filters`}
open={isCancelingFilteredJobs}
on:confirmed={async () => {
@@ -965,6 +969,14 @@
options={{ right: 'Auto-refresh' }}
textClass="whitespace-nowrap"
/>
{#if !isSelectingJobsToCancel}
<Toggle
size="xs"
bind:checked={isBatch}
options={{ right: 'Batch run' }}
textClass="whitespace-nowrap"
/>
{/if}
</div>
</div>
@@ -979,6 +991,7 @@
showExternalJobs={!graphIsRunsChart}
activeLabel={label}
{isSelectingJobsToCancel}
{isBatch}
bind:selectedIds
bind:selectedWorkspace
bind:lastFetchWentToEnd
@@ -1012,9 +1025,7 @@
/>
{/if}
{:else if selectedIds.length > 1}
<div class="text-xs m-4"
>There are {selectedIds.length} jobs selected. Choose 1 to see detailed information</div
>
<RunsBatch {jobs} {selectedIds} />
{:else}
<div class="text-xs m-4">No job selected</div>
{/if}