@runwisp/ui 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -5
- package/src/lib/actions/floating.ts +73 -0
- package/src/lib/actions/portal.ts +5 -1
- package/src/lib/components/Accordion.svelte +6 -2
- package/src/lib/components/AccordionItem.svelte +80 -23
- package/src/lib/components/Avatar.svelte +0 -1
- package/src/lib/components/CodeBlock.svelte +84 -35
- package/src/lib/components/CronInput.svelte +5 -2
- package/src/lib/components/DataGrid.svelte +95 -133
- package/src/lib/components/Drawer.svelte +2 -6
- package/src/lib/components/Dropdown.svelte +19 -57
- package/src/lib/components/EmptyState.svelte +2 -1
- package/src/lib/components/ErrorState.svelte +2 -1
- package/src/lib/components/Heading.svelte +19 -12
- package/src/lib/components/Kbd.svelte +25 -10
- package/src/lib/components/Link.svelte +7 -1
- package/src/lib/components/LogConsole.svelte +30 -32
- package/src/lib/components/Modal.svelte +44 -33
- package/src/lib/components/NotificationBell.svelte +2 -1
- package/src/lib/components/PageContainer.svelte +1 -10
- package/src/lib/components/PageHeader.svelte +8 -4
- package/src/lib/components/Popover.svelte +9 -51
- package/src/lib/components/Progress.svelte +19 -6
- package/src/lib/components/Radio.svelte +16 -4
- package/src/lib/components/RadioGroup.svelte +15 -2
- package/src/lib/components/Select.svelte +13 -45
- package/src/lib/components/Table.svelte +13 -4
- package/src/lib/components/TableBody.svelte +2 -1
- package/src/lib/components/TableCell.svelte +38 -8
- package/src/lib/components/TableHead.svelte +12 -4
- package/src/lib/components/TableRow.svelte +23 -3
- package/src/lib/components/TaskCard.svelte +1 -1
- package/src/lib/components/TimezoneSelect.svelte +2 -2
- package/src/lib/components/accordion.svelte.ts +19 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +91 -63
- package/src/lib/components/dashboard/RunFilterPopover.svelte +7 -16
- package/src/lib/components/dashboard/RunsList.svelte +9 -18
- package/src/lib/components/dashboard/cron-format.ts +1 -1
- package/src/lib/components/dashboard/run-filters.test.ts +2 -58
- package/src/lib/components/dashboard/run-filters.ts +5 -53
- package/src/lib/components/dashboard/run-helpers.test.ts +17 -7
- package/src/lib/components/dashboard/run-helpers.ts +12 -1
- package/src/lib/components/dashboard/status-config.ts +38 -83
- package/src/lib/components/dashboard/types.ts +0 -26
- package/src/lib/components/radio-group.ts +11 -0
- package/src/lib/index.ts +8 -43
- package/src/lib/log-console/LogFetcher.svelte.ts +3 -14
- package/src/lib/log-console/highlight-scroll.test.ts +59 -0
- package/src/lib/log-console/highlight-scroll.ts +41 -0
- package/src/lib/theme.css +10 -4
- package/src/lib/utils/clipboard.svelte.test.ts +92 -0
- package/src/lib/utils/clipboard.svelte.ts +73 -0
- package/src/lib/utils/format.ts +65 -94
- package/src/lib/utils/id.test.ts +3 -7
- package/src/lib/utils/id.ts +3 -2
- package/src/lib/utils/theme.svelte.test.ts +0 -6
- package/src/lib/utils/theme.svelte.ts +3 -5
- package/src/lib/utils/toast.svelte.ts +3 -11
- package/src/stories/Accordion.stories.svelte +61 -0
- package/src/stories/CodeBlock.stories.svelte +55 -0
- package/src/stories/DataGrid.stories.svelte +0 -5
- package/src/stories/Heading.stories.svelte +31 -0
- package/src/stories/Kbd.stories.svelte +36 -0
- package/src/stories/Link.stories.svelte +38 -0
- package/src/stories/PageHeader.stories.svelte +31 -0
- package/src/stories/Progress.stories.svelte +26 -0
- package/src/stories/RadioGroup.stories.svelte +43 -0
- package/src/stories/Table.stories.svelte +69 -0
- package/src/svelte-shims.d.ts +0 -5
- package/src/lib/components/dashboard/status-config.test.ts +0 -18
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
import { TRIGGERS, type Trigger } from "@runwisp/common";
|
|
5
5
|
|
|
6
|
-
export type RunsListSortDirection = "asc" | "desc" | "";
|
|
7
|
-
|
|
8
6
|
/**
|
|
9
7
|
* The single filter shape shared by the runs list, the filter popover, the
|
|
10
8
|
* SSE-merge source, and the bulk selector. Every field beyond `search` and
|
|
@@ -19,7 +17,7 @@ export type RunsListSortDirection = "asc" | "desc" | "";
|
|
|
19
17
|
export interface RunsListFilters {
|
|
20
18
|
search: string;
|
|
21
19
|
statuses: string[];
|
|
22
|
-
sortDirection:
|
|
20
|
+
sortDirection: "asc" | "desc" | "";
|
|
23
21
|
// Optional dimensions explicitly admit `undefined` so a dimension can be
|
|
24
22
|
// cleared by reassignment (`{ ...f, x: undefined }`) under the project's
|
|
25
23
|
// exactOptionalPropertyTypes — a fresh object reference is what re-triggers
|
|
@@ -52,21 +50,6 @@ export function emptyRunFilters(): RunsListFilters {
|
|
|
52
50
|
*/
|
|
53
51
|
export const FAILURE_STATUS_TOKEN = "failure";
|
|
54
52
|
|
|
55
|
-
/**
|
|
56
|
-
* The status set backing the "Failed" browse filter and the one-click preset
|
|
57
|
-
* that serves Prime Directive #1 — nothing silently fails. Just the failure
|
|
58
|
-
* sentinel: the server resolves it to the per-task `isFailure` classification
|
|
59
|
-
* rather than a fixed end-reason list.
|
|
60
|
-
*/
|
|
61
|
-
export const NEEDS_ATTENTION_STATUSES: readonly string[] = [FAILURE_STATUS_TOKEN];
|
|
62
|
-
|
|
63
|
-
/** True when `statuses` is exactly the needs-attention set (order-insensitive). */
|
|
64
|
-
export function isNeedsAttention(statuses: string[]): boolean {
|
|
65
|
-
if (statuses.length !== NEEDS_ATTENTION_STATUSES.length) return false;
|
|
66
|
-
const set = new Set(statuses);
|
|
67
|
-
return NEEDS_ATTENTION_STATUSES.every((s) => set.has(s));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
53
|
/**
|
|
71
54
|
* Outcome buckets: the 14 individual run statuses collapsed into five
|
|
72
55
|
* plain-language groups, so the common case is a five-item pick instead of a
|
|
@@ -74,8 +57,9 @@ export function isNeedsAttention(statuses: string[]): boolean {
|
|
|
74
57
|
* one adds/removes its members, and the popover's "Advanced" section still
|
|
75
58
|
* exposes the individual statuses for surgical filters (e.g. only `timeout`).
|
|
76
59
|
*
|
|
77
|
-
* Every status appears in exactly one bucket; `Failed` is the
|
|
78
|
-
*
|
|
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
|
|
79
63
|
* RUN_STATUS_CONFIG so the bucket reads at a glance.
|
|
80
64
|
*/
|
|
81
65
|
export interface StatusBucket {
|
|
@@ -93,7 +77,7 @@ export const STATUS_BUCKETS: readonly StatusBucket[] = [
|
|
|
93
77
|
key: "failed",
|
|
94
78
|
label: "Failed",
|
|
95
79
|
dot: "bg-danger-surface",
|
|
96
|
-
statuses:
|
|
80
|
+
statuses: [FAILURE_STATUS_TOKEN],
|
|
97
81
|
},
|
|
98
82
|
{
|
|
99
83
|
key: "skipped",
|
|
@@ -199,23 +183,6 @@ export function clearDimension(f: RunsListFilters, dim: FilterDimension): RunsLi
|
|
|
199
183
|
}
|
|
200
184
|
}
|
|
201
185
|
|
|
202
|
-
/**
|
|
203
|
-
* Clear every popover dimension at once, preserving the header search and the
|
|
204
|
-
* sort direction (those are separate toolbar controls, not popover state).
|
|
205
|
-
*/
|
|
206
|
-
export function clearPopoverFilters(f: RunsListFilters): RunsListFilters {
|
|
207
|
-
return {
|
|
208
|
-
...f,
|
|
209
|
-
statuses: [],
|
|
210
|
-
createdAfter: undefined,
|
|
211
|
-
createdBefore: undefined,
|
|
212
|
-
taskName: undefined,
|
|
213
|
-
triggeredBy: undefined,
|
|
214
|
-
exitCode: undefined,
|
|
215
|
-
retriesOnly: undefined,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
186
|
/** Human label for a status value, e.g. `log_overflow` → "Log overflow". */
|
|
220
187
|
export function humanizeStatus(status: string): string {
|
|
221
188
|
if (!status) return status;
|
|
@@ -401,18 +368,3 @@ export function isExitCodeExprValid(expr: string): boolean {
|
|
|
401
368
|
export function exitCodeChipLabel(expr: string | undefined): string {
|
|
402
369
|
return `Exit ${(expr ?? "").trim()}`;
|
|
403
370
|
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Whether the popover's exit-code text buffer must resync to the `filters`
|
|
407
|
-
* prop. True only when the committed `exitCode` value actually moved since
|
|
408
|
-
* the buffer last matched it — e.g. the "Exit code" chip was removed, or
|
|
409
|
-
* "clear filters" ran elsewhere. An unrelated filters update (any other
|
|
410
|
-
* dimension) leaves `exitCode` untouched and must not clobber in-progress
|
|
411
|
-
* typing in the still-open popover.
|
|
412
|
-
*/
|
|
413
|
-
export function exitCodePropChanged(
|
|
414
|
-
previous: string | undefined,
|
|
415
|
-
next: string | undefined,
|
|
416
|
-
): boolean {
|
|
417
|
-
return previous !== next;
|
|
418
|
-
}
|
|
@@ -2,23 +2,17 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
import { RUN_STATUSES } from "@runwisp/common";
|
|
6
5
|
import {
|
|
7
6
|
runDuration,
|
|
8
7
|
runStartDelay,
|
|
9
8
|
runVerdict,
|
|
10
9
|
formatTriggeredByLabel,
|
|
11
10
|
runRetryLabel,
|
|
11
|
+
runUsageLabel,
|
|
12
12
|
instanceSuffix,
|
|
13
13
|
} from "./run-helpers.js";
|
|
14
14
|
|
|
15
15
|
describe("runVerdict", () => {
|
|
16
|
-
it("covers every run status", () => {
|
|
17
|
-
for (const status of RUN_STATUSES) {
|
|
18
|
-
expect(runVerdict(status).verb, status).toBeTruthy();
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
16
|
it("phrases a timed outcome so a duration reads after it", () => {
|
|
23
17
|
expect(runVerdict("succeeded")).toEqual({ verb: "succeeded in", timed: true });
|
|
24
18
|
expect(runVerdict("failed")).toEqual({ verb: "failed after", timed: true });
|
|
@@ -116,6 +110,22 @@ describe("formatTriggeredByLabel", () => {
|
|
|
116
110
|
});
|
|
117
111
|
});
|
|
118
112
|
|
|
113
|
+
describe("runUsageLabel", () => {
|
|
114
|
+
it("is undefined for an unmeasured run", () => {
|
|
115
|
+
expect(runUsageLabel({})).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("joins peak memory and CPU time", () => {
|
|
119
|
+
expect(runUsageLabel({ peakMemoryBytes: 48 * 1024 * 1024, cpuTimeMs: 1500 })).toBe(
|
|
120
|
+
"peak 48 MB · CPU 1s",
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("shows whichever was measured", () => {
|
|
125
|
+
expect(runUsageLabel({ cpuTimeMs: 0 })).toBe("CPU 0ms");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
119
129
|
describe("runRetryLabel", () => {
|
|
120
130
|
it("returns undefined for a first attempt that is not a retry", () => {
|
|
121
131
|
expect(runRetryLabel({ retryAttempt: 0 })).toBeUndefined();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import type { Run, RunStatus } from "@runwisp/common";
|
|
5
|
-
import { formatDuration } from "../../utils/format.js";
|
|
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`. */
|
|
@@ -107,6 +107,17 @@ export function runRetryLabel(run: Pick<Run, "retryAttempt" | "retryOfRunId">):
|
|
|
107
107
|
return undefined;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* "peak 48 MB · CPU 1s" for a run whose resources were measured (shell runs
|
|
112
|
+
* only), else undefined.
|
|
113
|
+
*/
|
|
114
|
+
export function runUsageLabel(run: Pick<Run, "peakMemoryBytes" | "cpuTimeMs">): string | undefined {
|
|
115
|
+
const parts: string[] = [];
|
|
116
|
+
if (run.peakMemoryBytes !== undefined) parts.push("peak " + formatBytes(run.peakMemoryBytes));
|
|
117
|
+
if (run.cpuTimeMs !== undefined) parts.push("CPU " + formatDuration(run.cpuTimeMs));
|
|
118
|
+
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
/**
|
|
111
122
|
* Monotonic position of a run's status in its lifecycle. Used to reject stale
|
|
112
123
|
* updates (e.g. a `pending` HTTP response arriving after an SSE already
|
|
@@ -16,25 +16,44 @@ import {
|
|
|
16
16
|
HeartCrack,
|
|
17
17
|
} from "@lucide/svelte";
|
|
18
18
|
import type { Component } from "svelte";
|
|
19
|
-
import {
|
|
19
|
+
import type { RunStatus } from "@runwisp/common";
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
interface RunStatusConfig {
|
|
22
22
|
icon: Component;
|
|
23
23
|
color: string;
|
|
24
24
|
bg: string;
|
|
25
|
-
border: string;
|
|
26
25
|
dot: string;
|
|
27
26
|
badge: string;
|
|
28
27
|
/** One-sentence explanation of what this status means, for tooltips. */
|
|
29
28
|
description: string;
|
|
30
29
|
}
|
|
31
30
|
|
|
31
|
+
// Full literal class strings (not built from a tone name) so Tailwind's
|
|
32
|
+
// scanner picks them up.
|
|
33
|
+
const DANGER = {
|
|
34
|
+
color: "text-danger-surface",
|
|
35
|
+
bg: "bg-danger-soft",
|
|
36
|
+
dot: "bg-danger-surface",
|
|
37
|
+
badge: "bg-danger-soft text-danger-soft-text",
|
|
38
|
+
};
|
|
39
|
+
const WARNING = {
|
|
40
|
+
color: "text-warning-surface",
|
|
41
|
+
bg: "bg-warning-soft",
|
|
42
|
+
dot: "bg-warning-surface",
|
|
43
|
+
badge: "bg-warning-soft text-warning-soft-text",
|
|
44
|
+
};
|
|
45
|
+
const NEUTRAL = {
|
|
46
|
+
color: "text-on-surface-muted",
|
|
47
|
+
bg: "bg-surface-sunken",
|
|
48
|
+
dot: "bg-on-surface-faint",
|
|
49
|
+
badge: "bg-surface-sunken text-on-surface",
|
|
50
|
+
};
|
|
51
|
+
|
|
32
52
|
export const RUN_STATUS_CONFIG: Record<RunStatus, RunStatusConfig> = {
|
|
33
53
|
running: {
|
|
34
54
|
icon: LoaderCircle,
|
|
35
55
|
color: "text-info-surface",
|
|
36
56
|
bg: "bg-info-soft",
|
|
37
|
-
border: "border-info-soft",
|
|
38
57
|
dot: "bg-info-surface animate-pulse",
|
|
39
58
|
badge: "bg-info-soft text-info-soft-text",
|
|
40
59
|
description: "This run is executing right now.",
|
|
@@ -43,151 +62,87 @@ export const RUN_STATUS_CONFIG: Record<RunStatus, RunStatusConfig> = {
|
|
|
43
62
|
icon: CircleCheck,
|
|
44
63
|
color: "text-success-surface",
|
|
45
64
|
bg: "bg-success-soft",
|
|
46
|
-
border: "border-success-soft",
|
|
47
65
|
dot: "bg-success-surface",
|
|
48
66
|
badge: "bg-success-soft text-success-soft-text",
|
|
49
67
|
description: "The run finished with exit code 0 - everything OK.",
|
|
50
68
|
},
|
|
51
69
|
failed: {
|
|
70
|
+
...DANGER,
|
|
52
71
|
icon: CircleX,
|
|
53
|
-
color: "text-danger-surface",
|
|
54
|
-
bg: "bg-danger-soft",
|
|
55
|
-
border: "border-danger-soft",
|
|
56
|
-
dot: "bg-danger-surface",
|
|
57
|
-
badge: "bg-danger-soft text-danger-soft-text",
|
|
58
72
|
description: "The run exited with a non-zero code.",
|
|
59
73
|
},
|
|
60
74
|
crashed: {
|
|
75
|
+
...DANGER,
|
|
61
76
|
icon: CircleAlert,
|
|
62
|
-
color: "text-danger-surface",
|
|
63
|
-
bg: "bg-danger-soft",
|
|
64
|
-
border: "border-danger-soft",
|
|
65
|
-
dot: "bg-danger-surface",
|
|
66
|
-
badge: "bg-danger-soft text-danger-soft-text",
|
|
67
77
|
description:
|
|
68
78
|
"The process was killed, or the daemon found it still 'running' after a hard crash and marked it crashed. It was not resumed.",
|
|
69
79
|
},
|
|
70
80
|
stopped: {
|
|
81
|
+
...WARNING,
|
|
71
82
|
icon: CircleStop,
|
|
72
|
-
color: "text-warning-surface",
|
|
73
|
-
bg: "bg-warning-soft",
|
|
74
|
-
border: "border-warning-soft",
|
|
75
|
-
dot: "bg-warning-surface",
|
|
76
|
-
badge: "bg-warning-soft text-warning-soft-text",
|
|
77
83
|
description: "A human or an external script manually stopped this run before it finished.",
|
|
78
84
|
},
|
|
79
85
|
timeout: {
|
|
86
|
+
...WARNING,
|
|
80
87
|
icon: TimerOff,
|
|
81
|
-
color: "text-warning-surface",
|
|
82
|
-
bg: "bg-warning-soft",
|
|
83
|
-
border: "border-warning-soft",
|
|
84
|
-
dot: "bg-warning-surface",
|
|
85
|
-
badge: "bg-warning-soft text-warning-soft-text",
|
|
86
88
|
description:
|
|
87
89
|
"The run exceeded its configured timeout and was terminated. Timeout duration can be changed.",
|
|
88
90
|
},
|
|
89
91
|
skipped: {
|
|
92
|
+
...NEUTRAL,
|
|
90
93
|
icon: SkipForward,
|
|
91
|
-
color: "text-on-surface-muted",
|
|
92
|
-
bg: "bg-surface-sunken",
|
|
93
|
-
border: "border-outline-faint",
|
|
94
|
-
dot: "bg-on-surface-faint",
|
|
95
|
-
badge: "bg-surface-sunken text-on-surface",
|
|
96
94
|
description:
|
|
97
95
|
'Skipped by the concurrency policy (on_overlap = "skip") because a previous run was still going.',
|
|
98
96
|
},
|
|
99
97
|
log_overflow: {
|
|
98
|
+
...DANGER,
|
|
100
99
|
icon: FileExclamationPoint,
|
|
101
|
-
color: "text-danger-surface",
|
|
102
|
-
bg: "bg-danger-soft",
|
|
103
|
-
border: "border-danger-soft",
|
|
104
|
-
dot: "bg-danger-surface",
|
|
105
|
-
badge: "bg-danger-soft text-danger-soft-text",
|
|
106
100
|
description: "The run hit log_max_size and was handled because of log_on_full.",
|
|
107
101
|
},
|
|
108
102
|
queue_full: {
|
|
103
|
+
...WARNING,
|
|
109
104
|
icon: SkipForward,
|
|
110
|
-
color: "text-warning-surface",
|
|
111
|
-
bg: "bg-warning-soft",
|
|
112
|
-
border: "border-warning-soft",
|
|
113
|
-
dot: "bg-warning-surface",
|
|
114
|
-
badge: "bg-warning-soft text-warning-soft-text",
|
|
115
105
|
description: "Skipped because the task's queue was already at max_queued.",
|
|
116
106
|
},
|
|
117
107
|
dst_skipped: {
|
|
108
|
+
...NEUTRAL,
|
|
118
109
|
icon: SkipForward,
|
|
119
|
-
color: "text-on-surface-muted",
|
|
120
|
-
bg: "bg-surface-sunken",
|
|
121
|
-
border: "border-outline-faint",
|
|
122
|
-
dot: "bg-on-surface-faint",
|
|
123
|
-
badge: "bg-surface-sunken text-on-surface",
|
|
124
110
|
description:
|
|
125
111
|
"Skipped: this cron tick was the duplicate half of a DST fall-back, so it was recorded but not run.",
|
|
126
112
|
},
|
|
127
113
|
daemon_stopped: {
|
|
114
|
+
...WARNING,
|
|
128
115
|
icon: CircleStop,
|
|
129
|
-
color: "text-warning-surface",
|
|
130
|
-
bg: "bg-warning-soft",
|
|
131
|
-
border: "border-warning-soft",
|
|
132
|
-
dot: "bg-warning-surface",
|
|
133
|
-
badge: "bg-warning-soft text-warning-soft-text",
|
|
134
116
|
description:
|
|
135
117
|
"The daemon shut down while this run was in flight and it exceeded shutdown_timeout; it was not resumed.",
|
|
136
118
|
},
|
|
137
119
|
missed: {
|
|
120
|
+
...DANGER,
|
|
138
121
|
icon: CalendarX,
|
|
139
|
-
color: "text-danger-surface",
|
|
140
|
-
bg: "bg-danger-soft",
|
|
141
|
-
border: "border-danger-soft",
|
|
142
|
-
dot: "bg-danger-surface",
|
|
143
|
-
badge: "bg-danger-soft text-danger-soft-text",
|
|
144
122
|
description:
|
|
145
123
|
"A scheduled run never happened because the daemon was down. Detected and recorded on restart.",
|
|
146
124
|
},
|
|
147
125
|
start_failed: {
|
|
126
|
+
...DANGER,
|
|
148
127
|
icon: OctagonX,
|
|
149
|
-
color: "text-danger-surface",
|
|
150
|
-
bg: "bg-danger-soft",
|
|
151
|
-
border: "border-danger-soft",
|
|
152
|
-
dot: "bg-danger-surface",
|
|
153
|
-
badge: "bg-danger-soft text-danger-soft-text",
|
|
154
128
|
description:
|
|
155
129
|
"The run (or service instance) kept failing; after restart_attempts consecutive failures, RunWisp gave up restarting it automatically.",
|
|
156
130
|
},
|
|
157
131
|
unhealthy: {
|
|
132
|
+
...DANGER,
|
|
158
133
|
icon: HeartCrack,
|
|
159
|
-
color: "text-danger-surface",
|
|
160
|
-
bg: "bg-danger-soft",
|
|
161
|
-
border: "border-danger-soft",
|
|
162
|
-
dot: "bg-danger-surface",
|
|
163
|
-
badge: "bg-danger-soft text-danger-soft-text",
|
|
164
134
|
description:
|
|
165
135
|
"The service instance kept failing its health_check, so RunWisp stopped it. Its restart policy decides what happens next.",
|
|
166
136
|
},
|
|
167
137
|
pending: {
|
|
138
|
+
...NEUTRAL,
|
|
168
139
|
icon: Clock,
|
|
169
|
-
color: "text-on-surface-muted",
|
|
170
|
-
bg: "bg-surface-sunken",
|
|
171
|
-
border: "border-outline",
|
|
172
|
-
dot: "bg-on-surface-faint",
|
|
173
|
-
badge: "bg-surface-sunken text-on-surface",
|
|
174
140
|
description: "Queued and waiting to start.",
|
|
175
141
|
},
|
|
176
142
|
ended: {
|
|
177
|
-
|
|
143
|
+
...NEUTRAL,
|
|
178
144
|
color: "text-on-surface-faint",
|
|
179
|
-
|
|
180
|
-
border: "border-outline-faint",
|
|
181
|
-
dot: "bg-on-surface-faint",
|
|
182
|
-
badge: "bg-surface-sunken text-on-surface",
|
|
145
|
+
icon: CircleDashed,
|
|
183
146
|
description: "The run finished. No specific end reason was recorded.",
|
|
184
147
|
},
|
|
185
148
|
};
|
|
186
|
-
|
|
187
|
-
export function getRunStatusConfig(status: RunStatus): RunStatusConfig {
|
|
188
|
-
return RUN_STATUS_CONFIG[status];
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function runDisplayStatus(run: Pick<Run, "status" | "endReason">): RunStatus {
|
|
192
|
-
return displayStatus(run.status, run.endReason);
|
|
193
|
-
}
|
|
@@ -1,32 +1,6 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: PoppyCake, s.r.o.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import type { Task as CommonTask, Run as CommonRun } from "@runwisp/common";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Web UI component types.
|
|
8
|
-
* Source of truth for status values: {@link RunStatus} from `@runwisp/common`.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export type Task = CommonTask;
|
|
12
|
-
|
|
13
|
-
export type Run = CommonRun;
|
|
14
|
-
|
|
15
|
-
export interface DaemonState {
|
|
16
|
-
name: string;
|
|
17
|
-
version: string;
|
|
18
|
-
uptime: string;
|
|
19
|
-
status: "connected" | "disconnected";
|
|
20
|
-
host: string;
|
|
21
|
-
backendUrl: string;
|
|
22
|
-
cpus: number;
|
|
23
|
-
memory: string;
|
|
24
|
-
os: string;
|
|
25
|
-
arch: string;
|
|
26
|
-
workDir: string;
|
|
27
|
-
fingerprint: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
4
|
export interface DaemonStats {
|
|
31
5
|
cpuUsage: number;
|
|
32
6
|
memUsage: number;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: PoppyCake, s.r.o.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
export const RADIO_GROUP_KEY = Symbol("radio-group");
|
|
5
|
+
|
|
6
|
+
// What a RadioGroup hands its Radio children: the shared input name and the
|
|
7
|
+
// selected value, read and written through the group's bindable `value`.
|
|
8
|
+
export interface RadioGroupContext {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
value: string;
|
|
11
|
+
}
|
package/src/lib/index.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: PoppyCake, s.r.o.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
export { default as theme } from "./theme.css?inline";
|
|
5
|
-
|
|
6
4
|
export { default as Logo } from "./components/Logo.svelte";
|
|
7
5
|
|
|
8
6
|
export { default as PageContainer } from "./components/PageContainer.svelte";
|
|
@@ -19,13 +17,6 @@ export { default as Spinner } from "./components/Spinner.svelte";
|
|
|
19
17
|
|
|
20
18
|
export { default as Button } from "./components/Button.svelte";
|
|
21
19
|
export { default as LinkButton } from "./components/LinkButton.svelte";
|
|
22
|
-
export {
|
|
23
|
-
BUTTON_BASE,
|
|
24
|
-
BUTTON_SIZES,
|
|
25
|
-
BUTTON_VARIANTS,
|
|
26
|
-
type ButtonSize,
|
|
27
|
-
type ButtonVariant,
|
|
28
|
-
} from "./components/button-styles.js";
|
|
29
20
|
export { default as Checkbox } from "./components/Checkbox.svelte";
|
|
30
21
|
export { default as CronInput } from "./components/CronInput.svelte";
|
|
31
22
|
export { default as DurationInput } from "./components/DurationInput.svelte";
|
|
@@ -33,7 +24,7 @@ export { default as FormField } from "./components/FormField.svelte";
|
|
|
33
24
|
export { default as Input } from "./components/Input.svelte";
|
|
34
25
|
export { default as Radio } from "./components/Radio.svelte";
|
|
35
26
|
export { default as RadioGroup } from "./components/RadioGroup.svelte";
|
|
36
|
-
export { default as Select
|
|
27
|
+
export { default as Select } from "./components/Select.svelte";
|
|
37
28
|
export { default as Textarea } from "./components/Textarea.svelte";
|
|
38
29
|
export { default as TimezoneSelect } from "./components/TimezoneSelect.svelte";
|
|
39
30
|
export { default as Toggle } from "./components/Toggle.svelte";
|
|
@@ -88,71 +79,45 @@ export { default as ThemeToggle } from "./components/ThemeToggle.svelte";
|
|
|
88
79
|
|
|
89
80
|
export { default as CodeBlock } from "./components/CodeBlock.svelte";
|
|
90
81
|
export { default as TaskCard } from "./components/TaskCard.svelte";
|
|
91
|
-
export type { TaskCardAccent } from "./components/TaskCard.svelte";
|
|
92
82
|
|
|
93
83
|
export { default as LogConsole } from "./components/LogConsole.svelte";
|
|
94
84
|
export type { FetchLogsFn, LogEvent, LogSlice } from "./log-console/types.js";
|
|
95
|
-
export { isLogEvent } from "./log-console/types.js";
|
|
96
|
-
export { LogCache } from "./log-console/LogCache.svelte.js";
|
|
97
|
-
export { LogFetcher } from "./log-console/LogFetcher.svelte.js";
|
|
98
85
|
|
|
99
86
|
export { default as RunDetailPanel } from "./components/dashboard/RunDetailPanel.svelte";
|
|
100
87
|
export { default as RunsList } from "./components/dashboard/RunsList.svelte";
|
|
101
88
|
export { default as RunFilterPopover } from "./components/dashboard/RunFilterPopover.svelte";
|
|
102
89
|
export type { RunOutputMatch } from "./components/dashboard/RunsList.svelte";
|
|
103
90
|
export {
|
|
91
|
+
activeFilterCount,
|
|
104
92
|
emptyRunFilters,
|
|
105
93
|
FAILURE_STATUS_TOKEN,
|
|
106
|
-
NEEDS_ATTENTION_STATUSES,
|
|
107
|
-
isNeedsAttention,
|
|
108
|
-
activeFilterCount,
|
|
109
|
-
activeDimensions,
|
|
110
|
-
dimensionActive,
|
|
111
|
-
clearDimension,
|
|
112
|
-
clearPopoverFilters,
|
|
113
94
|
humanizeStatus,
|
|
114
|
-
statusChipLabel,
|
|
115
95
|
exitCodeRange,
|
|
116
96
|
type ExitCodeRange,
|
|
117
97
|
type RunsListFilters,
|
|
118
|
-
type RunsListSortDirection,
|
|
119
|
-
type FilterDimension,
|
|
120
98
|
} from "./components/dashboard/run-filters.js";
|
|
121
|
-
export {
|
|
122
|
-
getRunStatusConfig,
|
|
123
|
-
runDisplayStatus,
|
|
124
|
-
RUN_STATUS_CONFIG,
|
|
125
|
-
type RunStatusConfig,
|
|
126
|
-
} from "./components/dashboard/status-config.js";
|
|
99
|
+
export { RUN_STATUS_CONFIG } from "./components/dashboard/status-config.js";
|
|
127
100
|
export {
|
|
128
101
|
runDuration,
|
|
129
|
-
runStartDelay,
|
|
130
102
|
runPhaseOrder,
|
|
131
103
|
formatTriggeredByLabel,
|
|
132
|
-
runRetryLabel,
|
|
133
104
|
instanceSuffix,
|
|
134
105
|
} from "./components/dashboard/run-helpers.js";
|
|
135
|
-
export { humanizeCron
|
|
136
|
-
export type {
|
|
106
|
+
export { humanizeCron } from "./components/dashboard/cron-format.js";
|
|
107
|
+
export type { DaemonStats } from "./components/dashboard/types.js";
|
|
137
108
|
|
|
138
|
-
export { toast
|
|
139
|
-
export {
|
|
140
|
-
themeStore,
|
|
141
|
-
THEME_STORAGE_KEY,
|
|
142
|
-
type ThemePreference,
|
|
143
|
-
type ResolvedTheme,
|
|
144
|
-
} from "./utils/theme.svelte.js";
|
|
109
|
+
export { toast } from "./utils/toast.svelte.js";
|
|
145
110
|
export { extractErrorMessage } from "./utils/error.js";
|
|
146
111
|
export {
|
|
147
112
|
formatBytes,
|
|
148
113
|
formatRelativeTime,
|
|
149
114
|
formatRelativeTimeWithAbsolute,
|
|
150
|
-
formatDateTime,
|
|
151
115
|
formatDuration,
|
|
152
116
|
formatFullDateTime,
|
|
153
117
|
} from "./utils/format.js";
|
|
154
118
|
export { formatShortId } from "./utils/id.js";
|
|
155
119
|
export { debounce } from "./utils/debounce.js";
|
|
156
120
|
export { TickingNow } from "./utils/ticking-now.svelte.js";
|
|
121
|
+
export { CopyFeedback, copyText } from "./utils/clipboard.svelte.js";
|
|
157
122
|
export { RunMotion } from "./utils/run-motion.js";
|
|
158
|
-
export { arrival, leave, shift
|
|
123
|
+
export { arrival, leave, shift } from "./actions/row-motion.js";
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: PoppyCake, s.r.o.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { createLogger } from "@runwisp/common";
|
|
5
5
|
import { SvelteSet } from "svelte/reactivity";
|
|
6
6
|
import type { LogCache } from "./LogCache.svelte.js";
|
|
7
7
|
import type { FetchLogsFn } from "./types.js";
|
|
8
8
|
import { isLogEvent } from "./types.js";
|
|
9
9
|
|
|
10
|
-
const createLogger = createLoggerFactory();
|
|
11
10
|
const logger = createLogger("LogFetcher");
|
|
12
11
|
|
|
13
12
|
/**
|
|
@@ -20,8 +19,8 @@ const logger = createLogger("LogFetcher");
|
|
|
20
19
|
*/
|
|
21
20
|
export class LogFetcher {
|
|
22
21
|
private readonly cache: LogCache;
|
|
23
|
-
private fetchLogsFn: FetchLogsFn | undefined;
|
|
24
|
-
private chunkSize
|
|
22
|
+
private readonly fetchLogsFn: FetchLogsFn | undefined;
|
|
23
|
+
private readonly chunkSize = 4096;
|
|
25
24
|
|
|
26
25
|
readonly MAX_INFLIGHT = 2;
|
|
27
26
|
readonly MIN_REQUEST_INTERVAL_MS = 140;
|
|
@@ -38,23 +37,13 @@ export class LogFetcher {
|
|
|
38
37
|
constructor(
|
|
39
38
|
cache: LogCache,
|
|
40
39
|
fetchLogsFn: FetchLogsFn | undefined,
|
|
41
|
-
chunkSize: number = 4096,
|
|
42
40
|
onDataLoaded?: (min: number, max: number) => void,
|
|
43
41
|
) {
|
|
44
42
|
this.cache = cache;
|
|
45
43
|
this.fetchLogsFn = fetchLogsFn;
|
|
46
|
-
this.chunkSize = Math.max(1, Math.floor(chunkSize));
|
|
47
44
|
this.onDataLoaded = onDataLoaded;
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
setFetchLogsFn(fn: FetchLogsFn | undefined) {
|
|
51
|
-
this.fetchLogsFn = fn;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
setChunkSize(size: number) {
|
|
55
|
-
this.chunkSize = Math.max(1, Math.floor(size));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
47
|
enqueue(from: number, to: number) {
|
|
59
48
|
if (!this.fetchLogsFn) return;
|
|
60
49
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: PoppyCake, s.r.o.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { createHighlightScroll } from "./highlight-scroll.js";
|
|
6
|
+
|
|
7
|
+
// `sync` is what LogConsole calls inside its $effect. The effect re-runs it
|
|
8
|
+
// whenever something it READ changes, so "no layout reads after the reveal"
|
|
9
|
+
// (ready not consulted again) is exactly "layout changes can't re-scroll".
|
|
10
|
+
function setup(initial: { line: number | null; ready: boolean }) {
|
|
11
|
+
const state = { ...initial };
|
|
12
|
+
const ready = vi.fn(() => state.ready);
|
|
13
|
+
const reveal = vi.fn();
|
|
14
|
+
const clear = vi.fn();
|
|
15
|
+
const sync = createHighlightScroll({ line: () => state.line, ready, reveal, clear });
|
|
16
|
+
return { state, ready, reveal, clear, sync };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("createHighlightScroll", () => {
|
|
20
|
+
it("reveals once and stops reading layout afterwards", () => {
|
|
21
|
+
const { ready, reveal, sync } = setup({ line: 5, ready: true });
|
|
22
|
+
sync();
|
|
23
|
+
expect(reveal).toHaveBeenCalledExactlyOnceWith(5);
|
|
24
|
+
expect(ready).toHaveBeenCalledTimes(1);
|
|
25
|
+
|
|
26
|
+
// A running run appends lines or the pane resizes: the effect re-runs,
|
|
27
|
+
// but must neither scroll again nor re-read the layout.
|
|
28
|
+
sync();
|
|
29
|
+
sync();
|
|
30
|
+
expect(reveal).toHaveBeenCalledTimes(1);
|
|
31
|
+
expect(ready).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("waits until the line can be placed, then reveals once", () => {
|
|
35
|
+
const { state, reveal, sync } = setup({ line: 5, ready: false });
|
|
36
|
+
sync();
|
|
37
|
+
expect(reveal).not.toHaveBeenCalled();
|
|
38
|
+
state.ready = true;
|
|
39
|
+
sync();
|
|
40
|
+
sync();
|
|
41
|
+
expect(reveal).toHaveBeenCalledExactlyOnceWith(5);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("reveals again for a new line and after the highlight is cleared", () => {
|
|
45
|
+
const { state, reveal, clear, sync } = setup({ line: 5, ready: true });
|
|
46
|
+
sync();
|
|
47
|
+
state.line = 9;
|
|
48
|
+
sync();
|
|
49
|
+
expect(reveal).toHaveBeenLastCalledWith(9);
|
|
50
|
+
|
|
51
|
+
state.line = null;
|
|
52
|
+
sync();
|
|
53
|
+
expect(clear).toHaveBeenCalledOnce();
|
|
54
|
+
|
|
55
|
+
state.line = 9;
|
|
56
|
+
sync();
|
|
57
|
+
expect(reveal).toHaveBeenCalledTimes(3);
|
|
58
|
+
});
|
|
59
|
+
});
|