@runwisp/ui 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/lib/actions/focusTrap.ts +3 -3
- package/src/lib/components/Badge.svelte +1 -1
- package/src/lib/components/DataGrid.svelte +5 -5
- package/src/lib/components/FilterBar.svelte +1 -1
- package/src/lib/components/FilterPill.svelte +2 -2
- package/src/lib/components/FormField.svelte +1 -1
- package/src/lib/components/LogConsole.svelte +10 -10
- package/src/lib/components/SearchInput.svelte +1 -1
- package/src/lib/components/Select.svelte +3 -2
- package/src/lib/components/Sparkline.svelte +2 -2
- package/src/lib/components/StatusDot.svelte +1 -1
- package/src/lib/components/Tooltip.svelte +1 -1
- package/src/lib/components/dashboard/RunDetailPanel.svelte +15 -16
- package/src/lib/components/dashboard/RunFilterPopover.svelte +5 -5
- package/src/lib/components/dashboard/RunsList.svelte +8 -8
- package/src/lib/components/dashboard/cron-format.test.ts +1 -1
- package/src/lib/components/dashboard/cron-format.ts +2 -2
- package/src/lib/components/dashboard/run-filters.test.ts +2 -2
- package/src/lib/components/dashboard/run-filters.ts +27 -94
- package/src/lib/components/dashboard/run-helpers.test.ts +9 -0
- package/src/lib/components/dashboard/run-helpers.ts +28 -38
- package/src/lib/components/data-grid-selection.ts +2 -9
- package/src/lib/components/filter-spec.ts +3 -3
- package/src/lib/log-console/LogCache.svelte.ts +1 -1
- package/src/lib/log-console/ansi.ts +1 -1
- package/src/lib/theme-tokens.css +21 -21
- package/src/lib/theme.css +13 -13
- package/src/lib/utils/format.test.ts +3 -3
- package/src/lib/utils/format.ts +4 -4
- package/src/lib/utils/toast.svelte.ts +2 -3
- package/src/stories/LogConsole.stories.svelte +2 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runwisp/ui",
|
|
3
3
|
"description": "Svelte 5 UI component library for RunWisp dashboards and admin interfaces.",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.1",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@chromatic-com/storybook": "^5.2.1",
|
|
51
51
|
"@eslint/js": "^10.0.1",
|
|
52
|
-
"@runwisp/eslint-config": "1.5.
|
|
53
|
-
"@runwisp/typescript-config": "1.5.
|
|
52
|
+
"@runwisp/eslint-config": "1.5.1",
|
|
53
|
+
"@runwisp/typescript-config": "1.5.1",
|
|
54
54
|
"@storybook/addon-a11y": "^10.5.5",
|
|
55
55
|
"@storybook/addon-docs": "^10.5.5",
|
|
56
56
|
"@storybook/addon-svelte-csf": "^5.1.2",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"@fontsource-variable/geist-mono": "^5.3.0",
|
|
94
94
|
"@fontsource/tasa-orbiter": "^5.3.0",
|
|
95
95
|
"@lucide/svelte": "^1.52.0",
|
|
96
|
-
"@runwisp/common": "1.5.
|
|
96
|
+
"@runwisp/common": "1.5.1",
|
|
97
97
|
"@tanstack/svelte-virtual": "^3.13.35",
|
|
98
98
|
"ansi-to-html": "^0.7.2",
|
|
99
99
|
"cron-parser": "^5.7.0",
|
|
@@ -25,7 +25,7 @@ export function getFocusableElements(container: HTMLElement): HTMLElement[] {
|
|
|
25
25
|
* Pure decision function: given a keydown event, the currently focusable
|
|
26
26
|
* elements (in order), and whatever currently has focus, returns the element
|
|
27
27
|
* that Tab/Shift+Tab should wrap focus to in order to keep it trapped inside
|
|
28
|
-
* the set
|
|
28
|
+
* the set, or `null` if the key doesn't need handling.
|
|
29
29
|
*
|
|
30
30
|
* Framework- and DOM-free on purpose (elements are compared by identity only)
|
|
31
31
|
* so this can be unit tested without a browser or jsdom.
|
|
@@ -59,7 +59,7 @@ export interface TrapFocusOptions {
|
|
|
59
59
|
* Svelte action that keeps keyboard focus inside `node` while it's mounted:
|
|
60
60
|
* Tab from the last focusable descendant wraps to the first (and Shift+Tab
|
|
61
61
|
* from the first wraps to the last), and focus is restored to whatever was
|
|
62
|
-
* focused beforehand once the trap is torn down
|
|
62
|
+
* focused beforehand once the trap is torn down, unless a click on some
|
|
63
63
|
* other real element already moved focus there, in which case that's left
|
|
64
64
|
* alone.
|
|
65
65
|
*/
|
|
@@ -97,7 +97,7 @@ export function trapFocus(node: HTMLElement, options: TrapFocusOptions = {}) {
|
|
|
97
97
|
destroy() {
|
|
98
98
|
node.removeEventListener("keydown", handleKeydown);
|
|
99
99
|
// By the time an action's destroy() runs, `node` (and whatever
|
|
100
|
-
// inside it held focus) has already been detached
|
|
100
|
+
// inside it held focus) has already been detached, removing a
|
|
101
101
|
// focused element resets focus to <body> as a browser default, so
|
|
102
102
|
// that can't be told apart from an explicit close (Escape, item
|
|
103
103
|
// activation) by checking `node.contains(activeElement)` here.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
outline?: boolean;
|
|
18
18
|
/** Shown in the styled hover tooltip (see {@link Tooltip}). */
|
|
19
19
|
tooltip?: string;
|
|
20
|
-
/** Side the tooltip opens on
|
|
20
|
+
/** Side the tooltip opens on, pick one that has room, e.g. "bottom" near the top of the viewport. */
|
|
21
21
|
tooltipPosition?: TooltipPosition;
|
|
22
22
|
children?: Snippet;
|
|
23
23
|
class?: string;
|
|
@@ -46,16 +46,16 @@
|
|
|
46
46
|
onRowClick?: (row: T) => void;
|
|
47
47
|
loading?: boolean;
|
|
48
48
|
stickyHeader?: boolean;
|
|
49
|
-
/** Hide the header row entirely
|
|
49
|
+
/** Hide the header row entirely, for single-column feeds (e.g. an
|
|
50
50
|
* inbox) where a column label would be noise. */
|
|
51
51
|
showHeader?: boolean;
|
|
52
|
-
/** Rendered below the table body, inside the frame
|
|
52
|
+
/** Rendered below the table body, inside the frame, e.g. a cursor
|
|
53
53
|
* "Load more" button for feeds that don't use offset pagination. */
|
|
54
54
|
footer?: Snippet;
|
|
55
55
|
/** Drop the outer border/shadow/bg so the grid sits flush inside a Card
|
|
56
56
|
* or panel that already supplies its own frame. */
|
|
57
57
|
bare?: boolean;
|
|
58
|
-
/** Extra classes per row
|
|
58
|
+
/** Extra classes per row, e.g. a left accent bar for a failing row.
|
|
59
59
|
* Rows always carry `group`, so a `render`/`rowAction` snippet can use
|
|
60
60
|
* `group-hover:` to reveal on-hover controls. */
|
|
61
61
|
rowClass?: (row: T) => string;
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
// Type-aware comparator. Nullish sorts last; numbers numerically; dates by
|
|
67
67
|
// epoch; everything else by locale with numeric-aware string compare.
|
|
68
68
|
// Returns 0 for non-comparable/equal, so a column whose key maps to no real
|
|
69
|
-
// field (a display-only key) leaves order untouched
|
|
69
|
+
// field (a display-only key) leaves order untouched, sort stays inert there.
|
|
70
70
|
function compareValues(a: unknown, b: unknown): number {
|
|
71
71
|
if (a == null && b == null) return 0;
|
|
72
72
|
if (a == null) return 1;
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
let allSelected = $derived(selection.allSelected);
|
|
194
194
|
let someSelected = $derived(selection.someSelected);
|
|
195
195
|
|
|
196
|
-
// Cell rhythm
|
|
196
|
+
// Cell rhythm, airy by default, tight when compact. Header + body share it.
|
|
197
197
|
const density = $derived(compact ? "compact" : "comfortable");
|
|
198
198
|
// Hover reveals a teal accent rail on the leftmost cell (see .group on <tr>).
|
|
199
199
|
const railHover = "group-hover:shadow-[inset_3px_0_0_var(--color-primary)]";
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
shown as pills; every other field stays behind "+ Add filter" until the user
|
|
12
12
|
adds it, then lives as a pill. Each pill is self-contained (label + value +
|
|
13
13
|
clear in one element, see FilterPill) so activating a filter never adds a
|
|
14
|
-
second row
|
|
14
|
+
second row, the table below never jumps. Data-mode agnostic: the parent feeds
|
|
15
15
|
`values` to applyFilters (client) or maps it to query params (server).
|
|
16
16
|
-->
|
|
17
17
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<!--
|
|
5
5
|
One filter, one element. The pill is both the control and its own "chip":
|
|
6
6
|
inactive it reads `Label ▾` (muted); active it reads `Label: value ✕` (accent)
|
|
7
|
-
with an inline clear. Active vs inactive is a restyle of the SAME element
|
|
7
|
+
with an inline clear. Active vs inactive is a restyle of the SAME element, it
|
|
8
8
|
never spawns a second row, so the table below never jumps. Clicking the body
|
|
9
9
|
opens a type-specific editor popover (select menu / number / date range).
|
|
10
10
|
-->
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
let {
|
|
27
27
|
field,
|
|
28
28
|
values = $bindable({}),
|
|
29
|
-
// Open the editor on mount
|
|
29
|
+
// Open the editor on mount, set when the pill was just added from the
|
|
30
30
|
// "+ Add filter" menu, so the user lands straight in the value picker.
|
|
31
31
|
autoOpen = false,
|
|
32
32
|
onClose,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
children: Snippet;
|
|
14
14
|
class?: string;
|
|
15
15
|
// The id of the control this field labels. Pass the SAME id to the wrapped
|
|
16
|
-
// input/select so the <label for> actually points at it
|
|
16
|
+
// input/select so the <label for> actually points at it, otherwise the
|
|
17
17
|
// label is dead (clicking it does nothing; screen readers can't associate
|
|
18
18
|
// it). Falls back to a generated id when the field has no focusable target.
|
|
19
19
|
id?: string;
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
// lineTop / totalHeight / the scroll-position→line lookup stay O(1) or
|
|
149
149
|
// O(log n) even though rows are no longer uniform. A plain Map is
|
|
150
150
|
// deliberate: reactivity is driven by the `rowCountVersion` counter (bumped
|
|
151
|
-
// on any change), not by per-key subscriptions
|
|
151
|
+
// on any change), not by per-key subscriptions, iterating a SvelteMap of
|
|
152
152
|
// tens of thousands of lines on every streamed line would be wasteful.
|
|
153
153
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
154
154
|
const rowCounts = new Map<number, number>();
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
// Measures wrapped row counts for exactly the lines in [min, max] and bumps
|
|
176
176
|
// rowCountVersion if anything changed. Called directly wherever new line
|
|
177
177
|
// text lands in the cache (streamed append, on-demand backfill), each of
|
|
178
|
-
// which already knows the touched range from LogCache's merge result
|
|
178
|
+
// which already knows the touched range from LogCache's merge result, so
|
|
179
179
|
// a single appended line costs O(1) here instead of re-walking the whole
|
|
180
180
|
// (up to 50k-line) cache. Plain function, not an $effect: reads/writes
|
|
181
181
|
// rowCounts and cache.lines by key, never by iterating the map.
|
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
// or the available column width changes (wrap width affects every line's
|
|
202
202
|
// row count, so there's no way to scope that to a range). Line
|
|
203
203
|
// appends/backfills are measured incrementally at their call sites via
|
|
204
|
-
// measureRange instead
|
|
204
|
+
// measureRange instead, reading `cache.lines` in this effect's tracked
|
|
205
205
|
// scope would resubscribe it to the SvelteMap's shared version signal and
|
|
206
206
|
// re-run this full O(n) walk on every single appended line. `untrack`
|
|
207
207
|
// (same pattern as the fetch/prune effects below) keeps that read out of
|
|
@@ -372,7 +372,7 @@
|
|
|
372
372
|
|
|
373
373
|
// Width of the virtual scroll surface: wide enough for the longest line
|
|
374
374
|
// seen so far, never narrower than the viewport (so short logs show no
|
|
375
|
-
// horizontal scrollbar). Wrapping pins the surface to the viewport
|
|
375
|
+
// horizontal scrollbar). Wrapping pins the surface to the viewport, there
|
|
376
376
|
// is no horizontal axis to scroll.
|
|
377
377
|
let surfaceWidth = $derived(
|
|
378
378
|
wrap
|
|
@@ -429,14 +429,14 @@
|
|
|
429
429
|
}
|
|
430
430
|
});
|
|
431
431
|
|
|
432
|
-
// Prune stale cache entries
|
|
432
|
+
// Prune stale cache entries, separate from reads to avoid a read-write cycle on SvelteMap.
|
|
433
433
|
$effect(() => {
|
|
434
434
|
const vs = visibleStart;
|
|
435
435
|
const ve = visibleEnd;
|
|
436
436
|
untrack(() => cache.prune(vs, ve));
|
|
437
437
|
});
|
|
438
438
|
|
|
439
|
-
let prevTailRows = 0; // plain variable
|
|
439
|
+
let prevTailRows = 0; // plain variable, intentionally non-reactive
|
|
440
440
|
|
|
441
441
|
$effect(() => {
|
|
442
442
|
// Track committed lines AND live overlay rows so an animating region at
|
|
@@ -493,7 +493,7 @@
|
|
|
493
493
|
containerWidth = containerEl.clientWidth;
|
|
494
494
|
// Resync the reactive scroll position with the DOM. The browser
|
|
495
495
|
// clamps scrollTop when the viewport grows (e.g. maximizing the
|
|
496
|
-
// console), but no scroll event fires for that clamp
|
|
496
|
+
// console), but no scroll event fires for that clamp, so without
|
|
497
497
|
// this the virtualizer keeps a stale, too-large scrollTop and
|
|
498
498
|
// renders an empty window until the first manual scroll.
|
|
499
499
|
scrollTop = containerEl.scrollTop;
|
|
@@ -649,7 +649,7 @@
|
|
|
649
649
|
: 'text-[var(--rw-con-dim)] hover:text-aurora-400'}"
|
|
650
650
|
title="{line.frameCount} earlier frame{line.frameCount === 1
|
|
651
651
|
? ''
|
|
652
|
-
: 's'}
|
|
652
|
+
: 's'}, click to {expandedLine === line.num
|
|
653
653
|
? 'hide'
|
|
654
654
|
: 'rewind'}"
|
|
655
655
|
aria-label="Toggle frame history for line {line.num + 1}"
|
|
@@ -909,7 +909,7 @@
|
|
|
909
909
|
border-top-color: color-mix(in srgb, var(--rw-term-warn) 30%, transparent);
|
|
910
910
|
}
|
|
911
911
|
|
|
912
|
-
/* Subtle row hover on the fixed-dark console surface
|
|
912
|
+
/* Subtle row hover on the fixed-dark console surface, a faint lift of the
|
|
913
913
|
console text colour, never a theme-flipping fill. */
|
|
914
914
|
.log-line:hover {
|
|
915
915
|
background-color: color-mix(in srgb, var(--rw-con-text) 4%, transparent);
|
|
@@ -939,7 +939,7 @@
|
|
|
939
939
|
|
|
940
940
|
/* Blinking teal block caret shown while a run is actively streaming. One
|
|
941
941
|
monospace cell wide so it reads as a terminal cursor parked at the write
|
|
942
|
-
position
|
|
942
|
+
position, inline after a still-appending line, or alone on the next. */
|
|
943
943
|
.stream-cursor {
|
|
944
944
|
display: inline-block;
|
|
945
945
|
width: 1ch;
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
// External writes to `value` (Reset / clear-all) win immediately: adopt them
|
|
42
42
|
// and drop any pending debounce. Depends only on `value` (raw is read
|
|
43
43
|
// untracked), so typing never re-enters here. Tiny race: a reset that leaves
|
|
44
|
-
// `value` unchanged ("" -> "") can't cancel a first-keystroke debounce
|
|
44
|
+
// `value` unchanged ("" -> "") can't cancel a first-keystroke debounce,
|
|
45
45
|
// negligible at 250ms. ponytail: guarded two-way sync, fine for one field.
|
|
46
46
|
$effect(() => {
|
|
47
47
|
const v = value;
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
import { floating, isOutsideClick } from "../actions/floating.js";
|
|
22
22
|
import { portal } from "../actions/portal.js";
|
|
23
23
|
import { trapFocus } from "../actions/focusTrap.js";
|
|
24
|
-
import { generateUlid } from "@runwisp/common";
|
|
25
24
|
|
|
26
25
|
type SelectSize = "sm" | "md" | "lg";
|
|
27
26
|
|
|
@@ -41,6 +40,8 @@
|
|
|
41
40
|
onchange?: (value: unknown) => void;
|
|
42
41
|
}
|
|
43
42
|
|
|
43
|
+
const uid = $props.id();
|
|
44
|
+
|
|
44
45
|
let {
|
|
45
46
|
value = $bindable(),
|
|
46
47
|
options = [],
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
error,
|
|
53
54
|
hint,
|
|
54
55
|
class: className = "",
|
|
55
|
-
id = `select-${
|
|
56
|
+
id = `select-${uid}`,
|
|
56
57
|
name,
|
|
57
58
|
onchange,
|
|
58
59
|
onsearch,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// `currentColor` by default so the caller can theme us with a Tailwind
|
|
15
|
-
// text-* class
|
|
15
|
+
// text-* class, that's the only way to flip on .dark without burning a
|
|
16
16
|
// hardcoded hex in here.
|
|
17
17
|
let {
|
|
18
18
|
data,
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
return `${pathD} L${lastX.toFixed(1)},${height} L0,${height} Z`;
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
// Use `fill-opacity` rather than appending "18" to a hex
|
|
50
|
+
// Use `fill-opacity` rather than appending "18" to a hex, that trick
|
|
51
51
|
// breaks for `currentColor` / `oklch()` / `var(...)` values.
|
|
52
52
|
let resolvedFill = $derived(fillColor ?? color);
|
|
53
53
|
let resolvedFillOpacity = $derived(fillColor === undefined ? fillOpacity : 1);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<script lang="ts">
|
|
9
9
|
interface Props {
|
|
10
10
|
tone?: StatusDotTone;
|
|
11
|
-
/** Radar ping around the dot
|
|
11
|
+
/** Radar ping around the dot, use for "live" states only. */
|
|
12
12
|
pulse?: boolean;
|
|
13
13
|
size?: "sm" | "md" | "lg";
|
|
14
14
|
class?: string;
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
{@render children()}
|
|
46
46
|
|
|
47
47
|
<!-- Hover intent: 100ms before it appears, so sweeping the pointer across a
|
|
48
|
-
dense row doesn't flash a trail of bubbles. Still no fade
|
|
48
|
+
dense row doesn't flash a trail of bubbles. Still no fade, `duration-0`
|
|
49
49
|
keeps the appearance instant once the delay is served, per DESIGN.md. -->
|
|
50
50
|
<div
|
|
51
51
|
class="
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
formatTriggeredByLabel,
|
|
48
48
|
runRetryLabel,
|
|
49
49
|
runUsageLabel,
|
|
50
|
+
runLogDownloadUrl,
|
|
50
51
|
instanceSuffix,
|
|
51
52
|
} from "./run-helpers.js";
|
|
52
53
|
|
|
@@ -102,7 +103,7 @@
|
|
|
102
103
|
// Stop the selected run while it is live. When provided and the run is
|
|
103
104
|
// running, the action cluster shows Stop alongside Run.
|
|
104
105
|
onStop?: ((runId: string) => void) | undefined;
|
|
105
|
-
// Trigger the task from the empty state (no run selected yet)
|
|
106
|
+
// Trigger the task from the empty state (no run selected yet), the
|
|
106
107
|
// cold-start path so a never-run task is still launchable from here.
|
|
107
108
|
onRunTask?: (() => void) | undefined;
|
|
108
109
|
// Service lifecycle controls. When either is set the cluster swaps the
|
|
@@ -132,7 +133,7 @@
|
|
|
132
133
|
motion?: RunMotion;
|
|
133
134
|
// True when a deep-linked run id resolved to no run (deleted by retention,
|
|
134
135
|
// or never existed). The empty state then says so plainly instead of the
|
|
135
|
-
// generic "Select a run"
|
|
136
|
+
// generic "Select a run", the caller must not silently substitute another.
|
|
136
137
|
notFound?: boolean;
|
|
137
138
|
// True while there is no run to show *yet* (the run list or a deep-linked
|
|
138
139
|
// run is still loading), so the empty state doesn't claim "No runs yet".
|
|
@@ -194,8 +195,8 @@
|
|
|
194
195
|
// below) and paints it into the console. Resolves the line the live
|
|
195
196
|
// stream should resume from, or null when there's nothing left to stream
|
|
196
197
|
// (the run already ended, or the caller cancelled while we were waiting).
|
|
197
|
-
// A failed fetch still falls back to an SSE tail backfill from scratch
|
|
198
|
-
// it does not stop the console from live-tailing
|
|
198
|
+
// A failed fetch still falls back to an SSE tail backfill from scratch,
|
|
199
|
+
// it does not stop the console from live-tailing, but records the
|
|
199
200
|
// failure so LogConsole can show it instead of looking like a quiet run.
|
|
200
201
|
async function seedConsoleTail(
|
|
201
202
|
id: string,
|
|
@@ -260,7 +261,7 @@
|
|
|
260
261
|
});
|
|
261
262
|
|
|
262
263
|
// Maximize the console to a full-bleed overlay. The console wrapper is
|
|
263
|
-
// never remounted
|
|
264
|
+
// never remounted, `maximizePortal` relocates the *live* node to <body>
|
|
264
265
|
// (escaping any transformed/blurred ancestor that would otherwise trap a
|
|
265
266
|
// position:fixed child) and moves it back on restore, so the SSE stream,
|
|
266
267
|
// scroll position, and the {#key run.id} mount all survive the toggle.
|
|
@@ -282,7 +283,7 @@
|
|
|
282
283
|
|
|
283
284
|
// The accent for a status that means "something to triage", or undefined
|
|
284
285
|
// when there is nothing wrong. Tint is reserved for alarms (DESIGN.md) so a
|
|
285
|
-
// failure is the one lit thing on the page
|
|
286
|
+
// failure is the one lit thing on the page, a deliberate stop or a skip is
|
|
286
287
|
// not a failure, and success least of all. Doubles as the switch for the
|
|
287
288
|
// verdict phrase's colour, so wash and wording light up together.
|
|
288
289
|
function alarmAccent(displayed: string): string | undefined {
|
|
@@ -306,7 +307,7 @@
|
|
|
306
307
|
consoleMaximized = false;
|
|
307
308
|
return;
|
|
308
309
|
}
|
|
309
|
-
// `F` toggles focus mode, the way a media player does
|
|
310
|
+
// `F` toggles focus mode, the way a media player does, but never while
|
|
310
311
|
// the operator is typing into a field (search box, etc.).
|
|
311
312
|
if (
|
|
312
313
|
(event.key === "f" || event.key === "F") &&
|
|
@@ -411,7 +412,7 @@
|
|
|
411
412
|
<!-- Detailed header: a readout whose ink scales with how much went wrong.
|
|
412
413
|
Every run states its outcome in one phrase over one quiet fact line;
|
|
413
414
|
only a run worth triaging colours the phrase and tints the surface.
|
|
414
|
-
Nothing here is a fixed slot
|
|
415
|
+
Nothing here is a fixed slot, every fact renders only when it is
|
|
415
416
|
true, so the header's height is itself a signal. -->
|
|
416
417
|
<div
|
|
417
418
|
class="head-region @container relative shrink-0 border-b border-outline-faint"
|
|
@@ -461,9 +462,9 @@
|
|
|
461
462
|
|
|
462
463
|
<!-- Verdict: the outcome as one sentence, and the only large
|
|
463
464
|
type in the panel. It is prose, so the phrase is sans and
|
|
464
|
-
only the tokens inside it
|
|
465
|
+
only the tokens inside it, the duration, a failing exit,
|
|
465
466
|
are mono (DESIGN.md's mono-vs-sans rule). The glyph is bare
|
|
466
|
-
|
|
467
|
+
(no plate, no ring) so it reads as part of the sentence,
|
|
467
468
|
and it alone carries the outcome colour on a healthy run:
|
|
468
469
|
green words are ink spent on nothing being wrong. -->
|
|
469
470
|
<Tooltip content={config.description} position="right" wide>
|
|
@@ -717,9 +718,7 @@
|
|
|
717
718
|
{/if}
|
|
718
719
|
{/if}
|
|
719
720
|
<a
|
|
720
|
-
href=
|
|
721
|
-
run.taskName,
|
|
722
|
-
)}/runs/{encodeURIComponent(run.id)}/log/raw"
|
|
721
|
+
href={runLogDownloadUrl(run.id)}
|
|
723
722
|
download="{run.taskName}-{run.id}.log"
|
|
724
723
|
class="inline-flex items-center justify-center rounded-[3px] border border-outline-faint bg-surface-raised p-2 text-on-surface-muted hover:border-outline-hover hover:bg-surface-sunken hover:text-primary"
|
|
725
724
|
title="Download the full log (rotated and current parts as one file)"
|
|
@@ -773,7 +772,7 @@
|
|
|
773
772
|
</div>
|
|
774
773
|
</div>
|
|
775
774
|
|
|
776
|
-
<!-- Console hero
|
|
775
|
+
<!-- Console hero, maximizes to a full-bleed overlay via portal -->
|
|
777
776
|
{#if consoleMaximized}
|
|
778
777
|
<div
|
|
779
778
|
use:portal
|
|
@@ -902,7 +901,7 @@
|
|
|
902
901
|
<EmptyState
|
|
903
902
|
title={notFound ? "Run not found" : onRunTask ? "No runs yet" : "Select a run"}
|
|
904
903
|
description={notFound
|
|
905
|
-
? "This run doesn't exist
|
|
904
|
+
? "This run doesn't exist. It may have been deleted by retention, or the link is wrong. Pick a run from the list to continue."
|
|
906
905
|
: onRunTask
|
|
907
906
|
? "This task hasn't run yet. Trigger it to see its output here."
|
|
908
907
|
: "Pick a run from the list to view details and logs."}
|
|
@@ -923,7 +922,7 @@
|
|
|
923
922
|
|
|
924
923
|
<style>
|
|
925
924
|
/* Only a run worth triaging is washed in its outcome colour (--rw-oc, set
|
|
926
|
-
inline). Everything else
|
|
925
|
+
inline). Everything else, success included, passes the surface itself so
|
|
927
926
|
the mix collapses to a plain surface, which is what lets a genuine failure
|
|
928
927
|
be the one lit thing on the page. */
|
|
929
928
|
.head-region {
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
renders removable chips for each.
|
|
10
10
|
|
|
11
11
|
Status is a five-bucket pick (Running / Succeeded / Failed / Skipped /
|
|
12
|
-
Stopped) with an "Advanced" expander for the individual statuses
|
|
12
|
+
Stopped) with an "Advanced" expander for the individual statuses, buckets are
|
|
13
13
|
pure UI groupings over the same `statuses` array. Time is an independent
|
|
14
14
|
From/To date pair; exit code is a free-form expression (137, >100, >100 <150)
|
|
15
15
|
normalized to an inclusive range at the wire.
|
|
16
16
|
|
|
17
|
-
All controls are NATIVE (<select>, <input>, checkbox, button)
|
|
17
|
+
All controls are NATIVE (<select>, <input>, checkbox, button), a portalled
|
|
18
18
|
Select component must not nest inside the portalled Popover, since clicking an
|
|
19
19
|
option lands outside the popover's DOM subtree and triggers its outside-click
|
|
20
20
|
close. Every mutation reassigns the whole `filters` object (never an in-place
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// Reflect a tri-state bucket onto the native checkbox (no `indeterminate`
|
|
78
|
-
// HTML attribute exists
|
|
78
|
+
// HTML attribute exists, it must be set on the DOM node).
|
|
79
79
|
function indeterminate(node: HTMLInputElement, value: boolean) {
|
|
80
80
|
node.indeterminate = value;
|
|
81
81
|
return {
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
// --- Time -------------------------------------------------------------
|
|
96
96
|
//
|
|
97
97
|
// Independent From/To date bounds. A bare date is that day's 00:00 (From) or
|
|
98
|
-
// its end (To), so either edge applies on its own
|
|
98
|
+
// its end (To), so either edge applies on its own, From alone means
|
|
99
99
|
// "everything since", To alone "everything before", and the same date in
|
|
100
100
|
// both captures the whole day.
|
|
101
101
|
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
class="flex cursor-pointer items-center gap-1 font-mono text-2xs font-medium text-on-surface-muted select-none marker:content-none hover:text-on-surface [&::-webkit-details-marker]:hidden"
|
|
187
187
|
>
|
|
188
188
|
<ChevronRight size={12} class={advancedOpen ? "rotate-90" : ""} />
|
|
189
|
-
Advanced
|
|
189
|
+
Advanced: pick exact statuses
|
|
190
190
|
</summary>
|
|
191
191
|
<div class="mt-2 flex flex-col gap-1.5">
|
|
192
192
|
{#each STATUS_GROUPS as group (group.label)}
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
// null = no active search; the full list shows.
|
|
115
115
|
outputMatches?: Map<string, RunOutputMatch> | null;
|
|
116
116
|
// True while a query is typed but its results aren't in yet (debounce
|
|
117
|
-
// window or request in flight)
|
|
117
|
+
// window or request in flight), the rail shows its searching shimmer.
|
|
118
118
|
outputSearchPending?: boolean;
|
|
119
119
|
} = $props();
|
|
120
120
|
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
const OVERSCAN = 8;
|
|
125
125
|
const LOAD_AHEAD = 10;
|
|
126
126
|
|
|
127
|
-
// Selection model: two modes
|
|
127
|
+
// Selection model: two modes,
|
|
128
128
|
// 1. explicit: user picked specific rows. explicitIds holds them.
|
|
129
129
|
// 2. selectAll: "all matching the current filter". exceptIds holds opt-outs.
|
|
130
130
|
let selectAllMode = $state(false);
|
|
@@ -139,13 +139,13 @@
|
|
|
139
139
|
|
|
140
140
|
// Loaded runs that matched the output search, in list order. A match in a
|
|
141
141
|
// not-yet-loaded run can't be shown until the rail scrolls far enough to
|
|
142
|
-
// load it
|
|
142
|
+
// load it, the count reflects what's loaded, not the whole history.
|
|
143
143
|
const matchedRuns = $derived(
|
|
144
144
|
outputMatches ? items.filter((r: Run) => outputMatches.has(r.id)) : [],
|
|
145
145
|
);
|
|
146
146
|
|
|
147
147
|
// Split the matching line into [before, match, after] around the first
|
|
148
|
-
// occurrence of the query, windowed to keep the match in view
|
|
148
|
+
// occurrence of the query, windowed to keep the match in view, mirrors the
|
|
149
149
|
// artifact's snippet. Rendered as plain text spans (Svelte auto-escapes), so
|
|
150
150
|
// no untrusted HTML ever reaches the DOM.
|
|
151
151
|
interface HighlightParts {
|
|
@@ -600,8 +600,8 @@
|
|
|
600
600
|
</div>
|
|
601
601
|
</div>
|
|
602
602
|
|
|
603
|
-
<!-- Status dot. With bulk actions on it fades out
|
|
604
|
-
selection exists
|
|
603
|
+
<!-- Status dot. With bulk actions on it fades out, on row hover, or whenever a
|
|
604
|
+
selection exists, so the row checkbox can take its place over it. -->
|
|
605
605
|
<!-- Placeholders shaped like this list's run rows (same padding, dot, text
|
|
606
606
|
and right readout), while a search or the first page is in flight. -->
|
|
607
607
|
{#snippet skeletonRows(label: string)}
|
|
@@ -697,7 +697,7 @@
|
|
|
697
697
|
>
|
|
698
698
|
{#if showTaskName}
|
|
699
699
|
<!-- Cross-task /runs variant: the same readout language as the task
|
|
700
|
-
rail
|
|
700
|
+
rail, status dot, status-colored outcome, mono right readout,
|
|
701
701
|
with the task name carried as the primary. -->
|
|
702
702
|
<div class="flex items-center gap-2.5">
|
|
703
703
|
{@render statusDot(config.color, running)}
|
|
@@ -735,7 +735,7 @@
|
|
|
735
735
|
</span>
|
|
736
736
|
</div>
|
|
737
737
|
{:else}
|
|
738
|
-
<!-- Task-rail variant (artifact ".run"): a single dense line
|
|
738
|
+
<!-- Task-rail variant (artifact ".run"): a single dense line,
|
|
739
739
|
time · date · outcome, with a mono exit/duration readout.
|
|
740
740
|
leading-tight matches the artifact's ~1.2 line-height so the
|
|
741
741
|
(descender-less) text optically centers instead of riding high
|
|
@@ -52,7 +52,7 @@ describe("humanizeCron", () => {
|
|
|
52
52
|
expect(result.isHumanized).toBe(false);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
it("falls back to raw for unparseable expressions
|
|
55
|
+
it("falls back to raw for unparseable expressions, never 'Invalid'", () => {
|
|
56
56
|
const result = humanizeCron("not a cron");
|
|
57
57
|
expect(result.humanized).toBe("not a cron");
|
|
58
58
|
expect(result.isHumanized).toBe(false);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import cronstrue from "cronstrue";
|
|
4
4
|
|
|
5
5
|
interface HumanizedCron {
|
|
6
|
-
/** Display text
|
|
6
|
+
/** Display text, humanized when possible, otherwise the raw expression. */
|
|
7
7
|
humanized: string;
|
|
8
8
|
/** The original cron expression, for tooltips. */
|
|
9
9
|
raw: string;
|
|
@@ -51,7 +51,7 @@ function humanizeEvery(duration: string): string | null {
|
|
|
51
51
|
/**
|
|
52
52
|
* humanizeCron renders a cron expression as plain English ("Every 5 minutes").
|
|
53
53
|
* Handles robfig's `@every <duration>` extension (cronstrue throws on it) and
|
|
54
|
-
* falls back to the raw expression on anything unparseable
|
|
54
|
+
* falls back to the raw expression on anything unparseable, never "Invalid".
|
|
55
55
|
*/
|
|
56
56
|
export function humanizeCron(cron: string): HumanizedCron {
|
|
57
57
|
const raw = cron.trim();
|
|
@@ -42,7 +42,7 @@ const base = (overrides: Partial<RunsListFilters> = {}): RunsListFilters => ({
|
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
describe("emptyRunFilters", () => {
|
|
45
|
-
it("is a fully-open filter
|
|
45
|
+
it("is a fully-open filter, no dimension active, newest-first", () => {
|
|
46
46
|
const f = emptyRunFilters();
|
|
47
47
|
expect(f.statuses).toEqual([]);
|
|
48
48
|
expect(f.sortDirection).toBe("desc");
|
|
@@ -132,7 +132,7 @@ describe("statusChipLabel", () => {
|
|
|
132
132
|
describe("STATUS_BUCKETS", () => {
|
|
133
133
|
// The Failed bucket is the failure sentinel (resolved server-side to the
|
|
134
134
|
// per-task is_failure classification), so the execution-failure reasons are
|
|
135
|
-
// not bucketed
|
|
135
|
+
// not bucketed, they stay reachable via the popover's Advanced expander.
|
|
136
136
|
const FAILURE_REASONS = [
|
|
137
137
|
"failed",
|
|
138
138
|
"crashed",
|