@runwisp/ui 1.5.1 → 1.7.1
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/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 +3 -24
- package/src/lib/components/Button.svelte +2 -9
- package/src/lib/components/CodeBlock.svelte +7 -6
- package/src/lib/components/Drawer.svelte +22 -30
- package/src/lib/components/DurationInput.svelte +6 -25
- package/src/lib/components/FilterBar.svelte +2 -6
- package/src/lib/components/Link.svelte +9 -4
- package/src/lib/components/LogConsole.svelte +95 -418
- package/src/lib/components/Modal.svelte +7 -30
- package/src/lib/components/Spinner.svelte +10 -5
- package/src/lib/components/StatusIndicator.svelte +37 -104
- package/src/lib/components/Tabs.svelte +15 -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 +130 -717
- 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 +4 -5
- 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 +265 -543
- 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 +9 -0
- package/src/lib/components/dashboard/cron-format.ts +5 -1
- package/src/lib/components/dashboard/run-filters.test.ts +60 -0
- package/src/lib/components/dashboard/run-filters.ts +78 -6
- package/src/lib/components/dashboard/run-helpers.test.ts +122 -12
- package/src/lib/components/dashboard/run-helpers.ts +122 -6
- package/src/lib/components/dashboard/status-config.ts +18 -0
- package/src/lib/components/dashboard/types.ts +6 -0
- 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.test.ts +135 -0
- package/src/lib/log-console/LogConsoleFooter.svelte +88 -0
- package/src/lib/log-console/LogFetcher.svelte.test.ts +159 -0
- 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/utils/format.test.ts +122 -0
- package/src/lib/utils/format.ts +97 -4
- package/src/lib/utils/toast.svelte.test.ts +46 -0
- 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/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
|
|
@@ -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>
|
|
@@ -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
|
|
@@ -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>
|