@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.
Files changed (70) hide show
  1. package/package.json +4 -5
  2. package/src/lib/actions/floating.ts +73 -0
  3. package/src/lib/actions/portal.ts +5 -1
  4. package/src/lib/components/Accordion.svelte +6 -2
  5. package/src/lib/components/AccordionItem.svelte +80 -23
  6. package/src/lib/components/Avatar.svelte +0 -1
  7. package/src/lib/components/CodeBlock.svelte +84 -35
  8. package/src/lib/components/CronInput.svelte +5 -2
  9. package/src/lib/components/DataGrid.svelte +95 -133
  10. package/src/lib/components/Drawer.svelte +2 -6
  11. package/src/lib/components/Dropdown.svelte +19 -57
  12. package/src/lib/components/EmptyState.svelte +2 -1
  13. package/src/lib/components/ErrorState.svelte +2 -1
  14. package/src/lib/components/Heading.svelte +19 -12
  15. package/src/lib/components/Kbd.svelte +25 -10
  16. package/src/lib/components/Link.svelte +7 -1
  17. package/src/lib/components/LogConsole.svelte +30 -32
  18. package/src/lib/components/Modal.svelte +44 -33
  19. package/src/lib/components/NotificationBell.svelte +2 -1
  20. package/src/lib/components/PageContainer.svelte +1 -10
  21. package/src/lib/components/PageHeader.svelte +8 -4
  22. package/src/lib/components/Popover.svelte +9 -51
  23. package/src/lib/components/Progress.svelte +19 -6
  24. package/src/lib/components/Radio.svelte +16 -4
  25. package/src/lib/components/RadioGroup.svelte +15 -2
  26. package/src/lib/components/Select.svelte +13 -45
  27. package/src/lib/components/Table.svelte +13 -4
  28. package/src/lib/components/TableBody.svelte +2 -1
  29. package/src/lib/components/TableCell.svelte +38 -8
  30. package/src/lib/components/TableHead.svelte +12 -4
  31. package/src/lib/components/TableRow.svelte +23 -3
  32. package/src/lib/components/TaskCard.svelte +1 -1
  33. package/src/lib/components/TimezoneSelect.svelte +2 -2
  34. package/src/lib/components/accordion.svelte.ts +19 -0
  35. package/src/lib/components/dashboard/RunDetailPanel.svelte +91 -63
  36. package/src/lib/components/dashboard/RunFilterPopover.svelte +7 -16
  37. package/src/lib/components/dashboard/RunsList.svelte +9 -18
  38. package/src/lib/components/dashboard/cron-format.ts +1 -1
  39. package/src/lib/components/dashboard/run-filters.test.ts +2 -58
  40. package/src/lib/components/dashboard/run-filters.ts +5 -53
  41. package/src/lib/components/dashboard/run-helpers.test.ts +17 -7
  42. package/src/lib/components/dashboard/run-helpers.ts +12 -1
  43. package/src/lib/components/dashboard/status-config.ts +38 -83
  44. package/src/lib/components/dashboard/types.ts +0 -26
  45. package/src/lib/components/radio-group.ts +11 -0
  46. package/src/lib/index.ts +8 -43
  47. package/src/lib/log-console/LogFetcher.svelte.ts +3 -14
  48. package/src/lib/log-console/highlight-scroll.test.ts +59 -0
  49. package/src/lib/log-console/highlight-scroll.ts +41 -0
  50. package/src/lib/theme.css +10 -4
  51. package/src/lib/utils/clipboard.svelte.test.ts +92 -0
  52. package/src/lib/utils/clipboard.svelte.ts +73 -0
  53. package/src/lib/utils/format.ts +65 -94
  54. package/src/lib/utils/id.test.ts +3 -7
  55. package/src/lib/utils/id.ts +3 -2
  56. package/src/lib/utils/theme.svelte.test.ts +0 -6
  57. package/src/lib/utils/theme.svelte.ts +3 -5
  58. package/src/lib/utils/toast.svelte.ts +3 -11
  59. package/src/stories/Accordion.stories.svelte +61 -0
  60. package/src/stories/CodeBlock.stories.svelte +55 -0
  61. package/src/stories/DataGrid.stories.svelte +0 -5
  62. package/src/stories/Heading.stories.svelte +31 -0
  63. package/src/stories/Kbd.stories.svelte +36 -0
  64. package/src/stories/Link.stories.svelte +38 -0
  65. package/src/stories/PageHeader.stories.svelte +31 -0
  66. package/src/stories/Progress.stories.svelte +26 -0
  67. package/src/stories/RadioGroup.stories.svelte +43 -0
  68. package/src/stories/Table.stories.svelte +69 -0
  69. package/src/svelte-shims.d.ts +0 -5
  70. 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: RunsListSortDirection;
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 needs-attention
78
- * set (Prime Directive #1). `dot` mirrors that group's color in
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: NEEDS_ATTENTION_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 { displayStatus, type RunStatus, type Run } from "@runwisp/common";
19
+ import type { RunStatus } from "@runwisp/common";
20
20
 
21
- export interface RunStatusConfig {
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
- icon: CircleDashed,
143
+ ...NEUTRAL,
178
144
  color: "text-on-surface-faint",
179
- bg: "bg-surface-sunken",
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, type SelectOption } from "./components/Select.svelte";
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, type HumanizedCron } from "./components/dashboard/cron-format.js";
136
- export type { DaemonState, DaemonStats } from "./components/dashboard/types.js";
106
+ export { humanizeCron } from "./components/dashboard/cron-format.js";
107
+ export type { DaemonStats } from "./components/dashboard/types.js";
137
108
 
138
- export { toast, type Toast, type ToastType } from "./utils/toast.svelte.js";
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, prefersReducedMotion } from "./actions/row-motion.js";
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 { createLoggerFactory } from "@runwisp/common";
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: number;
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
+ });