@runwisp/ui 1.4.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 -5
- package/src/lib/actions/floating.ts +73 -0
- package/src/lib/actions/focusTrap.ts +3 -3
- package/src/lib/actions/portal.ts +5 -1
- package/src/lib/components/Accordion.svelte +6 -2
- package/src/lib/components/AccordionItem.svelte +80 -23
- package/src/lib/components/Avatar.svelte +0 -1
- package/src/lib/components/Badge.svelte +1 -1
- package/src/lib/components/CodeBlock.svelte +84 -35
- package/src/lib/components/CronInput.svelte +5 -2
- package/src/lib/components/DataGrid.svelte +100 -138
- package/src/lib/components/Drawer.svelte +2 -6
- package/src/lib/components/Dropdown.svelte +19 -57
- package/src/lib/components/EmptyState.svelte +2 -1
- package/src/lib/components/ErrorState.svelte +2 -1
- 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/Heading.svelte +19 -12
- package/src/lib/components/Kbd.svelte +25 -10
- package/src/lib/components/Link.svelte +7 -1
- package/src/lib/components/LogConsole.svelte +40 -42
- package/src/lib/components/Modal.svelte +44 -33
- package/src/lib/components/NotificationBell.svelte +2 -1
- package/src/lib/components/PageContainer.svelte +1 -10
- package/src/lib/components/PageHeader.svelte +8 -4
- package/src/lib/components/Popover.svelte +9 -51
- package/src/lib/components/Progress.svelte +19 -6
- package/src/lib/components/Radio.svelte +16 -4
- package/src/lib/components/RadioGroup.svelte +15 -2
- package/src/lib/components/SearchInput.svelte +1 -1
- package/src/lib/components/Select.svelte +16 -47
- package/src/lib/components/Sparkline.svelte +2 -2
- package/src/lib/components/StatusDot.svelte +1 -1
- package/src/lib/components/Table.svelte +13 -4
- package/src/lib/components/TableBody.svelte +2 -1
- package/src/lib/components/TableCell.svelte +38 -8
- package/src/lib/components/TableHead.svelte +12 -4
- package/src/lib/components/TableRow.svelte +23 -3
- package/src/lib/components/TaskCard.svelte +1 -1
- package/src/lib/components/TimezoneSelect.svelte +2 -2
- package/src/lib/components/Tooltip.svelte +1 -1
- package/src/lib/components/accordion.svelte.ts +19 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +106 -79
- package/src/lib/components/dashboard/RunFilterPopover.svelte +12 -21
- package/src/lib/components/dashboard/RunsList.svelte +17 -26
- package/src/lib/components/dashboard/cron-format.test.ts +1 -1
- package/src/lib/components/dashboard/cron-format.ts +3 -3
- package/src/lib/components/dashboard/run-filters.test.ts +4 -60
- package/src/lib/components/dashboard/run-filters.ts +29 -144
- package/src/lib/components/dashboard/run-helpers.test.ts +25 -6
- package/src/lib/components/dashboard/run-helpers.ts +40 -39
- package/src/lib/components/dashboard/status-config.ts +38 -83
- package/src/lib/components/dashboard/types.ts +0 -26
- package/src/lib/components/data-grid-selection.ts +2 -9
- package/src/lib/components/filter-spec.ts +3 -3
- package/src/lib/components/radio-group.ts +11 -0
- package/src/lib/index.ts +8 -43
- package/src/lib/log-console/LogCache.svelte.ts +1 -1
- package/src/lib/log-console/LogFetcher.svelte.ts +3 -14
- package/src/lib/log-console/ansi.ts +1 -1
- package/src/lib/log-console/highlight-scroll.test.ts +59 -0
- package/src/lib/log-console/highlight-scroll.ts +41 -0
- package/src/lib/theme-tokens.css +21 -21
- package/src/lib/theme.css +22 -16
- package/src/lib/utils/clipboard.svelte.test.ts +92 -0
- package/src/lib/utils/clipboard.svelte.ts +73 -0
- package/src/lib/utils/format.test.ts +3 -3
- package/src/lib/utils/format.ts +65 -94
- package/src/lib/utils/id.test.ts +3 -7
- package/src/lib/utils/id.ts +3 -2
- package/src/lib/utils/theme.svelte.test.ts +0 -6
- package/src/lib/utils/theme.svelte.ts +3 -5
- package/src/lib/utils/toast.svelte.ts +5 -14
- package/src/stories/Accordion.stories.svelte +61 -0
- package/src/stories/CodeBlock.stories.svelte +55 -0
- package/src/stories/DataGrid.stories.svelte +0 -5
- package/src/stories/Heading.stories.svelte +31 -0
- package/src/stories/Kbd.stories.svelte +36 -0
- package/src/stories/Link.stories.svelte +38 -0
- package/src/stories/LogConsole.stories.svelte +2 -2
- package/src/stories/PageHeader.stories.svelte +31 -0
- package/src/stories/Progress.stories.svelte +26 -0
- package/src/stories/RadioGroup.stories.svelte +43 -0
- package/src/stories/Table.stories.svelte +69 -0
- package/src/svelte-shims.d.ts +0 -5
- package/src/lib/components/dashboard/status-config.test.ts +0 -18
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
RotateCw,
|
|
16
16
|
RefreshCcw,
|
|
17
17
|
ChevronDown,
|
|
18
|
-
|
|
18
|
+
ArrowLeft,
|
|
19
19
|
PanelLeftClose,
|
|
20
|
+
PanelLeftOpen,
|
|
20
21
|
SlidersHorizontal,
|
|
21
22
|
Maximize2,
|
|
22
23
|
Minimize2,
|
|
@@ -25,24 +26,28 @@
|
|
|
25
26
|
} from "@lucide/svelte";
|
|
26
27
|
import Button from "../Button.svelte";
|
|
27
28
|
import EmptyState from "../EmptyState.svelte";
|
|
29
|
+
import Kbd from "../Kbd.svelte";
|
|
28
30
|
import LogConsole from "../LogConsole.svelte";
|
|
29
31
|
import Popover from "../Popover.svelte";
|
|
30
32
|
import Tooltip from "../Tooltip.svelte";
|
|
31
33
|
import { portal } from "../../actions/portal.js";
|
|
32
34
|
import { prefersReducedMotion } from "../../actions/row-motion.js";
|
|
33
35
|
import type { RunMotion } from "../../utils/run-motion.js";
|
|
34
|
-
import type { Run } from "./types.js";
|
|
35
36
|
import { isLogEvent, type LogEvent, type LogSlice } from "../../log-console/types.js";
|
|
36
|
-
import { formatClockTime, formatCalendarDate } from "../../utils/format.js";
|
|
37
|
+
import { formatBytes, formatClockTime, formatCalendarDate } from "../../utils/format.js";
|
|
37
38
|
import { formatShortId } from "../../utils/id.js";
|
|
38
39
|
import { TickingNow } from "../../utils/ticking-now.svelte.js";
|
|
39
|
-
import {
|
|
40
|
+
import { CopyFeedback } from "../../utils/clipboard.svelte.js";
|
|
41
|
+
import { displayStatus, type ResourceUsage, type Run } from "@runwisp/common";
|
|
42
|
+
import { RUN_STATUS_CONFIG } from "./status-config.js";
|
|
40
43
|
import {
|
|
41
44
|
runDuration,
|
|
42
45
|
runStartDelay,
|
|
43
46
|
runVerdict,
|
|
44
47
|
formatTriggeredByLabel,
|
|
45
48
|
runRetryLabel,
|
|
49
|
+
runUsageLabel,
|
|
50
|
+
runLogDownloadUrl,
|
|
46
51
|
instanceSuffix,
|
|
47
52
|
} from "./run-helpers.js";
|
|
48
53
|
|
|
@@ -61,10 +66,12 @@
|
|
|
61
66
|
onRestartService,
|
|
62
67
|
serviceStopped = false,
|
|
63
68
|
serviceBusy = false,
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
onBack,
|
|
70
|
+
onToggleList,
|
|
71
|
+
listVisible = true,
|
|
66
72
|
highlightLine = null,
|
|
67
73
|
getInstanceCount = () => 1,
|
|
74
|
+
getLiveUsage = () => undefined,
|
|
68
75
|
motion,
|
|
69
76
|
notFound = false,
|
|
70
77
|
loading = false,
|
|
@@ -96,23 +103,29 @@
|
|
|
96
103
|
// Stop the selected run while it is live. When provided and the run is
|
|
97
104
|
// running, the action cluster shows Stop alongside Run.
|
|
98
105
|
onStop?: ((runId: string) => void) | undefined;
|
|
99
|
-
// Trigger the task from the empty state (no run selected yet)
|
|
106
|
+
// Trigger the task from the empty state (no run selected yet), the
|
|
100
107
|
// cold-start path so a never-run task is still launchable from here.
|
|
101
108
|
onRunTask?: (() => void) | undefined;
|
|
102
109
|
// Service lifecycle controls. When either is set the cluster swaps the
|
|
103
|
-
// Run control for Stop
|
|
110
|
+
// Run control for Stop / Restart (chosen by serviceStopped).
|
|
104
111
|
onStopService?: (() => void) | undefined;
|
|
105
112
|
onRestartService?: (() => void) | undefined;
|
|
106
113
|
serviceStopped?: boolean;
|
|
107
114
|
serviceBusy?: boolean;
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
// Return to the run list when the panel replaces it (a phone), shown as
|
|
116
|
+
// a back arrow in the header's top-left corner.
|
|
117
|
+
onBack?: (() => void) | undefined;
|
|
118
|
+
// Fold the run list away (or back) when it sits beside the panel on a
|
|
119
|
+
// screen too narrow to keep both comfortably, in the same corner.
|
|
120
|
+
onToggleList?: (() => void) | undefined;
|
|
121
|
+
listVisible?: boolean;
|
|
112
122
|
highlightLine?: number | null;
|
|
113
123
|
// Resolves a task's currently configured instance count so multi-instance
|
|
114
124
|
// services render a 1-based #N suffix. Defaults to single-instance.
|
|
115
125
|
getInstanceCount?: (taskName: string) => number;
|
|
126
|
+
// Live CPU and memory of a running run, by run ID; undefined when it
|
|
127
|
+
// isn't measured. Shown in the header while the run is running.
|
|
128
|
+
getLiveUsage?: (runId: string) => ResourceUsage | undefined;
|
|
116
129
|
// Which runs arrived or were removed live moments ago. The panel eases in
|
|
117
130
|
// when a new run takes it (triggered, or a scheduled run auto-selected)
|
|
118
131
|
// or when its run was deleted and another takes its place; picking a
|
|
@@ -120,7 +133,7 @@
|
|
|
120
133
|
motion?: RunMotion;
|
|
121
134
|
// True when a deep-linked run id resolved to no run (deleted by retention,
|
|
122
135
|
// or never existed). The empty state then says so plainly instead of the
|
|
123
|
-
// generic "Select a run"
|
|
136
|
+
// generic "Select a run", the caller must not silently substitute another.
|
|
124
137
|
notFound?: boolean;
|
|
125
138
|
// True while there is no run to show *yet* (the run list or a deep-linked
|
|
126
139
|
// run is still loading), so the empty state doesn't claim "No runs yet".
|
|
@@ -148,7 +161,7 @@
|
|
|
148
161
|
|
|
149
162
|
let canDelete = $derived.by(() => {
|
|
150
163
|
if (!run || !onDelete) return false;
|
|
151
|
-
const status =
|
|
164
|
+
const status = displayStatus(run.status, run.endReason);
|
|
152
165
|
return status !== "running" && status !== "pending";
|
|
153
166
|
});
|
|
154
167
|
|
|
@@ -182,8 +195,8 @@
|
|
|
182
195
|
// below) and paints it into the console. Resolves the line the live
|
|
183
196
|
// stream should resume from, or null when there's nothing left to stream
|
|
184
197
|
// (the run already ended, or the caller cancelled while we were waiting).
|
|
185
|
-
// A failed fetch still falls back to an SSE tail backfill from scratch
|
|
186
|
-
// 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
|
|
187
200
|
// failure so LogConsole can show it instead of looking like a quiet run.
|
|
188
201
|
async function seedConsoleTail(
|
|
189
202
|
id: string,
|
|
@@ -248,7 +261,7 @@
|
|
|
248
261
|
});
|
|
249
262
|
|
|
250
263
|
// Maximize the console to a full-bleed overlay. The console wrapper is
|
|
251
|
-
// never remounted
|
|
264
|
+
// never remounted, `maximizePortal` relocates the *live* node to <body>
|
|
252
265
|
// (escaping any transformed/blurred ancestor that would otherwise trap a
|
|
253
266
|
// position:fixed child) and moves it back on restore, so the SSE stream,
|
|
254
267
|
// scroll position, and the {#key run.id} mount all survive the toggle.
|
|
@@ -266,23 +279,11 @@
|
|
|
266
279
|
let confirmDeleteOpen = $state(false);
|
|
267
280
|
// Dropdown half of the Run split button (the "reuse parameters" variant).
|
|
268
281
|
let runMenuOpen = $state(false);
|
|
269
|
-
|
|
270
|
-
let copyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
271
|
-
|
|
272
|
-
async function copyRunId(id: string) {
|
|
273
|
-
try {
|
|
274
|
-
await navigator.clipboard.writeText(id);
|
|
275
|
-
copiedId = true;
|
|
276
|
-
if (copyTimer) clearTimeout(copyTimer);
|
|
277
|
-
copyTimer = setTimeout(() => (copiedId = false), 1200);
|
|
278
|
-
} catch {
|
|
279
|
-
// Clipboard blocked (insecure context / denied) — leave the chip as-is.
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
+
const runIdCopy = new CopyFeedback(1200);
|
|
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") &&
|
|
@@ -362,8 +363,8 @@
|
|
|
362
363
|
<svelte:window onkeydown={handleConsoleKeydown} />
|
|
363
364
|
|
|
364
365
|
{#if run}
|
|
365
|
-
{@const status =
|
|
366
|
-
{@const config =
|
|
366
|
+
{@const status = displayStatus(run.status, run.endReason)}
|
|
367
|
+
{@const config = RUN_STATUS_CONFIG[status]}
|
|
367
368
|
{@const DetailIcon = config.icon}
|
|
368
369
|
{@const duration = runDuration(
|
|
369
370
|
run,
|
|
@@ -372,6 +373,8 @@
|
|
|
372
373
|
{@const startDelay = runStartDelay(run)}
|
|
373
374
|
{@const startedAt = run.startedAt ?? run.createdAt}
|
|
374
375
|
{@const retry = runRetryLabel(run)}
|
|
376
|
+
{@const usage = runUsageLabel(run)}
|
|
377
|
+
{@const live = run.status === "running" ? getLiveUsage(run.id) : undefined}
|
|
375
378
|
{@const paramEntries = run.params ? Object.entries(run.params) : []}
|
|
376
379
|
{@const suffix = instanceSuffix(run.instanceIndex, getInstanceCount(run.taskName))}
|
|
377
380
|
{@const spine = config.dot.replace(" animate-pulse", "")}
|
|
@@ -409,15 +412,43 @@
|
|
|
409
412
|
<!-- Detailed header: a readout whose ink scales with how much went wrong.
|
|
410
413
|
Every run states its outcome in one phrase over one quiet fact line;
|
|
411
414
|
only a run worth triaging colours the phrase and tints the surface.
|
|
412
|
-
Nothing here is a fixed slot
|
|
415
|
+
Nothing here is a fixed slot, every fact renders only when it is
|
|
413
416
|
true, so the header's height is itself a signal. -->
|
|
414
417
|
<div
|
|
415
418
|
class="head-region @container relative shrink-0 border-b border-outline-faint"
|
|
416
419
|
style="--rw-oc: {alarm ?? 'var(--color-surface-raised)'}"
|
|
417
420
|
>
|
|
418
421
|
<div class="pt-[18px] pr-[22px] pb-[16px] pl-[26px]">
|
|
419
|
-
|
|
420
|
-
|
|
422
|
+
<!-- The actions wrap onto their own row rather than squeeze the
|
|
423
|
+
readout below a legible width on a narrow panel. -->
|
|
424
|
+
<div class="flex flex-wrap items-start gap-x-3 gap-y-3">
|
|
425
|
+
{#if onBack}
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
428
|
+
onclick={() => onBack()}
|
|
429
|
+
class="-mt-1 -ml-2 shrink-0 rounded-[3px] p-1.5 text-on-surface-muted hover:bg-surface-sunken hover:text-primary"
|
|
430
|
+
title="Back to runs"
|
|
431
|
+
aria-label="Back to runs"
|
|
432
|
+
>
|
|
433
|
+
<ArrowLeft size={20} />
|
|
434
|
+
</button>
|
|
435
|
+
{:else if onToggleList}
|
|
436
|
+
<button
|
|
437
|
+
type="button"
|
|
438
|
+
onclick={() => onToggleList()}
|
|
439
|
+
class="-mt-1 -ml-2 shrink-0 rounded-[3px] p-1.5 text-on-surface-muted hover:bg-surface-sunken hover:text-primary"
|
|
440
|
+
title={listVisible ? "Hide run list" : "Show run list"}
|
|
441
|
+
aria-label={listVisible ? "Hide run list" : "Show run list"}
|
|
442
|
+
aria-expanded={listVisible}
|
|
443
|
+
>
|
|
444
|
+
{#if listVisible}
|
|
445
|
+
<PanelLeftClose size={20} />
|
|
446
|
+
{:else}
|
|
447
|
+
<PanelLeftOpen size={20} />
|
|
448
|
+
{/if}
|
|
449
|
+
</button>
|
|
450
|
+
{/if}
|
|
451
|
+
<div class="min-w-60 flex-1">
|
|
421
452
|
<!-- Identity, but only where the page around the panel isn't
|
|
422
453
|
already saying it: the cross-task runs list needs the task
|
|
423
454
|
name, a task's own page already has it in the breadcrumb. -->
|
|
@@ -431,9 +462,9 @@
|
|
|
431
462
|
|
|
432
463
|
<!-- Verdict: the outcome as one sentence, and the only large
|
|
433
464
|
type in the panel. It is prose, so the phrase is sans and
|
|
434
|
-
only the tokens inside it
|
|
465
|
+
only the tokens inside it, the duration, a failing exit,
|
|
435
466
|
are mono (DESIGN.md's mono-vs-sans rule). The glyph is bare
|
|
436
|
-
|
|
467
|
+
(no plate, no ring) so it reads as part of the sentence,
|
|
437
468
|
and it alone carries the outcome colour on a healthy run:
|
|
438
469
|
green words are ink spent on nothing being wrong. -->
|
|
439
470
|
<Tooltip content={config.description} position="right" wide>
|
|
@@ -544,16 +575,35 @@
|
|
|
544
575
|
</div>
|
|
545
576
|
</Popover>
|
|
546
577
|
{/if}
|
|
578
|
+
{#if live}
|
|
579
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
580
|
+
<span
|
|
581
|
+
data-testid="run-live-usage"
|
|
582
|
+
class="text-on-surface-muted"
|
|
583
|
+
title="Live CPU (100% = one core) and memory of the run's processes"
|
|
584
|
+
>CPU {Math.round(live.cpuPercent)}% · {formatBytes(
|
|
585
|
+
live.memoryBytes,
|
|
586
|
+
)}</span
|
|
587
|
+
>
|
|
588
|
+
{/if}
|
|
589
|
+
{#if usage}
|
|
590
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
591
|
+
<span
|
|
592
|
+
data-testid="run-usage"
|
|
593
|
+
title="Peak memory and CPU time of the run's processes"
|
|
594
|
+
>{usage}</span
|
|
595
|
+
>
|
|
596
|
+
{/if}
|
|
547
597
|
<!-- Run-id chip: click to copy the full ULID. Last, because
|
|
548
598
|
it is the fact you reach for least and the only one you
|
|
549
599
|
interact with. -->
|
|
550
600
|
<button
|
|
551
601
|
type="button"
|
|
552
|
-
onclick={() =>
|
|
602
|
+
onclick={() => void runIdCopy.copy(run.id)}
|
|
553
603
|
title="Copy run ID"
|
|
554
604
|
class="inline-flex items-center gap-1 rounded-[3px] border border-outline-faint bg-surface-sunken px-1.5 text-on-surface-faint hover:border-outline-hover hover:text-primary"
|
|
555
605
|
>
|
|
556
|
-
{#if
|
|
606
|
+
{#if runIdCopy.copied}
|
|
557
607
|
<Check size={11} class="text-success-surface" />Copied
|
|
558
608
|
{:else}
|
|
559
609
|
<Hash size={11} />{run.id}
|
|
@@ -562,27 +612,9 @@
|
|
|
562
612
|
</div>
|
|
563
613
|
</div>
|
|
564
614
|
|
|
565
|
-
<!-- Actions:
|
|
615
|
+
<!-- Actions: run (split) / service lifecycle ·
|
|
566
616
|
stop · download · delete (delete behind an inline confirm) -->
|
|
567
|
-
<div class="flex
|
|
568
|
-
{#if onToggleHistory}
|
|
569
|
-
<button
|
|
570
|
-
type="button"
|
|
571
|
-
onclick={() => onToggleHistory()}
|
|
572
|
-
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"
|
|
573
|
-
title={historyVisible ? "Hide run history" : "Show run history"}
|
|
574
|
-
aria-label={historyVisible
|
|
575
|
-
? "Hide run history"
|
|
576
|
-
: "Show run history"}
|
|
577
|
-
>
|
|
578
|
-
{#if historyVisible}
|
|
579
|
-
<PanelLeftClose size={15} />
|
|
580
|
-
{:else}
|
|
581
|
-
<RotateCcwClock size={15} />
|
|
582
|
-
{/if}
|
|
583
|
-
</button>
|
|
584
|
-
{/if}
|
|
585
|
-
|
|
617
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
586
618
|
{#if onStopService || onRestartService}
|
|
587
619
|
<!-- Service lifecycle replaces the run control.
|
|
588
620
|
Restart is always available (it cancels + respawns
|
|
@@ -604,7 +636,7 @@
|
|
|
604
636
|
{:else}
|
|
605
637
|
<RefreshCcw size={15} />
|
|
606
638
|
{/if}
|
|
607
|
-
<span class="@max-
|
|
639
|
+
<span class="@max-xs:hidden"
|
|
608
640
|
>{serviceStopped ? "Start" : "Restart"}</span
|
|
609
641
|
>
|
|
610
642
|
</button>
|
|
@@ -618,7 +650,7 @@
|
|
|
618
650
|
class="inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-[3px] border border-danger-soft-border bg-surface-raised px-3 font-mono text-sm font-medium text-danger-surface hover:bg-danger-soft active:translate-y-px disabled:cursor-not-allowed disabled:opacity-60"
|
|
619
651
|
>
|
|
620
652
|
<Square size={15} fill="currentColor" stroke="none" />
|
|
621
|
-
<span class="@max-
|
|
653
|
+
<span class="@max-xs:hidden">Stop</span>
|
|
622
654
|
</button>
|
|
623
655
|
{/if}
|
|
624
656
|
{:else}
|
|
@@ -635,7 +667,7 @@
|
|
|
635
667
|
class="inline-flex h-9 cursor-pointer items-center gap-1.5 bg-surface-raised px-3 font-mono text-sm font-medium text-primary hover:bg-primary-soft active:translate-y-px"
|
|
636
668
|
>
|
|
637
669
|
<Play size={15} fill="currentColor" stroke="none" />
|
|
638
|
-
<span class="@max-
|
|
670
|
+
<span class="@max-xs:hidden">Run</span>
|
|
639
671
|
</button>
|
|
640
672
|
{#if onRunAgain}
|
|
641
673
|
<Popover bind:open={runMenuOpen} placement="bottom-end">
|
|
@@ -681,14 +713,12 @@
|
|
|
681
713
|
class="inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-[3px] border border-danger-soft-border bg-surface-raised px-3 font-mono text-sm font-medium text-danger-surface hover:bg-danger-soft active:translate-y-px"
|
|
682
714
|
>
|
|
683
715
|
<Square size={15} fill="currentColor" stroke="none" />
|
|
684
|
-
<span class="@max-
|
|
716
|
+
<span class="@max-xs:hidden">Stop</span>
|
|
685
717
|
</button>
|
|
686
718
|
{/if}
|
|
687
719
|
{/if}
|
|
688
720
|
<a
|
|
689
|
-
href=
|
|
690
|
-
run.taskName,
|
|
691
|
-
)}/runs/{encodeURIComponent(run.id)}/log/raw"
|
|
721
|
+
href={runLogDownloadUrl(run.id)}
|
|
692
722
|
download="{run.taskName}-{run.id}.log"
|
|
693
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"
|
|
694
724
|
title="Download the full log (rotated and current parts as one file)"
|
|
@@ -742,7 +772,7 @@
|
|
|
742
772
|
</div>
|
|
743
773
|
</div>
|
|
744
774
|
|
|
745
|
-
<!-- Console hero
|
|
775
|
+
<!-- Console hero, maximizes to a full-bleed overlay via portal -->
|
|
746
776
|
{#if consoleMaximized}
|
|
747
777
|
<div
|
|
748
778
|
use:portal
|
|
@@ -758,7 +788,7 @@
|
|
|
758
788
|
: "ml-1 flex min-h-[300px] flex-1 flex-col overflow-hidden border-t border-[var(--rw-con-gutter)] bg-[var(--rw-con-bg)]"}
|
|
759
789
|
>
|
|
760
790
|
<div
|
|
761
|
-
class="flex shrink-0 items-center justify-between gap-3 border-b border-[var(--rw-con-gutter)] bg-[var(--rw-con-panel)] px-3.5 py-[9px] font-mono text-[11.5px] text-[var(--rw-con-dim)]"
|
|
791
|
+
class="@container flex shrink-0 items-center justify-between gap-3 border-b border-[var(--rw-con-gutter)] bg-[var(--rw-con-panel)] px-3.5 py-[9px] font-mono text-[11.5px] text-[var(--rw-con-dim)]"
|
|
762
792
|
>
|
|
763
793
|
<div class="flex min-w-0 items-center gap-3">
|
|
764
794
|
{#if consoleMaximized}
|
|
@@ -773,12 +803,12 @@
|
|
|
773
803
|
</span>
|
|
774
804
|
{:else}
|
|
775
805
|
<span
|
|
776
|
-
class="flex items-center gap-2 font-semibold text-[var(--rw-con-text)]"
|
|
806
|
+
class="flex items-center gap-2 font-semibold whitespace-nowrap text-[var(--rw-con-text)]"
|
|
777
807
|
>
|
|
778
808
|
<TerminalIcon size={14} class="opacity-70" />
|
|
779
809
|
Console output
|
|
780
810
|
</span>
|
|
781
|
-
<span class="hidden text-[var(--rw-con-gutter)]
|
|
811
|
+
<span class="hidden text-[var(--rw-con-gutter)] @lg:inline"
|
|
782
812
|
>stdout + stderr</span
|
|
783
813
|
>
|
|
784
814
|
{/if}
|
|
@@ -795,7 +825,7 @@
|
|
|
795
825
|
aria-label="Toggle line wrapping"
|
|
796
826
|
>
|
|
797
827
|
<TextWrap size={13} />
|
|
798
|
-
<span class="hidden sm:inline">Wrap</span>
|
|
828
|
+
<span class="hidden @sm:inline">Wrap</span>
|
|
799
829
|
</button>
|
|
800
830
|
<button
|
|
801
831
|
type="button"
|
|
@@ -806,10 +836,7 @@
|
|
|
806
836
|
>
|
|
807
837
|
{#if consoleMaximized}
|
|
808
838
|
<Minimize2 size={13} />
|
|
809
|
-
<
|
|
810
|
-
class="rounded-[3px] border border-[var(--rw-con-gutter)] px-1.5 text-[10px] tracking-wide"
|
|
811
|
-
>Esc</span
|
|
812
|
-
>
|
|
839
|
+
<Kbd keys="Esc" size="xs" tone="console" />
|
|
813
840
|
{:else}
|
|
814
841
|
<Maximize2 size={13} />
|
|
815
842
|
Expand
|
|
@@ -874,7 +901,7 @@
|
|
|
874
901
|
<EmptyState
|
|
875
902
|
title={notFound ? "Run not found" : onRunTask ? "No runs yet" : "Select a run"}
|
|
876
903
|
description={notFound
|
|
877
|
-
? "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."
|
|
878
905
|
: onRunTask
|
|
879
906
|
? "This task hasn't run yet. Trigger it to see its output here."
|
|
880
907
|
: "Pick a run from the list to view details and logs."}
|
|
@@ -895,7 +922,7 @@
|
|
|
895
922
|
|
|
896
923
|
<style>
|
|
897
924
|
/* Only a run worth triaging is washed in its outcome colour (--rw-oc, set
|
|
898
|
-
inline). Everything else
|
|
925
|
+
inline). Everything else, success included, passes the surface itself so
|
|
899
926
|
the mix collapses to a plain surface, which is what lets a genuine failure
|
|
900
927
|
be the one lit thing on the page. */
|
|
901
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
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
dayStartIso,
|
|
41
41
|
dayEndIso,
|
|
42
42
|
isExitCodeExprValid,
|
|
43
|
-
exitCodePropChanged,
|
|
44
43
|
type RunsListFilters,
|
|
45
44
|
} from "./run-filters.js";
|
|
46
45
|
|
|
@@ -48,13 +47,10 @@
|
|
|
48
47
|
filters = $bindable(),
|
|
49
48
|
showTask = false,
|
|
50
49
|
tasks = [],
|
|
51
|
-
compact = false,
|
|
52
50
|
}: {
|
|
53
51
|
filters: RunsListFilters;
|
|
54
52
|
showTask?: boolean;
|
|
55
53
|
tasks?: { name: string }[];
|
|
56
|
-
// Icon-only trigger for narrow rails where a "Filter" label wouldn't fit.
|
|
57
|
-
compact?: boolean;
|
|
58
54
|
} = $props();
|
|
59
55
|
|
|
60
56
|
// The individual statuses behind the "Advanced" expander, grouped the way
|
|
@@ -79,7 +75,7 @@
|
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
// Reflect a tri-state bucket onto the native checkbox (no `indeterminate`
|
|
82
|
-
// HTML attribute exists
|
|
78
|
+
// HTML attribute exists, it must be set on the DOM node).
|
|
83
79
|
function indeterminate(node: HTMLInputElement, value: boolean) {
|
|
84
80
|
node.indeterminate = value;
|
|
85
81
|
return {
|
|
@@ -99,7 +95,7 @@
|
|
|
99
95
|
// --- Time -------------------------------------------------------------
|
|
100
96
|
//
|
|
101
97
|
// Independent From/To date bounds. A bare date is that day's 00:00 (From) or
|
|
102
|
-
// its end (To), so either edge applies on its own
|
|
98
|
+
// its end (To), so either edge applies on its own, From alone means
|
|
103
99
|
// "everything since", To alone "everything before", and the same date in
|
|
104
100
|
// both captures the whole day.
|
|
105
101
|
|
|
@@ -121,18 +117,14 @@
|
|
|
121
117
|
filters = { ...filters, triggeredBy: value || undefined };
|
|
122
118
|
}
|
|
123
119
|
|
|
124
|
-
// Exit
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// buffer does NOT re-seed on its own — it must explicitly resync whenever
|
|
129
|
-
// `filters.exitCode` changes from outside (a chip removal, "clear
|
|
130
|
-
// filters"), without clobbering in-progress typing when some unrelated
|
|
131
|
-
// filter dimension changes instead.
|
|
120
|
+
// Exit-code text is edited in a local buffer and committed on change. The
|
|
121
|
+
// component outlives the popover, so the buffer resyncs only when the
|
|
122
|
+
// committed `exitCode` itself moves (chip removed, filters cleared); an
|
|
123
|
+
// update to another dimension must not clobber in-progress typing.
|
|
132
124
|
let exitCodeInput = $state(filters.exitCode ?? "");
|
|
133
125
|
let lastExitCode = filters.exitCode;
|
|
134
126
|
$effect(() => {
|
|
135
|
-
if (
|
|
127
|
+
if (lastExitCode !== filters.exitCode) {
|
|
136
128
|
exitCodeInput = filters.exitCode ?? "";
|
|
137
129
|
}
|
|
138
130
|
lastExitCode = filters.exitCode;
|
|
@@ -152,8 +144,8 @@
|
|
|
152
144
|
<Popover placement="right-start" mobileSheet>
|
|
153
145
|
{#snippet trigger()}
|
|
154
146
|
<span
|
|
155
|
-
class="inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-[3px] border font-mono text-xs font-medium {
|
|
156
|
-
|
|
147
|
+
class="inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-[3px] border font-mono text-xs font-medium {count ===
|
|
148
|
+
0
|
|
157
149
|
? 'w-7 justify-center px-0'
|
|
158
150
|
: 'px-2'} {count > 0
|
|
159
151
|
? 'border-primary-soft-border bg-primary-soft text-primary-soft-text'
|
|
@@ -161,7 +153,6 @@
|
|
|
161
153
|
title="Filter runs"
|
|
162
154
|
>
|
|
163
155
|
<Funnel size={13} />
|
|
164
|
-
{#if !compact}Filter{/if}
|
|
165
156
|
{#if count > 0}
|
|
166
157
|
<Badge variant="primary" size="sm" class="px-1.5 py-0">{count}</Badge>
|
|
167
158
|
{/if}
|
|
@@ -195,7 +186,7 @@
|
|
|
195
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"
|
|
196
187
|
>
|
|
197
188
|
<ChevronRight size={12} class={advancedOpen ? "rotate-90" : ""} />
|
|
198
|
-
Advanced
|
|
189
|
+
Advanced: pick exact statuses
|
|
199
190
|
</summary>
|
|
200
191
|
<div class="mt-2 flex flex-col gap-1.5">
|
|
201
192
|
{#each STATUS_GROUPS as group (group.label)}
|