@runwisp/ui 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/lib/actions/focusTrap.ts +3 -3
- package/src/lib/components/Badge.svelte +1 -1
- package/src/lib/components/DataGrid.svelte +5 -5
- package/src/lib/components/FilterBar.svelte +1 -1
- package/src/lib/components/FilterPill.svelte +2 -2
- package/src/lib/components/FormField.svelte +1 -1
- package/src/lib/components/LogConsole.svelte +10 -10
- 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/StatusDot.svelte +1 -1
- package/src/lib/components/Tooltip.svelte +1 -1
- package/src/lib/components/dashboard/RunDetailPanel.svelte +15 -16
- package/src/lib/components/dashboard/RunFilterPopover.svelte +5 -5
- package/src/lib/components/dashboard/RunsList.svelte +8 -8
- package/src/lib/components/dashboard/cron-format.test.ts +1 -1
- package/src/lib/components/dashboard/cron-format.ts +2 -2
- package/src/lib/components/dashboard/run-filters.test.ts +2 -2
- package/src/lib/components/dashboard/run-filters.ts +27 -94
- package/src/lib/components/dashboard/run-helpers.test.ts +9 -0
- package/src/lib/components/dashboard/run-helpers.ts +28 -38
- package/src/lib/components/data-grid-selection.ts +2 -9
- package/src/lib/components/filter-spec.ts +3 -3
- package/src/lib/log-console/LogCache.svelte.ts +1 -1
- package/src/lib/log-console/ansi.ts +1 -1
- package/src/lib/theme-tokens.css +21 -21
- package/src/lib/theme.css +13 -13
- package/src/lib/utils/format.test.ts +3 -3
- package/src/lib/utils/format.ts +4 -4
- package/src/lib/utils/toast.svelte.ts +2 -3
- package/src/stories/LogConsole.stories.svelte +2 -2
|
@@ -2,17 +2,12 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import { TRIGGERS, type Trigger } from "@runwisp/common";
|
|
5
|
+
import { TRIGGER_LABELS } from "./run-helpers.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* value (`emptyRunFilters`) matches every run.
|
|
11
|
-
*
|
|
12
|
-
* `statuses` is a multi-select set joined to a comma-separated string only at
|
|
13
|
-
* the two wire boundaries (the list query and the bulk selector). Time bounds
|
|
14
|
-
* are absolute RFC3339 instants — presets resolve to `now − delta` at apply
|
|
15
|
-
* time so a live SSE row created after the boundary keeps matching.
|
|
8
|
+
* The filter shared by the runs list, the filter popover, the SSE-merge source
|
|
9
|
+
* and the bulk selector. Time bounds are absolute RFC3339 instants, so a live
|
|
10
|
+
* SSE row created after a preset was applied keeps matching.
|
|
16
11
|
*/
|
|
17
12
|
export interface RunsListFilters {
|
|
18
13
|
search: string;
|
|
@@ -20,52 +15,38 @@ export interface RunsListFilters {
|
|
|
20
15
|
sortDirection: "asc" | "desc" | "";
|
|
21
16
|
// Optional dimensions explicitly admit `undefined` so a dimension can be
|
|
22
17
|
// cleared by reassignment (`{ ...f, x: undefined }`) under the project's
|
|
23
|
-
// exactOptionalPropertyTypes
|
|
18
|
+
// exactOptionalPropertyTypes, a fresh object reference is what re-triggers
|
|
24
19
|
// the parent's fetch effect through several levels of `bind:`.
|
|
25
20
|
taskName?: string | undefined;
|
|
26
21
|
createdAfter?: string | undefined;
|
|
27
22
|
createdBefore?: string | undefined;
|
|
28
23
|
triggeredBy?: string | undefined;
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// inclusive [min, max] range (`exitCodeRange`) at the wire boundary; the
|
|
32
|
-
// server only ever sees `exitCodeMin` / `exitCodeMax`.
|
|
24
|
+
// An exit-code expression such as `137`, `>100` or `>100 <150`; see
|
|
25
|
+
// parseExitCodeRange.
|
|
33
26
|
exitCode?: string | undefined;
|
|
34
27
|
retriesOnly?: boolean | undefined;
|
|
35
28
|
}
|
|
36
29
|
|
|
37
|
-
/** A fresh default filter
|
|
30
|
+
/** A fresh default filter, no dimensions active, newest-first. */
|
|
38
31
|
export function emptyRunFilters(): RunsListFilters {
|
|
39
32
|
return { search: "", statuses: [], sortDirection: "desc" };
|
|
40
33
|
}
|
|
41
34
|
|
|
42
35
|
/**
|
|
43
|
-
* Reserved `status` token the "Failed"
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* with the attention badges, the failed stat, and notifications — all of which
|
|
47
|
-
* follow the per-task `failures` policy. It is not a real status/end-reason, so
|
|
48
|
-
* it OR-combines with the other buckets. Must match `failureStatusToken` in the
|
|
49
|
-
* daemon.
|
|
36
|
+
* Reserved `status` token for the "Failed" filter. The daemon matches it on each
|
|
37
|
+
* run's `isFailure` bit (per-task `failures` policy) instead of a fixed
|
|
38
|
+
* end-reason list. Must match `failureStatusToken` in the daemon.
|
|
50
39
|
*/
|
|
51
40
|
export const FAILURE_STATUS_TOKEN = "failure";
|
|
52
41
|
|
|
53
42
|
/**
|
|
54
|
-
* Outcome buckets: the
|
|
55
|
-
* plain-language
|
|
56
|
-
*
|
|
57
|
-
* one adds/removes its members, and the popover's "Advanced" section still
|
|
58
|
-
* exposes the individual statuses for surgical filters (e.g. only `timeout`).
|
|
59
|
-
*
|
|
60
|
-
* Every status appears in exactly one bucket; `Failed` is the failure sentinel,
|
|
61
|
-
* which the server resolves to each run's `isFailure` bit. `dot` mirrors that
|
|
62
|
-
* group's color in
|
|
63
|
-
* RUN_STATUS_CONFIG so the bucket reads at a glance.
|
|
43
|
+
* Outcome buckets: a UI grouping of the individual run statuses into five
|
|
44
|
+
* plain-language picks. The popover's "Advanced" section still exposes the
|
|
45
|
+
* individual statuses. `dot` mirrors the group's color in RUN_STATUS_CONFIG.
|
|
64
46
|
*/
|
|
65
47
|
export interface StatusBucket {
|
|
66
48
|
key: string;
|
|
67
49
|
label: string;
|
|
68
|
-
/** Tailwind background class for the bucket's status dot. */
|
|
69
50
|
dot: string;
|
|
70
51
|
statuses: readonly string[];
|
|
71
52
|
}
|
|
@@ -95,7 +76,6 @@ export const STATUS_BUCKETS: readonly StatusBucket[] = [
|
|
|
95
76
|
|
|
96
77
|
export type BucketState = "on" | "partial" | "off";
|
|
97
78
|
|
|
98
|
-
/** Whether none, some, or all of a bucket's statuses are currently selected. */
|
|
99
79
|
export function bucketState(statuses: string[], bucket: StatusBucket): BucketState {
|
|
100
80
|
const set = new Set(statuses);
|
|
101
81
|
const present = bucket.statuses.filter((s) => set.has(s)).length;
|
|
@@ -122,7 +102,7 @@ function exactBucket(statuses: string[]): StatusBucket | undefined {
|
|
|
122
102
|
|
|
123
103
|
/**
|
|
124
104
|
* The popover-managed dimensions, in display (most→least useful) order.
|
|
125
|
-
* `task` only ever applies on the cross-task /runs view
|
|
105
|
+
* `task` only ever applies on the cross-task /runs view, on a single task's
|
|
126
106
|
* page the task name is the page scope (injected at fetch time), never a
|
|
127
107
|
* popover-set filter, so it stays absent from those filters and is not counted.
|
|
128
108
|
*/
|
|
@@ -137,7 +117,6 @@ const DIMENSION_ORDER: FilterDimension[] = [
|
|
|
137
117
|
"retries",
|
|
138
118
|
];
|
|
139
119
|
|
|
140
|
-
/** Whether a single dimension is currently constraining the result set. */
|
|
141
120
|
export function dimensionActive(f: RunsListFilters, dim: FilterDimension): boolean {
|
|
142
121
|
switch (dim) {
|
|
143
122
|
case "status":
|
|
@@ -155,17 +134,16 @@ export function dimensionActive(f: RunsListFilters, dim: FilterDimension): boole
|
|
|
155
134
|
}
|
|
156
135
|
}
|
|
157
136
|
|
|
158
|
-
/** The active dimensions, in display order
|
|
137
|
+
/** The active dimensions, in display order, drives the chip row. */
|
|
159
138
|
export function activeDimensions(f: RunsListFilters): FilterDimension[] {
|
|
160
139
|
return DIMENSION_ORDER.filter((dim) => dimensionActive(f, dim));
|
|
161
140
|
}
|
|
162
141
|
|
|
163
|
-
/** How many dimensions are active
|
|
142
|
+
/** How many dimensions are active, the count badge on the Filter button. */
|
|
164
143
|
export function activeFilterCount(f: RunsListFilters): number {
|
|
165
144
|
return activeDimensions(f).length;
|
|
166
145
|
}
|
|
167
146
|
|
|
168
|
-
/** Reset one dimension, leaving the others (and search/sort) untouched. */
|
|
169
147
|
export function clearDimension(f: RunsListFilters, dim: FilterDimension): RunsListFilters {
|
|
170
148
|
switch (dim) {
|
|
171
149
|
case "status":
|
|
@@ -190,47 +168,20 @@ export function humanizeStatus(status: string): string {
|
|
|
190
168
|
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
191
169
|
}
|
|
192
170
|
|
|
193
|
-
/**
|
|
194
|
-
* Descriptive label for a run's trigger source — fuller than the one-word row
|
|
195
|
-
* badge (`formatTriggeredByLabel`) for the "cron"/"station"/"service"/"startup"
|
|
196
|
-
* sources, where the extra words disambiguate (e.g. "REST API" vs a bare
|
|
197
|
-
* "API"). "ui", "cli" and "hook" already read fine as their bare badge word,
|
|
198
|
-
* so those three match `formatTriggeredByLabel` exactly.
|
|
199
|
-
*
|
|
200
|
-
* Note `cron` covers both on-time schedule firings and catch-up for missed
|
|
201
|
-
* runs, and `startup` is specifically `run_on_start` (not catch-up).
|
|
202
|
-
*/
|
|
171
|
+
/** Descriptive label for a run's trigger source (see {@link TRIGGER_LABELS}). */
|
|
203
172
|
export function triggerDescription(trigger: string): string {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return "Scheduled (cron)";
|
|
207
|
-
case "api":
|
|
208
|
-
return "REST API";
|
|
209
|
-
case "ui":
|
|
210
|
-
return "UI";
|
|
211
|
-
case "cli":
|
|
212
|
-
return "CLI";
|
|
213
|
-
case "station":
|
|
214
|
-
return "Control plane";
|
|
215
|
-
case "service":
|
|
216
|
-
return "Service auto-start";
|
|
217
|
-
case "startup":
|
|
218
|
-
return "On daemon start";
|
|
219
|
-
case "hook":
|
|
220
|
-
return "Hook";
|
|
221
|
-
default:
|
|
222
|
-
return humanizeStatus(trigger);
|
|
223
|
-
}
|
|
173
|
+
const known = TRIGGERS.find((t) => t === trigger);
|
|
174
|
+
return known ? TRIGGER_LABELS[known].long : humanizeStatus(trigger);
|
|
224
175
|
}
|
|
225
176
|
|
|
226
177
|
/**
|
|
227
178
|
* The triggers offered in the filter dropdown. `station` is intentionally
|
|
228
|
-
* excluded for now
|
|
179
|
+
* excluded for now, control-plane runs still carry the `station` trigger, but
|
|
229
180
|
* it isn't a selectable filter dimension.
|
|
230
181
|
*/
|
|
231
182
|
export const FILTERABLE_TRIGGERS: readonly Trigger[] = TRIGGERS.filter((t) => t !== "station");
|
|
232
183
|
|
|
233
|
-
/** Chip label for the status dimension
|
|
184
|
+
/** Chip label for the status dimension, names a whole bucket when it matches. */
|
|
234
185
|
export function statusChipLabel(statuses: string[]): string {
|
|
235
186
|
const bucket = exactBucket(statuses);
|
|
236
187
|
if (bucket) return bucket.label;
|
|
@@ -239,15 +190,6 @@ export function statusChipLabel(statuses: string[]): string {
|
|
|
239
190
|
return `${String(statuses.length)} statuses`;
|
|
240
191
|
}
|
|
241
192
|
|
|
242
|
-
// --- Time helpers --------------------------------------------------------
|
|
243
|
-
//
|
|
244
|
-
// A bare calendar day (`YYYY-MM-DD` from a native date input) maps to a local
|
|
245
|
-
// instant: the "from" edge is that day's 00:00, the "to" edge is its end
|
|
246
|
-
// (23:59:59.999) so picking the same day for both bounds — or the one-click
|
|
247
|
-
// "on day" shortcut — captures the whole day and round-trips back to the same
|
|
248
|
-
// date in the inputs. Bounds are stored as absolute RFC3339 (UTC) instants, the
|
|
249
|
-
// shape the list query and SSE-merge filter already expect.
|
|
250
|
-
|
|
251
193
|
/** Parse `YYYY-MM-DD` into the local Date for that day, or null if malformed. */
|
|
252
194
|
function localDay(dateStr: string): Date | null {
|
|
253
195
|
const parts = dateStr.split("-");
|
|
@@ -295,14 +237,6 @@ export function isWholeDay(after?: string, before?: string): boolean {
|
|
|
295
237
|
);
|
|
296
238
|
}
|
|
297
239
|
|
|
298
|
-
// --- Exit-code expression ------------------------------------------------
|
|
299
|
-
//
|
|
300
|
-
// The popover takes a free-form exit-code expression and normalizes it to an
|
|
301
|
-
// inclusive integer [min, max] range — the shape the server gates on. Because
|
|
302
|
-
// exit codes are integers, a strict comparison collapses to an inclusive
|
|
303
|
-
// bound: `>100` is min 101, `<150` is max 149. Tokens combine (`>100 <150`),
|
|
304
|
-
// and a bare number is an exact match (min = max = n).
|
|
305
|
-
|
|
306
240
|
export interface ExitCodeRange {
|
|
307
241
|
min?: number;
|
|
308
242
|
max?: number;
|
|
@@ -311,10 +245,10 @@ export interface ExitCodeRange {
|
|
|
311
245
|
const EXIT_CODE_TOKEN = /^(>=|<=|>|<)?(-?\d+)$/;
|
|
312
246
|
|
|
313
247
|
/**
|
|
314
|
-
* Parse an exit-code expression into
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
248
|
+
* Parse an exit-code expression into the inclusive integer range the server
|
|
249
|
+
* filters on: `>100` is min 101, `<150` is max 149, a bare number is an exact
|
|
250
|
+
* match, and tokens combine. Any unparseable token makes the whole expression
|
|
251
|
+
* invalid so the UI can flag the typo. An empty expression is valid and unbounded.
|
|
318
252
|
*/
|
|
319
253
|
export function parseExitCodeRange(expr: string): { range: ExitCodeRange; valid: boolean } {
|
|
320
254
|
const range: ExitCodeRange = {};
|
|
@@ -353,13 +287,12 @@ export function exitCodeRange(expr: string | undefined): ExitCodeRange {
|
|
|
353
287
|
return valid ? range : {};
|
|
354
288
|
}
|
|
355
289
|
|
|
356
|
-
/** True when the expression resolves to at least one bound (a real filter). */
|
|
357
290
|
export function exitCodeRangeActive(expr: string | undefined): boolean {
|
|
358
291
|
const { min, max } = exitCodeRange(expr);
|
|
359
292
|
return min !== undefined || max !== undefined;
|
|
360
293
|
}
|
|
361
294
|
|
|
362
|
-
/** True when the expression is empty or fully parseable
|
|
295
|
+
/** True when the expression is empty or fully parseable, drives input validation. */
|
|
363
296
|
export function isExitCodeExprValid(expr: string): boolean {
|
|
364
297
|
return parseExitCodeRange(expr).valid;
|
|
365
298
|
}
|
|
@@ -10,8 +10,17 @@ import {
|
|
|
10
10
|
runRetryLabel,
|
|
11
11
|
runUsageLabel,
|
|
12
12
|
instanceSuffix,
|
|
13
|
+
runLogDownloadUrl,
|
|
13
14
|
} from "./run-helpers.js";
|
|
14
15
|
|
|
16
|
+
describe("runLogDownloadUrl", () => {
|
|
17
|
+
it("points at the daemon's raw log route", () => {
|
|
18
|
+
expect(runLogDownloadUrl("01J0000000000000000000000A")).toBe(
|
|
19
|
+
"/api/runs/01J0000000000000000000000A/log/raw",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
15
24
|
describe("runVerdict", () => {
|
|
16
25
|
it("phrases a timed outcome so a duration reads after it", () => {
|
|
17
26
|
expect(runVerdict("succeeded")).toEqual({ verb: "succeeded in", timed: true });
|
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: PoppyCake, s.r.o.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import type { Run, RunStatus } from "@runwisp/common";
|
|
4
|
+
import type { APIPaths, Run, RunStatus, Trigger } from "@runwisp/common";
|
|
5
5
|
import { formatBytes, formatDuration } from "../../utils/format.js";
|
|
6
6
|
|
|
7
7
|
export interface RunVerdict {
|
|
8
8
|
/** Verb phrase for the outcome, ending in its preposition when `timed`. */
|
|
9
9
|
verb: string;
|
|
10
|
-
/**
|
|
11
|
-
* Whether the phrase expects a duration after it. False for statuses that
|
|
12
|
-
* never produced one (nothing ran, or nothing has yet), so the caller
|
|
13
|
-
* renders the verb alone rather than "skipped after —".
|
|
14
|
-
*/
|
|
10
|
+
/** Whether a duration follows the verb; false when nothing ran yet. */
|
|
15
11
|
timed: boolean;
|
|
16
12
|
}
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
* The run's outcome as a verb phrase, so a detail view can state it as one
|
|
20
|
-
* sentence ("succeeded in 933ms") instead of a status badge competing with a
|
|
21
|
-
* separate duration readout for the same glance.
|
|
22
|
-
*/
|
|
14
|
+
// The run's outcome as a verb phrase, read as one sentence: "succeeded in 933ms".
|
|
23
15
|
const RUN_VERDICTS: Record<RunStatus, RunVerdict> = {
|
|
24
16
|
succeeded: { verb: "succeeded in", timed: true },
|
|
25
17
|
failed: { verb: "failed after", timed: true },
|
|
@@ -54,12 +46,8 @@ export function runDuration(
|
|
|
54
46
|
}
|
|
55
47
|
|
|
56
48
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* within a second of each other. This is the visible face of `jitter`: a
|
|
60
|
-
* jittered run is created at its tick but starts later inside the window, and
|
|
61
|
-
* this is by how much. It also surfaces queue-wait, since a queued run is
|
|
62
|
-
* created when it joins the line and started when the line clears.
|
|
49
|
+
* How long a run waited between `createdAt` and `startedAt` (jitter or queue
|
|
50
|
+
* wait), or undefined when it started within a second.
|
|
63
51
|
*/
|
|
64
52
|
export function runStartDelay(run: Pick<Run, "createdAt" | "startedAt">): string | undefined {
|
|
65
53
|
if (!run.startedAt) return undefined;
|
|
@@ -68,14 +56,7 @@ export function runStartDelay(run: Pick<Run, "createdAt" | "startedAt">): string
|
|
|
68
56
|
return formatDuration(delay);
|
|
69
57
|
}
|
|
70
58
|
|
|
71
|
-
/**
|
|
72
|
-
* Display suffix for a run's instance slot. A service configured with more than
|
|
73
|
-
* one instance gets a 1-based suffix (`#1`, `#2`, …) on every one of its runs;
|
|
74
|
-
* a single-instance task (or any non-service, where `instanceCount` is 1)
|
|
75
|
-
* returns an empty string so the bare task name is shown. `instanceIndex` is
|
|
76
|
-
* the stored 0-based slot; `instanceCount` is the task's currently configured
|
|
77
|
-
* instance count.
|
|
78
|
-
*/
|
|
59
|
+
/** 1-based `#N` suffix for a multi-instance service run, else "". */
|
|
79
60
|
export function instanceSuffix(instanceIndex: number, instanceCount: number): string {
|
|
80
61
|
if (instanceCount > 1) {
|
|
81
62
|
return `#${String(instanceIndex + 1)}`;
|
|
@@ -83,23 +64,32 @@ export function instanceSuffix(instanceIndex: number, instanceCount: number): st
|
|
|
83
64
|
return "";
|
|
84
65
|
}
|
|
85
66
|
|
|
67
|
+
// Typed against the generated paths so a route rename fails the build.
|
|
68
|
+
const RAW_LOG_PATH: keyof APIPaths = "/api/runs/{runId}/log/raw";
|
|
69
|
+
|
|
70
|
+
/** The full log of a run as one plain-text download. */
|
|
71
|
+
export function runLogDownloadUrl(runId: string): string {
|
|
72
|
+
return RAW_LOG_PATH.replace("{runId}", encodeURIComponent(runId));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Labels per trigger source: `short` for the row badge, `long` for descriptions. */
|
|
76
|
+
export const TRIGGER_LABELS: Record<Trigger, { short: string; long: string }> = {
|
|
77
|
+
cron: { short: "Cron", long: "Scheduled (cron)" },
|
|
78
|
+
api: { short: "API", long: "REST API" },
|
|
79
|
+
ui: { short: "UI", long: "UI" },
|
|
80
|
+
cli: { short: "CLI", long: "CLI" },
|
|
81
|
+
station: { short: "Station", long: "Control plane" },
|
|
82
|
+
service: { short: "Service", long: "Service auto-start" },
|
|
83
|
+
startup: { short: "Startup", long: "On daemon start" },
|
|
84
|
+
hook: { short: "Hook", long: "Hook" },
|
|
85
|
+
};
|
|
86
|
+
|
|
86
87
|
/** Human label for why a run fired (the `triggeredBy` source). */
|
|
87
88
|
export function formatTriggeredByLabel(triggeredBy: Run["triggeredBy"]): string {
|
|
88
|
-
|
|
89
|
-
if (triggeredBy === "ui") return "UI";
|
|
90
|
-
if (triggeredBy === "cli") return "CLI";
|
|
91
|
-
if (triggeredBy === "cron") return "Cron";
|
|
92
|
-
if (triggeredBy === "service") return "Service";
|
|
93
|
-
if (triggeredBy === "startup") return "Startup";
|
|
94
|
-
if (triggeredBy === "hook") return "Hook";
|
|
95
|
-
return "Station";
|
|
89
|
+
return TRIGGER_LABELS[triggeredBy].short;
|
|
96
90
|
}
|
|
97
91
|
|
|
98
|
-
/**
|
|
99
|
-
* "retry #N" label when a run is a retry of an earlier one, else undefined.
|
|
100
|
-
* A run is a retry if it carries a positive attempt number or points back at
|
|
101
|
-
* the run it re-attempts.
|
|
102
|
-
*/
|
|
92
|
+
/** "retry #N" when the run re-attempts an earlier one, else undefined. */
|
|
103
93
|
export function runRetryLabel(run: Pick<Run, "retryAttempt" | "retryOfRunId">): string | undefined {
|
|
104
94
|
if (run.retryAttempt > 0 || run.retryOfRunId) {
|
|
105
95
|
return `retry #${String(run.retryAttempt)}`;
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: PoppyCake, s.r.o.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
// DataGrid's header checkbox (checked / indeterminate / unchecked) must
|
|
5
|
-
// reflect which rows on the current page are ACTUALLY selected, not merely
|
|
6
|
-
// how many rows are selected. A bare count comparison is wrong the moment
|
|
7
|
-
// `selectedRows` holds a different set of rows than `pagedRows` but happens
|
|
8
|
-
// to be the same size — e.g. rows selected on a page the grid has since
|
|
9
|
-
// paged/filtered away from. Extracted so the membership check is unit
|
|
10
|
-
// testable without a component-render harness.
|
|
11
|
-
|
|
12
4
|
export interface SelectionState {
|
|
13
5
|
allSelected: boolean;
|
|
14
6
|
someSelected: boolean;
|
|
@@ -17,7 +9,8 @@ export interface SelectionState {
|
|
|
17
9
|
/**
|
|
18
10
|
* Header checkbox state for `pagedRows`: fully checked only when every row
|
|
19
11
|
* currently on the page is selected, indeterminate when some (but not all)
|
|
20
|
-
* of them are. Matches rows by `rowKey`, not by
|
|
12
|
+
* of them are. Matches rows by `rowKey`, not by count: the selection can
|
|
13
|
+
* hold rows from a page the grid has since moved away from.
|
|
21
14
|
*/
|
|
22
15
|
export function selectionState<T>(
|
|
23
16
|
pagedRows: readonly T[],
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
// Declarative filter spec. A `FilterField[]` + one bound `FilterValues` object
|
|
5
5
|
// drives <FilterBar>, the client engine (`applyFilters`), and the URL sync
|
|
6
|
-
// helper
|
|
6
|
+
// helper, the same way DataGrid's `Column[]` drives the table. Add a field
|
|
7
7
|
// type here, and every table that adopts it renders identically.
|
|
8
8
|
|
|
9
9
|
import type { SelectOption } from "./Select.svelte";
|
|
@@ -44,7 +44,7 @@ export type FilterField =
|
|
|
44
44
|
primary?: boolean;
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
// All values are strings
|
|
47
|
+
// All values are strings, DOM inputs are string-valued and URL params are
|
|
48
48
|
// strings; the client engine coerces (e.g. Number()) where a field needs it.
|
|
49
49
|
export type FilterValues = Record<string, string>;
|
|
50
50
|
|
|
@@ -65,7 +65,7 @@ export function isBlank(v: string | undefined): boolean {
|
|
|
65
65
|
return !v || v === "all";
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
// Which value keys a field owns
|
|
68
|
+
// Which value keys a field owns, daterange owns two, everything else one.
|
|
69
69
|
// Used by chips, active-state detection, clear, and URL (de)serialization so
|
|
70
70
|
// none of them has to special-case the range type.
|
|
71
71
|
export function fieldKeys(field: FilterField): string[] {
|
|
@@ -14,7 +14,7 @@ import { visibleColumns } from "./ansi.js";
|
|
|
14
14
|
*/
|
|
15
15
|
// Streams pinned to the front of the overlay, in render order. The companion
|
|
16
16
|
// Set backs the membership test in `overlayRows`. Both are module-level,
|
|
17
|
-
// immutable, and non-reactive
|
|
17
|
+
// immutable, and non-reactive, a fixed lookup table, not component state.
|
|
18
18
|
const PREFERRED_STREAM_ORDER = ["stdout", "stderr"] as const;
|
|
19
19
|
const PREFERRED_STREAMS: ReadonlySet<string> = new Set(PREFERRED_STREAM_ORDER);
|
|
20
20
|
|
|
@@ -45,7 +45,7 @@ const ANSI_ESCAPE = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
|
45
45
|
const TAB_WIDTH = 8;
|
|
46
46
|
|
|
47
47
|
// Count the visible columns a log line occupies once ANSI escape sequences
|
|
48
|
-
// are stripped. Tabs are charged the full tab width
|
|
48
|
+
// are stripped. Tabs are charged the full tab width, an over-estimate that
|
|
49
49
|
// only adds trailing slack to the horizontal scroll surface, never clips.
|
|
50
50
|
export function visibleColumns(text: string): number {
|
|
51
51
|
let columns = 0;
|
package/src/lib/theme-tokens.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* SPDX-FileCopyrightText: PoppyCake, s.r.o. */
|
|
2
2
|
/* SPDX-License-Identifier: Apache-2.0 */
|
|
3
3
|
|
|
4
|
-
/* RunWisp Design Tokens
|
|
4
|
+
/* RunWisp Design Tokens, plain CSS, no Tailwind directives.
|
|
5
5
|
*
|
|
6
6
|
* Single source of truth for the OKLch palette and the semantic `--rw-*`
|
|
7
7
|
* tokens that drive all apps:
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* The union selector covers both. Light mode is the default.
|
|
20
20
|
*
|
|
21
21
|
* Webfonts are @imported here (not in theme.css) so every consumer that reads
|
|
22
|
-
* these tokens also gets the faces they name
|
|
22
|
+
* these tokens also gets the faces they name, apps/docs imports this file
|
|
23
23
|
* alone. Both families ship in the binary; nothing is fetched from a CDN.
|
|
24
24
|
*/
|
|
25
25
|
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
@import "@fontsource-variable/geist-mono/index.css";
|
|
31
31
|
|
|
32
32
|
:root {
|
|
33
|
-
/* Typography
|
|
33
|
+
/* Typography, body sans for prose, mono for all chrome and identifiers.
|
|
34
34
|
* Geist Mono is variable (100–900), so heading weight 800 is a real cut. */
|
|
35
35
|
--rw-font-sans: "TASA Orbiter", system-ui, -apple-system, sans-serif;
|
|
36
36
|
--rw-font-mono:
|
|
37
37
|
"Geist Mono Variable", "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
38
38
|
|
|
39
|
-
/* Primary
|
|
39
|
+
/* Primary, Wisp Teal (brand = #0E7C86, the artifact's deep teal). Deeper
|
|
40
40
|
* and less cyan than Aurora (running/info) so the two stay distinct. */
|
|
41
41
|
--rw-color-wisp-50: oklch(0.97 0.02 205);
|
|
42
42
|
--rw-color-wisp-100: oklch(0.95 0.035 205);
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
--rw-color-wisp-900: oklch(0.3 0.048 208);
|
|
51
51
|
--rw-color-wisp-950: oklch(0.2 0.034 210);
|
|
52
52
|
|
|
53
|
-
/* Accent
|
|
53
|
+
/* Accent, Aurora Teal (running status = #13A0A8, a cyan-leaning teal) */
|
|
54
54
|
--rw-color-aurora-50: oklch(0.97 0.02 192);
|
|
55
55
|
--rw-color-aurora-100: oklch(0.94 0.045 192);
|
|
56
56
|
--rw-color-aurora-200: oklch(0.88 0.075 190);
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
--rw-color-aurora-800: oklch(0.35 0.07 192);
|
|
63
63
|
--rw-color-aurora-900: oklch(0.28 0.055 192);
|
|
64
64
|
|
|
65
|
-
/* Mist
|
|
65
|
+
/* Mist, teal-gray neutrals (the runbook's #F3F6F6 / #E0E7E7 /
|
|
66
66
|
* #14262A / #0C1719 family). No purple tint; hue ~195-205, low chroma,
|
|
67
67
|
* so bg-mist-* utilities agree with the new teal-black surfaces. The light
|
|
68
68
|
* end stays near-neutral to match the desaturated canvas. */
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
--rw-color-mist-900: oklch(0.266 0.025 204); /* ≈ #16292B */
|
|
79
79
|
--rw-color-mist-950: oklch(0.195 0.016 208); /* ≈ #0C1719 */
|
|
80
80
|
|
|
81
|
-
/* Success
|
|
81
|
+
/* Success, Field Green (success status = #1F9D57, a true grass green) */
|
|
82
82
|
--rw-color-success-50: oklch(0.97 0.02 150);
|
|
83
83
|
--rw-color-success-100: oklch(0.94 0.04 150);
|
|
84
84
|
--rw-color-success-200: oklch(0.88 0.07 150);
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
--rw-color-success-600: oklch(0.5 0.13 150);
|
|
89
89
|
--rw-color-success-700: oklch(0.42 0.11 150);
|
|
90
90
|
|
|
91
|
-
/* Danger
|
|
91
|
+
/* Danger, Signal Red (failed status = #D6453D, a warm tomato red) */
|
|
92
92
|
--rw-color-danger-50: oklch(0.97 0.016 27);
|
|
93
93
|
--rw-color-danger-100: oklch(0.94 0.03 27);
|
|
94
94
|
--rw-color-danger-200: oklch(0.88 0.06 27);
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
--rw-color-danger-600: oklch(0.5 0.17 27);
|
|
99
99
|
--rw-color-danger-700: oklch(0.42 0.145 27);
|
|
100
100
|
|
|
101
|
-
/* Warning
|
|
101
|
+
/* Warning, Ember Amber (stopped status = #D98324, a warm orange-amber) */
|
|
102
102
|
--rw-color-warning-50: oklch(0.97 0.03 78);
|
|
103
103
|
--rw-color-warning-100: oklch(0.94 0.07 74);
|
|
104
104
|
--rw-color-warning-200: oklch(0.9 0.11 72);
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
--rw-color-warning-600: oklch(0.6 0.14 62);
|
|
109
109
|
--rw-color-warning-700: oklch(0.52 0.12 60);
|
|
110
110
|
|
|
111
|
-
/* ANSI console palette
|
|
111
|
+
/* ANSI console palette, 16-color map for the log console, tuned for
|
|
112
112
|
* its always-dark surface (no .dark override needed). Hues follow the
|
|
113
113
|
* semantic scales: red→danger, green→success, yellow→warning,
|
|
114
114
|
* blue→wisp, cyan→aurora, grays→mist. */
|
|
@@ -129,11 +129,11 @@
|
|
|
129
129
|
--rw-ansi-bright-cyan: var(--rw-color-aurora-300);
|
|
130
130
|
--rw-ansi-bright-white: var(--rw-color-mist-50);
|
|
131
131
|
|
|
132
|
-
/* Fixed-dark sibling palettes
|
|
132
|
+
/* Fixed-dark sibling palettes, the `--rw-con-*` log console (a "black box"
|
|
133
133
|
* device) and the `--rw-term-*`/`--rw-syn-*` website terminal surfaces both
|
|
134
134
|
* keep their own dark palette in BOTH themes; they never flip light/dark. */
|
|
135
135
|
|
|
136
|
-
/* Console device surface
|
|
136
|
+
/* Console device surface, the log console instrument.
|
|
137
137
|
* Exact values from the approved execution-detail design. */
|
|
138
138
|
--rw-con-bg: #0e141b; /* body / gutter backing */
|
|
139
139
|
--rw-con-panel: #11181f; /* toolbar + footer chrome */
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
--rw-con-dim: #6c7d8c; /* footer + muted readouts */
|
|
142
142
|
--rw-con-gutter: #3c4a58; /* line numbers + sentinel rule */
|
|
143
143
|
|
|
144
|
-
/* Terminal surface
|
|
144
|
+
/* Terminal surface, always dark in both themes (website --term-* set) */
|
|
145
145
|
--rw-term: #0a1116;
|
|
146
146
|
--rw-term-2: #0d151b;
|
|
147
147
|
--rw-term-line: #1d2932;
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
--rw-syn-punc: #9aa7b0;
|
|
162
162
|
--rw-syn-fn: #2fd4dd;
|
|
163
163
|
|
|
164
|
-
/* Elevation
|
|
164
|
+
/* Elevation, light-mode drop shadows; flipped to `none` in dark so the
|
|
165
165
|
* UI reads as borders-only there. */
|
|
166
166
|
--rw-shadow-xs: 0 1px 2px rgba(16, 41, 43, 0.04);
|
|
167
167
|
--rw-shadow-sm: 0 1px 2px rgba(16, 41, 43, 0.04), 0 12px 28px -20px rgba(16, 41, 43, 0.22);
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
--rw-shadow-lg: 0 1px 2px rgba(16, 41, 43, 0.06), 0 30px 60px -28px rgba(16, 41, 43, 0.45);
|
|
170
170
|
--rw-shadow-xl: 0 1px 2px rgba(16, 41, 43, 0.06), 0 40px 80px -30px rgba(16, 41, 43, 0.5);
|
|
171
171
|
|
|
172
|
-
/* Light-mode semantic tokens (default)
|
|
172
|
+
/* Light-mode semantic tokens (default), runbook palette (exact hex).
|
|
173
173
|
*
|
|
174
174
|
* The canvas carries only a whisper of the brand teal. The website can
|
|
175
175
|
* afford a minty `--ground` because dark panes and terminals cover most
|
|
@@ -223,7 +223,7 @@
|
|
|
223
223
|
--rw-warning-soft: rgba(196, 120, 26, 0.1);
|
|
224
224
|
--rw-warning-soft-text: #c4781a;
|
|
225
225
|
--rw-warning-soft-border: rgba(196, 120, 26, 0.25);
|
|
226
|
-
/* Info
|
|
226
|
+
/* Info, matches primary teal */
|
|
227
227
|
--rw-info: #15a0a8;
|
|
228
228
|
--rw-info-hover: #0e7c86;
|
|
229
229
|
--rw-on-info: #ffffff;
|
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
--rw-glass-blur: blur(12px);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
/* Dark-mode semantic tokens
|
|
238
|
+
/* Dark-mode semantic tokens, `.dark` class for Svelte apps, `[data-theme="dark"]`
|
|
239
239
|
* attribute for Starlight. Both selectors apply the same token set. */
|
|
240
240
|
:root.dark,
|
|
241
241
|
:root[data-theme="dark"] {
|
|
@@ -254,7 +254,7 @@
|
|
|
254
254
|
--rw-outline-faint: #1d2932;
|
|
255
255
|
--rw-ring: #2fd4dd;
|
|
256
256
|
--rw-backdrop: rgba(4, 10, 12, 0.7);
|
|
257
|
-
/* Primary
|
|
257
|
+
/* Primary, bright teal in dark, matches info */
|
|
258
258
|
--rw-primary: #2fd4dd;
|
|
259
259
|
--rw-primary-hover: #5fe3ea;
|
|
260
260
|
--rw-primary-active: #5fe3ea;
|
|
@@ -284,7 +284,7 @@
|
|
|
284
284
|
--rw-warning-soft: rgba(224, 165, 46, 0.14);
|
|
285
285
|
--rw-warning-soft-text: #e0a52e;
|
|
286
286
|
--rw-warning-soft-border: rgba(224, 165, 46, 0.28);
|
|
287
|
-
/* Info
|
|
287
|
+
/* Info, bright teal in dark */
|
|
288
288
|
--rw-info: #2fd4dd;
|
|
289
289
|
--rw-info-hover: #5fe3ea;
|
|
290
290
|
--rw-on-info: #04181a;
|
|
@@ -293,7 +293,7 @@
|
|
|
293
293
|
--rw-info-soft-border: rgba(47, 212, 221, 0.28);
|
|
294
294
|
/* Glass */
|
|
295
295
|
--rw-glass: rgba(8, 15, 17, 0.9);
|
|
296
|
-
/* Elevation
|
|
296
|
+
/* Elevation, borders-only in dark, no drop shadow */
|
|
297
297
|
--rw-shadow-xs: none;
|
|
298
298
|
--rw-shadow-sm: none;
|
|
299
299
|
--rw-shadow-md: none;
|
|
@@ -301,7 +301,7 @@
|
|
|
301
301
|
--rw-shadow-xl: none;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
/* Platform polish
|
|
304
|
+
/* Platform polish, focus ring, text selection, scrollbar. Lifted from
|
|
305
305
|
* theme.css so apps/docs (no Tailwind) gets the same treatment as the
|
|
306
306
|
* Svelte apps. */
|
|
307
307
|
|