diff --git a/frontend/src/lib/components/EditorTheme.svelte b/frontend/src/lib/components/EditorTheme.svelte index c83f96fa5a..0dd40346bd 100644 --- a/frontend/src/lib/components/EditorTheme.svelte +++ b/frontend/src/lib/components/EditorTheme.svelte @@ -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() }) } diff --git a/frontend/src/lib/components/JobArgs.svelte b/frontend/src/lib/components/JobArgs.svelte index 9b1fcdcaf5..ec67079b7d 100644 --- a/frontend/src/lib/components/JobArgs.svelte +++ b/frontend/src/lib/components/JobArgs.svelte @@ -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)
-
- -
- - - Argument - Value - - + + + + Argument + Value + + + + + + + {#if args && Object.keys(args).length > 0} {#each Object.entries(args) as [arg, value]} - - {arg} - - + + {arg} + + {/each} {:else if args} -
No arguments
+ No arguments {:else} - - + + - - + + - - + + {/if} -
+
- - - - - Download - - - - {#if jsonStr.length > 100000} -
- Download - JSON is too large to be displayed in full. -
- {:else} - - {/if} -
-
- - -

Envs

- If using the wmill client in your code, set the following env variables: -
BASE_URL="{window.location.origin}"
-WM_TOKEN="{''}"
+ + + Download -

TypeScript

-
{typescriptCode()}
-

Python

-
{pythonCode()}
-
-
-
+ + + + {#if jsonStr.length > 100000} +
+ Download + JSON is too large to be displayed in full. +
+ {:else} + + {/if} + + + + +

Envs

+ If using the wmill client in your code, set the following env variables: +
BASE_URL="{window.location.origin}"
+WM_TOKEN="{''}"
+

TypeScript

+
{typescriptCode()}
+

Python

+
{pythonCode()}
+
+
diff --git a/frontend/src/lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte b/frontend/src/lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte index 26217649b8..348510c3c5 100644 --- a/frontend/src/lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte +++ b/frontend/src/lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte @@ -23,7 +23,7 @@ $: setSelected(selected) - $: $selectedContent && dispatch('selected', $selectedContent) + $: dispatch('selected', $selectedContent) setContext('ToggleButtonGroup', { selected: selectedContent, select: (value: any) => { diff --git a/frontend/src/lib/components/runs/JobPreview.svelte b/frontend/src/lib/components/runs/JobPreview.svelte new file mode 100644 index 0000000000..1430233231 --- /dev/null +++ b/frontend/src/lib/components/runs/JobPreview.svelte @@ -0,0 +1,130 @@ + + + ['Escape', 'Esc'].includes(key) && close()} /> + + +
+
+ + {#if job && 'duration_ms' in job && job.duration_ms != undefined} + Ran in ({msToSec(job.duration_ms)}s) + {/if} + + {#if job?.['mem_peak']} + + Mem: {job?.['mem_peak'] ? `${(job['mem_peak'] / 1024).toPrecision(4)}MB` : 'N/A'} + + {/if} +
+ + + Arguments + +
+ +
+ + Results + + {#if job && 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)} +
+
Job is scheduled for
+
{new Date(job?.['scheduled_for']).toLocaleString()}
+
+ {/if} + +
+ {#if job?.type === Job.type.COMPLETED_JOB} + + Result + Logs + {#if job?.job_kind == 'dependencies'} + Code + {:else if job?.job_kind == 'preview'} + Code + {/if} + + + + {#if job} +
+ {#if viewTab == 'logs'} +
+ +
+ {:else if viewTab == 'code'} + {#if job && 'raw_code' in job && job.raw_code} +
+ +
+ {:else if job} + No code is available + {:else} + + {/if} + {:else if job !== undefined && 'result' in job && job.result !== undefined} + + {:else if job} + No output is available yet + {/if} +
+ {/if} + {:else if job && `running` in job ? job.running : false} +
Job is still running
+ + {/if} +
+
diff --git a/frontend/src/lib/components/runs/MobileFilters.svelte b/frontend/src/lib/components/runs/MobileFilters.svelte new file mode 100644 index 0000000000..1c8dc264e4 --- /dev/null +++ b/frontend/src/lib/components/runs/MobileFilters.svelte @@ -0,0 +1,21 @@ + + + + + + +
+ +
+
diff --git a/frontend/src/lib/components/runs/RunRow.svelte b/frontend/src/lib/components/runs/RunRow.svelte new file mode 100644 index 0000000000..0898f95e04 --- /dev/null +++ b/frontend/src/lib/components/runs/RunRow.svelte @@ -0,0 +1,196 @@ + + + goto('/schedules')} bind:this={scheduleEditor} /> + + { + selectedId = job.id + dispatch('select') + }} +> + +
+ {#if 'success' in job && job.success} + {#if job.is_skipped} + + + + {:else} + + + + {/if} + {:else if 'success' in job} + + + + {:else if 'running' in job && job.running} + + + + {:else if job && 'running' in job && job.scheduled_for && forLater(job.scheduled_for)} + + + + {:else} + + + + {/if} +
+
+ +
+ {#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} +
+ Started + {#key time} + {displayDaysAgo(job.started_at ?? '')} + {/key} +
+ {/if} + {/if} + + {#if job && 'running' in job && job.scheduled_for && forLater(job.scheduled_for)} + + Scheduled + + {#if 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)} + for {displayDate(job.scheduled_for ?? '')} + {/if} + {/if} +
+
+ + +
+ {#if job === undefined} + No job found + {:else} +
+
+ {#if job.script_path} +
+ {job.script_path} + +
+ {:else if 'job_kind' in job && job.job_kind == 'preview'} + Preview without path + {:else if 'job_kind' in job && job.job_kind == 'dependencies'} + lock deps of {truncateHash(job.script_hash ?? '')} + {:else if 'job_kind' in job && job.job_kind == 'identity'} + no op + {/if} +
+
+ {/if} +
+ + {#if job && job.parent_job} + {#if job.is_flow_step} + + + Step of flow {truncateRev(job.parent_job, 6)} + + {:else} + + + Parent {job.parent_job} + + {/if} + {/if} +
+ + {#if job && job.schedule_path} +
+ + +
+ {:else} + {job.created_by} + {/if} +
+
diff --git a/frontend/src/lib/components/runs/RunsFilter.svelte b/frontend/src/lib/components/runs/RunsFilter.svelte new file mode 100644 index 0000000000..937fb6b46e --- /dev/null +++ b/frontend/src/lib/components/runs/RunsFilter.svelte @@ -0,0 +1,152 @@ + + +
+ {#key selectedPath} +
+ {#if selectedPath} + + {:else} + + {/if} + + Path + + +
+ {/key} +
+
+ Kind + { + const url = new URL($page.url) + + url.searchParams.set('job_kinds', e.detail) + goto(url) + }} + > + + + + + +
+
+ Status + + await setQuery($page.url, 'success', success === undefined ? success : String(success))} + > + + + + +
+
+ + Flow + Skipped flows are flows that did an early break + + + await setQuery( + $page.url, + 'is_skipped', + isSkipped === undefined ? isSkipped : String(isSkipped) + )} + > + + + + +
+
+ + + + + +
+ Filters + + {`Filter by a json being a subset of the args/result. Try '\{"foo": "bar"\}'`} + + Filter by args + + Filter by result + + +
+ + +
+
+
+
diff --git a/frontend/src/lib/components/runs/RunsTable.svelte b/frontend/src/lib/components/runs/RunsTable.svelte new file mode 100644 index 0000000000..c9c1142051 --- /dev/null +++ b/frontend/src/lib/components/runs/RunsTable.svelte @@ -0,0 +1,97 @@ + + + console.log('load more')} + loadMore={loadMoreQuantity} + shouldLoadMore={nbObJobs < jobs.length} + on:loadMore={() => (nbObJobs += loadMoreQuantity)} +> + + + Timestamp + Path + Triggered by + + + + {#each Object.entries(groupedJobs) as [date, jobsByDay]} + + + {date} + + + {#each jobsByDay as job (job.id)} + + {/each} + {/each} + + {#if jobs.length == 0} + + +
No jobs found for the selected filters.
+ + + {/if} +
diff --git a/frontend/src/lib/components/table/DataTable.svelte b/frontend/src/lib/components/table/DataTable.svelte index 114c3622e2..f39a2a0a7f 100644 --- a/frontend/src/lib/components/table/DataTable.svelte +++ b/frontend/src/lib/components/table/DataTable.svelte @@ -7,7 +7,7 @@ diff --git a/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte index f8cdf3dffd..b9fd1ffffc 100644 --- a/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte @@ -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 { + 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 - - +
+
+
+
+

Runs

-
-
- - - Skipped flows are flows that did an early break -
-
-
- { - const url = new URL($page.url) - url.searchParams.set('job_kinds', e.detail) - goto(url) - }} + - All - Runs - Previews - Dependencies - + 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. +
-
- { - minTs = e.detail.min.toISOString() - maxTs = e.detail.max.toISOString() - jobs = await fetchJobs(maxTs, minTs) + -
-
-
- Min datetime - - - - { - minTs = new Date(detail).toISOString() - jobs = await fetchJobs(maxTs, minTs) - }} - /> -
-
-
-
- Max datetime - - { - maxTs = new Date(detail).toISOString() - jobs = await fetchJobs(maxTs, minTs) - }} - /> -
-
- -
-
-
-
- {#key path} - - {/key} - -
-
-
-
+ /> + +
- - - - {#if jobs} -
- {#each jobs.slice(0, nbOfJobs) as job (job.id)} - -
- {/each} -
- {#if jobs.length == 0} - - {/if} - {/if}
- {#if error} -
- {JSON.stringify(error)} -
- {/if}
- {#if (jobs?.length ?? 0) >= nbOfJobs} -
- -
- {/if} - +
+ { + minTs = e.detail.min.toISOString() + maxTs = e.detail.max.toISOString() + jobs = await fetchJobs(maxTs, minTs) + }} + /> +
+
+
+
+
+ Min datetime - + + + { + minTs = new Date(detail).toISOString() + jobs = await fetchJobs(maxTs, minTs) + }} + /> +
+
+
+
+ Max datetime + + { + maxTs = new Date(detail).toISOString() + jobs = await fetchJobs(maxTs, minTs) + }} + /> +
+
+
+
+ + + + +
+
+ + + +
+ {#if jobs} + { + runDrawer.openDrawer() + }} + /> + {/if} +
+
+ + + + {#if selectedId} + + {/if} + +