@runwisp/ui 1.4.0 → 1.5.0
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/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/CodeBlock.svelte +84 -35
- package/src/lib/components/CronInput.svelte +5 -2
- package/src/lib/components/DataGrid.svelte +95 -133
- 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/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 +30 -32
- 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/Select.svelte +13 -45
- 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/accordion.svelte.ts +19 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +91 -63
- package/src/lib/components/dashboard/RunFilterPopover.svelte +7 -16
- package/src/lib/components/dashboard/RunsList.svelte +9 -18
- package/src/lib/components/dashboard/cron-format.ts +1 -1
- package/src/lib/components/dashboard/run-filters.test.ts +2 -58
- package/src/lib/components/dashboard/run-filters.ts +5 -53
- package/src/lib/components/dashboard/run-helpers.test.ts +17 -7
- package/src/lib/components/dashboard/run-helpers.ts +12 -1
- package/src/lib/components/dashboard/status-config.ts +38 -83
- package/src/lib/components/dashboard/types.ts +0 -26
- package/src/lib/components/radio-group.ts +11 -0
- package/src/lib/index.ts +8 -43
- package/src/lib/log-console/LogFetcher.svelte.ts +3 -14
- 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.css +10 -4
- package/src/lib/utils/clipboard.svelte.test.ts +92 -0
- package/src/lib/utils/clipboard.svelte.ts +73 -0
- 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 +3 -11
- 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/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,27 @@
|
|
|
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,
|
|
46
50
|
instanceSuffix,
|
|
47
51
|
} from "./run-helpers.js";
|
|
48
52
|
|
|
@@ -61,10 +65,12 @@
|
|
|
61
65
|
onRestartService,
|
|
62
66
|
serviceStopped = false,
|
|
63
67
|
serviceBusy = false,
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
onBack,
|
|
69
|
+
onToggleList,
|
|
70
|
+
listVisible = true,
|
|
66
71
|
highlightLine = null,
|
|
67
72
|
getInstanceCount = () => 1,
|
|
73
|
+
getLiveUsage = () => undefined,
|
|
68
74
|
motion,
|
|
69
75
|
notFound = false,
|
|
70
76
|
loading = false,
|
|
@@ -100,19 +106,25 @@
|
|
|
100
106
|
// cold-start path so a never-run task is still launchable from here.
|
|
101
107
|
onRunTask?: (() => void) | undefined;
|
|
102
108
|
// Service lifecycle controls. When either is set the cluster swaps the
|
|
103
|
-
// Run control for Stop
|
|
109
|
+
// Run control for Stop / Restart (chosen by serviceStopped).
|
|
104
110
|
onStopService?: (() => void) | undefined;
|
|
105
111
|
onRestartService?: (() => void) | undefined;
|
|
106
112
|
serviceStopped?: boolean;
|
|
107
113
|
serviceBusy?: boolean;
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
// Return to the run list when the panel replaces it (a phone), shown as
|
|
115
|
+
// a back arrow in the header's top-left corner.
|
|
116
|
+
onBack?: (() => void) | undefined;
|
|
117
|
+
// Fold the run list away (or back) when it sits beside the panel on a
|
|
118
|
+
// screen too narrow to keep both comfortably, in the same corner.
|
|
119
|
+
onToggleList?: (() => void) | undefined;
|
|
120
|
+
listVisible?: boolean;
|
|
112
121
|
highlightLine?: number | null;
|
|
113
122
|
// Resolves a task's currently configured instance count so multi-instance
|
|
114
123
|
// services render a 1-based #N suffix. Defaults to single-instance.
|
|
115
124
|
getInstanceCount?: (taskName: string) => number;
|
|
125
|
+
// Live CPU and memory of a running run, by run ID; undefined when it
|
|
126
|
+
// isn't measured. Shown in the header while the run is running.
|
|
127
|
+
getLiveUsage?: (runId: string) => ResourceUsage | undefined;
|
|
116
128
|
// Which runs arrived or were removed live moments ago. The panel eases in
|
|
117
129
|
// when a new run takes it (triggered, or a scheduled run auto-selected)
|
|
118
130
|
// or when its run was deleted and another takes its place; picking a
|
|
@@ -148,7 +160,7 @@
|
|
|
148
160
|
|
|
149
161
|
let canDelete = $derived.by(() => {
|
|
150
162
|
if (!run || !onDelete) return false;
|
|
151
|
-
const status =
|
|
163
|
+
const status = displayStatus(run.status, run.endReason);
|
|
152
164
|
return status !== "running" && status !== "pending";
|
|
153
165
|
});
|
|
154
166
|
|
|
@@ -266,19 +278,7 @@
|
|
|
266
278
|
let confirmDeleteOpen = $state(false);
|
|
267
279
|
// Dropdown half of the Run split button (the "reuse parameters" variant).
|
|
268
280
|
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
|
-
}
|
|
281
|
+
const runIdCopy = new CopyFeedback(1200);
|
|
282
282
|
|
|
283
283
|
// The accent for a status that means "something to triage", or undefined
|
|
284
284
|
// when there is nothing wrong. Tint is reserved for alarms (DESIGN.md) so a
|
|
@@ -362,8 +362,8 @@
|
|
|
362
362
|
<svelte:window onkeydown={handleConsoleKeydown} />
|
|
363
363
|
|
|
364
364
|
{#if run}
|
|
365
|
-
{@const status =
|
|
366
|
-
{@const config =
|
|
365
|
+
{@const status = displayStatus(run.status, run.endReason)}
|
|
366
|
+
{@const config = RUN_STATUS_CONFIG[status]}
|
|
367
367
|
{@const DetailIcon = config.icon}
|
|
368
368
|
{@const duration = runDuration(
|
|
369
369
|
run,
|
|
@@ -372,6 +372,8 @@
|
|
|
372
372
|
{@const startDelay = runStartDelay(run)}
|
|
373
373
|
{@const startedAt = run.startedAt ?? run.createdAt}
|
|
374
374
|
{@const retry = runRetryLabel(run)}
|
|
375
|
+
{@const usage = runUsageLabel(run)}
|
|
376
|
+
{@const live = run.status === "running" ? getLiveUsage(run.id) : undefined}
|
|
375
377
|
{@const paramEntries = run.params ? Object.entries(run.params) : []}
|
|
376
378
|
{@const suffix = instanceSuffix(run.instanceIndex, getInstanceCount(run.taskName))}
|
|
377
379
|
{@const spine = config.dot.replace(" animate-pulse", "")}
|
|
@@ -416,8 +418,36 @@
|
|
|
416
418
|
style="--rw-oc: {alarm ?? 'var(--color-surface-raised)'}"
|
|
417
419
|
>
|
|
418
420
|
<div class="pt-[18px] pr-[22px] pb-[16px] pl-[26px]">
|
|
419
|
-
|
|
420
|
-
|
|
421
|
+
<!-- The actions wrap onto their own row rather than squeeze the
|
|
422
|
+
readout below a legible width on a narrow panel. -->
|
|
423
|
+
<div class="flex flex-wrap items-start gap-x-3 gap-y-3">
|
|
424
|
+
{#if onBack}
|
|
425
|
+
<button
|
|
426
|
+
type="button"
|
|
427
|
+
onclick={() => onBack()}
|
|
428
|
+
class="-mt-1 -ml-2 shrink-0 rounded-[3px] p-1.5 text-on-surface-muted hover:bg-surface-sunken hover:text-primary"
|
|
429
|
+
title="Back to runs"
|
|
430
|
+
aria-label="Back to runs"
|
|
431
|
+
>
|
|
432
|
+
<ArrowLeft size={20} />
|
|
433
|
+
</button>
|
|
434
|
+
{:else if onToggleList}
|
|
435
|
+
<button
|
|
436
|
+
type="button"
|
|
437
|
+
onclick={() => onToggleList()}
|
|
438
|
+
class="-mt-1 -ml-2 shrink-0 rounded-[3px] p-1.5 text-on-surface-muted hover:bg-surface-sunken hover:text-primary"
|
|
439
|
+
title={listVisible ? "Hide run list" : "Show run list"}
|
|
440
|
+
aria-label={listVisible ? "Hide run list" : "Show run list"}
|
|
441
|
+
aria-expanded={listVisible}
|
|
442
|
+
>
|
|
443
|
+
{#if listVisible}
|
|
444
|
+
<PanelLeftClose size={20} />
|
|
445
|
+
{:else}
|
|
446
|
+
<PanelLeftOpen size={20} />
|
|
447
|
+
{/if}
|
|
448
|
+
</button>
|
|
449
|
+
{/if}
|
|
450
|
+
<div class="min-w-60 flex-1">
|
|
421
451
|
<!-- Identity, but only where the page around the panel isn't
|
|
422
452
|
already saying it: the cross-task runs list needs the task
|
|
423
453
|
name, a task's own page already has it in the breadcrumb. -->
|
|
@@ -544,16 +574,35 @@
|
|
|
544
574
|
</div>
|
|
545
575
|
</Popover>
|
|
546
576
|
{/if}
|
|
577
|
+
{#if live}
|
|
578
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
579
|
+
<span
|
|
580
|
+
data-testid="run-live-usage"
|
|
581
|
+
class="text-on-surface-muted"
|
|
582
|
+
title="Live CPU (100% = one core) and memory of the run's processes"
|
|
583
|
+
>CPU {Math.round(live.cpuPercent)}% · {formatBytes(
|
|
584
|
+
live.memoryBytes,
|
|
585
|
+
)}</span
|
|
586
|
+
>
|
|
587
|
+
{/if}
|
|
588
|
+
{#if usage}
|
|
589
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
590
|
+
<span
|
|
591
|
+
data-testid="run-usage"
|
|
592
|
+
title="Peak memory and CPU time of the run's processes"
|
|
593
|
+
>{usage}</span
|
|
594
|
+
>
|
|
595
|
+
{/if}
|
|
547
596
|
<!-- Run-id chip: click to copy the full ULID. Last, because
|
|
548
597
|
it is the fact you reach for least and the only one you
|
|
549
598
|
interact with. -->
|
|
550
599
|
<button
|
|
551
600
|
type="button"
|
|
552
|
-
onclick={() =>
|
|
601
|
+
onclick={() => void runIdCopy.copy(run.id)}
|
|
553
602
|
title="Copy run ID"
|
|
554
603
|
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
604
|
>
|
|
556
|
-
{#if
|
|
605
|
+
{#if runIdCopy.copied}
|
|
557
606
|
<Check size={11} class="text-success-surface" />Copied
|
|
558
607
|
{:else}
|
|
559
608
|
<Hash size={11} />{run.id}
|
|
@@ -562,27 +611,9 @@
|
|
|
562
611
|
</div>
|
|
563
612
|
</div>
|
|
564
613
|
|
|
565
|
-
<!-- Actions:
|
|
614
|
+
<!-- Actions: run (split) / service lifecycle ·
|
|
566
615
|
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
|
-
|
|
616
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
586
617
|
{#if onStopService || onRestartService}
|
|
587
618
|
<!-- Service lifecycle replaces the run control.
|
|
588
619
|
Restart is always available (it cancels + respawns
|
|
@@ -604,7 +635,7 @@
|
|
|
604
635
|
{:else}
|
|
605
636
|
<RefreshCcw size={15} />
|
|
606
637
|
{/if}
|
|
607
|
-
<span class="@max-
|
|
638
|
+
<span class="@max-xs:hidden"
|
|
608
639
|
>{serviceStopped ? "Start" : "Restart"}</span
|
|
609
640
|
>
|
|
610
641
|
</button>
|
|
@@ -618,7 +649,7 @@
|
|
|
618
649
|
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
650
|
>
|
|
620
651
|
<Square size={15} fill="currentColor" stroke="none" />
|
|
621
|
-
<span class="@max-
|
|
652
|
+
<span class="@max-xs:hidden">Stop</span>
|
|
622
653
|
</button>
|
|
623
654
|
{/if}
|
|
624
655
|
{:else}
|
|
@@ -635,7 +666,7 @@
|
|
|
635
666
|
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
667
|
>
|
|
637
668
|
<Play size={15} fill="currentColor" stroke="none" />
|
|
638
|
-
<span class="@max-
|
|
669
|
+
<span class="@max-xs:hidden">Run</span>
|
|
639
670
|
</button>
|
|
640
671
|
{#if onRunAgain}
|
|
641
672
|
<Popover bind:open={runMenuOpen} placement="bottom-end">
|
|
@@ -681,7 +712,7 @@
|
|
|
681
712
|
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
713
|
>
|
|
683
714
|
<Square size={15} fill="currentColor" stroke="none" />
|
|
684
|
-
<span class="@max-
|
|
715
|
+
<span class="@max-xs:hidden">Stop</span>
|
|
685
716
|
</button>
|
|
686
717
|
{/if}
|
|
687
718
|
{/if}
|
|
@@ -758,7 +789,7 @@
|
|
|
758
789
|
: "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
790
|
>
|
|
760
791
|
<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)]"
|
|
792
|
+
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
793
|
>
|
|
763
794
|
<div class="flex min-w-0 items-center gap-3">
|
|
764
795
|
{#if consoleMaximized}
|
|
@@ -773,12 +804,12 @@
|
|
|
773
804
|
</span>
|
|
774
805
|
{:else}
|
|
775
806
|
<span
|
|
776
|
-
class="flex items-center gap-2 font-semibold text-[var(--rw-con-text)]"
|
|
807
|
+
class="flex items-center gap-2 font-semibold whitespace-nowrap text-[var(--rw-con-text)]"
|
|
777
808
|
>
|
|
778
809
|
<TerminalIcon size={14} class="opacity-70" />
|
|
779
810
|
Console output
|
|
780
811
|
</span>
|
|
781
|
-
<span class="hidden text-[var(--rw-con-gutter)]
|
|
812
|
+
<span class="hidden text-[var(--rw-con-gutter)] @lg:inline"
|
|
782
813
|
>stdout + stderr</span
|
|
783
814
|
>
|
|
784
815
|
{/if}
|
|
@@ -795,7 +826,7 @@
|
|
|
795
826
|
aria-label="Toggle line wrapping"
|
|
796
827
|
>
|
|
797
828
|
<TextWrap size={13} />
|
|
798
|
-
<span class="hidden sm:inline">Wrap</span>
|
|
829
|
+
<span class="hidden @sm:inline">Wrap</span>
|
|
799
830
|
</button>
|
|
800
831
|
<button
|
|
801
832
|
type="button"
|
|
@@ -806,10 +837,7 @@
|
|
|
806
837
|
>
|
|
807
838
|
{#if consoleMaximized}
|
|
808
839
|
<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
|
-
>
|
|
840
|
+
<Kbd keys="Esc" size="xs" tone="console" />
|
|
813
841
|
{:else}
|
|
814
842
|
<Maximize2 size={13} />
|
|
815
843
|
Expand
|
|
@@ -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
|
|
@@ -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}
|
|
@@ -19,9 +19,8 @@
|
|
|
19
19
|
import type { RunMotion } from "../../utils/run-motion.js";
|
|
20
20
|
import EmptyState from "../EmptyState.svelte";
|
|
21
21
|
import RunFilterPopover from "./RunFilterPopover.svelte";
|
|
22
|
-
import type
|
|
23
|
-
import
|
|
24
|
-
import { getRunStatusConfig, runDisplayStatus } from "./status-config.js";
|
|
22
|
+
import { displayStatus, type Run, type RunSelector, type RunStatus } from "@runwisp/common";
|
|
23
|
+
import { RUN_STATUS_CONFIG } from "./status-config.js";
|
|
25
24
|
import {
|
|
26
25
|
activeDimensions,
|
|
27
26
|
clearDimension,
|
|
@@ -71,7 +70,6 @@
|
|
|
71
70
|
onBulkRerun,
|
|
72
71
|
getInstanceCount = () => 1,
|
|
73
72
|
motion,
|
|
74
|
-
flush = false,
|
|
75
73
|
outputSearch = false,
|
|
76
74
|
outputQuery = "",
|
|
77
75
|
outputMatches = null,
|
|
@@ -105,11 +103,6 @@
|
|
|
105
103
|
// Which runs arrived or were removed live moments ago: their rows animate
|
|
106
104
|
// in or out. Page loads, scrolling and filtering never do.
|
|
107
105
|
motion?: RunMotion;
|
|
108
|
-
// Flush rail mode (task detail page): render as a borderless rail that
|
|
109
|
-
// fills its column and is divided from the detail panel by a single right
|
|
110
|
-
// border — no card chrome of its own. Default renders the standalone card
|
|
111
|
-
// the cross-task /runs grid expects.
|
|
112
|
-
flush?: boolean;
|
|
113
106
|
// Output search (history rail): filters runs by what they printed. The
|
|
114
107
|
// search box lives in the app header now; the parent owns the box, the
|
|
115
108
|
// query, and the async log search. This component just renders the
|
|
@@ -392,17 +385,15 @@
|
|
|
392
385
|
});
|
|
393
386
|
</script>
|
|
394
387
|
|
|
388
|
+
<!-- A borderless rail that fills its column, divided from the detail panel by
|
|
389
|
+
a single right border. -->
|
|
395
390
|
<div
|
|
396
|
-
class=
|
|
397
|
-
? "flex h-full w-full flex-col overflow-hidden border-b border-outline bg-surface md:w-[300px] md:shrink-0 md:border-r md:border-b-0"
|
|
398
|
-
: "flex flex-col overflow-hidden rounded-[4px] border border-outline bg-surface-raised shadow-sm md:col-span-4 lg:col-span-3"}
|
|
391
|
+
class="flex h-full w-full flex-col overflow-hidden border-b border-outline bg-surface md:w-[300px] md:shrink-0 md:border-r md:border-b-0"
|
|
399
392
|
>
|
|
400
393
|
<div
|
|
401
394
|
class="flex shrink-0 items-center gap-2 border-b px-3 py-2 {hasSelection
|
|
402
395
|
? 'border-outline-faint bg-primary-soft/40'
|
|
403
|
-
:
|
|
404
|
-
? 'border-transparent bg-surface'
|
|
405
|
-
: 'border-outline-faint bg-surface-sunken'}"
|
|
396
|
+
: 'border-transparent bg-surface'}"
|
|
406
397
|
>
|
|
407
398
|
{#if bulkActions}
|
|
408
399
|
<label
|
|
@@ -483,7 +474,7 @@
|
|
|
483
474
|
</span>
|
|
484
475
|
<div class="ml-auto flex items-center gap-1">
|
|
485
476
|
{#if showFilters}
|
|
486
|
-
<RunFilterPopover bind:filters {showTask} {tasks}
|
|
477
|
+
<RunFilterPopover bind:filters {showTask} {tasks} />
|
|
487
478
|
{/if}
|
|
488
479
|
<Button
|
|
489
480
|
variant="ghost"
|
|
@@ -688,8 +679,8 @@
|
|
|
688
679
|
{/snippet}
|
|
689
680
|
|
|
690
681
|
{#snippet runRowButton(run: Run, isActive: boolean, match: RunOutputMatch | undefined)}
|
|
691
|
-
{@const dstatus =
|
|
692
|
-
{@const config =
|
|
682
|
+
{@const dstatus = displayStatus(run.status, run.endReason)}
|
|
683
|
+
{@const config = RUN_STATUS_CONFIG[dstatus]}
|
|
693
684
|
{@const running = run.status === "running"}
|
|
694
685
|
{@const spine = config.dot.replace(" animate-pulse", "")}
|
|
695
686
|
{@const startedAt = run.startedAt ?? run.createdAt}
|
|
@@ -6,13 +6,10 @@ import { END_REASONS } from "@runwisp/common";
|
|
|
6
6
|
import {
|
|
7
7
|
emptyRunFilters,
|
|
8
8
|
FAILURE_STATUS_TOKEN,
|
|
9
|
-
NEEDS_ATTENTION_STATUSES,
|
|
10
|
-
isNeedsAttention,
|
|
11
9
|
dimensionActive,
|
|
12
10
|
activeDimensions,
|
|
13
11
|
activeFilterCount,
|
|
14
12
|
clearDimension,
|
|
15
|
-
clearPopoverFilters,
|
|
16
13
|
humanizeStatus,
|
|
17
14
|
triggerDescription,
|
|
18
15
|
FILTERABLE_TRIGGERS,
|
|
@@ -29,7 +26,6 @@ import {
|
|
|
29
26
|
exitCodeRangeActive,
|
|
30
27
|
isExitCodeExprValid,
|
|
31
28
|
exitCodeChipLabel,
|
|
32
|
-
exitCodePropChanged,
|
|
33
29
|
type StatusBucket,
|
|
34
30
|
type RunsListFilters,
|
|
35
31
|
} from "./run-filters.js";
|
|
@@ -54,23 +50,6 @@ describe("emptyRunFilters", () => {
|
|
|
54
50
|
});
|
|
55
51
|
});
|
|
56
52
|
|
|
57
|
-
describe("isNeedsAttention", () => {
|
|
58
|
-
it("is true for exactly the attention set, order-insensitive", () => {
|
|
59
|
-
expect(isNeedsAttention([...NEEDS_ATTENTION_STATUSES])).toBe(true);
|
|
60
|
-
expect(isNeedsAttention([...NEEDS_ATTENTION_STATUSES].reverse())).toBe(true);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("is exactly the failure sentinel (server resolves it to is_failure)", () => {
|
|
64
|
-
expect(NEEDS_ATTENTION_STATUSES).toEqual([FAILURE_STATUS_TOKEN]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("is false for a raw end reason or a different set", () => {
|
|
68
|
-
expect(isNeedsAttention(["failed"])).toBe(false);
|
|
69
|
-
expect(isNeedsAttention(["succeeded"])).toBe(false);
|
|
70
|
-
expect(isNeedsAttention([])).toBe(false);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
53
|
describe("dimensionActive / activeDimensions", () => {
|
|
75
54
|
it("detects each dimension independently", () => {
|
|
76
55
|
expect(dimensionActive(base({ statuses: ["failed"] }), "status")).toBe(true);
|
|
@@ -125,25 +104,6 @@ describe("clearDimension", () => {
|
|
|
125
104
|
});
|
|
126
105
|
});
|
|
127
106
|
|
|
128
|
-
describe("clearPopoverFilters", () => {
|
|
129
|
-
it("clears every popover dimension but keeps search and sort", () => {
|
|
130
|
-
const f = base({
|
|
131
|
-
search: "nightly",
|
|
132
|
-
sortDirection: "asc",
|
|
133
|
-
statuses: ["failed"],
|
|
134
|
-
taskName: "backup",
|
|
135
|
-
triggeredBy: "cron",
|
|
136
|
-
exitCode: "1",
|
|
137
|
-
retriesOnly: true,
|
|
138
|
-
createdAfter: "2026-01-01T00:00:00Z",
|
|
139
|
-
});
|
|
140
|
-
const cleared = clearPopoverFilters(f);
|
|
141
|
-
expect(activeFilterCount(cleared)).toBe(0);
|
|
142
|
-
expect(cleared.search).toBe("nightly");
|
|
143
|
-
expect(cleared.sortDirection).toBe("asc");
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
107
|
describe("humanizeStatus", () => {
|
|
148
108
|
it("title-cases and de-snakes a status token", () => {
|
|
149
109
|
expect(humanizeStatus("log_overflow")).toBe("Log overflow");
|
|
@@ -154,8 +114,8 @@ describe("humanizeStatus", () => {
|
|
|
154
114
|
|
|
155
115
|
describe("statusChipLabel", () => {
|
|
156
116
|
it("names a bucket when the selection matches one exactly", () => {
|
|
157
|
-
// The Failed bucket is the
|
|
158
|
-
expect(statusChipLabel([
|
|
117
|
+
// The Failed bucket is the failure sentinel.
|
|
118
|
+
expect(statusChipLabel([FAILURE_STATUS_TOKEN])).toBe("Failed");
|
|
159
119
|
expect(statusChipLabel(["pending", "running"])).toBe("Running");
|
|
160
120
|
expect(statusChipLabel(["succeeded"])).toBe("Succeeded");
|
|
161
121
|
});
|
|
@@ -332,19 +292,3 @@ describe("exit-code expression", () => {
|
|
|
332
292
|
expect(exitCodeChipLabel(" 137 ")).toBe("Exit 137");
|
|
333
293
|
});
|
|
334
294
|
});
|
|
335
|
-
|
|
336
|
-
describe("exitCodePropChanged", () => {
|
|
337
|
-
it("is false when the committed value is unchanged (unrelated filters update)", () => {
|
|
338
|
-
expect(exitCodePropChanged("137", "137")).toBe(false);
|
|
339
|
-
expect(exitCodePropChanged(undefined, undefined)).toBe(false);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it("is true when the exit-code filter was cleared externally (chip removed)", () => {
|
|
343
|
-
expect(exitCodePropChanged("137", undefined)).toBe(true);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it("is true when the exit-code filter was set to a new committed value", () => {
|
|
347
|
-
expect(exitCodePropChanged(undefined, "200")).toBe(true);
|
|
348
|
-
expect(exitCodePropChanged("137", "200")).toBe(true);
|
|
349
|
-
});
|
|
350
|
-
});
|