@runwisp/ui 1.5.0 → 1.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 +5 -7
- package/src/lib/actions/focusTrap.ts +3 -3
- package/src/lib/actions/fold-to-fit.test.ts +36 -0
- package/src/lib/actions/fold-to-fit.ts +89 -0
- package/src/lib/actions/modal-dialog.test.ts +47 -0
- package/src/lib/actions/modal-dialog.ts +48 -0
- package/src/lib/components/AccordionItem.svelte +16 -5
- package/src/lib/components/Badge.svelte +4 -25
- package/src/lib/components/Button.svelte +2 -9
- package/src/lib/components/CodeBlock.svelte +7 -6
- package/src/lib/components/DataGrid.svelte +5 -5
- package/src/lib/components/Drawer.svelte +22 -30
- package/src/lib/components/DurationInput.svelte +6 -25
- package/src/lib/components/FilterBar.svelte +3 -7
- package/src/lib/components/FilterPill.svelte +2 -2
- package/src/lib/components/FormField.svelte +1 -1
- package/src/lib/components/Link.svelte +9 -4
- package/src/lib/components/LogConsole.svelte +101 -424
- package/src/lib/components/Modal.svelte +7 -30
- package/src/lib/components/SearchInput.svelte +1 -1
- package/src/lib/components/Select.svelte +3 -2
- package/src/lib/components/Sparkline.svelte +2 -2
- package/src/lib/components/Spinner.svelte +10 -5
- package/src/lib/components/StatusDot.svelte +1 -1
- package/src/lib/components/StatusIndicator.svelte +37 -104
- package/src/lib/components/Tabs.svelte +15 -1
- package/src/lib/components/Tooltip.svelte +1 -1
- package/src/lib/components/dashboard/RunActions.svelte +156 -0
- package/src/lib/components/dashboard/RunConsoleFrame.svelte +319 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +137 -725
- package/src/lib/components/dashboard/RunFacts.svelte +119 -0
- package/src/lib/components/dashboard/RunFilterChips.svelte +52 -0
- package/src/lib/components/dashboard/RunFilterPopover.svelte +9 -10
- package/src/lib/components/dashboard/RunHeaderLine.svelte +295 -0
- package/src/lib/components/dashboard/RunListSkeleton.svelte +43 -0
- package/src/lib/components/dashboard/RunRow.svelte +202 -0
- package/src/lib/components/dashboard/RunsList.svelte +267 -545
- package/src/lib/components/dashboard/bulk-selection.svelte.test.ts +42 -0
- package/src/lib/components/dashboard/bulk-selection.svelte.ts +50 -0
- package/src/lib/components/dashboard/cron-format.test.ts +10 -1
- package/src/lib/components/dashboard/cron-format.ts +7 -3
- package/src/lib/components/dashboard/run-filters.test.ts +62 -2
- package/src/lib/components/dashboard/run-filters.ts +104 -99
- package/src/lib/components/dashboard/run-helpers.test.ts +131 -12
- package/src/lib/components/dashboard/run-helpers.ts +150 -44
- package/src/lib/components/dashboard/status-config.ts +18 -0
- package/src/lib/components/dashboard/types.ts +6 -0
- package/src/lib/components/data-grid-selection.ts +2 -9
- package/src/lib/components/filter-spec.ts +3 -3
- package/src/lib/index.ts +13 -2
- package/src/lib/log-console/AnsiLine.svelte +12 -0
- package/src/lib/log-console/FrameHistoryBlock.svelte +57 -0
- package/src/lib/log-console/LogCache.svelte.ts +1 -1
- package/src/lib/log-console/LogConsoleFooter.svelte +88 -0
- package/src/lib/log-console/ansi.ts +1 -1
- package/src/lib/log-console/frame-history.svelte.test.ts +47 -0
- package/src/lib/log-console/frame-history.svelte.ts +71 -0
- package/src/lib/log-console/wrap-layout.svelte.test.ts +92 -0
- package/src/lib/log-console/wrap-layout.svelte.ts +188 -0
- package/src/lib/theme-tokens.css +21 -21
- package/src/lib/theme.css +13 -13
- package/src/lib/utils/format.test.ts +125 -3
- package/src/lib/utils/format.ts +101 -8
- package/src/lib/utils/toast.svelte.ts +2 -3
- package/src/lib/utils/typing-target.ts +10 -0
- package/src/stories/Accordion.stories.svelte +60 -1
- package/src/stories/CodeBlock.stories.svelte +13 -0
- package/src/stories/Link.stories.svelte +25 -1
- package/src/stories/LogConsole.stories.svelte +2 -2
- package/src/stories/Tabs.stories.svelte +17 -0
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
import { trapFocus } from "../actions/focusTrap.js";
|
|
25
|
+
import { modalDialogHandlers, showModal } from "../actions/modal-dialog.js";
|
|
25
26
|
|
|
26
27
|
let {
|
|
27
28
|
open = $bindable(false),
|
|
@@ -51,41 +52,17 @@
|
|
|
51
52
|
onClose?.();
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function handleCancel(e: Event) {
|
|
61
|
-
e.preventDefault();
|
|
62
|
-
if (closable) handleClose();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Chrome ignores preventDefault() on a repeated Escape and closes anyway;
|
|
66
|
-
// reopen so a non-closable modal stays up while mounted.
|
|
67
|
-
function handleNativeClose(e: Event & { currentTarget: HTMLDialogElement }) {
|
|
68
|
-
if (open) e.currentTarget.showModal();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Only a press that starts and ends on the backdrop closes the modal, so a
|
|
72
|
-
// text selection dragged out of the panel doesn't.
|
|
73
|
-
let pressedBackdrop = false;
|
|
74
|
-
function handlePointerDown(e: PointerEvent) {
|
|
75
|
-
pressedBackdrop = e.target === e.currentTarget;
|
|
76
|
-
}
|
|
77
|
-
function handleBackdropClick(e: MouseEvent) {
|
|
78
|
-
if (closable && pressedBackdrop && e.target === e.currentTarget) handleClose();
|
|
79
|
-
}
|
|
55
|
+
const dialogHandlers = modalDialogHandlers({
|
|
56
|
+
closable: () => closable,
|
|
57
|
+
isOpen: () => open,
|
|
58
|
+
close: handleClose,
|
|
59
|
+
});
|
|
80
60
|
</script>
|
|
81
61
|
|
|
82
62
|
{#if open}
|
|
83
63
|
<dialog
|
|
84
64
|
use:showModal
|
|
85
|
-
|
|
86
|
-
onclose={handleNativeClose}
|
|
87
|
-
onpointerdown={handlePointerDown}
|
|
88
|
-
onclick={handleBackdropClick}
|
|
65
|
+
{...dialogHandlers}
|
|
89
66
|
aria-labelledby={title ? titleId : undefined}
|
|
90
67
|
class="
|
|
91
68
|
fixed inset-0 m-0 flex h-full max-h-none w-full max-w-none
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
// External writes to `value` (Reset / clear-all) win immediately: adopt them
|
|
42
42
|
// and drop any pending debounce. Depends only on `value` (raw is read
|
|
43
43
|
// untracked), so typing never re-enters here. Tiny race: a reset that leaves
|
|
44
|
-
// `value` unchanged ("" -> "") can't cancel a first-keystroke debounce
|
|
44
|
+
// `value` unchanged ("" -> "") can't cancel a first-keystroke debounce,
|
|
45
45
|
// negligible at 250ms. ponytail: guarded two-way sync, fine for one field.
|
|
46
46
|
$effect(() => {
|
|
47
47
|
const v = value;
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
import { floating, isOutsideClick } from "../actions/floating.js";
|
|
22
22
|
import { portal } from "../actions/portal.js";
|
|
23
23
|
import { trapFocus } from "../actions/focusTrap.js";
|
|
24
|
-
import { generateUlid } from "@runwisp/common";
|
|
25
24
|
|
|
26
25
|
type SelectSize = "sm" | "md" | "lg";
|
|
27
26
|
|
|
@@ -41,6 +40,8 @@
|
|
|
41
40
|
onchange?: (value: unknown) => void;
|
|
42
41
|
}
|
|
43
42
|
|
|
43
|
+
const uid = $props.id();
|
|
44
|
+
|
|
44
45
|
let {
|
|
45
46
|
value = $bindable(),
|
|
46
47
|
options = [],
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
error,
|
|
53
54
|
hint,
|
|
54
55
|
class: className = "",
|
|
55
|
-
id = `select-${
|
|
56
|
+
id = `select-${uid}`,
|
|
56
57
|
name,
|
|
57
58
|
onchange,
|
|
58
59
|
onsearch,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// `currentColor` by default so the caller can theme us with a Tailwind
|
|
15
|
-
// text-* class
|
|
15
|
+
// text-* class, that's the only way to flip on .dark without burning a
|
|
16
16
|
// hardcoded hex in here.
|
|
17
17
|
let {
|
|
18
18
|
data,
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
return `${pathD} L${lastX.toFixed(1)},${height} L0,${height} Z`;
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
// Use `fill-opacity` rather than appending "18" to a hex
|
|
50
|
+
// Use `fill-opacity` rather than appending "18" to a hex, that trick
|
|
51
51
|
// breaks for `currentColor` / `oklch()` / `var(...)` values.
|
|
52
52
|
let resolvedFill = $derived(fillColor ?? color);
|
|
53
53
|
let resolvedFillOpacity = $derived(fillColor === undefined ? fillOpacity : 1);
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
type SpinnerSize = "xs" | "sm" | "md" | "lg";
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
|
-
size
|
|
8
|
+
/** A preset, or an exact pixel size. */
|
|
9
|
+
size?: SpinnerSize | number;
|
|
9
10
|
color?: string;
|
|
11
|
+
/** Screen-reader text. An empty label drops the status role, for spinners inside a labelled control. */
|
|
10
12
|
label?: string;
|
|
11
13
|
class?: string;
|
|
12
14
|
}
|
|
@@ -26,12 +28,15 @@
|
|
|
26
28
|
};
|
|
27
29
|
</script>
|
|
28
30
|
|
|
29
|
-
<
|
|
31
|
+
<span role={label ? "status" : undefined} class="inline-flex {className}">
|
|
30
32
|
<svg
|
|
31
|
-
class="animate-spin {sizeClasses[size]}"
|
|
33
|
+
class="animate-spin {typeof size === 'number' ? '' : sizeClasses[size]}"
|
|
34
|
+
width={typeof size === "number" ? size : undefined}
|
|
35
|
+
height={typeof size === "number" ? size : undefined}
|
|
32
36
|
viewBox="0 0 24 24"
|
|
33
37
|
fill="none"
|
|
34
38
|
xmlns="http://www.w3.org/2000/svg"
|
|
39
|
+
aria-hidden="true"
|
|
35
40
|
>
|
|
36
41
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke={color} stroke-width="4"></circle>
|
|
37
42
|
<path
|
|
@@ -40,5 +45,5 @@
|
|
|
40
45
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
41
46
|
></path>
|
|
42
47
|
</svg>
|
|
43
|
-
<span class="sr-only">{label}</span>
|
|
44
|
-
</
|
|
48
|
+
{#if label}<span class="sr-only">{label}</span>{/if}
|
|
49
|
+
</span>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<script lang="ts">
|
|
9
9
|
interface Props {
|
|
10
10
|
tone?: StatusDotTone;
|
|
11
|
-
/** Radar ping around the dot
|
|
11
|
+
/** Radar ping around the dot, use for "live" states only. */
|
|
12
12
|
pulse?: boolean;
|
|
13
13
|
size?: "sm" | "md" | "lg";
|
|
14
14
|
class?: string;
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
|
+
import { Calendar, Ban, Check, Clock, Pause, X } from "@lucide/svelte";
|
|
6
|
+
import type { Component } from "svelte";
|
|
5
7
|
import { fade } from "svelte/transition";
|
|
8
|
+
import Spinner from "./Spinner.svelte";
|
|
6
9
|
|
|
7
10
|
type Status =
|
|
8
11
|
"running" | "success" | "failed" | "pending" | "paused" | "scheduled" | "cancelled";
|
|
@@ -26,41 +29,65 @@
|
|
|
26
29
|
class: className = "",
|
|
27
30
|
}: Props = $props();
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
// `icon` is undefined for "running", which shows the spinner.
|
|
33
|
+
const STATUS_CONFIG: Record<
|
|
34
|
+
Status,
|
|
35
|
+
{
|
|
36
|
+
label: string;
|
|
37
|
+
classes: string;
|
|
38
|
+
iconColor: string;
|
|
39
|
+
icon: Component | undefined;
|
|
40
|
+
strokeWidth: number;
|
|
41
|
+
}
|
|
42
|
+
> = {
|
|
30
43
|
running: {
|
|
31
44
|
label: "Running",
|
|
32
45
|
classes: "bg-info-soft text-info-soft-text border-info-soft-border",
|
|
33
46
|
iconColor: "text-info-surface",
|
|
47
|
+
icon: undefined,
|
|
48
|
+
strokeWidth: 3,
|
|
34
49
|
},
|
|
35
50
|
success: {
|
|
36
51
|
label: "Success",
|
|
37
52
|
classes: "bg-success-soft text-success-soft-text border-success-soft-border",
|
|
38
53
|
iconColor: "text-success-surface",
|
|
54
|
+
icon: Check,
|
|
55
|
+
strokeWidth: 3,
|
|
39
56
|
},
|
|
40
57
|
failed: {
|
|
41
58
|
label: "Failed",
|
|
42
59
|
classes: "bg-danger-soft text-danger-soft-text border-danger-soft-border",
|
|
43
60
|
iconColor: "text-danger-surface",
|
|
61
|
+
icon: X,
|
|
62
|
+
strokeWidth: 3,
|
|
44
63
|
},
|
|
45
64
|
pending: {
|
|
46
65
|
label: "Pending",
|
|
47
66
|
classes: "bg-warning-soft text-warning-soft-text border-warning-soft-border",
|
|
48
67
|
iconColor: "text-warning-surface",
|
|
68
|
+
icon: Clock,
|
|
69
|
+
strokeWidth: 3,
|
|
49
70
|
},
|
|
50
71
|
paused: {
|
|
51
72
|
label: "Paused",
|
|
52
73
|
classes: "bg-surface-sunken text-on-surface-muted border-outline",
|
|
53
74
|
iconColor: "text-on-surface-faint",
|
|
75
|
+
icon: Pause,
|
|
76
|
+
strokeWidth: 0,
|
|
54
77
|
},
|
|
55
78
|
scheduled: {
|
|
56
79
|
label: "Scheduled",
|
|
57
80
|
classes: "bg-primary-soft text-primary-soft-text border-primary-soft-border",
|
|
58
81
|
iconColor: "text-primary",
|
|
82
|
+
icon: Calendar,
|
|
83
|
+
strokeWidth: 2.5,
|
|
59
84
|
},
|
|
60
85
|
cancelled: {
|
|
61
86
|
label: "Cancelled",
|
|
62
87
|
classes: "bg-surface-sunken text-on-surface-faint border-outline opacity-75",
|
|
63
88
|
iconColor: "text-on-surface-faint",
|
|
89
|
+
icon: Ban,
|
|
90
|
+
strokeWidth: 2.5,
|
|
64
91
|
},
|
|
65
92
|
};
|
|
66
93
|
|
|
@@ -80,109 +107,15 @@
|
|
|
80
107
|
in:fade={{ duration: 150 }}
|
|
81
108
|
>
|
|
82
109
|
<span class="flex shrink-0 items-center justify-center {config.iconColor}">
|
|
83
|
-
{#if
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
class="opacity-25"
|
|
93
|
-
cx="12"
|
|
94
|
-
cy="12"
|
|
95
|
-
r="10"
|
|
96
|
-
stroke="currentColor"
|
|
97
|
-
stroke-width="4"
|
|
98
|
-
></circle>
|
|
99
|
-
<path
|
|
100
|
-
class="opacity-75"
|
|
101
|
-
fill="currentColor"
|
|
102
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
103
|
-
></path>
|
|
104
|
-
</svg>
|
|
105
|
-
{:else if status === "success"}
|
|
106
|
-
<svg
|
|
107
|
-
width={sizeConfig.iconSize}
|
|
108
|
-
height={sizeConfig.iconSize}
|
|
109
|
-
viewBox="0 0 24 24"
|
|
110
|
-
fill="none"
|
|
111
|
-
stroke="currentColor"
|
|
112
|
-
stroke-width="3"
|
|
113
|
-
stroke-linecap="round"
|
|
114
|
-
stroke-linejoin="round"
|
|
115
|
-
>
|
|
116
|
-
<polyline points="20 6 9 17 4 12"></polyline>
|
|
117
|
-
</svg>
|
|
118
|
-
{:else if status === "failed"}
|
|
119
|
-
<svg
|
|
120
|
-
width={sizeConfig.iconSize}
|
|
121
|
-
height={sizeConfig.iconSize}
|
|
122
|
-
viewBox="0 0 24 24"
|
|
123
|
-
fill="none"
|
|
124
|
-
stroke="currentColor"
|
|
125
|
-
stroke-width="3"
|
|
126
|
-
stroke-linecap="round"
|
|
127
|
-
stroke-linejoin="round"
|
|
128
|
-
>
|
|
129
|
-
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
130
|
-
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
131
|
-
</svg>
|
|
132
|
-
{:else if status === "pending"}
|
|
133
|
-
<svg
|
|
134
|
-
width={sizeConfig.iconSize}
|
|
135
|
-
height={sizeConfig.iconSize}
|
|
136
|
-
viewBox="0 0 24 24"
|
|
137
|
-
fill="none"
|
|
138
|
-
stroke="currentColor"
|
|
139
|
-
stroke-width="3"
|
|
140
|
-
stroke-linecap="round"
|
|
141
|
-
stroke-linejoin="round"
|
|
142
|
-
>
|
|
143
|
-
<circle cx="12" cy="12" r="10"></circle>
|
|
144
|
-
<polyline points="12 6 12 12 16 14"></polyline>
|
|
145
|
-
</svg>
|
|
146
|
-
{:else if status === "scheduled"}
|
|
147
|
-
<svg
|
|
148
|
-
width={sizeConfig.iconSize}
|
|
149
|
-
height={sizeConfig.iconSize}
|
|
150
|
-
viewBox="0 0 24 24"
|
|
151
|
-
fill="none"
|
|
152
|
-
stroke="currentColor"
|
|
153
|
-
stroke-width="2.5"
|
|
154
|
-
stroke-linecap="round"
|
|
155
|
-
stroke-linejoin="round"
|
|
156
|
-
>
|
|
157
|
-
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
|
158
|
-
<line x1="16" y1="2" x2="16" y2="6"></line>
|
|
159
|
-
<line x1="8" y1="2" x2="8" y2="6"></line>
|
|
160
|
-
<line x1="3" y1="10" x2="21" y2="10"></line>
|
|
161
|
-
</svg>
|
|
162
|
-
{:else if status === "paused"}
|
|
163
|
-
<svg
|
|
164
|
-
width={sizeConfig.iconSize}
|
|
165
|
-
height={sizeConfig.iconSize}
|
|
166
|
-
viewBox="0 0 24 24"
|
|
167
|
-
fill="currentColor"
|
|
168
|
-
>
|
|
169
|
-
<rect x="6" y="4" width="4" height="16" rx="1" />
|
|
170
|
-
<rect x="14" y="4" width="4" height="16" rx="1" />
|
|
171
|
-
</svg>
|
|
172
|
-
{:else if status === "cancelled"}
|
|
173
|
-
<svg
|
|
174
|
-
width={sizeConfig.iconSize}
|
|
175
|
-
height={sizeConfig.iconSize}
|
|
176
|
-
viewBox="0 0 24 24"
|
|
177
|
-
fill="none"
|
|
178
|
-
stroke="currentColor"
|
|
179
|
-
stroke-width="2.5"
|
|
180
|
-
stroke-linecap="round"
|
|
181
|
-
stroke-linejoin="round"
|
|
182
|
-
>
|
|
183
|
-
<circle cx="12" cy="12" r="10"></circle>
|
|
184
|
-
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line>
|
|
185
|
-
</svg>
|
|
110
|
+
{#if !config.icon || pulse}
|
|
111
|
+
<Spinner size={sizeConfig.iconSize} label="" />
|
|
112
|
+
{:else}
|
|
113
|
+
{@const Icon = config.icon}
|
|
114
|
+
<Icon
|
|
115
|
+
size={sizeConfig.iconSize}
|
|
116
|
+
strokeWidth={config.strokeWidth}
|
|
117
|
+
fill={status === "paused" ? "currentColor" : "none"}
|
|
118
|
+
/>
|
|
186
119
|
{/if}
|
|
187
120
|
</span>
|
|
188
121
|
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
onChange?: (tabId: string) => void;
|
|
21
21
|
variant?: "underline" | "pills" | "enclosed";
|
|
22
22
|
fullWidth?: boolean;
|
|
23
|
+
/** Scroll the tab bar sideways instead of wrapping when the labels outgrow it. */
|
|
24
|
+
scrollable?: boolean;
|
|
23
25
|
children?: Snippet<[string]>;
|
|
24
26
|
class?: string;
|
|
25
27
|
}
|
|
@@ -30,6 +32,7 @@
|
|
|
30
32
|
onChange,
|
|
31
33
|
variant = "underline",
|
|
32
34
|
fullWidth = false,
|
|
35
|
+
scrollable = false,
|
|
33
36
|
children,
|
|
34
37
|
class: className = "",
|
|
35
38
|
}: Props = $props();
|
|
@@ -79,12 +82,13 @@
|
|
|
79
82
|
}
|
|
80
83
|
</script>
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
{#snippet tablist()}
|
|
83
86
|
<div
|
|
84
87
|
class="
|
|
85
88
|
{variant === 'underline' ? 'border-b border-outline' : ''}
|
|
86
89
|
{variant === 'pills' ? 'rounded-[4px] bg-surface-sunken p-1' : ''}
|
|
87
90
|
{variant === 'enclosed' ? 'rounded-[4px] border border-outline bg-surface-sunken p-1' : ''}
|
|
91
|
+
{scrollable ? 'w-max min-w-full whitespace-nowrap' : ''}
|
|
88
92
|
"
|
|
89
93
|
role="tablist"
|
|
90
94
|
>
|
|
@@ -131,6 +135,16 @@
|
|
|
131
135
|
{/each}
|
|
132
136
|
</nav>
|
|
133
137
|
</div>
|
|
138
|
+
{/snippet}
|
|
139
|
+
|
|
140
|
+
<div class={className}>
|
|
141
|
+
{#if scrollable}
|
|
142
|
+
<div class="overflow-x-auto">
|
|
143
|
+
{@render tablist()}
|
|
144
|
+
</div>
|
|
145
|
+
{:else}
|
|
146
|
+
{@render tablist()}
|
|
147
|
+
{/if}
|
|
134
148
|
|
|
135
149
|
{#if children && renderedActiveTab}
|
|
136
150
|
<div
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
{@render children()}
|
|
46
46
|
|
|
47
47
|
<!-- Hover intent: 100ms before it appears, so sweeping the pointer across a
|
|
48
|
-
dense row doesn't flash a trail of bubbles. Still no fade
|
|
48
|
+
dense row doesn't flash a trail of bubbles. Still no fade, `duration-0`
|
|
49
49
|
keeps the appearance instant once the delay is served, per DESIGN.md. -->
|
|
50
50
|
<div
|
|
51
51
|
class="
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { ChevronDown, Download, Play, RotateCw, Square, Trash } from "@lucide/svelte";
|
|
6
|
+
import Button from "../Button.svelte";
|
|
7
|
+
import Popover from "../Popover.svelte";
|
|
8
|
+
import { runLogDownloadUrl } from "./run-helpers.js";
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
runId,
|
|
12
|
+
taskName,
|
|
13
|
+
isRunning,
|
|
14
|
+
canDelete,
|
|
15
|
+
onRun,
|
|
16
|
+
onRunAgain,
|
|
17
|
+
onStop,
|
|
18
|
+
onDelete,
|
|
19
|
+
stopLabel = "Stop",
|
|
20
|
+
}: {
|
|
21
|
+
runId: string;
|
|
22
|
+
taskName: string;
|
|
23
|
+
isRunning: boolean;
|
|
24
|
+
canDelete: boolean;
|
|
25
|
+
onRun: (() => void) | undefined;
|
|
26
|
+
onRunAgain: (() => void) | undefined;
|
|
27
|
+
onStop: ((runId: string) => void) | undefined;
|
|
28
|
+
onDelete: ((runId: string) => void) | undefined;
|
|
29
|
+
stopLabel?: string | undefined;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
// Inline-confirm popover for delete.
|
|
33
|
+
let confirmDeleteOpen = $state(false);
|
|
34
|
+
// Dropdown half of the Run split button (the "reuse parameters" variant).
|
|
35
|
+
let runMenuOpen = $state(false);
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<!-- Run-level actions: run (split) · stop · download · delete (delete behind an
|
|
39
|
+
inline confirm). Task-level ones (pause, service start/stop/restart) live
|
|
40
|
+
on the task header. -->
|
|
41
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
42
|
+
{#if onRun}
|
|
43
|
+
<!-- Split button: Run (defaults) + an arrow for the reuse-parameters
|
|
44
|
+
variant, shown only when there is one. -->
|
|
45
|
+
<div class="inline-flex overflow-hidden rounded-[3px] border border-primary-soft-border">
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
onclick={() => onRun()}
|
|
49
|
+
title="Run this task now with default options"
|
|
50
|
+
class="inline-flex h-9 cursor-pointer items-center gap-1.5 bg-surface-raised px-3 font-mono text-sm font-medium text-primary hover:bg-primary-soft active:translate-y-px"
|
|
51
|
+
>
|
|
52
|
+
<Play size={15} fill="currentColor" stroke="none" />
|
|
53
|
+
<span class="@max-xs:hidden">Run</span>
|
|
54
|
+
</button>
|
|
55
|
+
{#if onRunAgain}
|
|
56
|
+
<Popover bind:open={runMenuOpen} placement="bottom-end">
|
|
57
|
+
{#snippet trigger()}
|
|
58
|
+
<span
|
|
59
|
+
class="flex h-9 cursor-pointer items-center border-l border-primary-soft-border bg-surface-raised px-2 text-primary hover:bg-primary-soft"
|
|
60
|
+
title="More run options"
|
|
61
|
+
aria-label="More run options"
|
|
62
|
+
>
|
|
63
|
+
<ChevronDown size={15} />
|
|
64
|
+
</span>
|
|
65
|
+
{/snippet}
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
onclick={() => {
|
|
69
|
+
runMenuOpen = false;
|
|
70
|
+
onRunAgain();
|
|
71
|
+
}}
|
|
72
|
+
class="flex w-56 items-start gap-2.5 text-left"
|
|
73
|
+
>
|
|
74
|
+
<RotateCw size={15} class="mt-0.5 shrink-0 text-on-surface-muted" />
|
|
75
|
+
<span class="flex flex-col">
|
|
76
|
+
<span class="font-mono text-sm font-medium text-on-surface"
|
|
77
|
+
>Run again</span
|
|
78
|
+
>
|
|
79
|
+
<span class="text-xs text-on-surface-muted"
|
|
80
|
+
>Reuse this run's parameters</span
|
|
81
|
+
>
|
|
82
|
+
</span>
|
|
83
|
+
</button>
|
|
84
|
+
</Popover>
|
|
85
|
+
{/if}
|
|
86
|
+
</div>
|
|
87
|
+
{/if}
|
|
88
|
+
{#if onRunAgain && !onRun && !isRunning}
|
|
89
|
+
<!-- No Run button here (the page has its own), so re-running with this
|
|
90
|
+
run's parameters stands alone. -->
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onclick={() => onRunAgain()}
|
|
94
|
+
title="Run again, reusing this run's parameters"
|
|
95
|
+
aria-label="Run again"
|
|
96
|
+
class="inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-[3px] border border-primary-soft-border bg-surface-raised px-3 font-mono text-sm font-medium text-primary hover:bg-primary-soft active:translate-y-px @max-md:px-2.5"
|
|
97
|
+
>
|
|
98
|
+
<RotateCw size={15} />
|
|
99
|
+
<span class="@max-md:hidden">Run again</span>
|
|
100
|
+
</button>
|
|
101
|
+
{/if}
|
|
102
|
+
{#if isRunning && onStop}
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onclick={() => onStop(runId)}
|
|
106
|
+
aria-label={stopLabel}
|
|
107
|
+
class="inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-[3px] border border-danger-soft-border bg-surface-raised px-3 font-mono text-sm font-medium text-danger-surface hover:bg-danger-soft active:translate-y-px"
|
|
108
|
+
>
|
|
109
|
+
<Square size={15} fill="currentColor" stroke="none" />
|
|
110
|
+
<span class="@max-xs:hidden">{stopLabel}</span>
|
|
111
|
+
</button>
|
|
112
|
+
{/if}
|
|
113
|
+
<a
|
|
114
|
+
href={runLogDownloadUrl(runId)}
|
|
115
|
+
download="{taskName}-{runId}.log"
|
|
116
|
+
class="inline-flex items-center justify-center rounded-[3px] border border-outline-faint bg-surface-raised p-2 text-on-surface-muted hover:border-outline-hover hover:bg-surface-sunken hover:text-primary"
|
|
117
|
+
title="Download the full log (rotated and current parts as one file)"
|
|
118
|
+
aria-label="Download log"
|
|
119
|
+
>
|
|
120
|
+
<Download size={15} />
|
|
121
|
+
</a>
|
|
122
|
+
{#if canDelete && onDelete}
|
|
123
|
+
<Popover bind:open={confirmDeleteOpen} placement="bottom-end">
|
|
124
|
+
{#snippet trigger()}
|
|
125
|
+
<span
|
|
126
|
+
class="flex cursor-pointer items-center justify-center rounded-[3px] border border-outline-faint bg-surface-raised p-2 text-on-surface-muted hover:border-danger-soft-border hover:bg-danger-soft hover:text-danger-surface"
|
|
127
|
+
title="Delete this run"
|
|
128
|
+
aria-label="Delete run"
|
|
129
|
+
>
|
|
130
|
+
<Trash size={15} />
|
|
131
|
+
</span>
|
|
132
|
+
{/snippet}
|
|
133
|
+
<div class="w-60">
|
|
134
|
+
<div class="font-mono text-sm font-semibold text-on-surface">Delete this run?</div>
|
|
135
|
+
<div class="mt-1 text-xs leading-relaxed text-on-surface-muted">
|
|
136
|
+
Its captured output is removed from disk. This can't be undone.
|
|
137
|
+
</div>
|
|
138
|
+
<div class="mt-3 flex justify-end gap-2">
|
|
139
|
+
<Button variant="ghost" size="sm" onclick={() => (confirmDeleteOpen = false)}>
|
|
140
|
+
Cancel
|
|
141
|
+
</Button>
|
|
142
|
+
<Button
|
|
143
|
+
variant="danger"
|
|
144
|
+
size="sm"
|
|
145
|
+
onclick={() => {
|
|
146
|
+
confirmDeleteOpen = false;
|
|
147
|
+
onDelete(runId);
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
Delete run
|
|
151
|
+
</Button>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</Popover>
|
|
155
|
+
{/if}
|
|
156
|
+
</div>
|