mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 16:03:47 +00:00
v0 ok
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user