@runwisp/ui 1.5.0 → 1.7.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 +5 -7
- package/src/lib/actions/focusTrap.ts +3 -3
- package/src/lib/actions/fold-to-fit.test.ts +36 -0
- package/src/lib/actions/fold-to-fit.ts +89 -0
- package/src/lib/actions/modal-dialog.test.ts +47 -0
- package/src/lib/actions/modal-dialog.ts +48 -0
- package/src/lib/components/AccordionItem.svelte +16 -5
- package/src/lib/components/Badge.svelte +4 -25
- package/src/lib/components/Button.svelte +2 -9
- package/src/lib/components/CodeBlock.svelte +7 -6
- package/src/lib/components/DataGrid.svelte +5 -5
- package/src/lib/components/Drawer.svelte +22 -30
- package/src/lib/components/DurationInput.svelte +6 -25
- package/src/lib/components/FilterBar.svelte +3 -7
- package/src/lib/components/FilterPill.svelte +2 -2
- package/src/lib/components/FormField.svelte +1 -1
- package/src/lib/components/Link.svelte +9 -4
- package/src/lib/components/LogConsole.svelte +101 -424
- package/src/lib/components/Modal.svelte +7 -30
- 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/Spinner.svelte +10 -5
- package/src/lib/components/StatusDot.svelte +1 -1
- package/src/lib/components/StatusIndicator.svelte +37 -104
- package/src/lib/components/Tabs.svelte +15 -1
- package/src/lib/components/Tooltip.svelte +1 -1
- package/src/lib/components/dashboard/RunActions.svelte +156 -0
- package/src/lib/components/dashboard/RunConsoleFrame.svelte +319 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +137 -725
- package/src/lib/components/dashboard/RunFacts.svelte +119 -0
- package/src/lib/components/dashboard/RunFilterChips.svelte +52 -0
- package/src/lib/components/dashboard/RunFilterPopover.svelte +9 -10
- package/src/lib/components/dashboard/RunHeaderLine.svelte +295 -0
- package/src/lib/components/dashboard/RunListSkeleton.svelte +43 -0
- package/src/lib/components/dashboard/RunRow.svelte +202 -0
- package/src/lib/components/dashboard/RunsList.svelte +267 -545
- package/src/lib/components/dashboard/bulk-selection.svelte.test.ts +42 -0
- package/src/lib/components/dashboard/bulk-selection.svelte.ts +50 -0
- package/src/lib/components/dashboard/cron-format.test.ts +10 -1
- package/src/lib/components/dashboard/cron-format.ts +7 -3
- package/src/lib/components/dashboard/run-filters.test.ts +62 -2
- package/src/lib/components/dashboard/run-filters.ts +104 -99
- package/src/lib/components/dashboard/run-helpers.test.ts +131 -12
- package/src/lib/components/dashboard/run-helpers.ts +150 -44
- package/src/lib/components/dashboard/status-config.ts +18 -0
- package/src/lib/components/dashboard/types.ts +6 -0
- package/src/lib/components/data-grid-selection.ts +2 -9
- package/src/lib/components/filter-spec.ts +3 -3
- package/src/lib/index.ts +13 -2
- package/src/lib/log-console/AnsiLine.svelte +12 -0
- package/src/lib/log-console/FrameHistoryBlock.svelte +57 -0
- package/src/lib/log-console/LogCache.svelte.ts +1 -1
- package/src/lib/log-console/LogConsoleFooter.svelte +88 -0
- package/src/lib/log-console/ansi.ts +1 -1
- package/src/lib/log-console/frame-history.svelte.test.ts +47 -0
- package/src/lib/log-console/frame-history.svelte.ts +71 -0
- package/src/lib/log-console/wrap-layout.svelte.test.ts +92 -0
- package/src/lib/log-console/wrap-layout.svelte.ts +188 -0
- package/src/lib/theme-tokens.css +21 -21
- package/src/lib/theme.css +13 -13
- package/src/lib/utils/format.test.ts +125 -3
- package/src/lib/utils/format.ts +101 -8
- package/src/lib/utils/toast.svelte.ts +2 -3
- package/src/lib/utils/typing-target.ts +10 -0
- package/src/stories/Accordion.stories.svelte +60 -1
- package/src/stories/CodeBlock.stories.svelte +13 -0
- package/src/stories/Link.stories.svelte +25 -1
- package/src/stories/LogConsole.stories.svelte +2 -2
- package/src/stories/Tabs.stories.svelte +17 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { Check, Hash, RotateCw, SlidersHorizontal } from "@lucide/svelte";
|
|
6
|
+
import Popover from "../Popover.svelte";
|
|
7
|
+
import { CopyFeedback } from "../../utils/clipboard.svelte.js";
|
|
8
|
+
import { formatBytes, formatCalendarDate, formatClockTime } from "../../utils/format.js";
|
|
9
|
+
import { formatShortId } from "../../utils/id.js";
|
|
10
|
+
import type { ResourceUsage, Run } from "@runwisp/common";
|
|
11
|
+
import {
|
|
12
|
+
formatTriggeredByLabel,
|
|
13
|
+
runRetryLabel,
|
|
14
|
+
runStartDelay,
|
|
15
|
+
runUsageLabel,
|
|
16
|
+
} from "./run-helpers.js";
|
|
17
|
+
|
|
18
|
+
let { run, live }: { run: Run; live: ResourceUsage | undefined } = $props();
|
|
19
|
+
|
|
20
|
+
const runIdCopy = new CopyFeedback(1200);
|
|
21
|
+
|
|
22
|
+
let startDelay = $derived(runStartDelay(run));
|
|
23
|
+
let retry = $derived(runRetryLabel(run));
|
|
24
|
+
let usage = $derived(runUsageLabel(run));
|
|
25
|
+
let paramEntries = $derived(run.params ? Object.entries(run.params) : []);
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<!-- Facts: when it happened and what shaped it. Queue wait, retries and
|
|
29
|
+
parameters are absent on an ordinary run and appear inline the moment they
|
|
30
|
+
exist. -->
|
|
31
|
+
<div
|
|
32
|
+
class="mt-2.5 flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-[11.5px] text-on-surface-faint tabular-nums"
|
|
33
|
+
>
|
|
34
|
+
<!-- No hover-for-the-full-timestamp here: the date sits right beside the
|
|
35
|
+
clock time, so a tooltip could only repeat it. The line wraps at narrow
|
|
36
|
+
widths instead of dropping the date and hiding it behind a hover. -->
|
|
37
|
+
<span data-testid="run-started" class="text-on-surface-muted"
|
|
38
|
+
>{run.startedAt ? formatClockTime(run.startedAt) : "—"}</span
|
|
39
|
+
>
|
|
40
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
41
|
+
<span>{formatCalendarDate(run.startedAt ?? run.createdAt)}</span>
|
|
42
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
43
|
+
<span>via {formatTriggeredByLabel(run.triggeredBy).toLowerCase()}</span>
|
|
44
|
+
{#if startDelay}
|
|
45
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
46
|
+
<span
|
|
47
|
+
class="text-warning-soft-text"
|
|
48
|
+
title="Waited between its scheduled tick and actually starting"
|
|
49
|
+
>{startDelay} queued</span
|
|
50
|
+
>
|
|
51
|
+
{/if}
|
|
52
|
+
{#if retry}
|
|
53
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
54
|
+
<span class="inline-flex items-center gap-1">
|
|
55
|
+
<RotateCw size={11} />
|
|
56
|
+
retry #{run.retryAttempt}{#if run.retryOfRunId} of
|
|
57
|
+
{formatShortId(run.retryOfRunId)}{/if}
|
|
58
|
+
</span>
|
|
59
|
+
{/if}
|
|
60
|
+
{#if paramEntries.length > 0}
|
|
61
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
62
|
+
<Popover placement="bottom-start">
|
|
63
|
+
{#snippet trigger()}
|
|
64
|
+
<span
|
|
65
|
+
class="inline-flex cursor-pointer items-center gap-1 border-b border-dotted border-outline-hover text-on-surface-muted hover:text-primary"
|
|
66
|
+
>
|
|
67
|
+
<SlidersHorizontal size={11} />
|
|
68
|
+
{paramEntries.length}
|
|
69
|
+
{paramEntries.length === 1 ? "parameter" : "parameters"}
|
|
70
|
+
</span>
|
|
71
|
+
{/snippet}
|
|
72
|
+
<div class="max-w-md min-w-48">
|
|
73
|
+
<span
|
|
74
|
+
class="font-mono text-xs font-semibold tracking-[0.14em] text-on-surface-faint uppercase"
|
|
75
|
+
>Parameters</span
|
|
76
|
+
>
|
|
77
|
+
<dl
|
|
78
|
+
class="mt-2 grid max-h-72 grid-cols-[max-content_1fr] gap-x-4 gap-y-1.5 overflow-y-auto"
|
|
79
|
+
>
|
|
80
|
+
{#each paramEntries as [key, value] (key)}
|
|
81
|
+
<dt class="font-mono text-xs text-on-surface-muted">{key}</dt>
|
|
82
|
+
<dd class="font-mono text-xs font-medium break-all text-on-surface">
|
|
83
|
+
{value}
|
|
84
|
+
</dd>
|
|
85
|
+
{/each}
|
|
86
|
+
</dl>
|
|
87
|
+
</div>
|
|
88
|
+
</Popover>
|
|
89
|
+
{/if}
|
|
90
|
+
{#if live}
|
|
91
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
92
|
+
<span
|
|
93
|
+
data-testid="run-live-usage"
|
|
94
|
+
class="text-on-surface-muted"
|
|
95
|
+
title="Live CPU (100% = one core) and memory of the run's processes"
|
|
96
|
+
>CPU {Math.round(live.cpuPercent)}% · {formatBytes(live.memoryBytes)}</span
|
|
97
|
+
>
|
|
98
|
+
{/if}
|
|
99
|
+
{#if usage}
|
|
100
|
+
<span class="text-outline-hover" aria-hidden="true">·</span>
|
|
101
|
+
<span data-testid="run-usage" title="CPU time and most memory the run's processes used"
|
|
102
|
+
>{usage}</span
|
|
103
|
+
>
|
|
104
|
+
{/if}
|
|
105
|
+
<!-- Run-id chip: click to copy the full ULID. Last, because it is the fact
|
|
106
|
+
you reach for least and the only one you interact with. -->
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onclick={() => void runIdCopy.copy(run.id)}
|
|
110
|
+
title="Copy run ID"
|
|
111
|
+
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"
|
|
112
|
+
>
|
|
113
|
+
{#if runIdCopy.copied}
|
|
114
|
+
<Check size={11} class="text-success-surface" />Copied
|
|
115
|
+
{:else}
|
|
116
|
+
<Hash size={11} />{run.id}
|
|
117
|
+
{/if}
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { X } from "@lucide/svelte";
|
|
6
|
+
import {
|
|
7
|
+
activeDimensions,
|
|
8
|
+
clearDimension,
|
|
9
|
+
filterChipLabel,
|
|
10
|
+
type RunsListFilters,
|
|
11
|
+
} from "./run-filters.js";
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
filters = $bindable(),
|
|
15
|
+
showTask,
|
|
16
|
+
}: {
|
|
17
|
+
filters: RunsListFilters;
|
|
18
|
+
/** `task` only chips on the cross-task view; elsewhere it is the page scope. */
|
|
19
|
+
showTask: boolean;
|
|
20
|
+
} = $props();
|
|
21
|
+
|
|
22
|
+
const chips = $derived(
|
|
23
|
+
activeDimensions(filters)
|
|
24
|
+
.filter((dim) => dim !== "task" || showTask)
|
|
25
|
+
.map((dim) => ({ dimension: dim, label: filterChipLabel(filters, dim) })),
|
|
26
|
+
);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<!-- Rendered only when filters are set, so the header stays clean when empty;
|
|
30
|
+
each chip's X clears its dimension. The filter controls themselves live in
|
|
31
|
+
the popover. -->
|
|
32
|
+
{#if chips.length > 0}
|
|
33
|
+
<div
|
|
34
|
+
class="flex shrink-0 flex-wrap gap-1 border-b border-outline-faint bg-surface-sunken px-3 py-2"
|
|
35
|
+
>
|
|
36
|
+
{#each chips as chip (chip.dimension)}
|
|
37
|
+
<span
|
|
38
|
+
class="inline-flex items-center gap-1 rounded-[3px] border border-primary-soft-border bg-primary-soft py-0.5 pr-1 pl-2 font-mono text-2xs font-medium text-primary-soft-text"
|
|
39
|
+
>
|
|
40
|
+
{chip.label}
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
onclick={() => (filters = clearDimension(filters, chip.dimension))}
|
|
44
|
+
class="flex size-3.5 items-center justify-center rounded-[3px] text-primary-soft-text/70 hover:bg-primary/15 hover:text-primary-soft-text"
|
|
45
|
+
aria-label="Remove {chip.label} filter"
|
|
46
|
+
>
|
|
47
|
+
<X size={11} />
|
|
48
|
+
</button>
|
|
49
|
+
</span>
|
|
50
|
+
{/each}
|
|
51
|
+
</div>
|
|
52
|
+
{/if}
|
|
@@ -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
|
|
@@ -70,12 +70,8 @@
|
|
|
70
70
|
const selectClass =
|
|
71
71
|
"h-9 w-full appearance-none rounded-[3px] border border-outline bg-surface-raised px-2.5 font-mono text-sm text-on-surface focus:border-ring focus:outline-none";
|
|
72
72
|
|
|
73
|
-
function statusDot(status: RunStatus): string {
|
|
74
|
-
return RUN_STATUS_CONFIG[status].dot.replace(" animate-pulse", "");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
73
|
// Reflect a tri-state bucket onto the native checkbox (no `indeterminate`
|
|
78
|
-
// HTML attribute exists
|
|
74
|
+
// HTML attribute exists, it must be set on the DOM node).
|
|
79
75
|
function indeterminate(node: HTMLInputElement, value: boolean) {
|
|
80
76
|
node.indeterminate = value;
|
|
81
77
|
return {
|
|
@@ -95,7 +91,7 @@
|
|
|
95
91
|
// --- Time -------------------------------------------------------------
|
|
96
92
|
//
|
|
97
93
|
// 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
|
|
94
|
+
// its end (To), so either edge applies on its own, From alone means
|
|
99
95
|
// "everything since", To alone "everything before", and the same date in
|
|
100
96
|
// both captures the whole day.
|
|
101
97
|
|
|
@@ -186,7 +182,7 @@
|
|
|
186
182
|
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
183
|
>
|
|
188
184
|
<ChevronRight size={12} class={advancedOpen ? "rotate-90" : ""} />
|
|
189
|
-
Advanced
|
|
185
|
+
Advanced: pick exact statuses
|
|
190
186
|
</summary>
|
|
191
187
|
<div class="mt-2 flex flex-col gap-1.5">
|
|
192
188
|
{#each STATUS_GROUPS as group (group.label)}
|
|
@@ -202,7 +198,10 @@
|
|
|
202
198
|
onchange={() => toggleStatus(status)}
|
|
203
199
|
class="size-3.5 shrink-0 cursor-pointer rounded border-outline accent-primary"
|
|
204
200
|
/>
|
|
205
|
-
<span
|
|
201
|
+
<span
|
|
202
|
+
class="size-2 shrink-0 rounded-full {RUN_STATUS_CONFIG[
|
|
203
|
+
status
|
|
204
|
+
].solidDot}"
|
|
206
205
|
></span>
|
|
207
206
|
<span class="truncate text-on-surface">
|
|
208
207
|
{humanizeStatus(status)}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
import { ArrowLeft, Check, Copy, Ellipsis, SlidersHorizontal } from "@lucide/svelte";
|
|
7
|
+
import Popover from "../Popover.svelte";
|
|
8
|
+
import { foldToFit } from "../../actions/fold-to-fit.js";
|
|
9
|
+
import { CopyFeedback } from "../../utils/clipboard.svelte.js";
|
|
10
|
+
import {
|
|
11
|
+
formatBytes,
|
|
12
|
+
formatClockTime,
|
|
13
|
+
formatDayMonth,
|
|
14
|
+
formatFullDateTime,
|
|
15
|
+
} from "../../utils/format.js";
|
|
16
|
+
import { formatShortId } from "../../utils/id.js";
|
|
17
|
+
import type { ResourceUsage, Run, RunStatus } from "@runwisp/common";
|
|
18
|
+
import { RUN_STATUS_CONFIG } from "./status-config.js";
|
|
19
|
+
import {
|
|
20
|
+
formatTriggeredByLabel,
|
|
21
|
+
runRetryLabel,
|
|
22
|
+
runStartDelay,
|
|
23
|
+
runUsageLabel,
|
|
24
|
+
RUN_VERDICTS,
|
|
25
|
+
} from "./run-helpers.js";
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
run,
|
|
29
|
+
status,
|
|
30
|
+
duration,
|
|
31
|
+
suffix,
|
|
32
|
+
live,
|
|
33
|
+
onBack,
|
|
34
|
+
actions,
|
|
35
|
+
}: {
|
|
36
|
+
run: Run;
|
|
37
|
+
status: RunStatus;
|
|
38
|
+
duration: string | undefined;
|
|
39
|
+
suffix: string;
|
|
40
|
+
live: ResourceUsage | undefined;
|
|
41
|
+
onBack: (() => void) | undefined;
|
|
42
|
+
actions: Snippet;
|
|
43
|
+
} = $props();
|
|
44
|
+
|
|
45
|
+
// The facts after the verdict, in reading order. `fold` is the order they
|
|
46
|
+
// give way when the line runs out of room: the ID you reach for least goes
|
|
47
|
+
// first, what the run used goes last.
|
|
48
|
+
type FactKey = "start" | "queued" | "via" | "retry" | "params" | "instance" | "usage" | "id";
|
|
49
|
+
const FOLD_ORDER: FactKey[] = [
|
|
50
|
+
"id",
|
|
51
|
+
"via",
|
|
52
|
+
"queued",
|
|
53
|
+
"start",
|
|
54
|
+
"params",
|
|
55
|
+
"retry",
|
|
56
|
+
"instance",
|
|
57
|
+
"usage",
|
|
58
|
+
];
|
|
59
|
+
interface Fact {
|
|
60
|
+
key: FactKey;
|
|
61
|
+
label: string;
|
|
62
|
+
value: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const runIdCopy = new CopyFeedback(1200);
|
|
66
|
+
|
|
67
|
+
let config = $derived(RUN_STATUS_CONFIG[status]);
|
|
68
|
+
let verdict = $derived(RUN_VERDICTS[status]);
|
|
69
|
+
let paramEntries = $derived(run.params ? Object.entries(run.params) : []);
|
|
70
|
+
let usage = $derived(
|
|
71
|
+
live
|
|
72
|
+
? `CPU ${String(Math.round(live.cpuPercent))}% · RAM ${formatBytes(live.memoryBytes)}`
|
|
73
|
+
: runUsageLabel(run),
|
|
74
|
+
);
|
|
75
|
+
let facts = $derived.by(() => {
|
|
76
|
+
const list: Fact[] = [];
|
|
77
|
+
const started = run.startedAt;
|
|
78
|
+
if (started) {
|
|
79
|
+
list.push({
|
|
80
|
+
key: "start",
|
|
81
|
+
label: "Started",
|
|
82
|
+
value: `${formatDayMonth(started)} ${formatClockTime(started)}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const delay = runStartDelay(run);
|
|
86
|
+
if (delay) list.push({ key: "queued", label: "Queued", value: `${delay} queued` });
|
|
87
|
+
list.push({
|
|
88
|
+
key: "via",
|
|
89
|
+
label: "Triggered",
|
|
90
|
+
value: `via ${formatTriggeredByLabel(run.triggeredBy).toLowerCase()}`,
|
|
91
|
+
});
|
|
92
|
+
if (runRetryLabel(run)) {
|
|
93
|
+
const of = run.retryOfRunId ? ` of ${formatShortId(run.retryOfRunId)}` : "";
|
|
94
|
+
list.push({
|
|
95
|
+
key: "retry",
|
|
96
|
+
label: "Retry",
|
|
97
|
+
value: `retry #${String(run.retryAttempt)}${of}`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (paramEntries.length > 0) {
|
|
101
|
+
const n = paramEntries.length;
|
|
102
|
+
list.push({
|
|
103
|
+
key: "params",
|
|
104
|
+
label: "Parameters",
|
|
105
|
+
value: `${String(n)} ${n === 1 ? "parameter" : "parameters"}`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (suffix) list.push({ key: "instance", label: "Instance", value: `instance ${suffix}` });
|
|
109
|
+
if (usage) list.push({ key: "usage", label: live ? "Now" : "Used", value: usage });
|
|
110
|
+
list.push({ key: "id", label: "Run ID", value: `#${formatShortId(run.id)}` });
|
|
111
|
+
return list;
|
|
112
|
+
});
|
|
113
|
+
let order = $derived(FOLD_ORDER.filter((key) => facts.some((f) => f.key === key)));
|
|
114
|
+
|
|
115
|
+
let folded = $state<string[]>([]);
|
|
116
|
+
let foldedFacts = $derived(facts.filter((f) => folded.includes(f.key)));
|
|
117
|
+
|
|
118
|
+
const FACT = "before:mx-1.5 before:text-outline-hover before:content-['·']";
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
{#snippet paramList()}
|
|
122
|
+
<dl class="grid max-h-72 grid-cols-[max-content_1fr] gap-x-4 gap-y-1.5 overflow-y-auto">
|
|
123
|
+
{#each paramEntries as [key, value] (key)}
|
|
124
|
+
<dt class="font-mono text-xs text-on-surface-muted">{key}</dt>
|
|
125
|
+
<dd class="font-mono text-xs font-medium break-all text-on-surface">{value}</dd>
|
|
126
|
+
{/each}
|
|
127
|
+
</dl>
|
|
128
|
+
{/snippet}
|
|
129
|
+
|
|
130
|
+
{#snippet copyId()}
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onclick={() => void runIdCopy.copy(run.id)}
|
|
134
|
+
class="inline-flex items-center gap-1 text-on-surface-muted hover:text-primary"
|
|
135
|
+
title="Copy run ID"
|
|
136
|
+
>
|
|
137
|
+
{#if runIdCopy.copied}
|
|
138
|
+
<Check size={11} class="text-success-surface" />Copied
|
|
139
|
+
{:else}
|
|
140
|
+
<Copy size={11} />copy
|
|
141
|
+
{/if}
|
|
142
|
+
</button>
|
|
143
|
+
{/snippet}
|
|
144
|
+
|
|
145
|
+
<!-- One line: verdict, then the facts that fit, then the actions. Facts that
|
|
146
|
+
don't fit fold into ⋯, the ID first. -->
|
|
147
|
+
<div
|
|
148
|
+
class="head-line @container relative flex shrink-0 items-center gap-2.5 border-b border-outline-faint py-[7px] pr-3.5 pl-[18px]"
|
|
149
|
+
style="--rw-oc: {config.alarm ?? 'var(--color-surface-raised)'}"
|
|
150
|
+
>
|
|
151
|
+
{#if onBack}
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
onclick={() => onBack()}
|
|
155
|
+
class="-ml-1.5 shrink-0 rounded-[3px] p-1.5 text-on-surface-muted hover:bg-surface-sunken hover:text-primary"
|
|
156
|
+
title="Back to runs"
|
|
157
|
+
aria-label="Back to runs"
|
|
158
|
+
>
|
|
159
|
+
<ArrowLeft size={18} />
|
|
160
|
+
</button>
|
|
161
|
+
{/if}
|
|
162
|
+
|
|
163
|
+
<div
|
|
164
|
+
use:foldToFit={{ order, onfold: (keys) => (folded = keys) }}
|
|
165
|
+
class="flex min-w-0 flex-1 items-baseline gap-2.5 overflow-hidden whitespace-nowrap"
|
|
166
|
+
>
|
|
167
|
+
<!-- A plain title, not Tooltip: the line clips anything that overflows it. -->
|
|
168
|
+
<h2
|
|
169
|
+
class="flex shrink-0 items-baseline gap-x-1.5 text-[15px]"
|
|
170
|
+
data-testid="run-verdict"
|
|
171
|
+
data-status={status}
|
|
172
|
+
data-run={run.id}
|
|
173
|
+
title={config.description}
|
|
174
|
+
>
|
|
175
|
+
<config.icon
|
|
176
|
+
size={14}
|
|
177
|
+
strokeWidth={2.5}
|
|
178
|
+
class="shrink-0 self-center {config.color} {status === 'running'
|
|
179
|
+
? 'animate-spin'
|
|
180
|
+
: ''}"
|
|
181
|
+
/>
|
|
182
|
+
<span class="font-sans font-semibold {config.alarm ? config.color : 'text-on-surface'}"
|
|
183
|
+
>{verdict.verb}</span
|
|
184
|
+
>
|
|
185
|
+
{#if verdict.timed && duration}
|
|
186
|
+
<span
|
|
187
|
+
class="font-mono text-[13px] font-medium text-on-surface tabular-nums"
|
|
188
|
+
data-testid="run-duration">{duration}</span
|
|
189
|
+
>
|
|
190
|
+
{/if}
|
|
191
|
+
{#if status === "failed"}
|
|
192
|
+
<span class="font-mono text-[13px] font-medium tabular-nums">
|
|
193
|
+
<span class="text-on-surface-faint" aria-hidden="true">·</span>
|
|
194
|
+
<span class="text-danger-surface" data-testid="run-exit"
|
|
195
|
+
>exit {run.exitCode}</span
|
|
196
|
+
>
|
|
197
|
+
</span>
|
|
198
|
+
{/if}
|
|
199
|
+
</h2>
|
|
200
|
+
|
|
201
|
+
<span
|
|
202
|
+
data-fold-shrink
|
|
203
|
+
class="min-w-0 truncate font-mono text-[11.5px] text-on-surface-faint tabular-nums"
|
|
204
|
+
>
|
|
205
|
+
{#each facts as fact (fact.key)}
|
|
206
|
+
{#if fact.key === "start" && run.startedAt}
|
|
207
|
+
<span
|
|
208
|
+
data-fold="start"
|
|
209
|
+
data-testid="run-started"
|
|
210
|
+
class={FACT}
|
|
211
|
+
title={formatFullDateTime(run.startedAt)}>{fact.value}</span
|
|
212
|
+
>
|
|
213
|
+
{:else if fact.key === "params"}
|
|
214
|
+
<span data-fold="params" class={FACT}>
|
|
215
|
+
<Popover placement="bottom-start">
|
|
216
|
+
{#snippet trigger()}
|
|
217
|
+
<span
|
|
218
|
+
class="inline-flex cursor-pointer items-center gap-1 border-b border-dotted border-outline-hover text-on-surface-muted hover:text-primary"
|
|
219
|
+
>
|
|
220
|
+
<SlidersHorizontal size={11} />{fact.value}
|
|
221
|
+
</span>
|
|
222
|
+
{/snippet}
|
|
223
|
+
<div class="max-w-md min-w-48">{@render paramList()}</div>
|
|
224
|
+
</Popover>
|
|
225
|
+
</span>
|
|
226
|
+
{:else if fact.key === "usage"}
|
|
227
|
+
<span
|
|
228
|
+
data-fold="usage"
|
|
229
|
+
data-testid={live ? "run-live-usage" : "run-usage"}
|
|
230
|
+
class="{FACT} text-on-surface-muted"
|
|
231
|
+
title={live
|
|
232
|
+
? "Live CPU (100% = one core) and memory of the run's processes"
|
|
233
|
+
: "CPU time and most memory the run's processes used"}
|
|
234
|
+
>{fact.value}</span
|
|
235
|
+
>
|
|
236
|
+
{:else if fact.key === "id"}
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
data-fold="id"
|
|
240
|
+
onclick={() => void runIdCopy.copy(run.id)}
|
|
241
|
+
class="{FACT} hover:text-primary"
|
|
242
|
+
title="Copy run ID {run.id}"
|
|
243
|
+
>{runIdCopy.copied ? "Copied" : fact.value}</button
|
|
244
|
+
>
|
|
245
|
+
{:else}
|
|
246
|
+
<span
|
|
247
|
+
data-fold={fact.key}
|
|
248
|
+
class="{FACT} {fact.key === 'queued' ? 'text-warning-soft-text' : ''}"
|
|
249
|
+
>{fact.value}</span
|
|
250
|
+
>
|
|
251
|
+
{/if}
|
|
252
|
+
{/each}
|
|
253
|
+
</span>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
257
|
+
{@render actions()}
|
|
258
|
+
{#if foldedFacts.length > 0}
|
|
259
|
+
<Popover placement="bottom-end">
|
|
260
|
+
{#snippet trigger()}
|
|
261
|
+
<span
|
|
262
|
+
class="flex cursor-pointer 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"
|
|
263
|
+
title="More about this run"
|
|
264
|
+
aria-label="More about this run"
|
|
265
|
+
>
|
|
266
|
+
<Ellipsis size={15} />
|
|
267
|
+
</span>
|
|
268
|
+
{/snippet}
|
|
269
|
+
<dl class="grid min-w-56 grid-cols-[max-content_1fr] gap-x-4 gap-y-2 text-xs">
|
|
270
|
+
{#each foldedFacts as fact (fact.key)}
|
|
271
|
+
<dt class="text-on-surface-muted">{fact.label}</dt>
|
|
272
|
+
<dd class="font-mono text-on-surface">
|
|
273
|
+
{#if fact.key === "id"}
|
|
274
|
+
<span class="mr-2 break-all">{run.id}</span>{@render copyId()}
|
|
275
|
+
{:else if fact.key === "params"}
|
|
276
|
+
{@render paramList()}
|
|
277
|
+
{:else if fact.key === "start" && run.startedAt}
|
|
278
|
+
{formatFullDateTime(run.startedAt)}
|
|
279
|
+
{:else}
|
|
280
|
+
{fact.value}
|
|
281
|
+
{/if}
|
|
282
|
+
</dd>
|
|
283
|
+
{/each}
|
|
284
|
+
</dl>
|
|
285
|
+
</Popover>
|
|
286
|
+
{/if}
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<style>
|
|
291
|
+
/* Same wash as the stacked header: only a run worth triaging tints it. */
|
|
292
|
+
.head-line {
|
|
293
|
+
background: color-mix(in srgb, var(--rw-oc) 7%, var(--color-surface-raised));
|
|
294
|
+
}
|
|
295
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
let { label, showTaskName }: { label: string; showTaskName: boolean } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<!-- Placeholders shaped like the list's run rows (same padding, dot, text and
|
|
9
|
+
right readout), while a search or the first page is in flight. -->
|
|
10
|
+
<div class="flex flex-col gap-0.5" role="status" aria-busy="true" aria-label={label}>
|
|
11
|
+
{#each [0, 1, 2, 3, 4, 5] as i (i)}
|
|
12
|
+
<div
|
|
13
|
+
class="rounded-[3px] border border-transparent {showTaskName
|
|
14
|
+
? 'p-3'
|
|
15
|
+
: 'px-3 py-[11px]'}"
|
|
16
|
+
>
|
|
17
|
+
<div class="flex items-center {showTaskName ? 'gap-2.5' : 'gap-[11px]'}">
|
|
18
|
+
<span class="size-[9px] shrink-0 animate-pulse rounded-full bg-outline-hover"
|
|
19
|
+
></span>
|
|
20
|
+
{#if showTaskName}
|
|
21
|
+
<span class="flex min-w-0 flex-1 flex-col gap-1.5 py-0.5">
|
|
22
|
+
<span
|
|
23
|
+
class="h-3 animate-pulse rounded-[3px] bg-outline-hover"
|
|
24
|
+
style:width="{62 - (i % 3) * 12}%"
|
|
25
|
+
></span>
|
|
26
|
+
<span class="h-2 w-2/5 animate-pulse rounded-[3px] bg-outline-hover"></span>
|
|
27
|
+
</span>
|
|
28
|
+
<span
|
|
29
|
+
class="h-2.5 w-9 shrink-0 animate-pulse self-start rounded-[3px] bg-outline-hover"
|
|
30
|
+
></span>
|
|
31
|
+
{:else}
|
|
32
|
+
<span
|
|
33
|
+
class="h-3 animate-pulse rounded-[3px] bg-outline-hover"
|
|
34
|
+
style:width="{58 - (i % 3) * 8}%"
|
|
35
|
+
></span>
|
|
36
|
+
<span
|
|
37
|
+
class="ml-auto h-2.5 w-9 shrink-0 animate-pulse rounded-[3px] bg-outline-hover"
|
|
38
|
+
></span>
|
|
39
|
+
{/if}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
{/each}
|
|
43
|
+
</div>
|