mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 16:02:59 +00:00
* feat: open a run as a pushed page on narrow runs page Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: keep modifier-click multi-select and arrow keys on the narrow runs list Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: leave the narrow re-run page once the re-run is sent, and clear the opened run on reset Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: close the re-run options once a table re-run completes Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
605 lines
19 KiB
Svelte
605 lines
19 KiB
Svelte
<script lang="ts">
|
|
import type { Job } from '$lib/gen'
|
|
import RunRow from './RunRow.svelte'
|
|
import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'
|
|
import { createEventDispatcher } from 'svelte'
|
|
import Tooltip from '../Tooltip.svelte'
|
|
import {
|
|
AlertTriangle,
|
|
CircleXIcon,
|
|
Code2Icon,
|
|
ExternalLinkIcon,
|
|
RefreshCwIcon,
|
|
Wrench
|
|
} from 'lucide-svelte'
|
|
import Popover from '../Popover.svelte'
|
|
import { userStore, workspaceStore } from '$lib/stores'
|
|
import './runs-grid.css'
|
|
import { useKeyPressed } from '$lib/svelte5Utils.svelte'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import RightClickPopover from '../RightClickPopover.svelte'
|
|
import DropdownMenu, { type Props as DropdownMenuProps } from '../DropdownMenu.svelte'
|
|
import { clickOutside, isJobCancelable, isJobReRunnable, isJobResolvable } from '$lib/utils'
|
|
import { goto } from '$lib/navigation'
|
|
import BarsStaggered from '../icons/BarsStaggered.svelte'
|
|
|
|
interface Props {
|
|
//import InfiniteLoading from 'svelte-infinite-loading'
|
|
jobs?: Job[] | undefined
|
|
externalJobs?: Job[]
|
|
omittedObscuredJobs: boolean
|
|
showExternalJobs?: boolean
|
|
selectedIds?: string[]
|
|
selectedWorkspace?: string | undefined
|
|
activeLabel?: string | null
|
|
// const loadMoreQuantity: number = 100
|
|
lastFetchWentToEnd?: boolean
|
|
perPage?: number
|
|
batchRerunOptionsIsOpen?: boolean
|
|
manualSelectionMode: undefined | 'cancel' | 'rerun' | 'resolve'
|
|
onCancelJobs: (jobIds: string[]) => void
|
|
onSetJobsResolution: (jobIds: string[], resolved: boolean) => void
|
|
loadingExtra?: boolean
|
|
}
|
|
|
|
let {
|
|
jobs = undefined,
|
|
externalJobs = [],
|
|
omittedObscuredJobs,
|
|
showExternalJobs = false,
|
|
selectedIds = $bindable([]),
|
|
selectedWorkspace = $bindable(undefined),
|
|
activeLabel = null,
|
|
lastFetchWentToEnd = false,
|
|
perPage = 1000,
|
|
manualSelectionMode,
|
|
onCancelJobs,
|
|
onSetJobsResolution,
|
|
batchRerunOptionsIsOpen = $bindable(),
|
|
loadingExtra = false
|
|
}: Props = $props()
|
|
|
|
let hasClickFocus = $state(false)
|
|
const keysPressed = useKeyPressed(['Shift', 'Control', 'Meta', 'A', 'ArrowDown', 'ArrowUp'], {
|
|
onKeyDown(key, e) {
|
|
if (!hasClickFocus) return
|
|
if (key === 'A' && (keysPressed.Control || keysPressed.Meta)) {
|
|
if (batchRerunOptionsIsOpen) return
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
selectedIds = flatJobs
|
|
? flatJobs
|
|
.filter((jobOrDate) => jobOrDate.type === 'job')
|
|
.map((jobOrDate) => jobOrDate.job.id)
|
|
: []
|
|
} else if ((key === 'ArrowDown' || key === 'ArrowUp') && selectedIds.length === 1) {
|
|
const idx = flatJobs?.findIndex(
|
|
(jobOrDate) => jobOrDate.type === 'job' && jobOrDate.job.id === selectedIds[0]
|
|
)
|
|
if (idx == undefined) return
|
|
let nextJob = flatJobs?.[idx + (key === 'ArrowDown' ? 1 : -1)]
|
|
if (nextJob?.type === 'date') nextJob = flatJobs?.[idx + (key === 'ArrowDown' ? 2 : -2)]
|
|
if (nextJob?.type !== 'job') return
|
|
selectedIds = [nextJob.job.id]
|
|
dispatch('select')
|
|
e.preventDefault()
|
|
}
|
|
}
|
|
})
|
|
let rightClickPopover: RightClickPopover | undefined = $state(undefined)
|
|
|
|
function getTime(job: Job): string | undefined {
|
|
return job['completed_at'] ?? job['started_at'] ?? job['scheduled_for'] ?? job['created_at']
|
|
}
|
|
|
|
function groupJobsByDay(jobs: Job[]): {
|
|
groupedJobs: Record<string, Job[]>
|
|
newContainsLabel: boolean | undefined
|
|
} {
|
|
const groupedJobs: Record<string, Job[]> = {}
|
|
|
|
if (!jobs) return { groupedJobs, newContainsLabel: undefined }
|
|
|
|
let newContainsLabel = false
|
|
for (const job of jobs) {
|
|
if (job?.['labels'] != undefined) {
|
|
newContainsLabel = true
|
|
}
|
|
const field: string | undefined = getTime(job)
|
|
if (field) {
|
|
const date = new Date(field)
|
|
date.setMilliseconds(date.getMilliseconds())
|
|
|
|
const day = date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
})
|
|
|
|
if (!groupedJobs[day]) {
|
|
groupedJobs[day] = []
|
|
}
|
|
|
|
groupedJobs[day].push(job)
|
|
}
|
|
}
|
|
|
|
for (const day in groupedJobs) {
|
|
groupedJobs[day].sort((a, b) => {
|
|
return new Date(getTime(b)!).getTime() - new Date(getTime(a)!).getTime()
|
|
})
|
|
}
|
|
|
|
const sortedJobs: Record<string, Job[]> = {}
|
|
Object.keys(groupedJobs)
|
|
.sort((a, b) => {
|
|
return new Date(b).getTime() - new Date(a).getTime()
|
|
})
|
|
.forEach((key) => {
|
|
sortedJobs[key] = groupedJobs[key]
|
|
})
|
|
|
|
return { groupedJobs: sortedJobs, newContainsLabel }
|
|
}
|
|
|
|
type FlatJobs =
|
|
| {
|
|
type: 'date'
|
|
date: string
|
|
}
|
|
| {
|
|
type: 'job'
|
|
job: Job
|
|
}
|
|
|
|
function flattenJobs(groupedJobs: Record<string, Job[]>): Array<FlatJobs> {
|
|
const flatJobs: Array<FlatJobs> = []
|
|
|
|
for (const [date, jobsByDay] of Object.entries(groupedJobs)) {
|
|
flatJobs.push({ type: 'date', date })
|
|
for (const job of jobsByDay) {
|
|
flatJobs.push({ type: 'job', job })
|
|
}
|
|
}
|
|
|
|
return flatJobs
|
|
}
|
|
|
|
let tableHeight: number = $state(0)
|
|
let containerWidth: number = $state(0)
|
|
// const MAX_ITEMS = perPage
|
|
|
|
/*
|
|
function infiniteHandler({ detail: { loaded, error, complete } }) {
|
|
try {
|
|
nbOfJobs += loadMoreQuantity
|
|
|
|
if (nbOfJobs >= MAX_ITEMS) {
|
|
complete()
|
|
} else {
|
|
loaded()
|
|
}
|
|
} catch (e) {
|
|
error()
|
|
}
|
|
}
|
|
*/
|
|
|
|
function jobCountString(
|
|
jobCount: number | undefined,
|
|
lastFetchWentToEnd: boolean,
|
|
hideLabel?: boolean
|
|
): string {
|
|
if (jobCount === undefined) {
|
|
return ''
|
|
}
|
|
const jc = jobCount
|
|
const isTruncated = jc >= perPage && !lastFetchWentToEnd
|
|
|
|
if (hideLabel) return `${jc}${isTruncated ? '+' : ''}`
|
|
else return `${jc}${isTruncated ? '+' : ''} job${jc != 1 ? 's' : ''}`
|
|
}
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
let scrollToIndex = $state(0)
|
|
|
|
export function scrollToRun(ids: string[]) {
|
|
if (flatJobs && ids.length > 0) {
|
|
const i = flatJobs.findIndex(
|
|
(jobOrDate) => jobOrDate.type === 'job' && jobOrDate.job.id === ids[0]
|
|
)
|
|
if (i !== -1) {
|
|
scrollToIndex = i
|
|
}
|
|
}
|
|
}
|
|
let { groupedJobs, newContainsLabel } = $derived(
|
|
jobs
|
|
? showExternalJobs
|
|
? groupJobsByDay([...jobs, ...externalJobs])
|
|
: groupJobsByDay(jobs)
|
|
: { groupedJobs: undefined, newContainsLabel: undefined }
|
|
)
|
|
|
|
let containsLabel = $state(false)
|
|
$effect(() => {
|
|
if (newContainsLabel != undefined) {
|
|
containsLabel = newContainsLabel
|
|
}
|
|
})
|
|
let flatJobs = $derived(groupedJobs ? flattenJobs(groupedJobs) : undefined)
|
|
let stickyIndices = $derived.by(() => {
|
|
const nstickyIndices: number[] = []
|
|
let index = 0
|
|
for (const entry of flatJobs ?? []) {
|
|
if (entry.type === 'date') {
|
|
nstickyIndices.push(index)
|
|
}
|
|
index++
|
|
}
|
|
return nstickyIndices
|
|
})
|
|
|
|
let showTag = $derived(containerWidth > 700)
|
|
let selectedIdsPossibleActions = $derived.by(() => {
|
|
const cancellableJobIds: string[] = []
|
|
const rerunnableJobIds: string[] = []
|
|
const resolvableJobIds: string[] = []
|
|
const unresolvableJobIds: string[] = []
|
|
for (const jobId of selectedIds) {
|
|
const job = flatJobs?.find(
|
|
(jobOrDate) => jobOrDate.type === 'job' && jobOrDate.job.id === jobId
|
|
)
|
|
if (job?.type === 'job') {
|
|
if (isJobCancelable(job.job)) cancellableJobIds.push(job.job.id)
|
|
if (isJobReRunnable(job.job)) rerunnableJobIds.push(job.job.id)
|
|
if (isJobResolvable(job.job)) {
|
|
if (job.job.resolved) unresolvableJobIds.push(job.job.id)
|
|
else resolvableJobIds.push(job.job.id)
|
|
}
|
|
}
|
|
}
|
|
return { cancellableJobIds, rerunnableJobIds, resolvableJobIds, unresolvableJobIds }
|
|
})
|
|
let hoveredDropdownAction: 'cancel' | 'rerun' | 'resolve' | null = $state(null)
|
|
|
|
let dropdownActions: DropdownMenuProps['items'] = $derived.by(() => {
|
|
let rerunnable = selectedIdsPossibleActions.rerunnableJobIds.length
|
|
let cancellable = selectedIdsPossibleActions.cancellableJobIds.length
|
|
const actions: DropdownMenuProps['items'] = []
|
|
if (selectedIds.length === 1) {
|
|
actions.push({
|
|
label: 'Show run details',
|
|
icon: ExternalLinkIcon,
|
|
onClick: () => goto(`/run/${selectedIds[0]}`)
|
|
})
|
|
const job = flatJobs?.find(
|
|
(jobOrDate) => jobOrDate.type === 'job' && jobOrDate.job.id === selectedIds[0]
|
|
)
|
|
if (job?.type === 'job') {
|
|
if (job.job.job_kind === 'script') {
|
|
actions.push({
|
|
label: 'Go to script page',
|
|
icon: Code2Icon,
|
|
onClick: () => goto(`/scripts/get/${job.job.script_hash}`)
|
|
})
|
|
}
|
|
if (job.job.job_kind === 'flow') {
|
|
actions.push({
|
|
label: 'Go to flow page',
|
|
icon: BarsStaggered,
|
|
onClick: () => goto(`/flows/get/${job.job.script_path}`)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
if (rerunnable)
|
|
actions.push({
|
|
label: 'Run again',
|
|
icon: RefreshCwIcon,
|
|
right: selectedIds.length >= 2 ? `${rerunnable}` : undefined,
|
|
onClick: () => {
|
|
selectedIds = selectedIdsPossibleActions.rerunnableJobIds
|
|
batchRerunOptionsIsOpen = true
|
|
},
|
|
onHover: (hover) => (hoveredDropdownAction = hover ? 'rerun' : null)
|
|
})
|
|
if (cancellable)
|
|
actions.push({
|
|
label: 'Cancel',
|
|
icon: CircleXIcon,
|
|
right: selectedIds.length >= 2 ? `${cancellable}` : undefined,
|
|
onClick: () => onCancelJobs?.(selectedIdsPossibleActions.cancellableJobIds),
|
|
onHover: (hover) => (hoveredDropdownAction = hover ? 'cancel' : null)
|
|
})
|
|
// Operators are rejected by the endpoint, so offering the action would only 403.
|
|
const canResolve = !$userStore?.operator
|
|
const resolvable = canResolve ? selectedIdsPossibleActions.resolvableJobIds.length : 0
|
|
const unresolvable = canResolve ? selectedIdsPossibleActions.unresolvableJobIds.length : 0
|
|
if (resolvable)
|
|
actions.push({
|
|
label: 'Mark resolved',
|
|
icon: Wrench,
|
|
right: selectedIds.length >= 2 ? `${resolvable}` : undefined,
|
|
onClick: () => onSetJobsResolution?.(selectedIdsPossibleActions.resolvableJobIds, true),
|
|
onHover: (hover) => (hoveredDropdownAction = hover ? 'resolve' : null)
|
|
})
|
|
if (unresolvable)
|
|
actions.push({
|
|
label: 'Unresolve',
|
|
icon: Wrench,
|
|
right: selectedIds.length >= 2 ? `${unresolvable}` : undefined,
|
|
onClick: () => onSetJobsResolution?.(selectedIdsPossibleActions.unresolvableJobIds, false),
|
|
onHover: (hover) => (hoveredDropdownAction = hover ? 'resolve' : null)
|
|
})
|
|
return actions
|
|
})
|
|
|
|
function jobIsSelectable(job: Job) {
|
|
if (
|
|
(rightClickPopover?.isOpen() && hoveredDropdownAction === 'cancel') ||
|
|
manualSelectionMode === 'cancel'
|
|
)
|
|
return isJobCancelable(job)
|
|
if (
|
|
(rightClickPopover?.isOpen() && hoveredDropdownAction === 'rerun') ||
|
|
manualSelectionMode === 'rerun' ||
|
|
batchRerunOptionsIsOpen
|
|
)
|
|
return isJobReRunnable(job)
|
|
if (
|
|
(rightClickPopover?.isOpen() && hoveredDropdownAction === 'resolve') ||
|
|
manualSelectionMode === 'resolve'
|
|
)
|
|
return isJobResolvable(job)
|
|
return true
|
|
}
|
|
|
|
let selectableJobs = $derived(jobs?.filter(jobIsSelectable) ?? [])
|
|
</script>
|
|
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="divide-y h-full flex flex-col min-w-[650px]"
|
|
id="runs-table-wrapper"
|
|
onclick={() => (hasClickFocus = true)}
|
|
use:clickOutside={{ onClickOutside: () => (hasClickFocus = false) }}
|
|
bind:clientWidth={containerWidth}
|
|
>
|
|
<div>
|
|
<div
|
|
class="grid sticky top-0 w-full min-h-6 my-2 pr-4 items-end"
|
|
class:grid-runs-table={!containsLabel && !manualSelectionMode && showTag}
|
|
class:grid-runs-table-with-labels={containsLabel && !manualSelectionMode && showTag}
|
|
class:grid-runs-table-selection={!containsLabel && manualSelectionMode && showTag}
|
|
class:grid-runs-table-with-labels-selection={containsLabel && manualSelectionMode && showTag}
|
|
class:grid-runs-table-no-tag={!containsLabel && !manualSelectionMode && !showTag}
|
|
class:grid-runs-table-with-labels-no-tag={containsLabel && !manualSelectionMode && !showTag}
|
|
class:grid-runs-table-selection-no-tag={!containsLabel && manualSelectionMode && !showTag}
|
|
class:grid-runs-table-with-labels-selection-no-tag={containsLabel &&
|
|
manualSelectionMode &&
|
|
!showTag}
|
|
>
|
|
{#if manualSelectionMode}
|
|
{@const allSelected = selectedIds.length === selectableJobs?.length}
|
|
<div class="w-4 h-4 ml-4">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={
|
|
() => allSelected,
|
|
() => (selectedIds = allSelected ? [] : (selectableJobs.map((j) => j.id) ?? []))
|
|
}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
<div class="text-2xs px-4 flex flex-row items-center gap-2 leading-3 whitespace-nowrap">
|
|
{#if showExternalJobs && externalJobs.length > 0}
|
|
<div class="flex flex-row">
|
|
{jobs
|
|
? jobCountString(jobs.length + externalJobs.length, lastFetchWentToEnd)
|
|
: ''}<Tooltip>{externalJobs.length} jobs obscured</Tooltip>
|
|
</div>
|
|
{:else if $workspaceStore !== 'admins' && omittedObscuredJobs}
|
|
<div class="flex flex-row">
|
|
{jobs ? jobCountString(jobs.length, lastFetchWentToEnd) : ''}
|
|
<Popover>
|
|
<AlertTriangle size={16} class="ml-0.5 text-yellow-500" />
|
|
{#snippet text()}
|
|
Too specific filtering may have caused the omission of obscured jobs. This is done
|
|
for security reasons. To see obscured jobs, try removing some filters.
|
|
{/snippet}
|
|
</Popover>
|
|
</div>
|
|
{:else}
|
|
{@const jobCount = jobs
|
|
? jobCountString(jobs.length, lastFetchWentToEnd, selectedIds.length >= 2)
|
|
: ''}
|
|
{selectedIds.length >= 2 ? `${selectedIds.length}/` : ''}<wbr />
|
|
{jobCount}
|
|
{/if}
|
|
</div>
|
|
<div></div>
|
|
<div class="text-xs font-semibold leading-3">Duration</div>
|
|
<div class="text-xs font-semibold leading-3">Path</div>
|
|
{#if containsLabel}
|
|
<div class="text-xs font-semibold leading-3">Label</div>
|
|
{/if}
|
|
<div class="text-xs font-semibold leading-3">Triggered by</div>
|
|
{#if showTag}
|
|
<div class="text-xs font-semibold leading-3">Tag</div>
|
|
{/if}
|
|
<div> </div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
bind:clientHeight={tableHeight}
|
|
class="relative flex-1 border rounded-t-md overflow-clip bg-surface-tertiary [&>.virtual-list-wrapper::-webkit-scrollbar-track]:bg-surface-tertiary"
|
|
>
|
|
{#if jobs?.length == 0 && (!showExternalJobs || externalJobs?.length == 0)}
|
|
<div class="text-xs text-secondary p-8"> No jobs found for the selected filters. </div>
|
|
{:else}
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="absolute inset-0 -mt-3"
|
|
oncontextmenu={(e) => {
|
|
e.preventDefault()
|
|
rightClickPopover?.open(e)
|
|
}}
|
|
>
|
|
<VirtualList
|
|
width="100%"
|
|
height={tableHeight}
|
|
itemCount={flatJobs?.length ?? 3}
|
|
itemSize={42}
|
|
overscanCount={20}
|
|
{stickyIndices}
|
|
{scrollToIndex}
|
|
scrollToAlignment="center"
|
|
>
|
|
{#snippet header()}{/snippet}
|
|
{#snippet item({ index, style })}
|
|
<div {style} class="w-full bg-surface-tertiary">
|
|
{#if flatJobs}
|
|
{@const jobOrDate = flatJobs[index]}
|
|
{#if jobOrDate}
|
|
{#if jobOrDate?.type === 'date'}
|
|
<div
|
|
class={twMerge(
|
|
'border-b py-1.5 font-semibold text-xs pl-4 h-[42px] flex items-end bg-surface-tertiary'
|
|
)}
|
|
>
|
|
{jobOrDate.date}
|
|
</div>
|
|
{:else}
|
|
{@const selected =
|
|
jobOrDate.job.id !== '-' && selectedIds.includes(jobOrDate.job.id)}
|
|
{@const nonSelectable = !jobIsSelectable(jobOrDate.job)}
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class={twMerge(
|
|
'flex flex-row items-center h-full w-full select-none transition-opacity',
|
|
nonSelectable || (rightClickPopover?.isOpen() && !selected)
|
|
? 'opacity-20'
|
|
: ''
|
|
)}
|
|
>
|
|
<RunRow
|
|
{manualSelectionMode}
|
|
{containsLabel}
|
|
{showTag}
|
|
job={jobOrDate.job}
|
|
{selected}
|
|
on:select={() => {
|
|
const jobId = jobOrDate.job.id
|
|
if (keysPressed.Shift && selectedIds.length > 0) {
|
|
if (nonSelectable) return
|
|
const lastSelectedId = selectedIds[selectedIds.length - 1]
|
|
const lastSelectedIndex = flatJobs?.findIndex(
|
|
(jobOrDate) =>
|
|
jobOrDate.type === 'job' && jobOrDate.job.id === lastSelectedId
|
|
)
|
|
if (lastSelectedIndex != undefined && flatJobs) {
|
|
const [start, end] =
|
|
index < lastSelectedIndex
|
|
? [index, lastSelectedIndex]
|
|
: [lastSelectedIndex, index]
|
|
const newSelectedIds = flatJobs
|
|
.slice(start, end + 1)
|
|
.filter((jobOrDate) => jobOrDate.type === 'job')
|
|
.map((jobOrDate) => jobOrDate.job.id)
|
|
selectedIds = Array.from(new Set([...selectedIds, ...newSelectedIds]))
|
|
}
|
|
} else if (
|
|
keysPressed.Control ||
|
|
keysPressed.Meta ||
|
|
manualSelectionMode
|
|
) {
|
|
if (nonSelectable) return
|
|
if (selectedIds.includes(jobOrDate.job.id)) {
|
|
selectedIds = selectedIds.filter((id) => id != jobId)
|
|
} else {
|
|
selectedIds.push(jobId)
|
|
selectedIds = selectedIds
|
|
}
|
|
} else {
|
|
if (batchRerunOptionsIsOpen) batchRerunOptionsIsOpen = false
|
|
if (
|
|
selectedIds.length !== 1 ||
|
|
selectedIds[0] !== jobOrDate.job.id ||
|
|
selectedWorkspace !== jobOrDate.job.workspace_id
|
|
) {
|
|
selectedWorkspace = jobOrDate.job.workspace_id
|
|
selectedIds = [jobOrDate.job.id]
|
|
dispatch('select')
|
|
} else {
|
|
selectedIds = []
|
|
selectedWorkspace = undefined
|
|
dispatch('select')
|
|
}
|
|
}
|
|
}}
|
|
{activeLabel}
|
|
on:filterByLabel
|
|
on:filterByPath
|
|
on:filterByUser
|
|
on:filterByFolder
|
|
on:filterByConcurrencyKey
|
|
on:filterBySchedule
|
|
on:filterByWorker
|
|
{containerWidth}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
{:else}
|
|
{JSON.stringify(jobOrDate)}
|
|
{/if}
|
|
{:else}
|
|
<div class="flex flex-row items-center h-full w-full">
|
|
<div class="w-1/12 text-2xs">...</div>
|
|
<div class="w-4/12 text-xs">...</div>
|
|
<div class="w-4/12 text-xs">...</div>
|
|
<div class="w-3/12 text-xs">...</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/snippet}
|
|
{#snippet footer()}
|
|
<div
|
|
>{#if !lastFetchWentToEnd && jobs && jobs.length >= perPage}
|
|
<button
|
|
class="text-xs text-accent text-center w-full pb-2"
|
|
disabled={loadingExtra}
|
|
onclick={() => {
|
|
dispatch('loadExtra')
|
|
}}
|
|
>
|
|
{#if loadingExtra}
|
|
Loading...
|
|
{:else}
|
|
Load next {perPage} jobs
|
|
{/if}
|
|
</button>
|
|
{/if}</div
|
|
>
|
|
{/snippet}
|
|
</VirtualList>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<RightClickPopover bind:this={rightClickPopover}>
|
|
<DropdownMenu closeCallback={() => rightClickPopover?.close()} items={dropdownActions} />
|
|
</RightClickPopover>
|
|
|
|
<style>
|
|
:global(.virtual-list-wrapper::-webkit-scrollbar) {
|
|
width: 8px !important;
|
|
height: 8px !important;
|
|
}
|
|
</style>
|