mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 16:02:23 +00:00
285 lines
8.1 KiB
Svelte
285 lines
8.1 KiB
Svelte
<script lang="ts">
|
|
import { base } from '$lib/base'
|
|
import { goto } from '$lib/navigation'
|
|
import type { Job } from '$lib/gen'
|
|
import { displayDate, msToReadableTime, truncateHash, truncateRev } from '$lib/utils'
|
|
import { Badge, Button } from '../common'
|
|
import ScheduleEditor from '../ScheduleEditor.svelte'
|
|
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
|
|
|
import {
|
|
Calendar,
|
|
Check,
|
|
FastForward,
|
|
Folder,
|
|
Hourglass,
|
|
ListFilter,
|
|
Play,
|
|
ShieldQuestion,
|
|
X
|
|
} from 'lucide-svelte'
|
|
import { createEventDispatcher } from 'svelte'
|
|
import TimeAgo from '../TimeAgo.svelte'
|
|
import { forLater } from '$lib/forLater'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import Portal from '$lib/components/Portal.svelte'
|
|
|
|
import WaitTimeWarning from '../common/waitTimeWarning/WaitTimeWarning.svelte'
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
export let job: Job
|
|
export let selected: boolean = false
|
|
export let containerWidth: number = 0
|
|
export let containsLabel: boolean = false
|
|
export let activeLabel: string | null
|
|
export let isSelectingJobsToCancel: boolean = false
|
|
|
|
let scheduleEditor: ScheduleEditor
|
|
|
|
$: isExternal = job && job.id === '-'
|
|
|
|
function isJobCancelable(j: Job): boolean {
|
|
return j.type === 'QueuedJob' && !j.schedule_path
|
|
}
|
|
</script>
|
|
|
|
<Portal>
|
|
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
|
</Portal>
|
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
<div
|
|
class={twMerge(
|
|
'hover:bg-surface-hover cursor-pointer',
|
|
selected ? 'bg-blue-50 dark:bg-blue-900/50' : '',
|
|
'flex flex-row items-center h-full'
|
|
)}
|
|
style="width: {containerWidth}px"
|
|
on:click={() => {
|
|
if (!isSelectingJobsToCancel || isJobCancelable(job)) {
|
|
dispatch('select')
|
|
}
|
|
}}
|
|
>
|
|
<div class="w-1/12 flex justify-center">
|
|
{#if isSelectingJobsToCancel && isJobCancelable(job)}
|
|
<div class="px-2">
|
|
<input type="checkbox" checked={selected} />
|
|
</div>
|
|
{/if}
|
|
{#if isExternal}
|
|
<Badge color="gray" baseClass="!px-1.5">
|
|
<ShieldQuestion size={14} />
|
|
</Badge>
|
|
{:else if 'success' in job && job.success}
|
|
{#if job.is_skipped}
|
|
<Badge color="green" rounded>
|
|
<FastForward size={14} />
|
|
</Badge>
|
|
{:else}
|
|
<Badge color="green" baseClass="!px-1.5">
|
|
<Check size={14} />
|
|
</Badge>
|
|
{/if}
|
|
{:else if 'success' in job}
|
|
<Badge color="red" baseClass="!px-1.5">
|
|
<X size={14} />
|
|
</Badge>
|
|
{:else if 'running' in job && job.running}
|
|
<Badge color="yellow" baseClass="!px-1.5">
|
|
<Play size={14} />
|
|
</Badge>
|
|
{:else if job && 'running' in job && job.scheduled_for && forLater(job.scheduled_for)}
|
|
<Badge color="blue" baseClass="!px-1.5">
|
|
<Calendar size={14} />
|
|
</Badge>
|
|
{:else}
|
|
<Badge baseClass="!px-1.5">
|
|
<Hourglass size={14} />
|
|
</Badge>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="w-4/12 flex justify-start">
|
|
<div class="flex flex-row items-center gap-1 text-gray-500 dark:text-gray-300 text-2xs">
|
|
{#if job}
|
|
{#if 'started_at' in job && job.started_at}
|
|
Started <TimeAgo withDate agoOnlyIfRecent date={job.started_at ?? ''} />
|
|
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
|
(Ran in {msToReadableTime(
|
|
job.duration_ms
|
|
)}{#if job.job_kind == 'flow' || job.job_kind == 'flowpreview'} total{/if})
|
|
{/if}
|
|
{#if job && (job.self_wait_time_ms || job.aggregate_wait_time_ms)}
|
|
<WaitTimeWarning
|
|
self_wait_time_ms={job.self_wait_time_ms}
|
|
aggregate_wait_time_ms={job.aggregate_wait_time_ms}
|
|
variant="icon"
|
|
/>
|
|
{/if}
|
|
{:else if `scheduled_for` in job && job.scheduled_for && forLater(job.scheduled_for)}
|
|
Scheduled for {displayDate(job.scheduled_for)}
|
|
{:else}
|
|
Waiting for executor (created <TimeAgo
|
|
withDate
|
|
agoOnlyIfRecent
|
|
date={job.created_at || ''}
|
|
/>)
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="w-4/12 flex justify-start flex-col">
|
|
<div class="flex flex-row text-sm">
|
|
{#if job === undefined}
|
|
No job found
|
|
{:else}
|
|
<div class="flex flex-row gap-1 min-w-0">
|
|
<div class="whitespace-nowrap text-xs font-semibold truncate">
|
|
{#if job.script_path}
|
|
<div class="flex flex-row gap-1 items-center">
|
|
{#if isExternal}
|
|
<span class="w-30 justify-center">-</span>
|
|
{:else}
|
|
<a
|
|
href="{base}/run/{job.id}?workspace={job.workspace_id}"
|
|
class="truncate w-30 dark:text-blue-400"
|
|
>
|
|
{job.script_path}
|
|
</a>
|
|
<Button
|
|
title="Filter by path"
|
|
size="xs2"
|
|
color="light"
|
|
on:click={() => {
|
|
dispatch('filterByPath', job.script_path)
|
|
}}
|
|
>
|
|
<ListFilter size={10} />
|
|
</Button>
|
|
{/if}
|
|
{#if job.script_path?.startsWith('f/')}
|
|
<Button
|
|
title="Filter by folder"
|
|
size="xs2"
|
|
color="light"
|
|
on:click={() => {
|
|
// split script_path by / and get the second element
|
|
const folder = job.script_path?.split('/')[1]
|
|
|
|
dispatch('filterByFolder', folder)
|
|
}}
|
|
>
|
|
<Folder size={10} />
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
{:else if 'job_kind' in job && job.job_kind == 'preview'}
|
|
<a href="{base}/run/{job.id}?workspace={job.workspace_id}">Preview without path </a>
|
|
{:else if 'job_kind' in job && job.job_kind == 'dependencies'}
|
|
<a href="{base}/run/{job.id}?workspace={job.workspace_id}">
|
|
lock deps of {truncateHash(job.script_hash ?? '')}
|
|
</a>
|
|
{:else if 'job_kind' in job && job.job_kind == 'identity'}
|
|
<a href="{base}/run/{job.id}?workspace={job.workspace_id}">no op</a>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if job && job.parent_job}
|
|
{#if job.is_flow_step}
|
|
<div class="flex flex-row gap-1 items-center">
|
|
<BarsStaggered class="text-secondary" size={14} />
|
|
<span class="mx-1 text-xs">
|
|
Step of flow <a href={`${base}/run/${job.parent_job}?workspace=${job.workspace_id}`}>
|
|
{truncateRev(job.parent_job, 6)}
|
|
</a>
|
|
</span>
|
|
</div>
|
|
{:else}
|
|
<div class="flex flex-row gap-1 items-center">
|
|
<span class="text-2xs text-tertiary truncate">
|
|
parent <a href={`${base}/run/${job.parent_job}?workspace=${job.workspace_id}`}>
|
|
{truncateRev(job.parent_job, 10)}
|
|
</a>
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
{#if containsLabel}
|
|
<div class="w-3/12 flex justify-start px-0.5">
|
|
{#if job && job?.['labels']}
|
|
<div class="flex flex-row items-center gap-1 overflow-x-auto">
|
|
{#if Array.isArray(job?.['labels'])}
|
|
{#each job?.['labels'] as label}
|
|
<Button
|
|
variant="border"
|
|
size="xs3"
|
|
btnClasses={twMerge(
|
|
activeLabel == label ? 'bg-blue-50 dark:bg-blue-900/50' : '',
|
|
'!text-2xs !font-normal truncate max-w-28'
|
|
)}
|
|
color="light"
|
|
on:click={() => {
|
|
dispatch('filterByLabel', label)
|
|
}}
|
|
endIcon={{ icon: ListFilter }}
|
|
>
|
|
{label}
|
|
</Button>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
<div class="w-3/12 flex justify-start">
|
|
{#if job && job.schedule_path}
|
|
<div class="flex flex-row items-center gap-1">
|
|
<Calendar size={14} />
|
|
<Button
|
|
size="xs2"
|
|
color="light"
|
|
btnClasses="font-normal"
|
|
on:click={() => scheduleEditor?.openEdit(job.schedule_path ?? '', job.job_kind == 'flow')}
|
|
>
|
|
<div class="truncate text-ellipsis text-left" title={job.schedule_path}>
|
|
{truncateRev(job.schedule_path, 20)}
|
|
</div>
|
|
</Button>
|
|
<Button
|
|
size="xs2"
|
|
color="light"
|
|
on:click={() => {
|
|
dispatch('filterBySchedule', job.schedule_path)
|
|
}}
|
|
>
|
|
<ListFilter size={10} />
|
|
</Button>
|
|
</div>
|
|
{:else}
|
|
<div class="flex flex-row gap-1 items-center">
|
|
<div class="text-xs truncate text-ellipsis text-left" title={job.created_by}>
|
|
{truncateRev(job.created_by ?? '', 20)}
|
|
</div>
|
|
{#if !isExternal}
|
|
<Button
|
|
size="xs2"
|
|
color="light"
|
|
on:click={() => {
|
|
dispatch('filterByUser', job.created_by ?? '')
|
|
}}
|
|
>
|
|
<ListFilter size={10} />
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|