mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-26 08:01:38 +00:00
feat(frontend): Runs rework v2 (#2012)
* wip * feat(frontend): Add disable prop to App Toggles * feat(frontend): run page v2 * feat(frontend): column ordering + load more * feat(frontend): column ordering + load more * feat(frontend): revert old coode * feat(frontend): simplify table + fix load more style + add groups * feat(frontend): Done * feat(frontend): Fix styling issues * feat(frontend): Fix styling issues * feat(frontend): Simplify filters * feat(frontend): Rework runs done * feat(frontend): fix drawer * feat(frontend): rework the filters * feat(frontend): rework the filters * feat(frontend): small ui fix * feat(frontend): correctly display title * feat(frontend): fix dark mode switch * feat(frontend): fix reset/clear buttons * feat(frontend): Rework runs done
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
import nord from 'svelte-highlight/styles/nord'
|
||||
import { each } from 'chart.js/helpers'
|
||||
import { Chart } from 'chart.js'
|
||||
import { deepMergeWithPriority } from '$lib/utils'
|
||||
|
||||
meditor.defineTheme('nord', {
|
||||
base: 'vs-dark',
|
||||
@@ -118,7 +119,7 @@
|
||||
meditor.setTheme('nord')
|
||||
darkMode = true
|
||||
each(Chart.instances, (instance) => {
|
||||
instance.options = {
|
||||
instance.options = deepMergeWithPriority(instance.options, {
|
||||
scales: {
|
||||
y: {
|
||||
ticks: {
|
||||
@@ -144,7 +145,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
instance.update()
|
||||
})
|
||||
} else {
|
||||
@@ -152,7 +153,7 @@
|
||||
darkMode = false
|
||||
|
||||
each(Chart.instances, (instance) => {
|
||||
instance.options = {
|
||||
instance.options = deepMergeWithPriority(instance.options, {
|
||||
scales: {
|
||||
y: {
|
||||
ticks: {
|
||||
@@ -178,7 +179,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
instance.update()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
import { ChevronRightSquare, ClipboardCopy, Download, Expand } from 'lucide-svelte'
|
||||
import ArgInfo from './ArgInfo.svelte'
|
||||
import { Button, Drawer, DrawerContent, Skeleton } from './common'
|
||||
import TableCustom from './TableCustom.svelte'
|
||||
import Portal from 'svelte-portal'
|
||||
|
||||
import { Highlight } from 'svelte-highlight'
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
import { json } from 'svelte-highlight/languages'
|
||||
import DataTable from './table/DataTable.svelte'
|
||||
import Cell from './table/Cell.svelte'
|
||||
import Head from './table/Head.svelte'
|
||||
import Row from './table/Row.svelte'
|
||||
|
||||
export let args: any
|
||||
export let tableClass = ''
|
||||
@@ -46,87 +49,90 @@ ${Object.entries(args)
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<div class="text-tertiary text-xs absolute top-8 right-0">
|
||||
<button
|
||||
on:click={() => {
|
||||
jsonStr = JSON.stringify(args, null, 4)
|
||||
jsonViewer.openDrawer()
|
||||
}}><Expand size={18} /></button
|
||||
>
|
||||
</div>
|
||||
<TableCustom class="py-2 {tableClass}">
|
||||
<tr slot="header-row">
|
||||
<th>Argument</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
<DataTable size="sm">
|
||||
<Head>
|
||||
<tr>
|
||||
<Cell head first>Argument</Cell>
|
||||
<Cell head last>Value</Cell>
|
||||
</tr>
|
||||
<svelte:fragment slot="headerAction">
|
||||
<button
|
||||
on:click={() => {
|
||||
jsonStr = JSON.stringify(args, null, 4)
|
||||
jsonViewer.openDrawer()
|
||||
}}
|
||||
>
|
||||
<Expand size={18} />
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</Head>
|
||||
|
||||
<tbody class="divide-y">
|
||||
{#if args && Object.keys(args).length > 0}
|
||||
{#each Object.entries(args) as [arg, value]}
|
||||
<tr>
|
||||
<td class="font-semibold">{arg}</td>
|
||||
<td><ArgInfo {value} /></td>
|
||||
</tr>
|
||||
<Row>
|
||||
<Cell first>{arg}</Cell>
|
||||
<Cell last><ArgInfo {value} /></Cell>
|
||||
</Row>
|
||||
{/each}
|
||||
{:else if args}
|
||||
<tr><div class="text-secondary pt-2 pl-1 text-sm">No arguments</div></tr>
|
||||
<Row><Cell>No arguments</Cell></Row>
|
||||
{:else}
|
||||
<tr>
|
||||
<td>
|
||||
<Row>
|
||||
<Cell first>
|
||||
<Skeleton layout={[[3], 0.5, [3]]} />
|
||||
</td>
|
||||
<td>
|
||||
</Cell>
|
||||
<Cell last>
|
||||
<Skeleton layout={[[3], 0.5, [3]]} />
|
||||
</td>
|
||||
</tr>
|
||||
</Cell>
|
||||
</Row>
|
||||
{/if}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<Portal>
|
||||
<Drawer bind:this={jsonViewer} size="900px">
|
||||
<DrawerContent title="Expanded Args" on:close={jsonViewer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
<a
|
||||
class="text-sm text-secondary mr-2 inline-flex gap-2 items-center py-2 px-2 hover:bg-surface-hover rounded-lg"
|
||||
download="windmill-args.json"
|
||||
href="data:text/json;charset=utf-8,{encodeURIComponent(jsonStr)}"
|
||||
>Download <Download size={14} /></a
|
||||
>
|
||||
<Button on:click={runLocally.openDrawer} color="light" size="xs">
|
||||
<div class="flex gap-2 items-center">Use in a local script<ChevronRightSquare /> </div>
|
||||
</Button>
|
||||
<Button on:click={() => copyToClipboard(jsonStr)} color="light" size="xs">
|
||||
<div class="flex gap-2 items-center">Copy to clipboard <ClipboardCopy /> </div>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
{#if jsonStr.length > 100000}
|
||||
<div class="text-sm mb-2 text-tertiary">
|
||||
<a
|
||||
download="windmill-args.json"
|
||||
href="data:text/json;charset=utf-8,{encodeURIComponent(jsonStr)}">Download</a
|
||||
>
|
||||
JSON is too large to be displayed in full.
|
||||
</div>
|
||||
{:else}
|
||||
<Highlight language={json} code={jsonStr.replace(/\\n/g, '\n')} />
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<Drawer bind:this={runLocally} size="900px">
|
||||
<DrawerContent title="Run locally" on:close={runLocally.closeDrawer}>
|
||||
<h3 class="mb-2">Envs</h3>
|
||||
If using the wmill client in your code, set the following env variables:
|
||||
<pre
|
||||
><code
|
||||
>BASE_URL="{window.location.origin}"
|
||||
WM_TOKEN="{'<TOKEN>'}"</code
|
||||
></pre
|
||||
<Drawer bind:this={jsonViewer} size="900px">
|
||||
<DrawerContent title="Expanded Args" on:close={jsonViewer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
<a
|
||||
class="text-sm text-secondary mr-2 inline-flex gap-2 items-center py-2 px-2 hover:bg-surface-hover rounded-lg"
|
||||
download="windmill-args.json"
|
||||
href="data:text/json;charset=utf-8,{encodeURIComponent(jsonStr)}"
|
||||
>Download <Download size={14} /></a
|
||||
>
|
||||
<h3 class="mt-8">TypeScript</h3>
|
||||
<pre><code>{typescriptCode()}</code></pre>
|
||||
<h3 class="mt-8">Python</h3>
|
||||
<pre><code>{pythonCode()}</code></pre>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</Portal>
|
||||
<Button on:click={runLocally.openDrawer} color="light" size="xs">
|
||||
<div class="flex gap-2 items-center">Use in a local script<ChevronRightSquare /> </div>
|
||||
</Button>
|
||||
<Button on:click={() => copyToClipboard(jsonStr)} color="light" size="xs">
|
||||
<div class="flex gap-2 items-center">Copy to clipboard <ClipboardCopy /> </div>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
{#if jsonStr.length > 100000}
|
||||
<div class="text-sm mb-2 text-tertiary">
|
||||
<a
|
||||
download="windmill-args.json"
|
||||
href="data:text/json;charset=utf-8,{encodeURIComponent(jsonStr)}">Download</a
|
||||
>
|
||||
JSON is too large to be displayed in full.
|
||||
</div>
|
||||
{:else}
|
||||
<Highlight language={json} code={jsonStr.replace(/\\n/g, '\n')} />
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<Drawer bind:this={runLocally} size="900px">
|
||||
<DrawerContent title="Run locally" on:close={runLocally.closeDrawer}>
|
||||
<h3 class="mb-2">Envs</h3>
|
||||
If using the wmill client in your code, set the following env variables:
|
||||
<pre
|
||||
><code
|
||||
>BASE_URL="{window.location.origin}"
|
||||
WM_TOKEN="{'<TOKEN>'}"</code
|
||||
></pre
|
||||
>
|
||||
<h3 class="mt-8">TypeScript</h3>
|
||||
<pre><code>{typescriptCode()}</code></pre>
|
||||
<h3 class="mt-8">Python</h3>
|
||||
<pre><code>{pythonCode()}</code></pre>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
$: setSelected(selected)
|
||||
|
||||
$: $selectedContent && dispatch('selected', $selectedContent)
|
||||
$: dispatch('selected', $selectedContent)
|
||||
setContext<ToggleButtonContext>('ToggleButtonGroup', {
|
||||
selected: selectedContent,
|
||||
select: (value: any) => {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte'
|
||||
import { Job } from '../../gen'
|
||||
import TestJobLoader from '../TestJobLoader.svelte'
|
||||
import DisplayResult from '../DisplayResult.svelte'
|
||||
import JobArgs from '../JobArgs.svelte'
|
||||
import LogViewer from '../LogViewer.svelte'
|
||||
import { forLater, msToSec } from '$lib/utils'
|
||||
import { Badge, Button, Skeleton, Tab, Tabs } from '../common'
|
||||
import { onMount } from 'svelte'
|
||||
import HighlightCode from '../HighlightCode.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
export let id: string
|
||||
let job: Job | undefined = undefined
|
||||
let timeout: NodeJS.Timeout | undefined
|
||||
let watchJob: (id: string) => Promise<void>
|
||||
let result: any
|
||||
|
||||
function onDone(event: { detail: Job }) {
|
||||
job = event.detail
|
||||
result = job['result']
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
watchJob(id)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
timeout && clearTimeout(timeout)
|
||||
})
|
||||
|
||||
let viewTab = 'result'
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={({ key }) => ['Escape', 'Esc'].includes(key) && close()} />
|
||||
<TestJobLoader bind:job bind:watchJob on:done={onDone} />
|
||||
|
||||
<div class="p-4 flex flex-col gap-2 items-start">
|
||||
<div class="flex gap-2">
|
||||
<Badge color="blue">
|
||||
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
||||
Ran in ({msToSec(job.duration_ms)}s)
|
||||
{/if}
|
||||
</Badge>
|
||||
{#if job?.['mem_peak']}
|
||||
<Badge color="blue">
|
||||
Mem: {job?.['mem_peak'] ? `${(job['mem_peak'] / 1024).toPrecision(4)}MB` : 'N/A'}
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<Button
|
||||
class="flex flex-row gap-1 items-center"
|
||||
on:click={() => {
|
||||
goto(`/run/${job?.id}`)
|
||||
}}
|
||||
>
|
||||
<span class="font-semibold text-xs leading-6">ID:</span>
|
||||
<span class="text-xs">{job?.id}</span>
|
||||
</Button>
|
||||
|
||||
<span class="font-semibold text-xs leading-6">Arguments</span>
|
||||
|
||||
<div class="w-full">
|
||||
<JobArgs args={job?.args} />
|
||||
</div>
|
||||
|
||||
<span class="font-semibold text-xs leading-6">Results</span>
|
||||
|
||||
{#if job && 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
<div class="text-sm font-semibold text-tertiary mb-1">
|
||||
<div>Job is scheduled for</div>
|
||||
<div>{new Date(job?.['scheduled_for']).toLocaleString()}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" w-full rounded-md overflow-auto">
|
||||
{#if job?.type === Job.type.COMPLETED_JOB}
|
||||
<Tabs bind:selected={viewTab}>
|
||||
<Tab size="xs" value="result">Result</Tab>
|
||||
<Tab size="xs" value="logs">Logs</Tab>
|
||||
{#if job?.job_kind == 'dependencies'}
|
||||
<Tab size="xs" value="code">Code</Tab>
|
||||
{:else if job?.job_kind == 'preview'}
|
||||
<Tab size="xs" value="code">Code</Tab>
|
||||
{/if}
|
||||
</Tabs>
|
||||
|
||||
<Skeleton loading={!job} layout={[[5]]} />
|
||||
{#if job}
|
||||
<div class="flex flex-row border rounded-md p-2 mt-2 max-h-1/2 overflow-auto">
|
||||
{#if viewTab == 'logs'}
|
||||
<div class="w-full">
|
||||
<LogViewer
|
||||
jobId={job.id}
|
||||
duration={job?.['duration_ms']}
|
||||
mem={job?.['mem_peak']}
|
||||
isLoading={!(job && 'logs' in job && job.logs)}
|
||||
content={job?.logs}
|
||||
/>
|
||||
</div>
|
||||
{:else if viewTab == 'code'}
|
||||
{#if job && 'raw_code' in job && job.raw_code}
|
||||
<div class="text-xs">
|
||||
<HighlightCode lines language={job.language} code={job.raw_code} />
|
||||
</div>
|
||||
{:else if job}
|
||||
No code is available
|
||||
{:else}
|
||||
<Skeleton layout={[[5]]} />
|
||||
{/if}
|
||||
{:else if job !== undefined && 'result' in job && job.result !== undefined}
|
||||
<DisplayResult workspaceId={job?.workspace_id} jobId={job?.id} {result} disableExpand />
|
||||
{:else if job}
|
||||
No output is available yet
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if job && `running` in job ? job.running : false}
|
||||
<div class="text-sm font-semibold text-tertiary mb-1"> Job is still running </div>
|
||||
<LogViewer
|
||||
jobId={job?.id}
|
||||
duration={job?.['duration_ms']}
|
||||
mem={job?.['mem_peak']}
|
||||
content={job?.logs}
|
||||
isLoading
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Filter } from 'lucide-svelte'
|
||||
import { Popup, Button } from '../common'
|
||||
</script>
|
||||
|
||||
<Popup
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button color="dark" size="xs" nonCaptureEvent={true}>
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
<Filter size={16} />
|
||||
Filters
|
||||
</div>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col w-80">
|
||||
<slot name="filters" />
|
||||
</div>
|
||||
</Popup>
|
||||
@@ -0,0 +1,196 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import type { Job } from '$lib/gen'
|
||||
import {
|
||||
displayDate,
|
||||
displayDaysAgo,
|
||||
forLater,
|
||||
msToSec,
|
||||
truncateHash,
|
||||
truncateRev
|
||||
} from '$lib/utils'
|
||||
import { faRobot, faBarsStaggered } from '@fortawesome/free-solid-svg-icons'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
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,
|
||||
CalendarCheck,
|
||||
Check,
|
||||
FastForward,
|
||||
Hourglass,
|
||||
ListFilter,
|
||||
Play,
|
||||
X
|
||||
} from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const SMALL_ICON_SCALE = 0.7
|
||||
|
||||
export let job: Job
|
||||
export let selectedId: string | undefined = undefined
|
||||
let scheduleEditor: ScheduleEditor
|
||||
|
||||
let time = Date.now()
|
||||
let interval
|
||||
onMount(() => {
|
||||
interval = setInterval(() => {
|
||||
time = Date.now()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
interval && clearInterval(interval)
|
||||
})
|
||||
|
||||
function endedDate(started_at: string, duration_ms: number): string {
|
||||
const started = new Date(started_at)
|
||||
started.setMilliseconds(started.getMilliseconds() + duration_ms)
|
||||
return `${started.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
|
||||
<Row
|
||||
hoverable
|
||||
selected={selectedId === job.id}
|
||||
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} />
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge baseClass="!px-1.5">
|
||||
<Hourglass size={14} />
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<div class="flex flex-row items-center gap-1 text-gray-500 dark:text-gray-300 text-2xs">
|
||||
{#if 'started_at' in job && job.started_at}
|
||||
{#if job?.['duration_ms']}
|
||||
Ended {endedDate(job.started_at, job?.['duration_ms'])}
|
||||
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
||||
(Ran in {msToSec(job.duration_ms)}s)
|
||||
{/if}
|
||||
{:else}
|
||||
<div>
|
||||
Started
|
||||
{#key time}
|
||||
{displayDaysAgo(job.started_at ?? '')}
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if job && 'running' in job && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
<CalendarCheck size={14} />
|
||||
Scheduled
|
||||
|
||||
{#if 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
for {displayDate(job.scheduled_for ?? '')}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</Cell>
|
||||
|
||||
<Cell>
|
||||
<div class="flex flex-row text-sm">
|
||||
{#if job === undefined}
|
||||
No job found
|
||||
{:else}
|
||||
<div class="flex flex-row space-x-2">
|
||||
<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>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
dispatch('filterByPath', job.script_path)
|
||||
}}
|
||||
>
|
||||
<ListFilter size={10} />
|
||||
</Button>
|
||||
</div>
|
||||
{: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
|
||||
>
|
||||
{:else if 'job_kind' in job && job.job_kind == 'identity'}
|
||||
<a href="/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}
|
||||
<Icon class="text-secondary" data={faBarsStaggered} scale={SMALL_ICON_SCALE} />
|
||||
<span class="mx-1">
|
||||
Step of flow <a href={`/run/${job.parent_job}`}>{truncateRev(job.parent_job, 6)} </a>
|
||||
</span>
|
||||
{:else}
|
||||
<Icon class="text-secondary" data={faRobot} scale={SMALL_ICON_SCALE} />
|
||||
<span class="mx-1">
|
||||
Parent <a href={`/run/${job.parent_job}`}>{job.parent_job}</a>
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</Cell>
|
||||
<Cell last>
|
||||
{#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')}
|
||||
>
|
||||
{job.schedule_path}
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
{job.created_by}
|
||||
{/if}
|
||||
</Cell>
|
||||
</Row>
|
||||
@@ -0,0 +1,152 @@
|
||||
<script lang="ts">
|
||||
import { Button, Popup } from '../common'
|
||||
import { page } from '$app/stores'
|
||||
import { setQuery } from '$lib/navigation'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import AutoComplete from 'simple-svelte-autocomplete'
|
||||
import { ChevronDown, Filter, X } from 'lucide-svelte'
|
||||
import JsonEditor from '../apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
|
||||
|
||||
export let paths: string[] = []
|
||||
export let selectedPath: string | undefined = undefined
|
||||
export let success: boolean | undefined = undefined
|
||||
export let isSkipped: boolean | undefined = undefined
|
||||
export let argFilter: string
|
||||
export let argError: string
|
||||
export let resultFilter: string
|
||||
export let resultError: string
|
||||
export let jobKindsCat: string
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-start gap-6 xl:gap-2 xl:flex-row mt-4 xl:mt-0">
|
||||
{#key selectedPath}
|
||||
<div class="relative">
|
||||
{#if selectedPath}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
selectedPath = undefined
|
||||
goto('/runs')
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{:else}
|
||||
<ChevronDown class="absolute top-2 right-2" size={14} />
|
||||
{/if}
|
||||
|
||||
<span class="text-xs absolute -top-4">Path</span>
|
||||
|
||||
<AutoComplete
|
||||
items={paths}
|
||||
value={selectedPath}
|
||||
bind:selectedItem={selectedPath}
|
||||
inputClassName="!h-[30px] py-1 !text-xs !w-64"
|
||||
hideArrow
|
||||
className={selectedPath ? '!font-bold' : ''}
|
||||
dropdownClassName="!font-normal !w-64 !max-w-64"
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
<div class="flex flex-col xl:flex-row gap-6 xl:gap-2 w-full">
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Kind</span>
|
||||
<ToggleButtonGroup
|
||||
bind:selected={jobKindsCat}
|
||||
on:selected={(e) => {
|
||||
const url = new URL($page.url)
|
||||
|
||||
url.searchParams.set('job_kinds', e.detail)
|
||||
goto(url)
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="all" label="All" />
|
||||
<ToggleButton value="runs" label="Runs" />
|
||||
<ToggleButton value="previews" label="Previews" />
|
||||
<ToggleButton value="dependencies" label="Dependencies" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Status</span>
|
||||
<ToggleButtonGroup
|
||||
bind:selected={success}
|
||||
on:selected={async () =>
|
||||
await setQuery($page.url, 'success', success === undefined ? success : String(success))}
|
||||
>
|
||||
<ToggleButton value={undefined} label="All" />
|
||||
<ToggleButton value={true} label="Success" class="whitespace-nowrap" />
|
||||
<ToggleButton value={false} label="Failure" class="whitespace-nowrap" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">
|
||||
Flow
|
||||
<Tooltip light>Skipped flows are flows that did an early break</Tooltip></span
|
||||
>
|
||||
|
||||
<ToggleButtonGroup
|
||||
bind:selected={isSkipped}
|
||||
on:selected={async () =>
|
||||
await setQuery(
|
||||
$page.url,
|
||||
'is_skipped',
|
||||
isSkipped === undefined ? isSkipped : String(isSkipped)
|
||||
)}
|
||||
>
|
||||
<ToggleButton value={undefined} label="All" class="whitespace-nowrap" />
|
||||
<ToggleButton value={false} label="Not skipped" class="whitespace-nowrap" />
|
||||
<ToggleButton value={true} label="Skipped" class="whitespace-nowrap" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Popup
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
let:close
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button color="light" size="xs" nonCaptureEvent={true} variant="border">
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
<Filter size={16} />
|
||||
Filter by args/result
|
||||
</div>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col w-72 p-2 gap-2">
|
||||
<span class="text-sm eading-6 font-semibold">Filters</span>
|
||||
<span class="text-xs leading-6">
|
||||
{`Filter by a json being a subset of the args/result. Try '\{"foo": "bar"\}'`}
|
||||
</span>
|
||||
<span class="text-xs eading-6 font-semibold">Filter by args</span>
|
||||
<JsonEditor on:change bind:error={argError} bind:code={argFilter} />
|
||||
<span class="text-xs eading-6 font-semibold">Filter by result</span>
|
||||
<JsonEditor on:change bind:error={resultError} bind:code={resultFilter} />
|
||||
|
||||
<div class="flex flex-row gap-2 justify-between">
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
argFilter = ''
|
||||
resultFilter = ''
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
on:click={() => {
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
@@ -0,0 +1,97 @@
|
||||
<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'
|
||||
|
||||
export let jobs: Job[] = []
|
||||
export let selectedId: string | undefined = undefined
|
||||
export let nbObJobs: number = 30
|
||||
export let loadMoreQuantity: number = 30
|
||||
|
||||
function groupJobsByDay(jobs: Job[]): Record<string, Job[]> {
|
||||
const groupedLogs: Record<string, Job[]> = {}
|
||||
|
||||
if (!jobs) return groupedLogs
|
||||
|
||||
for (const job of jobs) {
|
||||
if ('started_at' in job && job.started_at) {
|
||||
const date = new Date(job.started_at)
|
||||
date.setMilliseconds(date.getMilliseconds() + (job['duration_ms'] ?? 0))
|
||||
|
||||
const day = date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
|
||||
if (!groupedLogs[day]) {
|
||||
groupedLogs[day] = []
|
||||
}
|
||||
|
||||
groupedLogs[day].push(job)
|
||||
}
|
||||
}
|
||||
|
||||
for (const day in groupedLogs) {
|
||||
groupedLogs[day].sort((a, b) => {
|
||||
return new Date(b.started_at!).getTime() - new Date(a.started_at!).getTime()
|
||||
})
|
||||
}
|
||||
|
||||
const sortedLogs: Record<string, Job[]> = {}
|
||||
Object.keys(groupedLogs)
|
||||
.sort((a, b) => {
|
||||
return new Date(b).getTime() - new Date(a).getTime()
|
||||
})
|
||||
.forEach((key) => {
|
||||
sortedLogs[key] = groupedLogs[key]
|
||||
})
|
||||
|
||||
return sortedLogs
|
||||
}
|
||||
|
||||
$: groupedJobs = groupJobsByDay(jobs.slice(0, nbObJobs))
|
||||
</script>
|
||||
|
||||
<DataTable
|
||||
rounded={false}
|
||||
size="sm"
|
||||
on:loadMore={() => console.log('load more')}
|
||||
loadMore={loadMoreQuantity}
|
||||
shouldLoadMore={nbObJobs < jobs.length}
|
||||
on:loadMore={() => (nbObJobs += loadMoreQuantity)}
|
||||
>
|
||||
<Head>
|
||||
<Cell first head class="w-8" />
|
||||
<Cell head>Timestamp</Cell>
|
||||
<Cell head>Path</Cell>
|
||||
<Cell head last>Triggered by</Cell>
|
||||
</Head>
|
||||
|
||||
<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 />
|
||||
{/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>
|
||||
@@ -7,7 +7,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, setContext } from 'svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-svelte'
|
||||
import { ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let paginated: boolean = false
|
||||
@@ -66,11 +66,11 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else if shouldLoadMore}
|
||||
<div class="bg-surface border-t flex flex-row justify-end p-1 items-center gap-2">
|
||||
<div class="bg-surface border-t flex flex-row justify-center py-4 items-center gap-2">
|
||||
<Button color="light" size="xs2" on:click={() => dispatch('loadMore')}>
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
Load {loadMore} more
|
||||
<ArrowRightIcon size={16} />
|
||||
<ArrowDownIcon size={16} />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
<thead class="bg-surface-secondary sticky top-0">
|
||||
<slot />
|
||||
<div class="absolute top-2 right-2">
|
||||
<slot name="headerAction" />
|
||||
</div>
|
||||
</thead>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<tr
|
||||
class={twMerge(
|
||||
hoverable ? 'hover:bg-surface-hover cursor-pointer' : '',
|
||||
selected ? 'bg-surface-selected' : '',
|
||||
selected ? 'bg-blue-50 dark:bg-blue-900/50' : '',
|
||||
'transition-all'
|
||||
)}
|
||||
on:click={() => {
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
}
|
||||
}
|
||||
let notfound = false
|
||||
|
||||
let forceCancel = false
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,29 +6,25 @@
|
||||
import { page } from '$app/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import JobDetail from '$lib/components/jobs/JobDetail.svelte'
|
||||
import { Button, Skeleton } from '$lib/components/common'
|
||||
import { Button, Drawer, DrawerContent, Skeleton } from '$lib/components/common'
|
||||
import { goto } from '$app/navigation'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import RunChart from '$lib/components/RunChart.svelte'
|
||||
import { faSearchMinus } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import AutoComplete from 'simple-svelte-autocomplete'
|
||||
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
|
||||
import Slider from '$lib/components/Slider.svelte'
|
||||
import JsonEditor from '$lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
|
||||
import { openStore } from '$lib/components/jobs/JobPreview.svelte'
|
||||
import { setQuery } from '$lib/navigation'
|
||||
|
||||
import JobPreview from '$lib/components/runs/JobPreview.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { RefreshCcw } from 'lucide-svelte'
|
||||
import { RefreshCw } from 'lucide-svelte'
|
||||
import CalendarPicker from '$lib/components/common/calendarPicker/CalendarPicker.svelte'
|
||||
|
||||
import RunsTable from '$lib/components/runs/RunsTable.svelte'
|
||||
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import RunsFilter from '$lib/components/runs/RunsFilter.svelte'
|
||||
import MobileFilters from '$lib/components/runs/MobileFilters.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
let jobs: Job[] | undefined
|
||||
let error: Error | undefined
|
||||
let intervalId: NodeJS.Timer | undefined
|
||||
let selectedId: string | undefined = undefined
|
||||
|
||||
let success: boolean | undefined =
|
||||
$page.url.searchParams.get('success') != undefined
|
||||
@@ -45,12 +41,10 @@
|
||||
let maxTs = $page.url.searchParams.get('max_ts') ?? undefined
|
||||
let schedulePath = $page.url.searchParams.get('schedule_path') ?? undefined
|
||||
|
||||
let nbOfJobs = 30
|
||||
let nbObJobs = 30
|
||||
|
||||
$: path = $page.params.path
|
||||
|
||||
$: jobKindsCat = $page.url.searchParams.get('job_kinds') ?? 'runs'
|
||||
|
||||
$: jobKinds = computeJobKinds(jobKindsCat)
|
||||
|
||||
function computeJobKinds(jobKindsCat: string | undefined): string {
|
||||
@@ -67,16 +61,6 @@
|
||||
|
||||
$: ($workspaceStore && loadJobs()) || (path && success && isSkipped && jobKinds)
|
||||
|
||||
let filterTimeout: NodeJS.Timeout | undefined = undefined
|
||||
function debounceSyncer() {
|
||||
if (resultError == '' && argError == '') {
|
||||
filterTimeout && clearTimeout(filterTimeout)
|
||||
filterTimeout = setTimeout(() => {
|
||||
loadJobs()
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchJobs(
|
||||
startedBefore: string | undefined,
|
||||
startedAfter: string | undefined
|
||||
@@ -100,22 +84,18 @@
|
||||
})
|
||||
}
|
||||
|
||||
let loading: boolean = false
|
||||
async function loadJobs(): Promise<void> {
|
||||
loading = true
|
||||
jobs = undefined
|
||||
try {
|
||||
const newJobs = await fetchJobs(maxTs, minTs)
|
||||
jobs = newJobs
|
||||
} catch (err) {
|
||||
sendUserToast(`There was a problem fetching jobs: ${err}`, true)
|
||||
error = err
|
||||
console.error(JSON.stringify(err))
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOlderJobs() {
|
||||
if (jobs) {
|
||||
nbOfJobs += 30
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function syncer() {
|
||||
@@ -143,8 +123,8 @@
|
||||
let sync = true
|
||||
onMount(() => {
|
||||
loadPaths()
|
||||
$openStore = ''
|
||||
intervalId = setInterval(syncer, 5000)
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
sync = false
|
||||
@@ -154,6 +134,20 @@
|
||||
})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
})
|
||||
|
||||
$: if (!intervalId && autoRefresh) {
|
||||
intervalId = setInterval(syncer, 5000)
|
||||
}
|
||||
$: if (intervalId && !autoRefresh) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = undefined
|
||||
}
|
||||
|
||||
let paths: string[] = []
|
||||
|
||||
async function loadPaths() {
|
||||
@@ -162,10 +156,6 @@
|
||||
paths = npaths_scripts.concat(npaths_flows).sort()
|
||||
}
|
||||
|
||||
function syncWithUrl(arg: string, value: string) {
|
||||
setQueryWithoutLoad($page.url, [{ key: arg, value }])
|
||||
}
|
||||
|
||||
async function syncTsWithURL(minTs?: string, maxTs?: string) {
|
||||
setQueryWithoutLoad($page.url, [
|
||||
{ key: 'min_ts', value: minTs },
|
||||
@@ -173,19 +163,11 @@
|
||||
])
|
||||
}
|
||||
|
||||
$: syncWithUrl('job_kinds', jobKindsCat)
|
||||
$: syncWithUrl('arg', argFilter)
|
||||
$: syncWithUrl('result', resultFilter)
|
||||
$: syncTsWithURL(minTs, maxTs)
|
||||
|
||||
$: completedJobs =
|
||||
jobs?.filter((x) => x.type == 'CompletedJob').map((x) => x as CompletedJob) ?? []
|
||||
|
||||
onDestroy(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
})
|
||||
let searchPath = ''
|
||||
$: searchPath = path
|
||||
|
||||
@@ -197,8 +179,24 @@
|
||||
|
||||
let argError = ''
|
||||
let resultError = ''
|
||||
let filterTimeout: NodeJS.Timeout | undefined = undefined
|
||||
function reloadLogsWithoutFilterError() {
|
||||
if (resultError == '' && argError == '') {
|
||||
filterTimeout && clearTimeout(filterTimeout)
|
||||
filterTimeout = setTimeout(() => {
|
||||
loadJobs()
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
const manualDates = [
|
||||
{
|
||||
label: 'Last 1000 runs',
|
||||
setMinMax: () => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Last 30 seconds',
|
||||
setMinMax: () => {
|
||||
@@ -233,210 +231,236 @@
|
||||
minTs = new Date(new Date().getTime() - 24 * 60 * 60 * 1000).toISOString()
|
||||
maxTs = new Date().toISOString()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Last 7 days',
|
||||
setMinMax: () => {
|
||||
minTs = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
||||
maxTs = new Date().toISOString()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Last month',
|
||||
setMinMax: () => {
|
||||
minTs = new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
||||
maxTs = new Date().toISOString()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
let selectedManualDate = 0
|
||||
let autoRefresh: boolean = true
|
||||
let runDrawer: Drawer
|
||||
</script>
|
||||
|
||||
<CenteredPage>
|
||||
<PageHeader
|
||||
title="Runs {path ? `of ${path}` : ''}"
|
||||
tooltip="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."
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs"
|
||||
/>
|
||||
<div class="w-full h-screen">
|
||||
<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>
|
||||
|
||||
<div class="max-w-7x mt-2">
|
||||
<div class="flex flex-row space-x-4">
|
||||
<select
|
||||
bind:value={success}
|
||||
on:change={async () => await setQuery($page.url, 'success', String(success))}
|
||||
>
|
||||
<option value={undefined}>Successful and error jobs</option>
|
||||
<option value={true}>Only successful jobs</option>
|
||||
<option value={false}>Only error jobs</option>
|
||||
</select>
|
||||
<select
|
||||
bind:value={isSkipped}
|
||||
on:change={async () => await setQuery($page.url, 'is_skipped', String(isSkipped))}
|
||||
>
|
||||
<option value={false}>Ignore skipped flow jobs</option>
|
||||
<option value={true}>If a flow job, show only if it was not skipped</option>
|
||||
<option value={undefined}>Show flow jobs regardless of being skipped or not</option>
|
||||
</select>
|
||||
<Tooltip>Skipped flows are flows that did an early break</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<div class="my-2 pb-2">
|
||||
<Tabs
|
||||
selected={jobKindsCat}
|
||||
on:selected={(e) => {
|
||||
const url = new URL($page.url)
|
||||
url.searchParams.set('job_kinds', e.detail)
|
||||
goto(url)
|
||||
}}
|
||||
<Tooltip
|
||||
light
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs"
|
||||
scale={0.9}
|
||||
wrapperClass="flex items-center"
|
||||
>
|
||||
<Tab value="all">All</Tab>
|
||||
<Tab value="runs">Runs</Tab>
|
||||
<Tab value="previews">Previews</Tab>
|
||||
<Tab value="dependencies">Dependencies</Tab>
|
||||
</Tabs>
|
||||
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="border mb-4">
|
||||
<RunChart
|
||||
jobs={completedJobs}
|
||||
on:zoom={async (e) => {
|
||||
minTs = e.detail.min.toISOString()
|
||||
maxTs = e.detail.max.toISOString()
|
||||
jobs = await fetchJobs(maxTs, minTs)
|
||||
<div class="hidden xl:block">
|
||||
<RunsFilter
|
||||
bind:isSkipped
|
||||
{paths}
|
||||
{jobKindsCat}
|
||||
bind:selectedPath={searchPath}
|
||||
bind:success
|
||||
bind:argFilter
|
||||
bind:resultFilter
|
||||
bind:argError
|
||||
bind:resultError
|
||||
on:change={reloadLogsWithoutFilterError}
|
||||
on:clearFilters={() => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
autoRefresh = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-2 w-full mb-2 mt-8">
|
||||
<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()
|
||||
jobs = await fetchJobs(maxTs, minTs)
|
||||
}}
|
||||
/>
|
||||
</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()
|
||||
jobs = await fetchJobs(maxTs, minTs)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
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">
|
||||
<RefreshCcw size={14} />
|
||||
{manualDates[selectedManualDate].label}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2 mb-2 w-full flex-wrap">
|
||||
<div>
|
||||
<div class="flex flex-row gap-x-2">
|
||||
{#key path}
|
||||
<AutoComplete
|
||||
items={paths}
|
||||
value={path}
|
||||
bind:selectedItem={searchPath}
|
||||
placeholder="Search by path"
|
||||
/>
|
||||
{/key}
|
||||
<Button
|
||||
title="Clear path and time filters"
|
||||
variant="border"
|
||||
on:click={async () => {
|
||||
<div class="xl:hidden">
|
||||
<MobileFilters>
|
||||
<svelte:fragment slot="filters">
|
||||
<span class="text-xs font-semibold leading-6">Filters</span>
|
||||
<RunsFilter
|
||||
bind:isSkipped
|
||||
{paths}
|
||||
{jobKindsCat}
|
||||
bind:selectedPath={searchPath}
|
||||
bind:success
|
||||
bind:argFilter
|
||||
bind:resultFilter
|
||||
bind:argError
|
||||
bind:resultError
|
||||
on:change={reloadLogsWithoutFilterError}
|
||||
on:clearFilters={() => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
jobs = undefined
|
||||
await goto('/runs?' + $page.url.searchParams.toString())
|
||||
loadJobs()
|
||||
autoRefresh = true
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
<Icon data={faSearchMinus} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
><Slider
|
||||
text="Filter by args {argFilter ? '(set)' : ''}"
|
||||
tooltip={'Filter by a json being a subset of the args. Try \'{"foo": "bar"}\''}
|
||||
><JsonEditor
|
||||
on:change={debounceSyncer}
|
||||
bind:error={argError}
|
||||
bind:code={argFilter}
|
||||
/></Slider
|
||||
></div
|
||||
>
|
||||
<div
|
||||
><Slider
|
||||
text="Filter by result {resultFilter ? '(set)' : ''}"
|
||||
tooltip={'Filter by a json being a subset of the result. Try \'{"foo": "bar"}\''}
|
||||
><JsonEditor
|
||||
on:change={debounceSyncer}
|
||||
bind:error={resultError}
|
||||
bind:code={resultFilter}
|
||||
/></Slider
|
||||
></div
|
||||
>
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</MobileFilters>
|
||||
</div>
|
||||
|
||||
<Skeleton loading={!jobs} layout={[[6], 1, [6], 1, [6], 1, [6], 1, [6]]} />
|
||||
|
||||
{#if jobs}
|
||||
<div class="space-y-0">
|
||||
{#each jobs.slice(0, nbOfJobs) as job (job.id)}
|
||||
<JobDetail {job} />
|
||||
<div class="line w-20 h-4" />
|
||||
{/each}
|
||||
</div>
|
||||
{#if jobs.length == 0}
|
||||
<NoItemFound />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if error}
|
||||
<div>
|
||||
{JSON.stringify(error)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if (jobs?.length ?? 0) >= nbOfJobs}
|
||||
<div class="text-center pb-6">
|
||||
<button class=" mt-4 text-blue-500 text-center text-sm" on:click={loadOlderJobs}>
|
||||
Load older jobs
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
<div class=" p-2">
|
||||
<RunChart
|
||||
jobs={completedJobs}
|
||||
on:zoom={async (e) => {
|
||||
minTs = e.detail.min.toISOString()
|
||||
maxTs = e.detail.max.toISOString()
|
||||
jobs = await fetchJobs(maxTs, minTs)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 md:flex-row w-full p-4 justify-between">
|
||||
<div class="flex flex-row gap-1 w-full">
|
||||
<div class="relative w-full">
|
||||
<div class="flex gap-1 relative w-full">
|
||||
<span class="text-xs absolute -top-4">Min datetime</span>
|
||||
|
||||
<style>
|
||||
.line {
|
||||
background: repeating-linear-gradient(to bottom, transparent 0 4px, #999 4px 8px) 50%/1px 100%
|
||||
no-repeat;
|
||||
}
|
||||
</style>
|
||||
<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()
|
||||
jobs = await fetchJobs(maxTs, minTs)
|
||||
}}
|
||||
/>
|
||||
</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()
|
||||
jobs = await fetchJobs(maxTs, minTs)
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
|
||||
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>
|
||||
|
||||
<SplitPanesWrapper class="hidden md:block">
|
||||
<Splitpanes>
|
||||
<Pane size={60} minSize={50}>
|
||||
{#if jobs}
|
||||
<RunsTable
|
||||
{jobs}
|
||||
bind:selectedId
|
||||
bind:nbObJobs
|
||||
loadMoreQuantity={30}
|
||||
on:filterByPath={(e) => {
|
||||
searchPath = e.detail
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class="gap-1 flex flex-col">
|
||||
{#each new Array(8) as _}
|
||||
<Skeleton layout={[[3]]} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane size={40} minSize={15} class="border-t">
|
||||
{#if selectedId}
|
||||
{#key selectedId}
|
||||
<JobPreview id={selectedId} />
|
||||
{/key}
|
||||
{:else}
|
||||
<div class="text-xs m-4">No job selected</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
|
||||
<div class="md:hidden">
|
||||
{#if jobs}
|
||||
<RunsTable
|
||||
{jobs}
|
||||
bind:selectedId
|
||||
bind:nbObJobs
|
||||
loadMoreQuantity={30}
|
||||
on:select={() => {
|
||||
runDrawer.openDrawer()
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={runDrawer}>
|
||||
<DrawerContent title="Run details" on:close={runDrawer.closeDrawer}>
|
||||
{#if selectedId}
|
||||
<JobPreview id={selectedId} />
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
Reference in New Issue
Block a user