@runwisp/ui 1.3.0 → 1.4.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 +5 -5
- package/src/lib/components/LogConsole.svelte +7 -2
- package/src/lib/components/Skeleton.svelte +6 -6
- package/src/lib/components/dashboard/RunDetailPanel.svelte +50 -4
- package/src/lib/components/dashboard/RunsList.svelte +48 -19
- package/src/stories/Button.stories.svelte +2 -2
- package/src/stories/DataGrid.stories.svelte +2 -2
- package/src/stories/Dropdown.stories.svelte +3 -3
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": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@chromatic-com/storybook": "^5.2.1",
|
|
51
51
|
"@eslint/js": "^10.0.1",
|
|
52
|
-
"@runwisp/eslint-config": "1.
|
|
53
|
-
"@runwisp/typescript-config": "1.
|
|
52
|
+
"@runwisp/eslint-config": "1.4.0",
|
|
53
|
+
"@runwisp/typescript-config": "1.4.0",
|
|
54
54
|
"@storybook/addon-a11y": "^10.5.5",
|
|
55
55
|
"@storybook/addon-docs": "^10.5.5",
|
|
56
56
|
"@storybook/addon-svelte-csf": "^5.1.2",
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
"@floating-ui/dom": "^1.8.0",
|
|
93
93
|
"@fontsource-variable/geist-mono": "^5.3.0",
|
|
94
94
|
"@fontsource/tasa-orbiter": "^5.3.0",
|
|
95
|
-
"@lucide/svelte": "^1.
|
|
96
|
-
"@runwisp/common": "1.
|
|
95
|
+
"@lucide/svelte": "^1.52.0",
|
|
96
|
+
"@runwisp/common": "1.4.0",
|
|
97
97
|
"@tanstack/svelte-virtual": "^3.13.35",
|
|
98
98
|
"ansi-to-html": "^0.7.2",
|
|
99
99
|
"cron-parser": "^5.7.0",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
// run that simply produced no output. Null/undefined renders nothing;
|
|
38
38
|
// the caller clears it once a subsequent fetch succeeds.
|
|
39
39
|
error?: string | null;
|
|
40
|
+
// The caller is still fetching this console's first output (e.g. the
|
|
41
|
+
// run panel seeding its tail). Shows the loading state rather than
|
|
42
|
+
// "No output yet" until that output arrives.
|
|
43
|
+
loading?: boolean;
|
|
40
44
|
// Wrap long lines instead of horizontally scrolling them. When on, each
|
|
41
45
|
// rendered row's height becomes a multiple of `lineHeight` (one per
|
|
42
46
|
// wrapped visual row), so the virtualizer switches from the fixed-height
|
|
@@ -55,6 +59,7 @@
|
|
|
55
59
|
endLabel = "end of output",
|
|
56
60
|
endTone = "muted",
|
|
57
61
|
error = null,
|
|
62
|
+
loading = false,
|
|
58
63
|
wrap = $bindable(false),
|
|
59
64
|
}: Props = $props();
|
|
60
65
|
|
|
@@ -798,7 +803,7 @@
|
|
|
798
803
|
{/each}
|
|
799
804
|
{/if}
|
|
800
805
|
|
|
801
|
-
{#if cache.totalLines === 0 && overlayRows.length === 0 && !fetcher?.isFetching}
|
|
806
|
+
{#if cache.totalLines === 0 && overlayRows.length === 0 && !loading && !fetcher?.isFetching}
|
|
802
807
|
<div
|
|
803
808
|
class="absolute inset-0 flex items-center justify-center text-[var(--rw-con-dim)]"
|
|
804
809
|
>
|
|
@@ -809,7 +814,7 @@
|
|
|
809
814
|
</div>
|
|
810
815
|
{/if}
|
|
811
816
|
|
|
812
|
-
{#if cache.totalLines === 0 && fetcher?.isFetching}
|
|
817
|
+
{#if cache.totalLines === 0 && (loading || fetcher?.isFetching)}
|
|
813
818
|
<div
|
|
814
819
|
class="absolute inset-0 flex items-center justify-center text-[var(--rw-con-dim)]"
|
|
815
820
|
>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
{#if shape === "circle"}
|
|
32
32
|
<div
|
|
33
|
-
class="animate-pulse rounded-full bg-
|
|
33
|
+
class="animate-pulse rounded-full bg-outline-hover {className}"
|
|
34
34
|
style="width: {width ?? '2.5rem'}; height: {height ?? width ?? '2.5rem'}"
|
|
35
35
|
role="status"
|
|
36
36
|
aria-label="Loading"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
</div>
|
|
40
40
|
{:else if shape === "rect"}
|
|
41
41
|
<div
|
|
42
|
-
class="animate-pulse rounded-[4px] bg-
|
|
42
|
+
class="animate-pulse rounded-[4px] bg-outline-hover {className}"
|
|
43
43
|
style="width: {width ?? '100%'}; height: {height ?? '2rem'}"
|
|
44
44
|
role="status"
|
|
45
45
|
aria-label="Loading"
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
<div class="animate-pulse space-y-6 p-6 {className}" role="status" aria-label="Loading content">
|
|
51
51
|
{#if header}
|
|
52
52
|
<div class="space-y-3">
|
|
53
|
-
<div class="h-7 w-48 rounded-[4px] bg-
|
|
54
|
-
<div class="h-4 w-72 rounded-[3px] bg-
|
|
53
|
+
<div class="h-7 w-48 rounded-[4px] bg-outline-hover"></div>
|
|
54
|
+
<div class="h-4 w-72 rounded-[3px] bg-outline-hover"></div>
|
|
55
55
|
</div>
|
|
56
56
|
{/if}
|
|
57
57
|
|
|
@@ -59,11 +59,11 @@
|
|
|
59
59
|
{#each Array.from({ length: rows }, (_, i) => i) as i (i)}
|
|
60
60
|
<div class="space-y-2">
|
|
61
61
|
<div
|
|
62
|
-
class="h-4 rounded-[3px] bg-
|
|
62
|
+
class="h-4 rounded-[3px] bg-outline-hover"
|
|
63
63
|
style="width: {70 + ((i * 17) % 30)}%"
|
|
64
64
|
></div>
|
|
65
65
|
<div
|
|
66
|
-
class="h-4 rounded-[3px] bg-
|
|
66
|
+
class="h-4 rounded-[3px] bg-outline-hover"
|
|
67
67
|
style="width: {40 + ((i * 23) % 40)}%"
|
|
68
68
|
></div>
|
|
69
69
|
</div>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
Check,
|
|
9
9
|
Terminal as TerminalIcon,
|
|
10
10
|
Download,
|
|
11
|
-
|
|
11
|
+
Trash,
|
|
12
12
|
Play,
|
|
13
13
|
Square,
|
|
14
14
|
MousePointerClick,
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
getInstanceCount = () => 1,
|
|
68
68
|
motion,
|
|
69
69
|
notFound = false,
|
|
70
|
+
loading = false,
|
|
70
71
|
}: {
|
|
71
72
|
run: Run | undefined;
|
|
72
73
|
fetchLogs: (
|
|
@@ -121,6 +122,9 @@
|
|
|
121
122
|
// or never existed). The empty state then says so plainly instead of the
|
|
122
123
|
// generic "Select a run" — the caller must not silently substitute another.
|
|
123
124
|
notFound?: boolean;
|
|
125
|
+
// True while there is no run to show *yet* (the run list or a deep-linked
|
|
126
|
+
// run is still loading), so the empty state doesn't claim "No runs yet".
|
|
127
|
+
loading?: boolean;
|
|
124
128
|
} = $props();
|
|
125
129
|
|
|
126
130
|
function easeInFresh(node: HTMLElement, runId: string) {
|
|
@@ -170,6 +174,10 @@
|
|
|
170
174
|
// changes, not on every run object reference swap from SSE array updates.
|
|
171
175
|
let runId = $derived(run?.id);
|
|
172
176
|
|
|
177
|
+
// The run whose seed fetch has settled. Until it matches the shown run the
|
|
178
|
+
// console says it's loading, not "No output yet".
|
|
179
|
+
let seededRunId = $state<string | null>(null);
|
|
180
|
+
|
|
173
181
|
// Fetches the batched tail page a run's console opens on (see the effect
|
|
174
182
|
// below) and paints it into the console. Resolves the line the live
|
|
175
183
|
// stream should resume from, or null when there's nothing left to stream
|
|
@@ -201,7 +209,10 @@
|
|
|
201
209
|
if (!id) return;
|
|
202
210
|
|
|
203
211
|
const stream = streamLogs;
|
|
204
|
-
if (!stream)
|
|
212
|
+
if (!stream) {
|
|
213
|
+
seededRunId = id;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
205
216
|
|
|
206
217
|
logFetchError = null;
|
|
207
218
|
|
|
@@ -217,7 +228,9 @@
|
|
|
217
228
|
// (its disk backfill from that anchor closes the fetch↔stream
|
|
218
229
|
// race, and the daemon dedupes anything already shown).
|
|
219
230
|
const seeded = await seedConsoleTail(id, () => cancelled);
|
|
220
|
-
if (cancelled
|
|
231
|
+
if (cancelled) return;
|
|
232
|
+
seededRunId = id;
|
|
233
|
+
if (!seeded) return;
|
|
221
234
|
cleanup = stream(
|
|
222
235
|
id,
|
|
223
236
|
(event: LogEvent) => {
|
|
@@ -691,7 +704,7 @@
|
|
|
691
704
|
title="Delete this run"
|
|
692
705
|
aria-label="Delete run"
|
|
693
706
|
>
|
|
694
|
-
<
|
|
707
|
+
<Trash size={15} />
|
|
695
708
|
</span>
|
|
696
709
|
{/snippet}
|
|
697
710
|
<div class="w-60">
|
|
@@ -817,10 +830,43 @@
|
|
|
817
830
|
{endLabel}
|
|
818
831
|
{endTone}
|
|
819
832
|
error={logFetchError}
|
|
833
|
+
loading={seededRunId !== run.id}
|
|
820
834
|
/>
|
|
821
835
|
{/key}
|
|
822
836
|
</div>
|
|
823
837
|
</div>
|
|
838
|
+
{:else if loading && !notFound}
|
|
839
|
+
<!-- Loading: the panel's own frame (verdict header over the console) with
|
|
840
|
+
placeholders where the run's facts go, so nothing jumps when it lands. -->
|
|
841
|
+
<div
|
|
842
|
+
class="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"
|
|
843
|
+
role="status"
|
|
844
|
+
aria-label="Loading run"
|
|
845
|
+
>
|
|
846
|
+
<div class="shrink-0 border-b border-outline-faint bg-surface-raised">
|
|
847
|
+
<div class="pt-[18px] pr-[22px] pb-[16px] pl-[26px]">
|
|
848
|
+
<div class="flex h-[33px] items-center gap-2.5">
|
|
849
|
+
<span class="size-[18px] animate-pulse rounded-full bg-outline-hover"></span>
|
|
850
|
+
<span class="h-[18px] w-40 animate-pulse rounded-[3px] bg-outline-hover"></span>
|
|
851
|
+
</div>
|
|
852
|
+
<div class="mt-2.5 flex h-[17px] items-center">
|
|
853
|
+
<span class="h-3 w-64 max-w-full animate-pulse rounded-[3px] bg-outline-hover"
|
|
854
|
+
></span>
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
</div>
|
|
858
|
+
<div
|
|
859
|
+
class="ml-1 flex min-h-[300px] flex-1 flex-col overflow-hidden border-t border-[var(--rw-con-gutter)] bg-[var(--rw-con-bg)]"
|
|
860
|
+
>
|
|
861
|
+
<div
|
|
862
|
+
class="flex shrink-0 items-center gap-2 border-b border-[var(--rw-con-gutter)] bg-[var(--rw-con-panel)] px-3.5 py-[9px] font-mono text-[11.5px] font-semibold text-[var(--rw-con-text)]"
|
|
863
|
+
>
|
|
864
|
+
<TerminalIcon size={14} class="opacity-70" />
|
|
865
|
+
Console output
|
|
866
|
+
</div>
|
|
867
|
+
<LogConsole fetchLogs={() => undefined} loading class="min-h-0 flex-1" />
|
|
868
|
+
</div>
|
|
869
|
+
</div>
|
|
824
870
|
{:else}
|
|
825
871
|
<div
|
|
826
872
|
class="flex min-w-0 flex-1 flex-col items-center justify-center gap-4 bg-surface-sunken/30"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</script>
|
|
11
11
|
|
|
12
12
|
<script lang="ts">
|
|
13
|
-
import { Clock, ArrowUpDown, X, Square,
|
|
13
|
+
import { Clock, ArrowUpDown, X, Square, Trash, RotateCw } from "@lucide/svelte";
|
|
14
14
|
import { untrack } from "svelte";
|
|
15
15
|
import { SvelteSet } from "svelte/reactivity";
|
|
16
16
|
import { createVirtualizer } from "@tanstack/svelte-virtual";
|
|
@@ -459,7 +459,7 @@
|
|
|
459
459
|
)}
|
|
460
460
|
title="Delete run{selectionCount === 1 ? '' : 's'}"
|
|
461
461
|
>
|
|
462
|
-
{#snippet icon()}<
|
|
462
|
+
{#snippet icon()}<Trash size={14} />{/snippet}
|
|
463
463
|
</Button>
|
|
464
464
|
{/if}
|
|
465
465
|
</div>
|
|
@@ -474,6 +474,8 @@
|
|
|
474
474
|
>
|
|
475
475
|
{#if outputSearchActive}
|
|
476
476
|
{matchedRuns.length} of {items.length}
|
|
477
|
+
{:else if loading && items.length === 0}
|
|
478
|
+
<!-- Count unknown until the first page lands. -->
|
|
477
479
|
{:else}
|
|
478
480
|
{total}
|
|
479
481
|
{total === 1 ? "run" : "runs"}
|
|
@@ -533,22 +535,7 @@
|
|
|
533
535
|
<!-- Output-search results: the rail filters to runs that printed the
|
|
534
536
|
query, each annotated with the matching line. -->
|
|
535
537
|
{#if outputSearchPending}
|
|
536
|
-
|
|
537
|
-
<div class="flex flex-col gap-0.5" aria-busy="true" aria-label="Searching output">
|
|
538
|
-
{#each [0, 1, 2, 3, 4] as i (i)}
|
|
539
|
-
<div class="flex items-center gap-2.5 rounded-[3px] px-3 py-2.5">
|
|
540
|
-
<span
|
|
541
|
-
class="size-[9px] shrink-0 animate-pulse rounded-full bg-surface-sunken"
|
|
542
|
-
></span>
|
|
543
|
-
<span
|
|
544
|
-
class="h-3 animate-pulse rounded bg-surface-sunken"
|
|
545
|
-
style:width="{70 - i * 8}%"
|
|
546
|
-
></span>
|
|
547
|
-
<span class="ml-auto h-3 w-9 animate-pulse rounded bg-surface-sunken"
|
|
548
|
-
></span>
|
|
549
|
-
</div>
|
|
550
|
-
{/each}
|
|
551
|
-
</div>
|
|
538
|
+
{@render skeletonRows("Searching output")}
|
|
552
539
|
{:else if matchedRuns.length === 0}
|
|
553
540
|
<div class="px-4 py-8 text-center text-xs leading-relaxed text-on-surface-muted">
|
|
554
541
|
No output matches
|
|
@@ -571,7 +558,9 @@
|
|
|
571
558
|
{/each}
|
|
572
559
|
</div>
|
|
573
560
|
{/if}
|
|
574
|
-
{:else if items.length === 0 &&
|
|
561
|
+
{:else if items.length === 0 && loading}
|
|
562
|
+
{@render skeletonRows("Loading runs")}
|
|
563
|
+
{:else if items.length === 0}
|
|
575
564
|
<EmptyState
|
|
576
565
|
title={emptyText}
|
|
577
566
|
description={emptyDescription}
|
|
@@ -622,6 +611,46 @@
|
|
|
622
611
|
|
|
623
612
|
<!-- Status dot. With bulk actions on it fades out — on row hover, or whenever a
|
|
624
613
|
selection exists — so the row checkbox can take its place over it. -->
|
|
614
|
+
<!-- Placeholders shaped like this list's run rows (same padding, dot, text
|
|
615
|
+
and right readout), while a search or the first page is in flight. -->
|
|
616
|
+
{#snippet skeletonRows(label: string)}
|
|
617
|
+
<div class="flex flex-col gap-0.5" role="status" aria-busy="true" aria-label={label}>
|
|
618
|
+
{#each [0, 1, 2, 3, 4, 5] as i (i)}
|
|
619
|
+
<div
|
|
620
|
+
class="rounded-[3px] border border-transparent {showTaskName
|
|
621
|
+
? 'p-3'
|
|
622
|
+
: 'px-3 py-[11px]'}"
|
|
623
|
+
>
|
|
624
|
+
<div class="flex items-center {showTaskName ? 'gap-2.5' : 'gap-[11px]'}">
|
|
625
|
+
<span class="size-[9px] shrink-0 animate-pulse rounded-full bg-outline-hover"
|
|
626
|
+
></span>
|
|
627
|
+
{#if showTaskName}
|
|
628
|
+
<span class="flex min-w-0 flex-1 flex-col gap-1.5 py-0.5">
|
|
629
|
+
<span
|
|
630
|
+
class="h-3 animate-pulse rounded-[3px] bg-outline-hover"
|
|
631
|
+
style:width="{62 - (i % 3) * 12}%"
|
|
632
|
+
></span>
|
|
633
|
+
<span class="h-2 w-2/5 animate-pulse rounded-[3px] bg-outline-hover"
|
|
634
|
+
></span>
|
|
635
|
+
</span>
|
|
636
|
+
<span
|
|
637
|
+
class="h-2.5 w-9 shrink-0 animate-pulse self-start rounded-[3px] bg-outline-hover"
|
|
638
|
+
></span>
|
|
639
|
+
{:else}
|
|
640
|
+
<span
|
|
641
|
+
class="h-3 animate-pulse rounded-[3px] bg-outline-hover"
|
|
642
|
+
style:width="{58 - (i % 3) * 8}%"
|
|
643
|
+
></span>
|
|
644
|
+
<span
|
|
645
|
+
class="ml-auto h-2.5 w-9 shrink-0 animate-pulse rounded-[3px] bg-outline-hover"
|
|
646
|
+
></span>
|
|
647
|
+
{/if}
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
{/each}
|
|
651
|
+
</div>
|
|
652
|
+
{/snippet}
|
|
653
|
+
|
|
625
654
|
{#snippet statusDot(colorClass: string, running: boolean)}
|
|
626
655
|
<span
|
|
627
656
|
class="{colorClass} size-[9px] shrink-0 rounded-full bg-current ring-[3px] ring-current/20 {running
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<script module>
|
|
5
5
|
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
6
6
|
import Button from "$lib/components/Button.svelte";
|
|
7
|
-
import { Play, Plus, ArrowRight, Download,
|
|
7
|
+
import { Play, Plus, ArrowRight, Download, Trash } from "@lucide/svelte";
|
|
8
8
|
|
|
9
9
|
const { Story } = defineMeta({
|
|
10
10
|
title: "Core/Button",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
Export
|
|
72
72
|
</Button>
|
|
73
73
|
<Button variant="danger">
|
|
74
|
-
{#snippet icon()}<
|
|
74
|
+
{#snippet icon()}<Trash size={16} />{/snippet}
|
|
75
75
|
Delete
|
|
76
76
|
</Button>
|
|
77
77
|
</div>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import FilterBar from "$lib/components/FilterBar.svelte";
|
|
10
10
|
import { seedValues } from "$lib/components/filter-spec.js";
|
|
11
11
|
import { applyFilters } from "$lib/utils/filter.js";
|
|
12
|
-
import { Play, Pause,
|
|
12
|
+
import { Play, Pause, Trash, SquarePen } from "@lucide/svelte";
|
|
13
13
|
|
|
14
14
|
const { Story } = defineMeta({
|
|
15
15
|
title: "Data/DataGrid",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
{ label: "Run Now", icon: Play },
|
|
76
76
|
{ label: "Pause", icon: Pause },
|
|
77
77
|
{ divider: true },
|
|
78
|
-
{ label: "Delete", icon:
|
|
78
|
+
{ label: "Delete", icon: Trash, danger: true },
|
|
79
79
|
];
|
|
80
80
|
|
|
81
81
|
/** @type {import("$lib/components/filter-spec.js").FilterField[]} */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<script module>
|
|
5
5
|
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
6
6
|
import Dropdown from "$lib/components/Dropdown.svelte";
|
|
7
|
-
import { Play, Pause, SquarePen, Copy,
|
|
7
|
+
import { Play, Pause, SquarePen, Copy, Trash, Settings, ChevronDown } from "@lucide/svelte";
|
|
8
8
|
|
|
9
9
|
const { Story } = defineMeta({
|
|
10
10
|
title: "Core/Dropdown",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
{ label: "Duplicate", icon: Copy, onClick: () => console.log("Duplicate") },
|
|
25
25
|
{ label: "Pause", icon: Pause, onClick: () => console.log("Pause") },
|
|
26
26
|
{ divider: true },
|
|
27
|
-
{ label: "Delete", icon:
|
|
27
|
+
{ label: "Delete", icon: Trash, danger: true, onClick: () => console.log("Delete") },
|
|
28
28
|
];
|
|
29
29
|
|
|
30
30
|
const settingsMenu = [
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
{ label: "Edit", icon: SquarePen },
|
|
64
64
|
{ label: "Duplicate", icon: Copy, disabled: true },
|
|
65
65
|
{ divider: true },
|
|
66
|
-
{ label: "Delete", icon:
|
|
66
|
+
{ label: "Delete", icon: Trash, danger: true, disabled: true },
|
|
67
67
|
],
|
|
68
68
|
}}
|
|
69
69
|
/>
|