@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
|
@@ -1,50 +1,32 @@
|
|
|
1
1
|
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
2
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
3
|
|
|
4
|
-
<script lang="ts" module>
|
|
5
|
-
/** A single output-search hit surfaced under its run in the history rail. */
|
|
6
|
-
export interface RunOutputMatch {
|
|
7
|
-
line: number;
|
|
8
|
-
text: string;
|
|
9
|
-
}
|
|
10
|
-
</script>
|
|
11
|
-
|
|
12
4
|
<script lang="ts">
|
|
13
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Clock,
|
|
7
|
+
ArrowUpDown,
|
|
8
|
+
PanelLeftClose,
|
|
9
|
+
PanelLeftOpen,
|
|
10
|
+
Square,
|
|
11
|
+
Trash,
|
|
12
|
+
RotateCw,
|
|
13
|
+
} from "@lucide/svelte";
|
|
14
14
|
import { untrack } from "svelte";
|
|
15
|
-
import { SvelteSet } from "svelte/reactivity";
|
|
16
15
|
import { createVirtualizer } from "@tanstack/svelte-virtual";
|
|
17
16
|
import Button from "../Button.svelte";
|
|
18
17
|
import { arrival, leave, prefersReducedMotion } from "../../actions/row-motion.js";
|
|
19
18
|
import type { RunMotion } from "../../utils/run-motion.js";
|
|
20
19
|
import EmptyState from "../EmptyState.svelte";
|
|
20
|
+
import { BulkSelection } from "./bulk-selection.svelte.js";
|
|
21
|
+
import RunFilterChips from "./RunFilterChips.svelte";
|
|
21
22
|
import RunFilterPopover from "./RunFilterPopover.svelte";
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
exitCodeRange,
|
|
30
|
-
exitCodeChipLabel,
|
|
31
|
-
isWholeDay,
|
|
32
|
-
type RunsListFilters,
|
|
33
|
-
type FilterDimension,
|
|
34
|
-
} from "./run-filters.js";
|
|
35
|
-
import {
|
|
36
|
-
runDuration,
|
|
37
|
-
formatTriggeredByLabel,
|
|
38
|
-
runRetryLabel,
|
|
39
|
-
instanceSuffix,
|
|
40
|
-
} from "./run-helpers.js";
|
|
41
|
-
import {
|
|
42
|
-
formatDateTime,
|
|
43
|
-
formatFullDateTime,
|
|
44
|
-
formatTimeHM,
|
|
45
|
-
formatDayMonth,
|
|
46
|
-
formatCalendarDate,
|
|
47
|
-
} from "../../utils/format.js";
|
|
23
|
+
import RunListSkeleton from "./RunListSkeleton.svelte";
|
|
24
|
+
import RunRow from "./RunRow.svelte";
|
|
25
|
+
import type { RunOutputMatch } from "./types.js";
|
|
26
|
+
import type { Run, RunSelector } from "@runwisp/common";
|
|
27
|
+
import { runFilterParams, type RunsListFilters } from "./run-filters.js";
|
|
28
|
+
import { instanceSuffix, runUsageScale } from "./run-helpers.js";
|
|
29
|
+
import { formatDateTime } from "../../utils/format.js";
|
|
48
30
|
|
|
49
31
|
type BulkHandler = (selector: RunSelector, affected: Run[]) => void;
|
|
50
32
|
|
|
@@ -61,7 +43,6 @@
|
|
|
61
43
|
tasks = [],
|
|
62
44
|
showTaskName = false,
|
|
63
45
|
taskNameFilter,
|
|
64
|
-
headerLabel = "Run History",
|
|
65
46
|
emptyText = "No runs yet",
|
|
66
47
|
emptyDescription,
|
|
67
48
|
bulkActions = false,
|
|
@@ -74,6 +55,9 @@
|
|
|
74
55
|
outputQuery = "",
|
|
75
56
|
outputMatches = null,
|
|
76
57
|
outputSearchPending = false,
|
|
58
|
+
usageBars = false,
|
|
59
|
+
folded = false,
|
|
60
|
+
onToggleFold,
|
|
77
61
|
}: {
|
|
78
62
|
items: Run[];
|
|
79
63
|
total: number;
|
|
@@ -90,7 +74,6 @@
|
|
|
90
74
|
tasks?: { name: string }[];
|
|
91
75
|
showTaskName?: boolean;
|
|
92
76
|
taskNameFilter?: string;
|
|
93
|
-
headerLabel?: string;
|
|
94
77
|
emptyText?: string;
|
|
95
78
|
emptyDescription?: string;
|
|
96
79
|
bulkActions?: boolean;
|
|
@@ -104,10 +87,10 @@
|
|
|
104
87
|
// in or out. Page loads, scrolling and filtering never do.
|
|
105
88
|
motion?: RunMotion;
|
|
106
89
|
// Output search (history rail): filters runs by what they printed. The
|
|
107
|
-
// search box
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
90
|
+
// parent owns the search box, the query and the async log search; this
|
|
91
|
+
// component just renders the matched rows. `outputSearch` enables the
|
|
92
|
+
// mode; `outputQuery` is the live query (drives the active state and
|
|
93
|
+
// snippet highlighting).
|
|
111
94
|
outputSearch?: boolean;
|
|
112
95
|
outputQuery?: string;
|
|
113
96
|
// run id → first matching line, supplied by the parent after a search.
|
|
@@ -116,25 +99,29 @@
|
|
|
116
99
|
// True while a query is typed but its results aren't in yet (debounce
|
|
117
100
|
// window or request in flight), the rail shows its searching shimmer.
|
|
118
101
|
outputSearchPending?: boolean;
|
|
102
|
+
// Task rail only: two small bars per finished run, CPU time over RAM,
|
|
103
|
+
// scaled to the largest loaded run and colored against the usual one.
|
|
104
|
+
usageBars?: boolean;
|
|
105
|
+
// Folds the list to a thin strip that still counts the runs, giving the
|
|
106
|
+
// run detail the width. Omitted where the screen keeps the list open.
|
|
107
|
+
folded?: boolean;
|
|
108
|
+
onToggleFold?: (() => void) | undefined;
|
|
119
109
|
} = $props();
|
|
120
110
|
|
|
111
|
+
const usageScale = $derived(usageBars ? runUsageScale(items) : undefined);
|
|
112
|
+
|
|
121
113
|
// Task-rail rows are one dense line (44px); the cross-task /runs view adds
|
|
122
114
|
// the task name on a second line (64px).
|
|
123
115
|
const rowHeight = $derived(showTaskName ? 64 : 44);
|
|
124
116
|
const OVERSCAN = 8;
|
|
125
117
|
const LOAD_AHEAD = 10;
|
|
126
118
|
|
|
127
|
-
|
|
128
|
-
// 1. explicit: user picked specific rows. explicitIds holds them.
|
|
129
|
-
// 2. selectAll: "all matching the current filter". exceptIds holds opt-outs.
|
|
130
|
-
let selectAllMode = $state(false);
|
|
131
|
-
const explicitIds = new SvelteSet<string>();
|
|
132
|
-
const exceptIds = new SvelteSet<string>();
|
|
119
|
+
const selection = new BulkSelection();
|
|
133
120
|
|
|
134
121
|
let scrollElement: HTMLDivElement | undefined = $state();
|
|
135
122
|
|
|
136
|
-
// Output search filters the rail by what each run printed
|
|
137
|
-
//
|
|
123
|
+
// Output search filters the rail by what each run printed; the query is the
|
|
124
|
+
// parent's, this component only renders against it.
|
|
138
125
|
const outputSearchActive = $derived(outputSearch && outputQuery.trim().length > 0);
|
|
139
126
|
|
|
140
127
|
// Loaded runs that matched the output search, in list order. A match in a
|
|
@@ -144,41 +131,6 @@
|
|
|
144
131
|
outputMatches ? items.filter((r: Run) => outputMatches.has(r.id)) : [],
|
|
145
132
|
);
|
|
146
133
|
|
|
147
|
-
// Split the matching line into [before, match, after] around the first
|
|
148
|
-
// occurrence of the query, windowed to keep the match in view, mirrors the
|
|
149
|
-
// artifact's snippet. Rendered as plain text spans (Svelte auto-escapes), so
|
|
150
|
-
// no untrusted HTML ever reaches the DOM.
|
|
151
|
-
interface HighlightParts {
|
|
152
|
-
before: string;
|
|
153
|
-
match: string;
|
|
154
|
-
after: string;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function highlightParts(text: string, query: string): HighlightParts {
|
|
158
|
-
const q = query.trim();
|
|
159
|
-
const idx = q ? text.toLowerCase().indexOf(q.toLowerCase()) : -1;
|
|
160
|
-
if (idx === -1) return { before: text, match: "", after: "" };
|
|
161
|
-
const start = Math.max(0, idx - 14);
|
|
162
|
-
const lead = start > 0 ? "…" : "";
|
|
163
|
-
const windowed = text.slice(start);
|
|
164
|
-
const fi = windowed.toLowerCase().indexOf(q.toLowerCase());
|
|
165
|
-
return {
|
|
166
|
-
before: lead + windowed.slice(0, fi),
|
|
167
|
-
match: windowed.slice(fi, fi + q.length),
|
|
168
|
-
after: windowed.slice(fi + q.length),
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Right-hand mono readout on a task-rail row: live / queued / exit N / dur.
|
|
173
|
-
function rowRightLabel(run: Run, displayed: RunStatus): string {
|
|
174
|
-
if (run.status === "running") return "live";
|
|
175
|
-
if (run.status === "pending") return "queued";
|
|
176
|
-
if (displayed === "failed" || displayed === "crashed") {
|
|
177
|
-
return "exit " + String(run.exitCode);
|
|
178
|
-
}
|
|
179
|
-
return runDuration(run) ?? "—";
|
|
180
|
-
}
|
|
181
|
-
|
|
182
134
|
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
|
|
183
135
|
count: 0,
|
|
184
136
|
getScrollElement: () => scrollElement ?? null,
|
|
@@ -232,45 +184,18 @@
|
|
|
232
184
|
untrack(() => $virtualizer.scrollToIndex(index, { align: "auto", behavior }));
|
|
233
185
|
});
|
|
234
186
|
|
|
235
|
-
function isRowSelected(id: string): boolean {
|
|
236
|
-
return selectAllMode ? !exceptIds.has(id) : explicitIds.has(id);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function toggleRow(id: string) {
|
|
240
|
-
if (selectAllMode) {
|
|
241
|
-
if (exceptIds.has(id)) exceptIds.delete(id);
|
|
242
|
-
else exceptIds.add(id);
|
|
243
|
-
} else {
|
|
244
|
-
if (explicitIds.has(id)) explicitIds.delete(id);
|
|
245
|
-
else explicitIds.add(id);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function clearSelection() {
|
|
250
|
-
selectAllMode = false;
|
|
251
|
-
explicitIds.clear();
|
|
252
|
-
exceptIds.clear();
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function selectAllVisible() {
|
|
256
|
-
selectAllMode = true;
|
|
257
|
-
explicitIds.clear();
|
|
258
|
-
exceptIds.clear();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
187
|
function selectRun(runId: string) {
|
|
262
188
|
selectedRunId = runId;
|
|
263
189
|
onselect?.(runId);
|
|
264
190
|
}
|
|
265
191
|
|
|
266
|
-
let selectedRuns = $derived(items.filter((r: Run) =>
|
|
192
|
+
let selectedRuns = $derived(items.filter((r: Run) => selection.isSelected(r.id)));
|
|
267
193
|
let selectionCount = $derived(selectedRuns.length);
|
|
268
194
|
let hasSelection = $derived(selectionCount > 0);
|
|
269
195
|
// Task-rail rows overlay the checkbox on the status dot (it appears on hover
|
|
270
196
|
// or once a selection exists). selectionActive forces all checkboxes visible
|
|
271
197
|
// so the operator can extend the selection without hunting per-row hovers.
|
|
272
198
|
let selectionActive = $derived(bulkActions && hasSelection);
|
|
273
|
-
let allSelected = $derived(selectAllMode && exceptIds.size === 0);
|
|
274
199
|
|
|
275
200
|
let anyRunning = $derived(selectedRuns.some((r: Run) => r.status === "running"));
|
|
276
201
|
let anyTerminal = $derived(
|
|
@@ -282,90 +207,33 @@
|
|
|
282
207
|
// popover dimensions only apply where the popover is shown; the task scope
|
|
283
208
|
// (page-injected or popover-set) always applies.
|
|
284
209
|
function buildSelectorFilter(): NonNullable<RunSelector["filter"]> {
|
|
285
|
-
const filter: NonNullable<RunSelector["filter"]> =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
const query = filters.search.trim();
|
|
291
|
-
if (query) filter.search = query;
|
|
292
|
-
if (filters.createdAfter) filter.createdAfter = filters.createdAfter;
|
|
293
|
-
if (filters.createdBefore) filter.createdBefore = filters.createdBefore;
|
|
294
|
-
if (filters.triggeredBy) filter.triggeredBy = filters.triggeredBy;
|
|
295
|
-
const exit = exitCodeRange(filters.exitCode);
|
|
296
|
-
if (exit.min !== undefined) filter.exitCodeMin = exit.min;
|
|
297
|
-
if (exit.max !== undefined) filter.exitCodeMax = exit.max;
|
|
298
|
-
if (filters.retriesOnly) filter.retriesOnly = true;
|
|
210
|
+
const filter: NonNullable<RunSelector["filter"]> = showFilters
|
|
211
|
+
? runFilterParams(filters)
|
|
212
|
+
: {};
|
|
213
|
+
const taskName = taskNameFilter || filters.taskName;
|
|
214
|
+
if (taskName) filter.taskName = taskName;
|
|
299
215
|
return filter;
|
|
300
216
|
}
|
|
301
217
|
|
|
302
|
-
function buildSelector(): RunSelector {
|
|
303
|
-
if (!selectAllMode) {
|
|
304
|
-
return { matchAll: false, ids: [...explicitIds] };
|
|
305
|
-
}
|
|
306
|
-
return {
|
|
307
|
-
matchAll: true,
|
|
308
|
-
filter: buildSelectorFilter(),
|
|
309
|
-
exceptIds: [...exceptIds],
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Active-filter chips for the header row. `task` only chips on the
|
|
314
|
-
// cross-task view; everywhere else the task name is the page scope.
|
|
315
|
-
const filterChips = $derived(
|
|
316
|
-
activeDimensions(filters)
|
|
317
|
-
.filter((dim) => dim !== "task" || showTask)
|
|
318
|
-
.map((dim) => ({ dimension: dim, label: chipLabel(dim) })),
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
function chipLabel(dim: FilterDimension): string {
|
|
322
|
-
switch (dim) {
|
|
323
|
-
case "status":
|
|
324
|
-
return statusChipLabel(filters.statuses);
|
|
325
|
-
case "time":
|
|
326
|
-
return timeChipLabel();
|
|
327
|
-
case "task":
|
|
328
|
-
return filters.taskName ?? "";
|
|
329
|
-
case "triggeredBy":
|
|
330
|
-
return "Trigger: " + triggerDescription(filters.triggeredBy ?? "");
|
|
331
|
-
case "exitCode":
|
|
332
|
-
return exitCodeChipLabel(filters.exitCode);
|
|
333
|
-
case "retries":
|
|
334
|
-
return "Retries only";
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function timeChipLabel(): string {
|
|
339
|
-
const after = filters.createdAfter;
|
|
340
|
-
const before = filters.createdBefore;
|
|
341
|
-
if (isWholeDay(after, before) && after) return `On ${formatCalendarDate(after)}`;
|
|
342
|
-
if (after && before) return `${formatDateTime(after)} – ${formatDateTime(before)}`;
|
|
343
|
-
if (after) return `Since ${formatDateTime(after)}`;
|
|
344
|
-
if (before) return `Before ${formatDateTime(before)}`;
|
|
345
|
-
return "";
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function removeChip(dim: FilterDimension) {
|
|
349
|
-
filters = clearDimension(filters, dim);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
218
|
function emitBulk(handler: BulkHandler | undefined, predicate: (r: Run) => boolean) {
|
|
353
219
|
if (!handler) return;
|
|
354
220
|
const affected = selectedRuns.filter(predicate);
|
|
355
221
|
if (affected.length === 0) return;
|
|
356
|
-
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
222
|
+
// In explicit mode the selector is narrowed to the affected rows, so
|
|
223
|
+
// the server is never asked to touch rows the UI excluded.
|
|
224
|
+
handler(
|
|
225
|
+
selection.selector(
|
|
226
|
+
buildSelectorFilter(),
|
|
227
|
+
affected.map((r) => r.id),
|
|
228
|
+
),
|
|
229
|
+
affected,
|
|
230
|
+
);
|
|
231
|
+
selection.clear();
|
|
364
232
|
}
|
|
365
233
|
|
|
366
234
|
function handleMasterToggle() {
|
|
367
|
-
if (hasSelection)
|
|
368
|
-
else
|
|
235
|
+
if (hasSelection) selection.clear();
|
|
236
|
+
else selection.selectAll();
|
|
369
237
|
}
|
|
370
238
|
|
|
371
239
|
function toggleSortDirection() {
|
|
@@ -378,282 +246,229 @@
|
|
|
378
246
|
};
|
|
379
247
|
}
|
|
380
248
|
|
|
249
|
+
// The count stands in for a title: "12 runs", or matches while searching.
|
|
250
|
+
// Empty until the first page lands, when the count isn't known yet.
|
|
251
|
+
const countLabel = $derived.by(() => {
|
|
252
|
+
if (outputSearchActive) return `${String(matchedRuns.length)} of ${String(items.length)}`;
|
|
253
|
+
if (loading && items.length === 0) return "";
|
|
254
|
+
return `${String(total)} ${total === 1 ? "run" : "runs"}`;
|
|
255
|
+
});
|
|
256
|
+
|
|
381
257
|
let masterCheckboxRef: HTMLInputElement | undefined = $state();
|
|
382
258
|
$effect(() => {
|
|
383
259
|
if (!masterCheckboxRef) return;
|
|
384
|
-
masterCheckboxRef.indeterminate = hasSelection && !allSelected;
|
|
260
|
+
masterCheckboxRef.indeterminate = hasSelection && !selection.allSelected;
|
|
385
261
|
});
|
|
386
262
|
</script>
|
|
387
263
|
|
|
388
|
-
|
|
264
|
+
{#if folded && onToggleFold}
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
class="flex w-[30px] shrink-0 flex-col items-center gap-2 border-r border-outline bg-surface pt-2.5 font-mono text-xs text-on-surface-muted hover:bg-surface-sunken hover:text-primary"
|
|
268
|
+
title="Show run list"
|
|
269
|
+
aria-label="Show run list"
|
|
270
|
+
aria-expanded="false"
|
|
271
|
+
onclick={onToggleFold}
|
|
272
|
+
>
|
|
273
|
+
<PanelLeftOpen size={16} />
|
|
274
|
+
<span class="tabular-nums [writing-mode:vertical-rl]">{countLabel}</span>
|
|
275
|
+
</button>
|
|
276
|
+
{:else}
|
|
277
|
+
<!-- A borderless rail that fills its column, divided from the detail panel by
|
|
389
278
|
a single right border. -->
|
|
390
|
-
<div
|
|
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"
|
|
392
|
-
>
|
|
393
279
|
<div
|
|
394
|
-
class="flex
|
|
395
|
-
? 'border-outline-faint bg-primary-soft/40'
|
|
396
|
-
: 'border-transparent bg-surface'}"
|
|
280
|
+
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"
|
|
397
281
|
>
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
282
|
+
<div
|
|
283
|
+
class="flex shrink-0 items-center gap-2 border-b px-3 py-2 {hasSelection
|
|
284
|
+
? 'border-outline-faint bg-primary-soft/40'
|
|
285
|
+
: 'border-transparent bg-surface'}"
|
|
286
|
+
>
|
|
287
|
+
{#if bulkActions}
|
|
288
|
+
<label
|
|
289
|
+
class="flex shrink-0 cursor-pointer items-center"
|
|
290
|
+
title={hasSelection ? "Clear selection" : "Select all"}
|
|
291
|
+
>
|
|
292
|
+
<input
|
|
293
|
+
bind:this={masterCheckboxRef}
|
|
294
|
+
type="checkbox"
|
|
295
|
+
checked={selection.allSelected}
|
|
296
|
+
onchange={handleMasterToggle}
|
|
297
|
+
class="h-3.5 w-3.5 cursor-pointer rounded border-outline accent-primary"
|
|
298
|
+
aria-label={hasSelection ? "Clear selection" : "Select all"}
|
|
299
|
+
/>
|
|
300
|
+
</label>
|
|
301
|
+
{/if}
|
|
413
302
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
303
|
+
{#if hasSelection}
|
|
304
|
+
<span class="font-mono text-xs font-medium text-on-surface tabular-nums">
|
|
305
|
+
{selectionCount} selected
|
|
306
|
+
</span>
|
|
307
|
+
<div class="ml-auto flex items-center gap-1">
|
|
308
|
+
{#if onBulkRerun}
|
|
309
|
+
<Button
|
|
310
|
+
variant="ghost"
|
|
311
|
+
size="xs"
|
|
312
|
+
class="h-7 w-7 px-0"
|
|
313
|
+
onclick={() => emitBulk(onBulkRerun, () => true)}
|
|
314
|
+
title="Re-run task{selectionCount === 1 ? '' : 's'}"
|
|
315
|
+
>
|
|
316
|
+
{#snippet icon()}<RotateCw size={14} />{/snippet}
|
|
317
|
+
</Button>
|
|
318
|
+
{/if}
|
|
319
|
+
{#if onBulkCancel && anyRunning}
|
|
320
|
+
<Button
|
|
321
|
+
variant="ghost"
|
|
322
|
+
size="xs"
|
|
323
|
+
class="h-7 w-7 px-0"
|
|
324
|
+
onclick={() => emitBulk(onBulkCancel, (r) => r.status === "running")}
|
|
325
|
+
title="Cancel running run{selectionCount === 1 ? '' : 's'}"
|
|
326
|
+
>
|
|
327
|
+
{#snippet icon()}<Square size={14} />{/snippet}
|
|
328
|
+
</Button>
|
|
329
|
+
{/if}
|
|
330
|
+
{#if onBulkDelete && anyTerminal}
|
|
331
|
+
<Button
|
|
332
|
+
variant="danger"
|
|
333
|
+
size="xs"
|
|
334
|
+
class="h-7 w-7 px-0"
|
|
335
|
+
onclick={() =>
|
|
336
|
+
emitBulk(
|
|
337
|
+
onBulkDelete,
|
|
338
|
+
(r) => r.status !== "running" && r.status !== "pending",
|
|
339
|
+
)}
|
|
340
|
+
title="Delete run{selectionCount === 1 ? '' : 's'}"
|
|
341
|
+
>
|
|
342
|
+
{#snippet icon()}<Trash size={14} />{/snippet}
|
|
343
|
+
</Button>
|
|
344
|
+
{/if}
|
|
345
|
+
</div>
|
|
346
|
+
{:else}
|
|
347
|
+
<span
|
|
348
|
+
class="shrink-0 font-mono text-xs whitespace-nowrap text-on-surface-muted tabular-nums"
|
|
349
|
+
>
|
|
350
|
+
{countLabel}
|
|
351
|
+
</span>
|
|
352
|
+
<div class="ml-auto flex items-center gap-1">
|
|
353
|
+
{#if showFilters}
|
|
354
|
+
<RunFilterPopover bind:filters {showTask} {tasks} />
|
|
355
|
+
{/if}
|
|
431
356
|
<Button
|
|
432
357
|
variant="ghost"
|
|
433
358
|
size="xs"
|
|
434
359
|
class="h-7 w-7 px-0"
|
|
435
|
-
onclick={
|
|
436
|
-
title="
|
|
360
|
+
onclick={toggleSortDirection}
|
|
361
|
+
title="Toggle sort order"
|
|
437
362
|
>
|
|
438
|
-
{#snippet icon()}
|
|
363
|
+
{#snippet icon()}
|
|
364
|
+
<ArrowUpDown
|
|
365
|
+
size={14}
|
|
366
|
+
class="text-on-surface-muted {filters.sortDirection === 'asc'
|
|
367
|
+
? 'rotate-180'
|
|
368
|
+
: ''}"
|
|
369
|
+
/>
|
|
370
|
+
{/snippet}
|
|
439
371
|
</Button>
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
)}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
{:else}
|
|
458
|
-
<span
|
|
459
|
-
class="shrink-0 font-mono text-xs font-semibold tracking-[0.14em] whitespace-nowrap text-on-surface-muted uppercase"
|
|
460
|
-
>
|
|
461
|
-
{headerLabel}
|
|
462
|
-
</span>
|
|
463
|
-
<span
|
|
464
|
-
class="shrink-0 font-mono text-xs whitespace-nowrap text-on-surface-faint tabular-nums"
|
|
465
|
-
>
|
|
466
|
-
{#if outputSearchActive}
|
|
467
|
-
{matchedRuns.length} of {items.length}
|
|
468
|
-
{:else if loading && items.length === 0}
|
|
469
|
-
<!-- Count unknown until the first page lands. -->
|
|
470
|
-
{:else}
|
|
471
|
-
{total}
|
|
472
|
-
{total === 1 ? "run" : "runs"}
|
|
473
|
-
{/if}
|
|
474
|
-
</span>
|
|
475
|
-
<div class="ml-auto flex items-center gap-1">
|
|
476
|
-
{#if showFilters}
|
|
477
|
-
<RunFilterPopover bind:filters {showTask} {tasks} />
|
|
478
|
-
{/if}
|
|
479
|
-
<Button
|
|
480
|
-
variant="ghost"
|
|
481
|
-
size="xs"
|
|
482
|
-
class="h-7 w-7 px-0"
|
|
483
|
-
onclick={toggleSortDirection}
|
|
484
|
-
title="Toggle sort order"
|
|
485
|
-
>
|
|
486
|
-
{#snippet icon()}
|
|
487
|
-
<ArrowUpDown
|
|
488
|
-
size={14}
|
|
489
|
-
class="text-on-surface-muted {filters.sortDirection === 'asc'
|
|
490
|
-
? 'rotate-180'
|
|
491
|
-
: ''}"
|
|
492
|
-
/>
|
|
493
|
-
{/snippet}
|
|
494
|
-
</Button>
|
|
495
|
-
</div>
|
|
496
|
-
{/if}
|
|
497
|
-
</div>
|
|
498
|
-
|
|
499
|
-
{#if showFilters && filterChips.length > 0}
|
|
500
|
-
<!-- Active-filter chips. Rendered only when filters are set, so the
|
|
501
|
-
header stays clean when empty; each chip's X clears its dimension.
|
|
502
|
-
The filter controls themselves live in the popover above. -->
|
|
503
|
-
<div
|
|
504
|
-
class="flex shrink-0 flex-wrap gap-1 border-b border-outline-faint bg-surface-sunken px-3 py-2"
|
|
505
|
-
>
|
|
506
|
-
{#each filterChips as chip (chip.dimension)}
|
|
507
|
-
<span
|
|
508
|
-
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"
|
|
509
|
-
>
|
|
510
|
-
{chip.label}
|
|
511
|
-
<button
|
|
512
|
-
type="button"
|
|
513
|
-
onclick={() => removeChip(chip.dimension)}
|
|
514
|
-
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"
|
|
515
|
-
aria-label="Remove {chip.label} filter"
|
|
516
|
-
>
|
|
517
|
-
<X size={11} />
|
|
518
|
-
</button>
|
|
519
|
-
</span>
|
|
520
|
-
{/each}
|
|
372
|
+
{#if onToggleFold}
|
|
373
|
+
<Button
|
|
374
|
+
variant="ghost"
|
|
375
|
+
size="xs"
|
|
376
|
+
class="h-7 w-7 px-0"
|
|
377
|
+
onclick={onToggleFold}
|
|
378
|
+
title="Hide run list"
|
|
379
|
+
aria-label="Hide run list"
|
|
380
|
+
aria-expanded="true"
|
|
381
|
+
>
|
|
382
|
+
{#snippet icon()}
|
|
383
|
+
<PanelLeftClose size={14} class="text-on-surface-muted" />
|
|
384
|
+
{/snippet}
|
|
385
|
+
</Button>
|
|
386
|
+
{/if}
|
|
387
|
+
</div>
|
|
388
|
+
{/if}
|
|
521
389
|
</div>
|
|
522
|
-
{/if}
|
|
523
390
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
391
|
+
{#if showFilters}
|
|
392
|
+
<RunFilterChips bind:filters {showTask} />
|
|
393
|
+
{/if}
|
|
394
|
+
|
|
395
|
+
<div bind:this={scrollElement} class="min-h-0 flex-1 overflow-y-auto p-2">
|
|
396
|
+
{#if outputSearchActive}
|
|
397
|
+
<!-- Output-search results: the rail filters to runs that printed the
|
|
527
398
|
query, each annotated with the matching line. -->
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
399
|
+
{#if outputSearchPending}
|
|
400
|
+
<RunListSkeleton label="Searching output" {showTaskName} />
|
|
401
|
+
{:else if matchedRuns.length === 0}
|
|
402
|
+
<div
|
|
403
|
+
class="px-4 py-8 text-center text-xs leading-relaxed text-on-surface-muted"
|
|
404
|
+
>
|
|
405
|
+
No output matches
|
|
406
|
+
<b class="font-mono text-on-surface">“{outputQuery.trim()}”</b>.<br />
|
|
407
|
+
Try a different term.
|
|
408
|
+
</div>
|
|
409
|
+
{:else}
|
|
410
|
+
<div class="flex flex-col gap-0.5">
|
|
411
|
+
{#each matchedRuns as run (run.id)}
|
|
412
|
+
<div class="group/row relative flex items-stretch gap-1">
|
|
413
|
+
{#if bulkActions}
|
|
414
|
+
{@render rowCheckboxOverlay(run)}
|
|
415
|
+
{/if}
|
|
416
|
+
{@render runRow(run, outputMatches?.get(run.id))}
|
|
417
|
+
</div>
|
|
418
|
+
{/each}
|
|
419
|
+
</div>
|
|
420
|
+
{/if}
|
|
421
|
+
{:else if items.length === 0 && loading}
|
|
422
|
+
<RunListSkeleton label="Loading runs" {showTaskName} />
|
|
423
|
+
{:else if items.length === 0}
|
|
424
|
+
<EmptyState
|
|
425
|
+
title={emptyText}
|
|
426
|
+
description={emptyDescription}
|
|
427
|
+
icon={Clock}
|
|
428
|
+
iconSize={32}
|
|
429
|
+
class="py-8"
|
|
430
|
+
/>
|
|
536
431
|
{:else}
|
|
537
|
-
<div
|
|
538
|
-
{
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
outputMatches?.get(run.id),
|
|
547
|
-
)}
|
|
548
|
-
</div>
|
|
549
|
-
{/each}
|
|
550
|
-
</div>
|
|
551
|
-
{/if}
|
|
552
|
-
{:else if items.length === 0 && loading}
|
|
553
|
-
{@render skeletonRows("Loading runs")}
|
|
554
|
-
{:else if items.length === 0}
|
|
555
|
-
<EmptyState
|
|
556
|
-
title={emptyText}
|
|
557
|
-
description={emptyDescription}
|
|
558
|
-
icon={Clock}
|
|
559
|
-
iconSize={32}
|
|
560
|
-
class="py-8"
|
|
561
|
-
/>
|
|
562
|
-
{:else}
|
|
563
|
-
<div
|
|
564
|
-
style:height="{$virtualizer.getTotalSize()}px"
|
|
565
|
-
style:width="100%"
|
|
566
|
-
style:position="relative"
|
|
567
|
-
>
|
|
568
|
-
{#each $virtualizer.getVirtualItems() as row (items[row.index]?.id ?? row.index)}
|
|
569
|
-
{@const run = items[row.index]}
|
|
570
|
-
{#if run}
|
|
571
|
-
<!-- The transform transition slides rows aside when a live
|
|
432
|
+
<div
|
|
433
|
+
style:height="{$virtualizer.getTotalSize()}px"
|
|
434
|
+
style:width="100%"
|
|
435
|
+
style:position="relative"
|
|
436
|
+
>
|
|
437
|
+
{#each $virtualizer.getVirtualItems() as row (items[row.index]?.id ?? row.index)}
|
|
438
|
+
{@const run = items[row.index]}
|
|
439
|
+
{#if run}
|
|
440
|
+
<!-- The transform transition slides rows aside when a live
|
|
572
441
|
run is inserted or removed. Navigation never animates:
|
|
573
442
|
filter/sort/task changes remount rows, pagination
|
|
574
443
|
appends, and scrolling doesn't move a row. `leave`
|
|
575
444
|
is global because this {#if}, not the row's each
|
|
576
445
|
item, is the block it would otherwise wait for; it
|
|
577
446
|
only plays for runs removed live. -->
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
</div>
|
|
599
|
-
{/if}
|
|
600
|
-
</div>
|
|
601
|
-
</div>
|
|
602
|
-
|
|
603
|
-
<!-- Status dot. With bulk actions on it fades out, on row hover, or whenever a
|
|
604
|
-
selection exists, so the row checkbox can take its place over it. -->
|
|
605
|
-
<!-- Placeholders shaped like this list's run rows (same padding, dot, text
|
|
606
|
-
and right readout), while a search or the first page is in flight. -->
|
|
607
|
-
{#snippet skeletonRows(label: string)}
|
|
608
|
-
<div class="flex flex-col gap-0.5" role="status" aria-busy="true" aria-label={label}>
|
|
609
|
-
{#each [0, 1, 2, 3, 4, 5] as i (i)}
|
|
610
|
-
<div
|
|
611
|
-
class="rounded-[3px] border border-transparent {showTaskName
|
|
612
|
-
? 'p-3'
|
|
613
|
-
: 'px-3 py-[11px]'}"
|
|
614
|
-
>
|
|
615
|
-
<div class="flex items-center {showTaskName ? 'gap-2.5' : 'gap-[11px]'}">
|
|
616
|
-
<span class="size-[9px] shrink-0 animate-pulse rounded-full bg-outline-hover"
|
|
617
|
-
></span>
|
|
618
|
-
{#if showTaskName}
|
|
619
|
-
<span class="flex min-w-0 flex-1 flex-col gap-1.5 py-0.5">
|
|
620
|
-
<span
|
|
621
|
-
class="h-3 animate-pulse rounded-[3px] bg-outline-hover"
|
|
622
|
-
style:width="{62 - (i % 3) * 12}%"
|
|
623
|
-
></span>
|
|
624
|
-
<span class="h-2 w-2/5 animate-pulse rounded-[3px] bg-outline-hover"
|
|
625
|
-
></span>
|
|
626
|
-
</span>
|
|
627
|
-
<span
|
|
628
|
-
class="h-2.5 w-9 shrink-0 animate-pulse self-start rounded-[3px] bg-outline-hover"
|
|
629
|
-
></span>
|
|
630
|
-
{:else}
|
|
631
|
-
<span
|
|
632
|
-
class="h-3 animate-pulse rounded-[3px] bg-outline-hover"
|
|
633
|
-
style:width="{58 - (i % 3) * 8}%"
|
|
634
|
-
></span>
|
|
635
|
-
<span
|
|
636
|
-
class="ml-auto h-2.5 w-9 shrink-0 animate-pulse rounded-[3px] bg-outline-hover"
|
|
637
|
-
></span>
|
|
638
|
-
{/if}
|
|
447
|
+
<div
|
|
448
|
+
data-run-id={run.id}
|
|
449
|
+
use:arrival={motion?.arrived(run.id) ?? false}
|
|
450
|
+
out:leave|global={motion?.removed}
|
|
451
|
+
class="group/row flex items-center gap-1 transition-transform duration-200 ease-out motion-reduce:transition-none"
|
|
452
|
+
style:transition-delay="var(--rw-shift-delay, 0ms)"
|
|
453
|
+
style:position="absolute"
|
|
454
|
+
style:top="0"
|
|
455
|
+
style:left="0"
|
|
456
|
+
style:width="100%"
|
|
457
|
+
style:height="{row.size}px"
|
|
458
|
+
style:transform="translateY({row.start}px)"
|
|
459
|
+
>
|
|
460
|
+
{#if bulkActions}
|
|
461
|
+
{@render rowCheckboxOverlay(run)}
|
|
462
|
+
{/if}
|
|
463
|
+
{@render runRow(run, undefined)}
|
|
464
|
+
</div>
|
|
465
|
+
{/if}
|
|
466
|
+
{/each}
|
|
639
467
|
</div>
|
|
640
|
-
|
|
641
|
-
|
|
468
|
+
{/if}
|
|
469
|
+
</div>
|
|
642
470
|
</div>
|
|
643
|
-
{/
|
|
644
|
-
|
|
645
|
-
{#snippet statusDot(colorClass: string, running: boolean)}
|
|
646
|
-
<span
|
|
647
|
-
class="{colorClass} size-[9px] shrink-0 rounded-full bg-current ring-[3px] ring-current/20 {running
|
|
648
|
-
? 'animate-pulse'
|
|
649
|
-
: ''} {bulkActions
|
|
650
|
-
? selectionActive
|
|
651
|
-
? 'opacity-0'
|
|
652
|
-
: 'group-hover/row:opacity-0'
|
|
653
|
-
: ''}"
|
|
654
|
-
aria-hidden="true"
|
|
655
|
-
></span>
|
|
656
|
-
{/snippet}
|
|
471
|
+
{/if}
|
|
657
472
|
|
|
658
473
|
<!-- Row checkbox: sits over the status dot (which fades out beneath it) so the
|
|
659
474
|
row keeps the artifact's geometry. Lives in the row wrapper (not the button)
|
|
@@ -667,8 +482,8 @@
|
|
|
667
482
|
>
|
|
668
483
|
<input
|
|
669
484
|
type="checkbox"
|
|
670
|
-
checked={
|
|
671
|
-
onchange={() =>
|
|
485
|
+
checked={selection.isSelected(run.id)}
|
|
486
|
+
onchange={() => selection.toggle(run.id)}
|
|
672
487
|
onclick={(e) => e.stopPropagation()}
|
|
673
488
|
aria-label={`Select run from ${formatDateTime(run.startedAt ?? run.createdAt)}`}
|
|
674
489
|
class="size-3.5 cursor-pointer rounded border-outline accent-primary opacity-0 {selectionActive
|
|
@@ -678,110 +493,17 @@
|
|
|
678
493
|
</label>
|
|
679
494
|
{/snippet}
|
|
680
495
|
|
|
681
|
-
{#snippet
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
: 'border-transparent hover:border-outline-hover hover:bg-surface-sunken'}"
|
|
695
|
-
onclick={() => selectRun(run.id)}
|
|
696
|
-
onkeydown={(e) => e.key === "Enter" && selectRun(run.id)}
|
|
697
|
-
>
|
|
698
|
-
{#if showTaskName}
|
|
699
|
-
<!-- Cross-task /runs variant: the same readout language as the task
|
|
700
|
-
rail, status dot, status-colored outcome, mono right readout,
|
|
701
|
-
with the task name carried as the primary. -->
|
|
702
|
-
<div class="flex items-center gap-2.5">
|
|
703
|
-
{@render statusDot(config.color, running)}
|
|
704
|
-
<span class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
705
|
-
<span class="flex items-center gap-1.5">
|
|
706
|
-
<span class="truncate font-mono text-[13px] font-semibold text-on-surface">
|
|
707
|
-
{run.taskName}{#if suffix}<span class="text-on-surface-muted"
|
|
708
|
-
>{suffix}</span
|
|
709
|
-
>{/if}
|
|
710
|
-
</span>
|
|
711
|
-
<span class="shrink-0 text-on-surface-faint">·</span>
|
|
712
|
-
<span
|
|
713
|
-
class="shrink-0 font-mono text-[12px] font-semibold capitalize {config.color}"
|
|
714
|
-
>{dstatus}</span
|
|
715
|
-
>
|
|
716
|
-
</span>
|
|
717
|
-
<span
|
|
718
|
-
class="flex min-w-0 items-center gap-1.5 font-mono text-2xs text-on-surface-faint"
|
|
719
|
-
title={formatFullDateTime(startedAt)}
|
|
720
|
-
>
|
|
721
|
-
<span class="truncate">{formatDateTime(startedAt)}</span>
|
|
722
|
-
<span class="shrink-0">· {formatTriggeredByLabel(run.triggeredBy)}</span>
|
|
723
|
-
{#if retry}
|
|
724
|
-
<span class="shrink-0 rounded bg-surface-sunken px-1 font-mono"
|
|
725
|
-
>{retry}</span
|
|
726
|
-
>
|
|
727
|
-
{/if}
|
|
728
|
-
</span>
|
|
729
|
-
</span>
|
|
730
|
-
<span
|
|
731
|
-
class="shrink-0 self-start pt-0.5 font-mono text-[11.5px] text-on-surface-faint tabular-nums"
|
|
732
|
-
title={retry ?? undefined}
|
|
733
|
-
>
|
|
734
|
-
{rowRightLabel(run, dstatus)}
|
|
735
|
-
</span>
|
|
736
|
-
</div>
|
|
737
|
-
{:else}
|
|
738
|
-
<!-- Task-rail variant (artifact ".run"): a single dense line,
|
|
739
|
-
time · date · outcome, with a mono exit/duration readout.
|
|
740
|
-
leading-tight matches the artifact's ~1.2 line-height so the
|
|
741
|
-
(descender-less) text optically centers instead of riding high
|
|
742
|
-
inside Tailwind's default 1.5 line box. -->
|
|
743
|
-
<div class="flex items-center gap-[11px] leading-tight">
|
|
744
|
-
{@render statusDot(config.color, running)}
|
|
745
|
-
<span class="flex min-w-0 flex-1 items-center gap-1.5 truncate">
|
|
746
|
-
<span
|
|
747
|
-
class="font-mono text-[12.5px] font-semibold tracking-tight text-on-surface tabular-nums"
|
|
748
|
-
title={formatFullDateTime(startedAt)}
|
|
749
|
-
>
|
|
750
|
-
{formatTimeHM(startedAt)} · {formatDayMonth(startedAt)}
|
|
751
|
-
</span>
|
|
752
|
-
<span class="text-on-surface-faint">·</span>
|
|
753
|
-
<span class="font-mono text-[12.5px] font-semibold capitalize {config.color}"
|
|
754
|
-
>{dstatus}</span
|
|
755
|
-
>
|
|
756
|
-
{#if suffix}
|
|
757
|
-
<span class="font-mono text-2xs text-on-surface-faint">{suffix}</span>
|
|
758
|
-
{/if}
|
|
759
|
-
</span>
|
|
760
|
-
<span
|
|
761
|
-
class="shrink-0 font-mono text-[11.5px] text-on-surface-faint tabular-nums"
|
|
762
|
-
title={retry ?? undefined}
|
|
763
|
-
>
|
|
764
|
-
{rowRightLabel(run, dstatus)}
|
|
765
|
-
</span>
|
|
766
|
-
</div>
|
|
767
|
-
{#if match}
|
|
768
|
-
{@const hl = highlightParts(match.text, outputQuery)}
|
|
769
|
-
<div
|
|
770
|
-
class="mt-1.5 truncate rounded-[3px] border border-outline-faint bg-surface-sunken px-2 py-1 font-mono text-2xs text-on-surface-muted"
|
|
771
|
-
>
|
|
772
|
-
{hl.before}<mark
|
|
773
|
-
class="rounded-[3px] bg-primary-soft px-0.5 text-primary-soft-text"
|
|
774
|
-
>{hl.match}</mark
|
|
775
|
-
>{hl.after}
|
|
776
|
-
</div>
|
|
777
|
-
{/if}
|
|
778
|
-
{/if}
|
|
779
|
-
|
|
780
|
-
<div
|
|
781
|
-
class="absolute inset-y-2 left-[-6px] w-[3px] rounded-[3px] {spine} {isActive
|
|
782
|
-
? 'opacity-100'
|
|
783
|
-
: 'opacity-0'}"
|
|
784
|
-
aria-hidden="true"
|
|
785
|
-
></div>
|
|
786
|
-
</button>
|
|
496
|
+
{#snippet runRow(run: Run, match: RunOutputMatch | undefined)}
|
|
497
|
+
<RunRow
|
|
498
|
+
{run}
|
|
499
|
+
active={selectedRunId === run.id}
|
|
500
|
+
suffix={instanceSuffix(run.instanceIndex, getInstanceCount(run.taskName))}
|
|
501
|
+
{showTaskName}
|
|
502
|
+
{match}
|
|
503
|
+
{outputQuery}
|
|
504
|
+
{bulkActions}
|
|
505
|
+
{selectionActive}
|
|
506
|
+
usage={usageBars ? (usageScale ?? null) : undefined}
|
|
507
|
+
onselect={selectRun}
|
|
508
|
+
/>
|
|
787
509
|
{/snippet}
|