mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 08:01:25 +00:00
fix(frontend): add virtual list (#2218)
* fix(frontend): add virtual list * feat(frontend): add virtual list to runs page * feat(frontend): fix everything * feat(frontend): add message when the limit is reached
This commit is contained in:
Generated
+22
@@ -39,8 +39,10 @@
|
||||
"svelte-chartjs": "^3.1.0",
|
||||
"svelte-dnd-action": "^0.9.22",
|
||||
"svelte-exmarkdown": "^2.1.0",
|
||||
"svelte-infinite-loading": "^1.3.8",
|
||||
"svelte-portal": "^2.2.0",
|
||||
"svelte-timezone-picker": "^2.0.3",
|
||||
"svelte-tiny-virtual-list": "^2.0.5",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"vscode-ws-jsonrpc": "3.0.0",
|
||||
"windmill-parser-wasm": "^1.154.3",
|
||||
@@ -8762,6 +8764,11 @@
|
||||
"svelte": "^3.19.0 || ^4.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-infinite-loading": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/svelte-infinite-loading/-/svelte-infinite-loading-1.3.8.tgz",
|
||||
"integrity": "sha512-hn4o848LKd2Q+M11hiMWnfFxM1GHKVDi92HPZ1FYvfed4bEeRZL+QvFAQzhy1SACq6Si0CAJcQFUZpIYmAEnpQ=="
|
||||
},
|
||||
"node_modules/svelte-multiselect": {
|
||||
"version": "8.6.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-multiselect/-/svelte-multiselect-8.6.2.tgz",
|
||||
@@ -8899,6 +8906,11 @@
|
||||
"svelte": "^3.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-tiny-virtual-list": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-2.0.5.tgz",
|
||||
"integrity": "sha512-xg9ckb8UeeIme4/5qlwCrl2QNmUZ8SCQYZn3Ji83cUsoASqRNy3KWjpmNmzYvPDqCHSZjruBBsoB7t5hwuzw5g=="
|
||||
},
|
||||
"node_modules/svelte2tsx": {
|
||||
"version": "0.6.19",
|
||||
"resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.6.19.tgz",
|
||||
@@ -16039,6 +16051,11 @@
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"svelte-infinite-loading": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/svelte-infinite-loading/-/svelte-infinite-loading-1.3.8.tgz",
|
||||
"integrity": "sha512-hn4o848LKd2Q+M11hiMWnfFxM1GHKVDi92HPZ1FYvfed4bEeRZL+QvFAQzhy1SACq6Si0CAJcQFUZpIYmAEnpQ=="
|
||||
},
|
||||
"svelte-multiselect": {
|
||||
"version": "8.6.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-multiselect/-/svelte-multiselect-8.6.2.tgz",
|
||||
@@ -16113,6 +16130,11 @@
|
||||
"compute-scroll-into-view": "^1.0.16"
|
||||
}
|
||||
},
|
||||
"svelte-tiny-virtual-list": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-2.0.5.tgz",
|
||||
"integrity": "sha512-xg9ckb8UeeIme4/5qlwCrl2QNmUZ8SCQYZn3Ji83cUsoASqRNy3KWjpmNmzYvPDqCHSZjruBBsoB7t5hwuzw5g=="
|
||||
},
|
||||
"svelte2tsx": {
|
||||
"version": "0.6.19",
|
||||
"resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.6.19.tgz",
|
||||
|
||||
@@ -103,8 +103,10 @@
|
||||
"svelte-chartjs": "^3.1.0",
|
||||
"svelte-dnd-action": "^0.9.22",
|
||||
"svelte-exmarkdown": "^2.1.0",
|
||||
"svelte-infinite-loading": "^1.3.8",
|
||||
"svelte-portal": "^2.2.0",
|
||||
"svelte-timezone-picker": "^2.0.3",
|
||||
"svelte-tiny-virtual-list": "^2.0.5",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"vscode-ws-jsonrpc": "3.0.0",
|
||||
"windmill-parser-wasm": "^1.154.3",
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
import { Badge, Button } from '../common'
|
||||
import ScheduleEditor from '../ScheduleEditor.svelte'
|
||||
import Row from '../table/Row.svelte'
|
||||
import Cell from '../table/Cell.svelte'
|
||||
|
||||
import {
|
||||
Calendar,
|
||||
Check,
|
||||
@@ -21,12 +20,16 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import TimeAgo from '../TimeAgo.svelte'
|
||||
import { forLater } from '$lib/forLater'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Portal from 'svelte-portal'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const SMALL_ICON_SCALE = 0.7
|
||||
|
||||
export let job: Job
|
||||
export let selectedId: string | undefined = undefined
|
||||
export let containerWidth: number = 0
|
||||
|
||||
let scheduleEditor: ScheduleEditor
|
||||
|
||||
function endedDate(started_at: string, duration_ms: number): string {
|
||||
@@ -40,48 +43,55 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
<Portal>
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
</Portal>
|
||||
|
||||
<Row
|
||||
hoverable
|
||||
selected={selectedId === job.id}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover cursor-pointer',
|
||||
selectedId === job.id ? 'bg-blue-50 dark:bg-blue-900/50' : '',
|
||||
'transition-all',
|
||||
'flex flex-row items-center h-full'
|
||||
)}
|
||||
style="width: {containerWidth}px"
|
||||
on:click={() => {
|
||||
selectedId = job.id
|
||||
dispatch('select')
|
||||
}}
|
||||
>
|
||||
<Cell first>
|
||||
<div>
|
||||
{#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} />
|
||||
<div class="w-1/12 flex justify-center">
|
||||
{#if 'success' in job && job.success}
|
||||
{#if job.is_skipped}
|
||||
<Badge color="green" rounded>
|
||||
<FastForward size={14} />
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge baseClass="!px-1.5">
|
||||
<Hourglass size={14} />
|
||||
<Badge color="green" baseClass="!px-1.5">
|
||||
<Check size={14} />
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
</Cell>
|
||||
<Cell>
|
||||
{: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}
|
||||
@@ -103,9 +113,9 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</Cell>
|
||||
</div>
|
||||
|
||||
<Cell>
|
||||
<div class="w-4/12 flex justify-star">
|
||||
<div class="flex flex-row text-sm">
|
||||
{#if job === undefined}
|
||||
No job found
|
||||
@@ -114,7 +124,9 @@
|
||||
<div class="whitespace-nowrap text-xs font-semibold">
|
||||
{#if job.script_path}
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
<a href="/run/{job.id}?workspace={job.workspace_id}">{job.script_path} </a>
|
||||
<a href="/run/{job.id}?workspace={job.workspace_id}" class="truncate w-8/12">
|
||||
{job.script_path}
|
||||
</a>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
@@ -142,9 +154,9 @@
|
||||
{:else if 'job_kind' in job && job.job_kind == 'preview'}
|
||||
<a href="/run/{job.id}?workspace={job.workspace_id}">Preview without path </a>
|
||||
{:else if 'job_kind' in job && job.job_kind == 'dependencies'}
|
||||
<a href="/run/{job.id}?workspace={job.workspace_id}"
|
||||
>lock deps of {truncateHash(job.script_hash ?? '')}</a
|
||||
>
|
||||
<a href="/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="/run/{job.id}?workspace={job.workspace_id}">no op</a>
|
||||
{/if}
|
||||
@@ -164,12 +176,14 @@
|
||||
{:else}
|
||||
<Icon class="text-secondary" data={faRobot} scale={SMALL_ICON_SCALE} />
|
||||
<span class="mx-1">
|
||||
Parent <a href={`/run/${job.parent_job}?workspace=${job.workspace_id}`}>{job.parent_job}</a>
|
||||
Parent <a href={`/run/${job.parent_job}?workspace=${job.workspace_id}`}
|
||||
>{job.parent_job}</a
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</Cell>
|
||||
<Cell last>
|
||||
</div>
|
||||
<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} />
|
||||
@@ -184,7 +198,9 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
{job.created_by}
|
||||
<div class="text-xs">
|
||||
{job.created_by}
|
||||
</div>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
@@ -196,5 +212,5 @@
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</Cell>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Cell from '$lib/components/table/Cell.svelte'
|
||||
import DataTable from '$lib/components/table/DataTable.svelte'
|
||||
import Head from '$lib/components/table/Head.svelte'
|
||||
import type { Job } from '$lib/gen'
|
||||
import RunRow from './RunRow.svelte'
|
||||
import VirtualList from 'svelte-tiny-virtual-list'
|
||||
import { onMount } from 'svelte'
|
||||
import InfiniteLoading from 'svelte-infinite-loading'
|
||||
|
||||
export let jobs: Job[] = []
|
||||
export let selectedId: string | undefined = undefined
|
||||
@@ -13,6 +13,7 @@
|
||||
function getTime(job: Job): string | undefined {
|
||||
return job['started_at'] ?? job['scheduled_for'] ?? job['created_at']
|
||||
}
|
||||
|
||||
function groupJobsByDay(jobs: Job[]): Record<string, Job[]> {
|
||||
const groupedLogs: Record<string, Job[]> = {}
|
||||
|
||||
@@ -57,44 +58,120 @@
|
||||
}
|
||||
|
||||
$: groupedJobs = groupJobsByDay(jobs.slice(0, nbOfJobs))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
$: flatJobs = flattenJobs(groupedJobs)
|
||||
|
||||
let stickyIndices: number[] = []
|
||||
|
||||
$: {
|
||||
stickyIndices = []
|
||||
let index = 0
|
||||
for (const entry of flatJobs) {
|
||||
if (entry.type === 'date') {
|
||||
stickyIndices.push(index)
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
let tableHeight: number = 0
|
||||
let header: number = 0
|
||||
let containerWidth: number = 0
|
||||
const MAX_ITEMS = 1000
|
||||
|
||||
function infiniteHandler({ detail: { loaded, error, complete } }) {
|
||||
try {
|
||||
nbOfJobs += loadMoreQuantity
|
||||
|
||||
if (nbOfJobs >= MAX_ITEMS) {
|
||||
complete()
|
||||
} else {
|
||||
loaded()
|
||||
}
|
||||
} catch (e) {
|
||||
error()
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
tableHeight = document.querySelector('#runs-table-wrapper')!.parentElement?.clientHeight ?? 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<DataTable
|
||||
rounded={false}
|
||||
size="sm"
|
||||
loadMore={loadMoreQuantity}
|
||||
shouldLoadMore={nbOfJobs < jobs.length}
|
||||
on:loadMore={() => (nbOfJobs += loadMoreQuantity)}
|
||||
>
|
||||
<Head>
|
||||
<Cell first head class="w-8" />
|
||||
<Cell head>Timestamp</Cell>
|
||||
<Cell head>Path</Cell>
|
||||
<Cell head last>Triggered by</Cell>
|
||||
</Head>
|
||||
<div class="divide-y min-w-[640px]" id="runs-table-wrapper" bind:clientWidth={containerWidth}>
|
||||
<div
|
||||
class="flex flex-row bg-surface-secondary sticky top-0 w-full p-2 pr-4 z-50"
|
||||
bind:clientHeight={header}
|
||||
>
|
||||
<div class="w-1/12" />
|
||||
<div class="w-4/12 text-xs font-semibold">Timestamp</div>
|
||||
<div class="w-4/12 text-xs font-semibold">Path</div>
|
||||
<div class="w-3/12 text-xs font-semibold">Triggered by</div>
|
||||
</div>
|
||||
|
||||
<tbody class="divide-y">
|
||||
{#each Object.entries(groupedJobs) as [date, jobsByDay]}
|
||||
<tr class="border-t">
|
||||
<Cell
|
||||
first
|
||||
colspan="6"
|
||||
scope="colgroup"
|
||||
class="bg-surface-secondary/30 py-2 border-b font-semibold"
|
||||
>
|
||||
{date}
|
||||
</Cell>
|
||||
</tr>
|
||||
{#each jobsByDay as job (job.id)}
|
||||
<RunRow {job} bind:selectedId on:select on:filterByPath on:filterByUser on:filterByFolder />
|
||||
{/each}
|
||||
{/each}
|
||||
</tbody>
|
||||
{#if jobs.length == 0}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-8">
|
||||
<div class="text-xs text-secondary"> No jobs found for the selected filters. </div>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</DataTable>
|
||||
<VirtualList width="100%" height={tableHeight - header} itemCount={flatJobs.length} itemSize={42}>
|
||||
<div slot="item" let:index let:style {style} class="w-full">
|
||||
{@const jobOrDate = flatJobs[index]}
|
||||
|
||||
{#if jobOrDate}
|
||||
{#if jobOrDate?.type === 'date'}
|
||||
<div class="bg-surface-secondary/30 py-2 border-b font-semibold text-xs pl-5">
|
||||
{jobOrDate.date}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row items-center h-full w-full">
|
||||
<RunRow
|
||||
job={jobOrDate.job}
|
||||
bind:selectedId
|
||||
on:select
|
||||
on:filterByPath
|
||||
on:filterByUser
|
||||
on:filterByFolder
|
||||
{containerWidth}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{JSON.stringify(jobOrDate)}
|
||||
{/if}
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<InfiniteLoading on:infinite={infiniteHandler}>
|
||||
<div slot="noMore">
|
||||
<div class="text-center text-xs text-secondary p-2">
|
||||
Reached the limit of {MAX_ITEMS} jobs. Please refine your search using filters.
|
||||
</div>
|
||||
</div>
|
||||
</InfiniteLoading>
|
||||
</div>
|
||||
</VirtualList>
|
||||
</div>
|
||||
{#if jobs.length == 0}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-8">
|
||||
<div class="text-xs text-secondary"> No jobs found for the selected filters. </div>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
@@ -382,7 +382,16 @@
|
||||
cancelAllJobs = false
|
||||
}}
|
||||
/>
|
||||
<div class="w-full h-screen">
|
||||
|
||||
<Drawer bind:this={runDrawer}>
|
||||
<DrawerContent title="Run details" on:close={runDrawer.closeDrawer}>
|
||||
{#if selectedId}
|
||||
<JobPreview id={selectedId} />
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="w-full h-screen hidden md:block">
|
||||
<div class="px-2">
|
||||
<div class="flex items-center space-x-2 flex-row justify-between">
|
||||
<div class="flex flex-row flex-wrap justify-between py-2 my-4 px-4 gap-1">
|
||||
@@ -552,9 +561,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SplitPanesWrapper class="hidden md:block">
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes>
|
||||
<Pane size={60} minSize={50}>
|
||||
<Pane size={60} minSize={60}>
|
||||
{#if jobs}
|
||||
<RunsTable
|
||||
{jobs}
|
||||
@@ -595,40 +604,211 @@
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
|
||||
<div class="md:hidden">
|
||||
{#if jobs}
|
||||
<RunsTable
|
||||
{jobs}
|
||||
bind:selectedId
|
||||
bind:nbOfJobs
|
||||
on:select={() => {
|
||||
runDrawer.openDrawer()
|
||||
}}
|
||||
on:filterByPath={(e) => {
|
||||
user = null
|
||||
folder = null
|
||||
path = e.detail
|
||||
}}
|
||||
on:filterByUser={(e) => {
|
||||
path = null
|
||||
folder = null
|
||||
user = e.detail
|
||||
}}
|
||||
on:filterByFolder={(e) => {
|
||||
path = null
|
||||
user = null
|
||||
folder = e.detail
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={runDrawer}>
|
||||
<DrawerContent title="Run details" on:close={runDrawer.closeDrawer}>
|
||||
{#if selectedId}
|
||||
<JobPreview id={selectedId} />
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<div class="md:hidden h-[calc(100vh-48px)]">
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes horizontal>
|
||||
<Pane size={50} minSize={20}>
|
||||
<div class="px-2">
|
||||
<div class="flex items-center space-x-2 flex-row justify-between">
|
||||
<div class="flex flex-row flex-wrap justify-between py-2 my-4 px-4 gap-1">
|
||||
<h1 class="!text-2xl font-semibold leading-6 tracking-tight"> Runs </h1>
|
||||
|
||||
<Tooltip
|
||||
light
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs"
|
||||
scale={0.9}
|
||||
wrapperClass="flex items-center"
|
||||
>
|
||||
All past and schedule executions of scripts and flows, including previews. You only
|
||||
see your own runs or runs of groups you belong to unless you are an admin.
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="hidden xl:block">
|
||||
<RunsFilter
|
||||
bind:isSkipped
|
||||
bind:user
|
||||
bind:folder
|
||||
bind:path
|
||||
bind:success
|
||||
bind:argFilter
|
||||
bind:resultFilter
|
||||
bind:argError
|
||||
bind:resultError
|
||||
bind:jobKindsCat
|
||||
on:change={reloadLogsWithoutFilterError}
|
||||
{usernames}
|
||||
{folders}
|
||||
{paths}
|
||||
/>
|
||||
</div>
|
||||
<div class="xl:hidden">
|
||||
<MobileFilters>
|
||||
<svelte:fragment slot="filters">
|
||||
<span class="text-xs font-semibold leading-6">Filters</span>
|
||||
<RunsFilter
|
||||
bind:isSkipped
|
||||
{paths}
|
||||
{usernames}
|
||||
{folders}
|
||||
bind:jobKindsCat
|
||||
bind:folder
|
||||
bind:path
|
||||
bind:user
|
||||
bind:success
|
||||
bind:argFilter
|
||||
bind:resultFilter
|
||||
bind:argError
|
||||
bind:resultError
|
||||
on:change={reloadLogsWithoutFilterError}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</MobileFilters>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 w-full">
|
||||
<RunChart
|
||||
maxIsNow={maxTs == undefined}
|
||||
jobs={completedJobs}
|
||||
on:zoom={async (e) => {
|
||||
minTs = e.detail.min.toISOString()
|
||||
maxTs = e.detail.max.toISOString()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 md:flex-row w-full p-4">
|
||||
<div class="flex gap-2 grow mb-2">
|
||||
<div class="flex gap-1 relative max-w-36 min-w-[50px]">
|
||||
<div class="text-xs absolute -top-4 truncate">Jobs waiting for a worker</div>
|
||||
<div class="mt-1">{queue_count ? ($queue_count ?? 0).toFixed(0) : '...'}</div>
|
||||
</div>
|
||||
<div class="flex"
|
||||
><Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="contained"
|
||||
title="Require to be an admin. Cancel all jobs in queue"
|
||||
disabled={!$userStore?.is_admin && !$superadmin}
|
||||
on:click={async () => (cancelAllJobs = true)}>Cancel All</Button
|
||||
></div
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-row gap-1 w-full max-w-xl">
|
||||
<div class="relative w-full">
|
||||
<div class="flex gap-1 relative w-full">
|
||||
<span class="text-xs absolute -top-4">Min datetime</span>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={minTs ?? 'zoom x axis to set min (drag with ctrl)'}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<CalendarPicker
|
||||
date={minTs}
|
||||
label="Min datetimes"
|
||||
on:change={async ({ detail }) => {
|
||||
minTs = new Date(detail).toISOString()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative w-full">
|
||||
<div class="flex gap-1 relative w-full">
|
||||
<span class="text-xs absolute -top-4">Max datetime</span>
|
||||
<input type="text" value={maxTs ?? 'zoom x axis to set max'} disabled />
|
||||
<CalendarPicker
|
||||
date={maxTs}
|
||||
label="Max datetimes"
|
||||
on:change={async ({ detail }) => {
|
||||
maxTs = new Date(detail).toISOString()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
|
||||
autoRefresh = true
|
||||
jobs = undefined
|
||||
completedJobs = undefined
|
||||
selectedManualDate = 0
|
||||
selectedId = undefined
|
||||
loadJobs()
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
wrapperClasses="border rounded-md"
|
||||
on:click={() => {
|
||||
manualDates[selectedManualDate].setMinMax()
|
||||
loadJobs()
|
||||
}}
|
||||
dropdownItems={[
|
||||
...manualDates.map((d, i) => ({
|
||||
label: d.label,
|
||||
onClick: () => {
|
||||
selectedManualDate = i
|
||||
d.setMinMax()
|
||||
loadJobs()
|
||||
}
|
||||
}))
|
||||
]}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<RefreshCw size={14} class={loading ? 'animate-spin' : ''} />
|
||||
{manualDates[selectedManualDate].label}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={autoRefresh}
|
||||
options={{ right: 'Auto-refresh' }}
|
||||
textClass="whitespace-nowrap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={50} minSize={20}>
|
||||
<div class="overflow-y-hidden h-full">
|
||||
<RunsTable
|
||||
{jobs}
|
||||
bind:selectedId
|
||||
bind:nbOfJobs
|
||||
on:select={() => {
|
||||
runDrawer.openDrawer()
|
||||
}}
|
||||
on:filterByPath={(e) => {
|
||||
user = null
|
||||
folder = null
|
||||
path = e.detail
|
||||
}}
|
||||
on:filterByUser={(e) => {
|
||||
path = null
|
||||
folder = null
|
||||
user = e.detail
|
||||
}}
|
||||
on:filterByFolder={(e) => {
|
||||
path = null
|
||||
user = null
|
||||
folder = e.detail
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user