@runwisp/ui 0.4.0 → 0.6.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 +6 -5
- 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 +36 -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.6.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.6.0",
|
|
62
|
+
"@runwisp/typescript-config": "0.6.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.6.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",
|
|
@@ -111,10 +111,11 @@
|
|
|
111
111
|
|
|
112
112
|
// Request missing data for the visible range (read-only w.r.t. cache).
|
|
113
113
|
$effect(() => {
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
const f = fetcher;
|
|
115
|
+
if (f && cache.totalLines > 0) {
|
|
116
|
+
f.pruneQueue(visibleStart, visibleEnd);
|
|
116
117
|
untrack(() => {
|
|
117
|
-
|
|
118
|
+
f.maybeRequestMissing(visibleStart, visibleEnd);
|
|
118
119
|
});
|
|
119
120
|
}
|
|
120
121
|
});
|
|
@@ -136,8 +137,8 @@
|
|
|
136
137
|
prevTotalLines = currentTotal;
|
|
137
138
|
});
|
|
138
139
|
|
|
139
|
-
function onScroll(e: Event) {
|
|
140
|
-
const target = e.
|
|
140
|
+
function onScroll(e: Event & { currentTarget: EventTarget & HTMLDivElement }) {
|
|
141
|
+
const target = e.currentTarget;
|
|
141
142
|
scrollTop = target.scrollTop;
|
|
142
143
|
|
|
143
144
|
const scrollHeight = target.scrollHeight;
|
|
@@ -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,7 @@
|
|
|
18
27
|
fetchLogs,
|
|
19
28
|
streamLogs,
|
|
20
29
|
showTaskName = false,
|
|
30
|
+
onDelete,
|
|
21
31
|
}: {
|
|
22
32
|
run: Run | undefined;
|
|
23
33
|
fetchLogs: (
|
|
@@ -31,8 +41,15 @@
|
|
|
31
41
|
initialState?: { fromLine: number },
|
|
32
42
|
) => () => void;
|
|
33
43
|
showTaskName?: boolean;
|
|
44
|
+
onDelete?: (runId: string) => void;
|
|
34
45
|
} = $props();
|
|
35
46
|
|
|
47
|
+
let canDelete = $derived.by(() => {
|
|
48
|
+
if (!run || !onDelete) return false;
|
|
49
|
+
const status = runDisplayStatus(run);
|
|
50
|
+
return status !== "running" && status !== "pending";
|
|
51
|
+
});
|
|
52
|
+
|
|
36
53
|
const TAIL_LINES = 1000;
|
|
37
54
|
|
|
38
55
|
let logConsole = $state<{ onStream: (event: LogEvent) => void } | null>(null);
|
|
@@ -109,6 +126,18 @@
|
|
|
109
126
|
>
|
|
110
127
|
{runDisplayStatus(run).toUpperCase()}
|
|
111
128
|
</Badge>
|
|
129
|
+
{#if canDelete}
|
|
130
|
+
<Button
|
|
131
|
+
variant="ghost"
|
|
132
|
+
size="xs"
|
|
133
|
+
class="ml-1 text-danger-surface hover:bg-danger-soft"
|
|
134
|
+
onclick={() => onDelete?.(run.id)}
|
|
135
|
+
title="Delete this run"
|
|
136
|
+
aria-label="Delete run"
|
|
137
|
+
>
|
|
138
|
+
{#snippet icon()}<Trash2 size={14} />{/snippet}
|
|
139
|
+
</Button>
|
|
140
|
+
{/if}
|
|
112
141
|
</div>
|
|
113
142
|
<div
|
|
114
143
|
class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-on-surface-muted"
|
|
@@ -240,13 +269,11 @@
|
|
|
240
269
|
{/key}
|
|
241
270
|
</div>
|
|
242
271
|
{: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>
|
|
272
|
+
<div class="flex flex-1 items-center justify-center bg-surface-sunken/30">
|
|
273
|
+
<EmptyState
|
|
274
|
+
title="Select a run"
|
|
275
|
+
description="Pick a run from the list to view details and logs."
|
|
276
|
+
icon={MousePointerClick}
|
|
277
|
+
/>
|
|
251
278
|
</div>
|
|
252
279
|
{/if}
|