@runwisp/ui 1.5.1 → 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/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 +3 -24
- package/src/lib/components/Button.svelte +2 -9
- package/src/lib/components/CodeBlock.svelte +7 -6
- package/src/lib/components/Drawer.svelte +22 -30
- package/src/lib/components/DurationInput.svelte +6 -25
- package/src/lib/components/FilterBar.svelte +2 -6
- package/src/lib/components/Link.svelte +9 -4
- package/src/lib/components/LogConsole.svelte +95 -418
- package/src/lib/components/Modal.svelte +7 -30
- package/src/lib/components/Spinner.svelte +10 -5
- package/src/lib/components/StatusIndicator.svelte +37 -104
- package/src/lib/components/Tabs.svelte +15 -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 +130 -717
- 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 +4 -5
- 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 +265 -543
- 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 +9 -0
- package/src/lib/components/dashboard/cron-format.ts +5 -1
- package/src/lib/components/dashboard/run-filters.test.ts +60 -0
- package/src/lib/components/dashboard/run-filters.ts +78 -6
- package/src/lib/components/dashboard/run-helpers.test.ts +122 -12
- package/src/lib/components/dashboard/run-helpers.ts +122 -6
- package/src/lib/components/dashboard/status-config.ts +18 -0
- package/src/lib/components/dashboard/types.ts +6 -0
- 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/LogConsoleFooter.svelte +88 -0
- 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/utils/format.test.ts +122 -0
- package/src/lib/utils/format.ts +97 -4
- 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/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}
|
|
@@ -70,10 +70,6 @@
|
|
|
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
74
|
// HTML attribute exists, it must be set on the DOM node).
|
|
79
75
|
function indeterminate(node: HTMLInputElement, value: boolean) {
|
|
@@ -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>
|