@runwisp/ui 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -16
- package/src/app.html +1 -0
- package/src/lib/actions/portal.ts +1 -3
- package/src/lib/components/AccordionItem.svelte +2 -4
- package/src/lib/components/Badge.svelte +4 -8
- package/src/lib/components/Card.svelte +5 -2
- package/src/lib/components/DataGrid.svelte +2 -3
- package/src/lib/components/Dropdown.svelte +3 -3
- package/src/lib/components/DurationInput.svelte +2 -2
- package/src/lib/components/Heading.svelte +1 -2
- package/src/lib/components/LogConsole.svelte +62 -7
- package/src/lib/components/PageHeader.svelte +29 -0
- package/src/lib/components/Pagination.svelte +2 -2
- package/src/lib/components/Popover.svelte +3 -3
- package/src/lib/components/Select.svelte +6 -10
- package/src/lib/components/TaskCard.svelte +33 -0
- package/src/lib/components/ToastContainer.svelte +12 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +39 -9
- package/src/lib/components/dashboard/RunsList.svelte +412 -243
- package/src/lib/components/dashboard/run-helpers.test.ts +37 -0
- package/src/lib/components/dashboard/run-helpers.ts +11 -0
- package/src/lib/components/dashboard/status-config.ts +1 -2
- package/src/lib/index.ts +9 -21
- package/src/lib/log-console/LogFetcher.svelte.ts +4 -4
- package/src/lib/log-console/types.ts +1 -4
- package/src/lib/theme-tokens.css +1 -3
- package/src/lib/utils/format.test.ts +88 -0
- package/src/lib/utils/toast.svelte.ts +25 -11
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runwisp/ui",
|
|
3
3
|
"description": "Svelte 5 UI component library for RunWisp dashboards and admin interfaces.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -18,20 +18,11 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"dev": "storybook dev -p 6006 --no-open",
|
|
20
20
|
"build": "echo '@runwisp/ui: no build step (raw source)'",
|
|
21
|
-
"check": "
|
|
21
|
+
"check": "bun run check:types && bun run check:lint",
|
|
22
|
+
"check:types": "svelte-check --tsconfig ./tsconfig.json",
|
|
23
|
+
"check:lint": "prettier --check . && eslint .",
|
|
22
24
|
"format": "prettier --write ."
|
|
23
25
|
},
|
|
24
|
-
"nx": {
|
|
25
|
-
"targets": {
|
|
26
|
-
"build": {
|
|
27
|
-
"cache": true,
|
|
28
|
-
"inputs": [
|
|
29
|
-
"production"
|
|
30
|
-
],
|
|
31
|
-
"outputs": []
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
26
|
"files": [
|
|
36
27
|
"src"
|
|
37
28
|
],
|
|
@@ -67,8 +58,8 @@
|
|
|
67
58
|
"@chromatic-com/storybook": "^4.1.3",
|
|
68
59
|
"@eslint/compat": "^1.4.0",
|
|
69
60
|
"@eslint/js": "^9.39.1",
|
|
70
|
-
"@runwisp/eslint-config": "0.
|
|
71
|
-
"@runwisp/typescript-config": "0.
|
|
61
|
+
"@runwisp/eslint-config": "0.7.0",
|
|
62
|
+
"@runwisp/typescript-config": "0.7.0",
|
|
72
63
|
"@storybook/addon-a11y": "^10.1.9",
|
|
73
64
|
"@storybook/addon-docs": "^10.1.9",
|
|
74
65
|
"@storybook/addon-svelte-csf": "^5.0.10",
|
|
@@ -116,7 +107,8 @@
|
|
|
116
107
|
"@floating-ui/dom": "^1.7.4",
|
|
117
108
|
"@fontsource/tasa-orbiter": "^5.2.2",
|
|
118
109
|
"@lucide/svelte": "^0.561.0",
|
|
119
|
-
"@runwisp/common": "0.
|
|
110
|
+
"@runwisp/common": "0.7.0",
|
|
111
|
+
"@tanstack/svelte-virtual": "^3.13.25",
|
|
120
112
|
"ansi-to-html": "^0.7.2",
|
|
121
113
|
"cron-parser": "^5.4.0",
|
|
122
114
|
"cronstrue": "^3.9.0",
|
package/src/app.html
CHANGED
|
@@ -26,8 +26,6 @@
|
|
|
26
26
|
open = !open;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
const isSnippet = $derived(typeof title === "function");
|
|
31
29
|
</script>
|
|
32
30
|
|
|
33
31
|
<div class="group {className}">
|
|
@@ -44,8 +42,8 @@
|
|
|
44
42
|
aria-expanded={open}
|
|
45
43
|
>
|
|
46
44
|
<span class="flex-1">
|
|
47
|
-
{#if
|
|
48
|
-
{@render
|
|
45
|
+
{#if typeof title === "function"}
|
|
46
|
+
{@render title()}
|
|
49
47
|
{:else if title}
|
|
50
48
|
{title}
|
|
51
49
|
{/if}
|
|
@@ -89,21 +89,17 @@
|
|
|
89
89
|
md: "h-3.5 w-3.5",
|
|
90
90
|
lg: "h-4 w-4",
|
|
91
91
|
};
|
|
92
|
-
|
|
93
|
-
let safeVariant = $derived(
|
|
94
|
-
(Object.hasOwn(variantClasses, variant) ? variant : "default") as BadgeVariant,
|
|
95
|
-
);
|
|
96
92
|
</script>
|
|
97
93
|
|
|
98
94
|
<span
|
|
99
95
|
class="{baseClasses} {outline
|
|
100
|
-
? variantClasses[
|
|
101
|
-
: variantClasses[
|
|
96
|
+
? variantClasses[variant].outline
|
|
97
|
+
: variantClasses[variant].solid} {sizeClasses[size]} {className}"
|
|
102
98
|
>
|
|
103
99
|
{#if dot}
|
|
104
100
|
{#if loading}
|
|
105
101
|
<svg
|
|
106
|
-
class="{spinnerSizeClasses[size]} animate-spin {indicatorTextColors[
|
|
102
|
+
class="{spinnerSizeClasses[size]} animate-spin {indicatorTextColors[variant]}"
|
|
107
103
|
viewBox="0 0 24 24"
|
|
108
104
|
fill="none"
|
|
109
105
|
>
|
|
@@ -122,7 +118,7 @@
|
|
|
122
118
|
></path>
|
|
123
119
|
</svg>
|
|
124
120
|
{:else}
|
|
125
|
-
<span class="h-1.5 w-1.5 rounded-full {dotColors[
|
|
121
|
+
<span class="h-1.5 w-1.5 rounded-full {dotColors[variant]}"></span>
|
|
126
122
|
{/if}
|
|
127
123
|
{/if}
|
|
128
124
|
{#if children}
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
padding?: "none" | "sm" | "md" | "lg";
|
|
9
9
|
hover?: boolean;
|
|
10
10
|
border?: boolean;
|
|
11
|
-
shadow?: "none" | "sm" | "md" | "lg";
|
|
11
|
+
shadow?: "none" | "sm" | "md" | "lg" | "xl";
|
|
12
12
|
header?: Snippet;
|
|
13
13
|
footer?: Snippet;
|
|
14
14
|
children?: Snippet;
|
|
15
15
|
class?: string;
|
|
16
|
+
bodyClass?: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
let {
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
footer,
|
|
25
26
|
children,
|
|
26
27
|
class: className = "",
|
|
28
|
+
bodyClass = "",
|
|
27
29
|
}: Props = $props();
|
|
28
30
|
|
|
29
31
|
const paddingClasses: Record<string, string> = {
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
sm: "shadow-sm",
|
|
39
41
|
md: "shadow-md",
|
|
40
42
|
lg: "shadow-lg",
|
|
43
|
+
xl: "shadow-xl shadow-mist-900/10",
|
|
41
44
|
};
|
|
42
45
|
</script>
|
|
43
46
|
|
|
@@ -56,7 +59,7 @@
|
|
|
56
59
|
</div>
|
|
57
60
|
{/if}
|
|
58
61
|
|
|
59
|
-
<div class={paddingClasses[padding]}>
|
|
62
|
+
<div class="{paddingClasses[padding]} {bodyClass}">
|
|
60
63
|
{#if children}
|
|
61
64
|
{@render children()}
|
|
62
65
|
{/if}
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
<Input
|
|
177
177
|
value={filterQuery}
|
|
178
178
|
oninput={(e) => {
|
|
179
|
-
filterQuery =
|
|
179
|
+
filterQuery = e.currentTarget.value;
|
|
180
180
|
if (paginate && !total) page = 1;
|
|
181
181
|
}}
|
|
182
182
|
type="search"
|
|
@@ -308,8 +308,7 @@
|
|
|
308
308
|
<td
|
|
309
309
|
class="px-4 {compact ? 'py-2' : 'py-3'} cursor-pointer"
|
|
310
310
|
onclick={(e) => {
|
|
311
|
-
if ((e.target
|
|
312
|
-
toggleRow(row);
|
|
311
|
+
if (!(e.target instanceof HTMLInputElement)) toggleRow(row);
|
|
313
312
|
}}
|
|
314
313
|
>
|
|
315
314
|
<Checkbox
|
|
@@ -90,9 +90,9 @@
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
function handleOutsideClick(e: MouseEvent) {
|
|
93
|
-
const
|
|
94
|
-
if (triggerEl
|
|
95
|
-
if (menuEl
|
|
93
|
+
const path = e.composedPath();
|
|
94
|
+
if (triggerEl && path.includes(triggerEl)) return;
|
|
95
|
+
if (menuEl && path.includes(menuEl)) return;
|
|
96
96
|
if (open) toggle(false);
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
updateFromValue();
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
function handleInput(e: Event) {
|
|
89
|
-
const val =
|
|
88
|
+
function handleInput(e: Event & { currentTarget: EventTarget & HTMLInputElement }) {
|
|
89
|
+
const val = e.currentTarget.value;
|
|
90
90
|
textValue = val;
|
|
91
91
|
|
|
92
92
|
const msValue = parseToMs(val);
|
|
@@ -22,13 +22,12 @@
|
|
|
22
22
|
3: "2xl",
|
|
23
23
|
4: "xl",
|
|
24
24
|
5: "lg",
|
|
25
|
-
6: "
|
|
25
|
+
6: "md",
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
const sizeClasses: Record<string, string> = {
|
|
29
29
|
xs: "text-xs font-semibold",
|
|
30
30
|
sm: "text-sm font-semibold",
|
|
31
|
-
base: "text-base font-semibold",
|
|
32
31
|
md: "text-base font-semibold",
|
|
33
32
|
lg: "text-lg font-semibold",
|
|
34
33
|
xl: "text-xl font-bold",
|
|
@@ -21,9 +21,23 @@
|
|
|
21
21
|
chunkSize?: number;
|
|
22
22
|
lineHeight?: number;
|
|
23
23
|
class?: string;
|
|
24
|
+
// highlightLine, when set, scrolls the console so the given absolute
|
|
25
|
+
// line number is visible and pulses a short-lived flash on that row.
|
|
26
|
+
// The pulse is one-shot: changing the prop to a new line restarts the
|
|
27
|
+
// animation; setting it to null clears immediately.
|
|
28
|
+
highlightLine?: number | null;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
let {
|
|
31
|
+
let {
|
|
32
|
+
fetchLogs,
|
|
33
|
+
chunkSize = 4096,
|
|
34
|
+
lineHeight = 20,
|
|
35
|
+
class: className = "",
|
|
36
|
+
highlightLine = null,
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
let flashLine = $state<number | null>(null);
|
|
40
|
+
let flashTimer: ReturnType<typeof setTimeout> | null = null;
|
|
27
41
|
|
|
28
42
|
const cache = new LogCache();
|
|
29
43
|
let fetcher: LogFetcher | null = $state(null);
|
|
@@ -111,10 +125,11 @@
|
|
|
111
125
|
|
|
112
126
|
// Request missing data for the visible range (read-only w.r.t. cache).
|
|
113
127
|
$effect(() => {
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
const f = fetcher;
|
|
129
|
+
if (f && cache.totalLines > 0) {
|
|
130
|
+
f.pruneQueue(visibleStart, visibleEnd);
|
|
116
131
|
untrack(() => {
|
|
117
|
-
|
|
132
|
+
f.maybeRequestMissing(visibleStart, visibleEnd);
|
|
118
133
|
});
|
|
119
134
|
}
|
|
120
135
|
});
|
|
@@ -136,8 +151,8 @@
|
|
|
136
151
|
prevTotalLines = currentTotal;
|
|
137
152
|
});
|
|
138
153
|
|
|
139
|
-
function onScroll(e: Event) {
|
|
140
|
-
const target = e.
|
|
154
|
+
function onScroll(e: Event & { currentTarget: EventTarget & HTMLDivElement }) {
|
|
155
|
+
const target = e.currentTarget;
|
|
141
156
|
scrollTop = target.scrollTop;
|
|
142
157
|
|
|
143
158
|
const scrollHeight = target.scrollHeight;
|
|
@@ -201,6 +216,30 @@
|
|
|
201
216
|
scrollTop = 0;
|
|
202
217
|
}
|
|
203
218
|
|
|
219
|
+
// Search-hit deep-link: when highlightLine flips to a non-null number,
|
|
220
|
+
// scroll the viewport so that line is centred, and pulse a one-shot
|
|
221
|
+
// flash class for ~1.5s. Cleaning up the timer on prop change keeps a
|
|
222
|
+
// rapid sequence of jumps from leaking timers.
|
|
223
|
+
$effect(() => {
|
|
224
|
+
const target = highlightLine;
|
|
225
|
+
if (target === null || target === undefined) {
|
|
226
|
+
flashLine = null;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!containerEl) return;
|
|
230
|
+
const targetY = lineTop(target) - containerHeight / 2 + lineHeight / 2;
|
|
231
|
+
const clamped = Math.max(0, Math.min(targetY, totalHeight - containerHeight));
|
|
232
|
+
containerEl.scrollTo({ top: clamped, behavior: "smooth" });
|
|
233
|
+
isAutoScroll = false;
|
|
234
|
+
userScrolledUp = true;
|
|
235
|
+
flashLine = target;
|
|
236
|
+
if (flashTimer !== null) clearTimeout(flashTimer);
|
|
237
|
+
flashTimer = setTimeout(() => {
|
|
238
|
+
flashLine = null;
|
|
239
|
+
flashTimer = null;
|
|
240
|
+
}, 1500);
|
|
241
|
+
});
|
|
242
|
+
|
|
204
243
|
$effect(() => {
|
|
205
244
|
if (!containerEl) return;
|
|
206
245
|
containerHeight = containerEl.clientHeight;
|
|
@@ -241,7 +280,10 @@
|
|
|
241
280
|
|
|
242
281
|
{#each renderedLines as line (line.num)}
|
|
243
282
|
<div
|
|
244
|
-
class="log-line absolute right-0 left-0 flex items-center hover:bg-slate-900/50
|
|
283
|
+
class="log-line absolute right-0 left-0 flex items-center hover:bg-slate-900/50 {flashLine ===
|
|
284
|
+
line.num
|
|
285
|
+
? 'log-line--flash'
|
|
286
|
+
: ''}"
|
|
245
287
|
style="top: {lineTop(line.num)}px; height: {lineHeight}px;"
|
|
246
288
|
>
|
|
247
289
|
<div
|
|
@@ -375,6 +417,19 @@
|
|
|
375
417
|
--log-line-height: 20px;
|
|
376
418
|
}
|
|
377
419
|
|
|
420
|
+
.log-line--flash {
|
|
421
|
+
animation: log-line-flash 1.5s ease-out;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
@keyframes log-line-flash {
|
|
425
|
+
0% {
|
|
426
|
+
background-color: rgba(251, 191, 36, 0.45);
|
|
427
|
+
}
|
|
428
|
+
100% {
|
|
429
|
+
background-color: transparent;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
378
433
|
.dot {
|
|
379
434
|
display: inline-block;
|
|
380
435
|
width: 4px;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
title: string;
|
|
9
|
+
subtitle?: string;
|
|
10
|
+
actions?: Snippet;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { title, subtitle, actions, class: className = "" }: Props = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class="flex shrink-0 flex-wrap items-start justify-between gap-3 px-1 {className}">
|
|
18
|
+
<div class="min-w-0">
|
|
19
|
+
<h1 class="text-2xl font-bold tracking-tight text-on-surface">{title}</h1>
|
|
20
|
+
{#if subtitle}
|
|
21
|
+
<p class="mt-0.5 text-sm text-on-surface-muted">{subtitle}</p>
|
|
22
|
+
{/if}
|
|
23
|
+
</div>
|
|
24
|
+
{#if actions}
|
|
25
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
26
|
+
{@render actions()}
|
|
27
|
+
</div>
|
|
28
|
+
{/if}
|
|
29
|
+
</div>
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
onPageChange?.(clamped);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function handlePageSizeChange(e: Event) {
|
|
40
|
-
const newSize = Number(
|
|
39
|
+
function handlePageSizeChange(e: Event & { currentTarget: EventTarget & HTMLSelectElement }) {
|
|
40
|
+
const newSize = Number(e.currentTarget.value);
|
|
41
41
|
onPageSizeChange?.(newSize);
|
|
42
42
|
}
|
|
43
43
|
</script>
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function handleOutsideClick(e: MouseEvent) {
|
|
67
|
-
const
|
|
68
|
-
if (triggerEl
|
|
69
|
-
if (contentEl
|
|
67
|
+
const path = e.composedPath();
|
|
68
|
+
if (triggerEl && path.includes(triggerEl)) return;
|
|
69
|
+
if (contentEl && path.includes(contentEl)) return;
|
|
70
70
|
if (open) {
|
|
71
71
|
open = false;
|
|
72
72
|
cleanupFloating?.();
|
|
@@ -212,16 +212,12 @@
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
function handleOutsideClick(e: MouseEvent) {
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
) {
|
|
222
|
-
isOpen = false;
|
|
223
|
-
cleanupFloating?.();
|
|
224
|
-
}
|
|
215
|
+
if (!isOpen) return;
|
|
216
|
+
const path = e.composedPath();
|
|
217
|
+
if (triggerEl && path.includes(triggerEl)) return;
|
|
218
|
+
if (menuEl && path.includes(menuEl)) return;
|
|
219
|
+
isOpen = false;
|
|
220
|
+
cleanupFloating?.();
|
|
225
221
|
}
|
|
226
222
|
|
|
227
223
|
onDestroy(() => {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
|
|
7
|
+
export type TaskCardAccent = "danger" | "wisp" | "aurora";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
accent?: TaskCardAccent;
|
|
11
|
+
onclick?: () => void;
|
|
12
|
+
children: Snippet;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { accent = "wisp", onclick, children, class: className = "" }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const accentClasses: Record<TaskCardAccent, string> = {
|
|
19
|
+
danger: "hover:border-danger-200",
|
|
20
|
+
wisp: "hover:border-wisp-200",
|
|
21
|
+
aurora: "hover:border-aurora-200",
|
|
22
|
+
};
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class="group w-full rounded-lg border border-mist-100 bg-surface-raised p-3 text-left transition-all hover:shadow-sm {accentClasses[
|
|
28
|
+
accent
|
|
29
|
+
]} {className}"
|
|
30
|
+
{onclick}
|
|
31
|
+
>
|
|
32
|
+
{@render children()}
|
|
33
|
+
</button>
|
|
@@ -72,6 +72,18 @@
|
|
|
72
72
|
aria-hidden="true"
|
|
73
73
|
/>
|
|
74
74
|
<p class="flex-1 text-sm font-medium">{toastItem.message}</p>
|
|
75
|
+
{#if toastItem.action}
|
|
76
|
+
{@const action = toastItem.action}
|
|
77
|
+
<button
|
|
78
|
+
onclick={() => {
|
|
79
|
+
action.onClick();
|
|
80
|
+
toast.remove(toastItem.id);
|
|
81
|
+
}}
|
|
82
|
+
class="flex-shrink-0 rounded-md px-2 py-1 text-xs font-semibold text-on-surface underline-offset-2 transition-colors hover:bg-surface-sunken hover:underline"
|
|
83
|
+
>
|
|
84
|
+
{action.label}
|
|
85
|
+
</button>
|
|
86
|
+
{/if}
|
|
75
87
|
<button
|
|
76
88
|
onclick={() => toast.remove(toastItem.id)}
|
|
77
89
|
class="flex-shrink-0 rounded-lg p-1 text-on-surface-muted transition-colors hover:bg-surface-sunken hover:text-on-surface"
|
|
@@ -3,8 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import { untrack } from "svelte";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Server,
|
|
8
|
+
Hash,
|
|
9
|
+
Terminal as TerminalIcon,
|
|
10
|
+
Download,
|
|
11
|
+
Trash2,
|
|
12
|
+
MousePointerClick,
|
|
13
|
+
} from "@lucide/svelte";
|
|
7
14
|
import Badge from "../Badge.svelte";
|
|
15
|
+
import Button from "../Button.svelte";
|
|
16
|
+
import EmptyState from "../EmptyState.svelte";
|
|
8
17
|
import LogConsole from "../LogConsole.svelte";
|
|
9
18
|
import type { Run } from "./types.js";
|
|
10
19
|
import type { LogEvent, LogSlice } from "../../log-console/types.js";
|
|
@@ -18,6 +27,8 @@
|
|
|
18
27
|
fetchLogs,
|
|
19
28
|
streamLogs,
|
|
20
29
|
showTaskName = false,
|
|
30
|
+
onDelete,
|
|
31
|
+
highlightLine = null,
|
|
21
32
|
}: {
|
|
22
33
|
run: Run | undefined;
|
|
23
34
|
fetchLogs: (
|
|
@@ -31,8 +42,16 @@
|
|
|
31
42
|
initialState?: { fromLine: number },
|
|
32
43
|
) => () => void;
|
|
33
44
|
showTaskName?: boolean;
|
|
45
|
+
onDelete?: (runId: string) => void;
|
|
46
|
+
highlightLine?: number | null;
|
|
34
47
|
} = $props();
|
|
35
48
|
|
|
49
|
+
let canDelete = $derived.by(() => {
|
|
50
|
+
if (!run || !onDelete) return false;
|
|
51
|
+
const status = runDisplayStatus(run);
|
|
52
|
+
return status !== "running" && status !== "pending";
|
|
53
|
+
});
|
|
54
|
+
|
|
36
55
|
const TAIL_LINES = 1000;
|
|
37
56
|
|
|
38
57
|
let logConsole = $state<{ onStream: (event: LogEvent) => void } | null>(null);
|
|
@@ -109,6 +128,18 @@
|
|
|
109
128
|
>
|
|
110
129
|
{runDisplayStatus(run).toUpperCase()}
|
|
111
130
|
</Badge>
|
|
131
|
+
{#if canDelete}
|
|
132
|
+
<Button
|
|
133
|
+
variant="ghost"
|
|
134
|
+
size="xs"
|
|
135
|
+
class="ml-1 text-danger-surface hover:bg-danger-soft"
|
|
136
|
+
onclick={() => onDelete?.(run.id)}
|
|
137
|
+
title="Delete this run"
|
|
138
|
+
aria-label="Delete run"
|
|
139
|
+
>
|
|
140
|
+
{#snippet icon()}<Trash2 size={14} />{/snippet}
|
|
141
|
+
</Button>
|
|
142
|
+
{/if}
|
|
112
143
|
</div>
|
|
113
144
|
<div
|
|
114
145
|
class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-on-surface-muted"
|
|
@@ -236,17 +267,16 @@
|
|
|
236
267
|
bind:this={logConsole}
|
|
237
268
|
fetchLogs={(f: number, t: number) => fetchLogs(run.id, f, t)}
|
|
238
269
|
class="min-h-0 flex-1"
|
|
270
|
+
{highlightLine}
|
|
239
271
|
/>
|
|
240
272
|
{/key}
|
|
241
273
|
</div>
|
|
242
274
|
{:else}
|
|
243
|
-
<div
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
<h3 class="mb-1 font-medium text-on-surface">No Run Selected</h3>
|
|
250
|
-
<p class="text-sm">Select a run from the history on the left to view details.</p>
|
|
275
|
+
<div class="flex flex-1 items-center justify-center bg-surface-sunken/30">
|
|
276
|
+
<EmptyState
|
|
277
|
+
title="Select a run"
|
|
278
|
+
description="Pick a run from the list to view details and logs."
|
|
279
|
+
icon={MousePointerClick}
|
|
280
|
+
/>
|
|
251
281
|
</div>
|
|
252
282
|
{/if}
|