From fd8de45cc7cfcba2c374dd86f87f4d3e8aec6ded Mon Sep 17 00:00:00 2001 From: Guilhem Date: Tue, 6 Oct 2026 14:45:03 +0200 Subject: [PATCH] feat: open a run as a pushed page on the narrow runs page (#11541) * feat: open a run as a pushed page on narrow runs page Co-Authored-By: Claude Opus 5.5 (1M context) * fix: keep modifier-click multi-select and arrow keys on the narrow runs list Co-Authored-By: Claude Opus 5.5 (1M context) * 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) * fix: close the re-run options once a table re-run completes Co-Authored-By: Claude Opus 5.5 (1M context) --------- Co-authored-by: Claude Opus 5.5 (1M context) --- frontend/src/lib/components/RunsPage.svelte | 601 ++++++++++-------- .../src/lib/components/runs/RunsTable.svelte | 1 + 2 files changed, 353 insertions(+), 249 deletions(-) diff --git a/frontend/src/lib/components/RunsPage.svelte b/frontend/src/lib/components/RunsPage.svelte index 97f2723943..ff32cbdbd7 100644 --- a/frontend/src/lib/components/RunsPage.svelte +++ b/frontend/src/lib/components/RunsPage.svelte @@ -51,10 +51,11 @@ import BatchLoadProgress from '$lib/components/BatchLoadProgress.svelte' import { pluralize, MAX_RESOLUTION_BATCH, MAX_RESOLUTION_NOTE_LEN } from '$lib/utils' import type { BatchReRunOptions } from '$lib/components/runs/BatchReRunOptionsPane.svelte' - import { untrack } from 'svelte' + import { untrack, type Snippet } from 'svelte' import { page } from '$app/state' import Select from '$lib/components/select/Select.svelte' import AnimatedPane from '$lib/components/splitPanes/AnimatedPane.svelte' + import PagedContent from '$lib/components/common/modal/PagedContent.svelte' import { StaleWhileLoading, useLocalStorageValue } from '$lib/svelte5Utils.svelte' import { Calendar, @@ -63,8 +64,9 @@ CirclePlay, Clock, Hourglass, - Loader2, - TriangleAlertIcon + TriangleAlertIcon, + ArrowLeft, + Loader2 } from 'lucide-svelte' import DropdownV2 from './DropdownV2.svelte' import TimeframeSelect, { @@ -208,6 +210,7 @@ function reset() { _timeframe.val = { ...runsTimeframes[0] } selectedIds = [] + openedRunId = undefined delete filters.val.schedule_path selectedWorkspace = undefined jobsLoader?.loadJobs(true) @@ -498,6 +501,9 @@ }) selectedIds = [] + // Opened from the table's "Run again", the options are done; left open, the next selection + // of one run would show them again. The re-run selection mode keeps them for its next batch. + if (!manualSelectionMode) batchRerunOptionsIsOpen = false jobsLoader?.loadJobs(true, true) } @@ -581,6 +587,27 @@ ) let manualSelectionMode: undefined | 'cancel' | 'rerun' | 'resolve' = $state() + + // Below 1024px (Tailwind `lg`) the side pane leaves the table too little room, so what it would + // show becomes a page pushed over the list. Selection modes keep the split: their rows are + // picked in the list while the pane holds the action. + const pagedLayout = $derived(innerWidth < 1024 && !manualSelectionMode) + // Set only by a plain row click, a chart point or the arrow keys, never by a modifier click: + // a Ctrl/Cmd/Shift-click starting a multi-selection selects one run first, and opening it would + // hide the list before the second click. + let openedRunId: string | undefined = $state() + const runPageOpen = $derived( + // A confirmed re-run clears the selection but leaves the options flag set. + (batchRerunOptionsIsOpen && selectedIds.length > 0) || + (selectedIds.length === 1 && selectedIds[0] === openedRunId) + ) + + // Clears the selection too: a re-run selection of one job would otherwise land on that job. + function backToList() { + batchRerunOptionsIsOpen = false + selectedIds = [] + openedRunId = undefined + } let resolutionNote = $state('') @@ -929,6 +956,7 @@ _timeframe.val = buildManualTimeframe(zoom.min.toISOString(), zoom.max.toISOString()) }} onPointClicked={(ids) => { + openedRunId = ids.length === 1 ? ids[0] : undefined runsTable?.scrollToRun(ids) }} /> @@ -946,256 +974,331 @@
- - -
-
- {#if scanProgress} -
- -
- jobsLoader.stopBatchLoading()} - /> -
-
- {/if} - {#if batchProgress} -
- jobsLoader.restreamWithBatchSize(v)} - onStop={() => jobsLoader.stopBatchLoading()} - /> -
- {/if} - -
- {#if jobs} - - {:else} -
- {#each new Array(8) as _} - - {/each} -
- {/if} -
-
- {#if !manualSelectionMode} - ((manualSelectionMode = 'cancel'), (selectedIds = [])) - }, - { - displayName: 'Re-run jobs', - action: () => { - manualSelectionMode = 'rerun' - selectedIds = [] - batchRerunOptionsIsOpen = true - } - }, - ...(!$userStore?.operator - ? [ - { - // Operators are rejected by the endpoint, so offering it would only 403. - displayName: 'Resolve failed jobs', - action: () => ( - (manualSelectionMode = 'resolve'), - (selectedIds = []), - (resolutionNote = '') - ) - } - ] - : []), - { - displayName: 'Cancel all jobs matching filters', - action: () => onCancelAllJobsMatchingFilters() - }, - { - displayName: 'Re-run all jobs matching filters', - action: () => onRerunAllJobsMatchingFilters() - } - ]} - /> - {:else} - - {/if} -
- - perPage.val, + (newPerPage) => { + perPage.val = newPerPage + if (newPerPage > (jobs?.length ?? 1000)) loadExtra() + } + } + onCreateItem={(v) => (perPage.val = parseInt(v))} + items={[ + { value: 25, label: '25' }, + { value: 100, label: '100' }, + { value: 1000, label: '1000' }, + { value: 10000, label: '10000' } + ]} + transformInputSelectedText={(_, v) => `${v} / page`} + /> +
+
+
+{/snippet} + +{#snippet sidePanel()} +
+ {#if manualSelectionMode === 'cancel'} +
+ +
+ {:else if manualSelectionMode === 'resolve'} +
+

+ Resolving keeps the run a failure but stops it showing as one in the runs list. +

+ +
+ + +
+
+ {:else if batchRerunOptionsIsOpen} + {@render batchRerunOptions()} + {:else if selectedIds.length === 1} + {#if selectedIds[0] === '-'} +
There is no information available for this job
+ {:else} + {@render jobPreview()} + {/if} + {:else if selectedIds.length > 1} +
+
{selectedIds.length} jobs selected
+
+ {/if} +
+{/snippet} + +{#snippet runPage()} + {@render runPageFrame(runPageBody)} +{/snippet} + +{#snippet runPagePlaceholder()} + {@render runPageFrame(runPageSkeleton)} +{/snippet} + +{#snippet runPageFrame(body: Snippet)} + +
+
+ +
+
+ {@render body()} +
+
+{/snippet} + +{#snippet runPageBody()} + {#if batchRerunOptionsIsOpen} + {@render batchRerunOptions()} + {:else if selectedIds[0] === '-'} +
There is no information available for this job
+ {:else if selectedIds.length === 1} + {@render jobPreview(runPageSkeleton)} + {/if} +{/snippet} + + +{#snippet runPageSkeleton()} + +{/snippet} + +{#snippet batchRerunOptions()} + {#await import('$lib/components/runs/BatchReRunOptionsPane.svelte') then BatchReRunOptionsPane} + ((batchRerunOptionsIsOpen = false), (manualSelectionMode = undefined))} + onConfirm={async (options) => { + await onReRunSelectedJobs(options) + }} + /> + {/await} +{/snippet} + +{#snippet jobPreview(pending?: Snippet)} + + {#await import('$lib/components/runs/JobRunsPreview.svelte')} + {@render pending?.()} + {:then JobRunsPreview} + jobsLoader?.loadJobs(true, true)} + /> + {/await} +{/snippet} +