@runwisp/ui 0.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/LICENSE +215 -0
- package/README.md +20 -0
- package/package.json +128 -0
- package/src/app.d.ts +8 -0
- package/src/app.html +12 -0
- package/src/astro/CTABand.astro +32 -0
- package/src/astro/Hero.astro +58 -0
- package/src/astro/SiteFooter.astro +47 -0
- package/src/astro/SiteHeader.astro +46 -0
- package/src/lib/actions/portal.ts +16 -0
- package/src/lib/assets/runwisp-logo.svg +16 -0
- package/src/lib/components/Accordion.svelte +20 -0
- package/src/lib/components/AccordionItem.svelte +74 -0
- package/src/lib/components/Alert.svelte +107 -0
- package/src/lib/components/AlertDialog.svelte +63 -0
- package/src/lib/components/Avatar.svelte +88 -0
- package/src/lib/components/Badge.svelte +131 -0
- package/src/lib/components/Breadcrumb.svelte +58 -0
- package/src/lib/components/Button.svelte +69 -0
- package/src/lib/components/Card.svelte +70 -0
- package/src/lib/components/Checkbox.svelte +90 -0
- package/src/lib/components/CodeBlock.svelte +66 -0
- package/src/lib/components/CronInput.svelte +130 -0
- package/src/lib/components/DataGrid.svelte +368 -0
- package/src/lib/components/Drawer.svelte +156 -0
- package/src/lib/components/Dropdown.svelte +195 -0
- package/src/lib/components/DurationInput.svelte +170 -0
- package/src/lib/components/EmptyState.svelte +46 -0
- package/src/lib/components/ErrorState.svelte +73 -0
- package/src/lib/components/FeatureCard.svelte +35 -0
- package/src/lib/components/FeatureGrid.svelte +37 -0
- package/src/lib/components/FormField.svelte +53 -0
- package/src/lib/components/Heading.svelte +46 -0
- package/src/lib/components/Input.svelte +114 -0
- package/src/lib/components/Kbd.svelte +31 -0
- package/src/lib/components/Link.svelte +56 -0
- package/src/lib/components/LinkButton.svelte +55 -0
- package/src/lib/components/LogConsole.svelte +424 -0
- package/src/lib/components/Logo.svelte +34 -0
- package/src/lib/components/Modal.svelte +138 -0
- package/src/lib/components/PageContainer.svelte +38 -0
- package/src/lib/components/Pagination.svelte +96 -0
- package/src/lib/components/Popover.svelte +130 -0
- package/src/lib/components/Progress.svelte +68 -0
- package/src/lib/components/Prose.svelte +43 -0
- package/src/lib/components/Radio.svelte +82 -0
- package/src/lib/components/RadioGroup.svelte +53 -0
- package/src/lib/components/Select.svelte +454 -0
- package/src/lib/components/Separator.svelte +30 -0
- package/src/lib/components/Skeleton.svelte +75 -0
- package/src/lib/components/Sparkline.svelte +58 -0
- package/src/lib/components/Spinner.svelte +44 -0
- package/src/lib/components/StatusIndicator.svelte +203 -0
- package/src/lib/components/Table.svelte +19 -0
- package/src/lib/components/TableBody.svelte +17 -0
- package/src/lib/components/TableCell.svelte +34 -0
- package/src/lib/components/TableHead.svelte +19 -0
- package/src/lib/components/TableRow.svelte +22 -0
- package/src/lib/components/Tabs.svelte +148 -0
- package/src/lib/components/Text.svelte +57 -0
- package/src/lib/components/Textarea.svelte +81 -0
- package/src/lib/components/ThemeToggle.svelte +47 -0
- package/src/lib/components/TimezoneSelect.svelte +127 -0
- package/src/lib/components/ToastContainer.svelte +84 -0
- package/src/lib/components/Toggle.svelte +84 -0
- package/src/lib/components/Tooltip.svelte +56 -0
- package/src/lib/components/button-styles.ts +51 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +252 -0
- package/src/lib/components/dashboard/RunsList.svelte +331 -0
- package/src/lib/components/dashboard/run-helpers.ts +12 -0
- package/src/lib/components/dashboard/status-config.ts +141 -0
- package/src/lib/components/dashboard/types.ts +35 -0
- package/src/lib/index.ts +122 -0
- package/src/lib/log-console/LogCache.svelte.ts +93 -0
- package/src/lib/log-console/LogFetcher.svelte.ts +179 -0
- package/src/lib/log-console/types.ts +24 -0
- package/src/lib/theme-tokens.css +235 -0
- package/src/lib/theme.css +276 -0
- package/src/lib/utils/error.ts +18 -0
- package/src/lib/utils/format.ts +76 -0
- package/src/lib/utils/id.ts +6 -0
- package/src/lib/utils/toast.svelte.ts +87 -0
- package/src/lib/utils/toast.ts +60 -0
- package/src/routes/+layout.svelte +10 -0
- package/src/routes/+page.svelte +5 -0
- package/src/routes/layout.css +3 -0
- package/src/stories/Alert.stories.svelte +64 -0
- package/src/stories/Avatar.stories.svelte +76 -0
- package/src/stories/Badge.stories.svelte +76 -0
- package/src/stories/Breadcrumb.stories.svelte +64 -0
- package/src/stories/Button.stories.svelte +101 -0
- package/src/stories/Card.stories.svelte +106 -0
- package/src/stories/Checkbox.stories.svelte +77 -0
- package/src/stories/DataGrid.stories.svelte +130 -0
- package/src/stories/Dropdown.stories.svelte +81 -0
- package/src/stories/EmptyState.stories.svelte +74 -0
- package/src/stories/Input.stories.svelte +88 -0
- package/src/stories/Logo.stories.svelte +82 -0
- package/src/stories/Modal.stories.svelte +101 -0
- package/src/stories/Progress.stories.svelte +65 -0
- package/src/stories/Select.stories.svelte +158 -0
- package/src/stories/Tabs.stories.svelte +103 -0
- package/src/stories/Textarea.stories.svelte +45 -0
- package/src/stories/Toggle.stories.svelte +74 -0
- package/src/stories/Tooltip.stories.svelte +77 -0
- package/src/stories/Welcome.mdx +241 -0
- package/src/stories/assets/runwisp_c.svg +16 -0
- package/src/svelte-shims.d.ts +18 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import Select from "./Select.svelte";
|
|
6
|
+
import { onMount } from "svelte";
|
|
7
|
+
import { type Component } from "svelte";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
value = $bindable(),
|
|
11
|
+
label = "Timezone",
|
|
12
|
+
disabled = false,
|
|
13
|
+
hint,
|
|
14
|
+
} = $props<{
|
|
15
|
+
value: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
hint?: string;
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
type ZoneOption = {
|
|
22
|
+
value: string;
|
|
23
|
+
label: string;
|
|
24
|
+
group?: string | undefined;
|
|
25
|
+
description?: string | undefined;
|
|
26
|
+
icon?: Component;
|
|
27
|
+
};
|
|
28
|
+
let options = $state<ZoneOption[]>([]);
|
|
29
|
+
|
|
30
|
+
function getOffset(zone: string): string {
|
|
31
|
+
try {
|
|
32
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
33
|
+
timeZone: zone,
|
|
34
|
+
timeZoneName: "shortOffset",
|
|
35
|
+
});
|
|
36
|
+
const parts = formatter.formatToParts(new Date());
|
|
37
|
+
const offset = parts.find((p) => p.type === "timeZoneName")?.value;
|
|
38
|
+
return offset?.replace("GMT", "UTC") || "";
|
|
39
|
+
} catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toRegion(zone: string): string {
|
|
45
|
+
const separatorIndex = zone.indexOf("/");
|
|
46
|
+
return separatorIndex === -1 ? "Other" : zone.slice(0, separatorIndex);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toPrettyLabel(zone: string): string {
|
|
50
|
+
return zone.split("/").slice(1).join(" / ").replace(/_/g, " ");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toZoneOption(zone: string): ZoneOption {
|
|
54
|
+
const offset = getOffset(zone);
|
|
55
|
+
return {
|
|
56
|
+
value: zone,
|
|
57
|
+
label: toPrettyLabel(zone),
|
|
58
|
+
group: toRegion(zone),
|
|
59
|
+
description: offset ? `Timezone offset: ${offset}` : undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildFixedOffsetOptions(): ZoneOption[] {
|
|
64
|
+
const fixedOffsets: ZoneOption[] = [];
|
|
65
|
+
|
|
66
|
+
for (let offsetHours = -12; offsetHours <= 14; offsetHours++) {
|
|
67
|
+
const signDisplay = offsetHours >= 0 ? "+" : "-";
|
|
68
|
+
const abs = Math.abs(offsetHours).toString().padStart(2, "0");
|
|
69
|
+
const label = `UTC${signDisplay}${abs}:00`;
|
|
70
|
+
const value =
|
|
71
|
+
offsetHours === 0
|
|
72
|
+
? "UTC"
|
|
73
|
+
: `Etc/GMT${offsetHours > 0 ? "-" : "+"}${Math.abs(offsetHours)}`;
|
|
74
|
+
|
|
75
|
+
fixedOffsets.push({
|
|
76
|
+
value,
|
|
77
|
+
label,
|
|
78
|
+
group: "Fixed Offsets",
|
|
79
|
+
description: "Fixed constant time offset",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return fixedOffsets;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildTimezoneOptions(): ZoneOption[] {
|
|
87
|
+
const regions: Record<string, ZoneOption[]> = {};
|
|
88
|
+
|
|
89
|
+
for (const zone of Intl.supportedValuesOf("timeZone")) {
|
|
90
|
+
if (zone.startsWith("Etc/")) continue;
|
|
91
|
+
|
|
92
|
+
const option = toZoneOption(zone);
|
|
93
|
+
const region = option.group ?? "Other";
|
|
94
|
+
regions[region] ??= [];
|
|
95
|
+
regions[region].push(option);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const regionalOptions = Object.keys(regions)
|
|
99
|
+
.sort()
|
|
100
|
+
.flatMap((region) => regions[region]!.sort((a, b) => a.label.localeCompare(b.label)));
|
|
101
|
+
|
|
102
|
+
return [...buildFixedOffsetOptions(), ...regionalOptions];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onMount(() => {
|
|
106
|
+
try {
|
|
107
|
+
options = buildTimezoneOptions();
|
|
108
|
+
|
|
109
|
+
if (!value) {
|
|
110
|
+
value = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.warn("Timezone load failed", e);
|
|
114
|
+
options = [{ value: "UTC", label: "UTC", description: "Universal Coordinated Time" }];
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<Select
|
|
120
|
+
{label}
|
|
121
|
+
bind:value
|
|
122
|
+
{disabled}
|
|
123
|
+
{hint}
|
|
124
|
+
placeholder="Select timezone..."
|
|
125
|
+
{options}
|
|
126
|
+
searchable={true}
|
|
127
|
+
></Select>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
/**
|
|
6
|
+
* Toast notification component
|
|
7
|
+
* Displays toast notifications for user feedback
|
|
8
|
+
*/
|
|
9
|
+
import { fly, fade } from "svelte/transition";
|
|
10
|
+
import { CircleCheck, CircleX, Info, TriangleAlert, X } from "@lucide/svelte";
|
|
11
|
+
import { toast, type Toast } from "../utils/toast.svelte.js";
|
|
12
|
+
|
|
13
|
+
function getIcon(type: Toast["type"]) {
|
|
14
|
+
switch (type) {
|
|
15
|
+
case "success":
|
|
16
|
+
return CircleCheck;
|
|
17
|
+
case "error":
|
|
18
|
+
return CircleX;
|
|
19
|
+
case "warning":
|
|
20
|
+
return TriangleAlert;
|
|
21
|
+
case "info":
|
|
22
|
+
return Info;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getColorClasses(type: Toast["type"]) {
|
|
27
|
+
switch (type) {
|
|
28
|
+
case "success":
|
|
29
|
+
return "border-success-200 text-success-700";
|
|
30
|
+
case "error":
|
|
31
|
+
return "border-danger-200 text-danger-700";
|
|
32
|
+
case "warning":
|
|
33
|
+
return "border-warning-200 text-warning-700";
|
|
34
|
+
case "info":
|
|
35
|
+
return "border-wisp-200 text-wisp-700";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getIconColor(type: Toast["type"]) {
|
|
40
|
+
switch (type) {
|
|
41
|
+
case "success":
|
|
42
|
+
return "text-success-surface";
|
|
43
|
+
case "error":
|
|
44
|
+
return "text-danger-surface";
|
|
45
|
+
case "warning":
|
|
46
|
+
return "text-warning-surface";
|
|
47
|
+
case "info":
|
|
48
|
+
return "text-info";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<!-- Toast Container -->
|
|
54
|
+
<div
|
|
55
|
+
class="pointer-events-none fixed bottom-4 left-1/2 z-[100] flex -translate-x-1/2 flex-col gap-2"
|
|
56
|
+
role="region"
|
|
57
|
+
aria-label="Notifications"
|
|
58
|
+
aria-live="polite"
|
|
59
|
+
>
|
|
60
|
+
{#each toast.items as toastItem (toastItem.id)}
|
|
61
|
+
{@const Icon = getIcon(toastItem.type)}
|
|
62
|
+
<div
|
|
63
|
+
in:fly={{ y: 20, duration: 300 }}
|
|
64
|
+
out:fade={{ duration: 200 }}
|
|
65
|
+
class="pointer-events-auto isolate flex max-w-md min-w-[320px] items-start gap-3 rounded-xl border bg-surface-overlay/95 px-4 py-3 shadow-lg ring-1 ring-on-surface/5 backdrop-blur-md {getColorClasses(
|
|
66
|
+
toastItem.type,
|
|
67
|
+
)}"
|
|
68
|
+
role="alert"
|
|
69
|
+
>
|
|
70
|
+
<Icon
|
|
71
|
+
class="mt-0.5 h-5 w-5 flex-shrink-0 {getIconColor(toastItem.type)}"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
/>
|
|
74
|
+
<p class="flex-1 text-sm font-medium">{toastItem.message}</p>
|
|
75
|
+
<button
|
|
76
|
+
onclick={() => toast.remove(toastItem.id)}
|
|
77
|
+
class="flex-shrink-0 rounded-lg p-1 text-on-surface-muted transition-colors hover:bg-surface-sunken hover:text-on-surface"
|
|
78
|
+
aria-label="Close notification"
|
|
79
|
+
>
|
|
80
|
+
<X class="h-4 w-4" aria-hidden="true" />
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
{/each}
|
|
84
|
+
</div>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
interface Props {
|
|
6
|
+
checked?: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
size?: "sm" | "md" | "lg";
|
|
9
|
+
label?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
onchange?: (checked: boolean) => void;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
checked = $bindable(),
|
|
17
|
+
disabled = false,
|
|
18
|
+
size = "md",
|
|
19
|
+
label,
|
|
20
|
+
description,
|
|
21
|
+
onchange,
|
|
22
|
+
class: className = "",
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
function toggle() {
|
|
26
|
+
if (disabled) return;
|
|
27
|
+
checked = !checked;
|
|
28
|
+
onchange?.(checked);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sizeClasses: Record<
|
|
32
|
+
"sm" | "md" | "lg",
|
|
33
|
+
{ track: string; thumb: string; translate: string; text: string }
|
|
34
|
+
> = {
|
|
35
|
+
sm: { track: "h-5 w-9", thumb: "h-4 w-4", translate: "translate-x-4", text: "text-sm" },
|
|
36
|
+
md: { track: "h-6 w-11", thumb: "h-5 w-5", translate: "translate-x-5", text: "text-sm" },
|
|
37
|
+
lg: { track: "h-7 w-14", thumb: "h-6 w-6", translate: "translate-x-7", text: "text-base" },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const sizeConfig = $derived(sizeClasses[size]);
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<label
|
|
44
|
+
class="inline-flex items-start gap-3 {disabled
|
|
45
|
+
? 'cursor-not-allowed opacity-50'
|
|
46
|
+
: 'cursor-pointer'} {className}"
|
|
47
|
+
>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
role="switch"
|
|
51
|
+
aria-checked={checked}
|
|
52
|
+
aria-label={label ?? description ?? "Toggle"}
|
|
53
|
+
{disabled}
|
|
54
|
+
onclick={toggle}
|
|
55
|
+
class="
|
|
56
|
+
relative inline-flex shrink-0 items-center rounded-full
|
|
57
|
+
{sizeConfig.track}
|
|
58
|
+
transition-colors duration-200
|
|
59
|
+
focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
|
|
60
|
+
{checked ? 'bg-primary' : 'bg-outline-hover'}
|
|
61
|
+
{disabled ? 'cursor-not-allowed' : 'cursor-pointer'}
|
|
62
|
+
"
|
|
63
|
+
>
|
|
64
|
+
<span
|
|
65
|
+
class="
|
|
66
|
+
inline-block rounded-full bg-surface-raised shadow-sm
|
|
67
|
+
{sizeConfig.thumb}
|
|
68
|
+
transform transition-transform duration-200
|
|
69
|
+
{checked ? sizeConfig.translate : 'translate-x-0.5'}
|
|
70
|
+
"
|
|
71
|
+
></span>
|
|
72
|
+
</button>
|
|
73
|
+
|
|
74
|
+
{#if label || description}
|
|
75
|
+
<div class="flex flex-col gap-0.5">
|
|
76
|
+
{#if label}
|
|
77
|
+
<span class="{sizeConfig.text} font-medium text-on-surface">{label}</span>
|
|
78
|
+
{/if}
|
|
79
|
+
{#if description}
|
|
80
|
+
<span class="text-sm text-on-surface-muted">{description}</span>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
{/if}
|
|
84
|
+
</label>
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
type TooltipPosition = "top" | "bottom" | "left" | "right";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
content: string;
|
|
11
|
+
position?: TooltipPosition;
|
|
12
|
+
children: Snippet;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { content, position = "top", children, class: className = "" }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const positionClasses: Record<TooltipPosition, string> = {
|
|
19
|
+
top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
|
|
20
|
+
bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
|
|
21
|
+
left: "right-full top-1/2 -translate-y-1/2 mr-2",
|
|
22
|
+
right: "left-full top-1/2 -translate-y-1/2 ml-2",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const arrowClasses: Record<TooltipPosition, string> = {
|
|
26
|
+
top: "top-full left-1/2 -translate-x-1/2 border-t-mist-800 border-x-transparent border-b-transparent",
|
|
27
|
+
bottom: "bottom-full left-1/2 -translate-x-1/2 border-b-mist-800 border-x-transparent border-t-transparent",
|
|
28
|
+
left: "left-full top-1/2 -translate-y-1/2 border-l-mist-800 border-y-transparent border-r-transparent",
|
|
29
|
+
right: "right-full top-1/2 -translate-y-1/2 border-r-mist-800 border-y-transparent border-l-transparent",
|
|
30
|
+
};
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class="group relative inline-block {className}">
|
|
34
|
+
{@render children()}
|
|
35
|
+
|
|
36
|
+
<div
|
|
37
|
+
class="
|
|
38
|
+
absolute z-50 {positionClasses[position]}
|
|
39
|
+
pointer-events-none invisible rounded-lg
|
|
40
|
+
bg-mist-800 px-2.5 py-1.5 text-xs
|
|
41
|
+
font-medium
|
|
42
|
+
whitespace-nowrap text-white opacity-0 transition-opacity
|
|
43
|
+
duration-150 group-hover:visible
|
|
44
|
+
group-hover:opacity-100
|
|
45
|
+
"
|
|
46
|
+
role="tooltip"
|
|
47
|
+
>
|
|
48
|
+
{content}
|
|
49
|
+
<span
|
|
50
|
+
class="
|
|
51
|
+
absolute h-0 w-0
|
|
52
|
+
border-4 {arrowClasses[position]}
|
|
53
|
+
"
|
|
54
|
+
></span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: PoppyCake, s.r.o.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "success";
|
|
5
|
+
export type ButtonSize = "xs" | "sm" | "md" | "lg";
|
|
6
|
+
|
|
7
|
+
export const BUTTON_BASE = `
|
|
8
|
+
group
|
|
9
|
+
inline-flex items-center justify-center gap-2
|
|
10
|
+
font-medium cursor-pointer select-none
|
|
11
|
+
border border-transparent
|
|
12
|
+
focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/10 focus-visible:border-ring
|
|
13
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
14
|
+
transition-all duration-normal ease-out
|
|
15
|
+
active:scale-[0.98]
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export const BUTTON_VARIANTS: Record<ButtonVariant, string> = {
|
|
19
|
+
primary: `
|
|
20
|
+
bg-primary text-on-primary shadow-sm shadow-primary/20
|
|
21
|
+
hover:bg-primary-hover hover:shadow-md hover:shadow-primary/30
|
|
22
|
+
active:bg-primary-active
|
|
23
|
+
`,
|
|
24
|
+
secondary: `
|
|
25
|
+
bg-surface-raised text-on-surface-muted border-outline shadow-sm
|
|
26
|
+
hover:bg-surface-sunken hover:text-on-surface hover:border-outline-hover
|
|
27
|
+
active:bg-surface-sunken
|
|
28
|
+
`,
|
|
29
|
+
ghost: `
|
|
30
|
+
bg-transparent text-on-surface-muted
|
|
31
|
+
hover:bg-surface-sunken hover:text-on-surface
|
|
32
|
+
active:bg-surface-sunken
|
|
33
|
+
`,
|
|
34
|
+
danger: `
|
|
35
|
+
bg-danger-surface text-on-danger shadow-sm shadow-danger-surface/20
|
|
36
|
+
hover:bg-danger-hover hover:shadow-md hover:shadow-danger-surface/30
|
|
37
|
+
active:bg-danger-active
|
|
38
|
+
`,
|
|
39
|
+
success: `
|
|
40
|
+
bg-success-surface text-on-success shadow-sm shadow-success-surface/20
|
|
41
|
+
hover:bg-success-hover hover:shadow-md hover:shadow-success-surface/30
|
|
42
|
+
active:bg-success-surface
|
|
43
|
+
`,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const BUTTON_SIZES: Record<ButtonSize, string> = {
|
|
47
|
+
xs: "text-xs px-2 py-1 rounded-md",
|
|
48
|
+
sm: "text-sm px-3 py-1.5 rounded-lg",
|
|
49
|
+
md: "text-sm px-4 py-2 rounded-lg",
|
|
50
|
+
lg: "text-base px-6 py-3 rounded-xl",
|
|
51
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { untrack } from "svelte";
|
|
6
|
+
import { Server, Hash, Terminal as TerminalIcon, Download } from "@lucide/svelte";
|
|
7
|
+
import Badge from "../Badge.svelte";
|
|
8
|
+
import LogConsole from "../LogConsole.svelte";
|
|
9
|
+
import type { Run } from "./types.js";
|
|
10
|
+
import type { LogEvent, LogSlice } from "../../log-console/types.js";
|
|
11
|
+
import { formatDateTime } from "../../utils/format.js";
|
|
12
|
+
import { formatShortId } from "../../utils/id.js";
|
|
13
|
+
import { getRunStatusConfig, runDisplayStatus } from "./status-config.js";
|
|
14
|
+
import { runDuration } from "./run-helpers.js";
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
run,
|
|
18
|
+
fetchLogs,
|
|
19
|
+
streamLogs,
|
|
20
|
+
showTaskName = false,
|
|
21
|
+
}: {
|
|
22
|
+
run: Run | undefined;
|
|
23
|
+
fetchLogs: (
|
|
24
|
+
runId: string,
|
|
25
|
+
from: number,
|
|
26
|
+
to: number,
|
|
27
|
+
) => Promise<LogSlice | LogEvent | undefined> | LogSlice | LogEvent | undefined;
|
|
28
|
+
streamLogs?: (
|
|
29
|
+
runId: string,
|
|
30
|
+
onEvent: (event: LogEvent) => void,
|
|
31
|
+
initialState?: { fromLine: number },
|
|
32
|
+
) => () => void;
|
|
33
|
+
showTaskName?: boolean;
|
|
34
|
+
} = $props();
|
|
35
|
+
|
|
36
|
+
const TAIL_LINES = 1000;
|
|
37
|
+
|
|
38
|
+
let logConsole = $state<{ onStream: (event: LogEvent) => void } | null>(null);
|
|
39
|
+
|
|
40
|
+
// Derive a stable scalar so the effect only re-runs when the id actually
|
|
41
|
+
// changes, not on every run object reference swap from SSE array updates.
|
|
42
|
+
let runId = $derived(run?.id);
|
|
43
|
+
|
|
44
|
+
$effect(() => {
|
|
45
|
+
const id = runId;
|
|
46
|
+
if (!id) return;
|
|
47
|
+
|
|
48
|
+
const stream = streamLogs;
|
|
49
|
+
if (!stream) return;
|
|
50
|
+
|
|
51
|
+
let cleanup: (() => void) | undefined;
|
|
52
|
+
|
|
53
|
+
untrack(() => {
|
|
54
|
+
// Single SSE call seeds the viewport via backfill (negative
|
|
55
|
+
// fromLine = tail-from-end) and continues with live events on
|
|
56
|
+
// the same connection. No tail+stream handoff, no duplicate
|
|
57
|
+
// replay, native Last-Event-ID resume on reconnect.
|
|
58
|
+
cleanup = stream(
|
|
59
|
+
id,
|
|
60
|
+
(event: LogEvent) => {
|
|
61
|
+
if (logConsole) logConsole.onStream(event);
|
|
62
|
+
},
|
|
63
|
+
{ fromLine: -TAIL_LINES },
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
if (cleanup) cleanup();
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
{#if run}
|
|
74
|
+
{@const config = getRunStatusConfig(runDisplayStatus(run))}
|
|
75
|
+
{@const DetailIcon = config.icon}
|
|
76
|
+
{@const duration = runDuration(run)}
|
|
77
|
+
<!-- Detailed Header -->
|
|
78
|
+
<div class="shrink-0 border-b border-outline-faint bg-surface-raised p-6">
|
|
79
|
+
<div class="flex flex-col justify-between gap-6 lg:flex-row lg:items-start">
|
|
80
|
+
<div class="flex items-start gap-4">
|
|
81
|
+
<!-- Large Status Icon -->
|
|
82
|
+
<div
|
|
83
|
+
class="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl {config.bg} {config.color}"
|
|
84
|
+
>
|
|
85
|
+
<DetailIcon size={32} class={run.status === "running" ? "animate-spin" : ""} />
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div>
|
|
89
|
+
<div class="mb-1 flex items-center gap-3">
|
|
90
|
+
<h2 class="text-xl font-bold text-on-surface">
|
|
91
|
+
{#if showTaskName}
|
|
92
|
+
{run.task_name}{#if run.instance_index > 0}<span
|
|
93
|
+
class="text-on-surface-muted">#{run.instance_index}</span
|
|
94
|
+
>{/if}
|
|
95
|
+
{:else}
|
|
96
|
+
Run #{formatShortId(run.id)}{#if run.instance_index > 0}
|
|
97
|
+
<span class="text-on-surface-muted"
|
|
98
|
+
>· instance #{run.instance_index}</span
|
|
99
|
+
>
|
|
100
|
+
{/if}
|
|
101
|
+
{/if}
|
|
102
|
+
</h2>
|
|
103
|
+
<Badge
|
|
104
|
+
variant={runDisplayStatus(run) === "success"
|
|
105
|
+
? "success"
|
|
106
|
+
: run.status === "running"
|
|
107
|
+
? "info"
|
|
108
|
+
: "danger"}
|
|
109
|
+
>
|
|
110
|
+
{runDisplayStatus(run).toUpperCase()}
|
|
111
|
+
</Badge>
|
|
112
|
+
</div>
|
|
113
|
+
<div
|
|
114
|
+
class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-on-surface-muted"
|
|
115
|
+
>
|
|
116
|
+
{#if showTaskName}
|
|
117
|
+
<div
|
|
118
|
+
class="flex items-center gap-1.5 font-mono text-xs text-on-surface-faint"
|
|
119
|
+
>
|
|
120
|
+
<Hash size={12} />
|
|
121
|
+
Run ID: <span class="text-on-surface-muted">{run.id}</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div
|
|
124
|
+
class="hidden h-1 w-1 rounded-full bg-outline-faint sm:block"
|
|
125
|
+
></div>
|
|
126
|
+
<div class="flex items-center gap-1.5">
|
|
127
|
+
<Server size={14} />
|
|
128
|
+
<span
|
|
129
|
+
>Triggered by <span class="font-medium text-on-surface"
|
|
130
|
+
>{run.triggered_by}</span
|
|
131
|
+
></span
|
|
132
|
+
>
|
|
133
|
+
</div>
|
|
134
|
+
{:else}
|
|
135
|
+
<div class="flex items-center gap-1.5">
|
|
136
|
+
<Server size={14} />
|
|
137
|
+
<span
|
|
138
|
+
>Triggered by <span class="font-medium text-on-surface"
|
|
139
|
+
>{run.triggered_by}</span
|
|
140
|
+
></span
|
|
141
|
+
>
|
|
142
|
+
</div>
|
|
143
|
+
<div
|
|
144
|
+
class="hidden h-1 w-1 rounded-full bg-outline-faint sm:block"
|
|
145
|
+
></div>
|
|
146
|
+
<div
|
|
147
|
+
class="flex items-center gap-1.5 font-mono text-xs text-on-surface-faint"
|
|
148
|
+
>
|
|
149
|
+
<Hash size={12} />
|
|
150
|
+
{run.id}
|
|
151
|
+
</div>
|
|
152
|
+
{/if}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<!-- Stats Grid -->
|
|
158
|
+
<div
|
|
159
|
+
class="grid grid-cols-3 gap-6 rounded-xl border border-outline-faint bg-surface-sunken px-6 py-3"
|
|
160
|
+
>
|
|
161
|
+
<div class="flex flex-col gap-1">
|
|
162
|
+
<span
|
|
163
|
+
class="text-xs font-semibold tracking-wide text-on-surface-faint uppercase"
|
|
164
|
+
>Started</span
|
|
165
|
+
>
|
|
166
|
+
<span class="font-mono text-sm font-medium whitespace-nowrap text-on-surface">
|
|
167
|
+
{run.start_at ? formatDateTime(run.start_at) : "--:--"}
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div class="flex flex-col gap-1 border-l border-outline pl-6">
|
|
172
|
+
<span
|
|
173
|
+
class="text-xs font-semibold tracking-wide text-on-surface-faint uppercase"
|
|
174
|
+
>Duration</span
|
|
175
|
+
>
|
|
176
|
+
<span class="font-mono text-sm font-medium text-on-surface">
|
|
177
|
+
{duration ?? "--"}
|
|
178
|
+
</span>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="flex flex-col gap-1 border-l border-outline pl-6">
|
|
182
|
+
<span
|
|
183
|
+
class="text-xs font-semibold tracking-wide text-on-surface-faint uppercase"
|
|
184
|
+
>Exited</span
|
|
185
|
+
>
|
|
186
|
+
<span
|
|
187
|
+
class="text-sm font-medium {run.exit_code === 0
|
|
188
|
+
? 'text-success-surface'
|
|
189
|
+
: 'text-on-surface'} font-mono"
|
|
190
|
+
>
|
|
191
|
+
{typeof run.exit_code === "number" ? `Code ${run.exit_code}` : "-"}
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<!-- Console View -->
|
|
199
|
+
<div class="flex min-h-0 flex-1 flex-col bg-slate-950">
|
|
200
|
+
<div
|
|
201
|
+
class="flex shrink-0 items-center justify-between border-b border-slate-800 bg-slate-900/50 px-4 py-2 font-mono text-xs text-slate-400"
|
|
202
|
+
>
|
|
203
|
+
<div class="flex items-center gap-2">
|
|
204
|
+
<TerminalIcon size={14} />
|
|
205
|
+
<span>Console Output</span>
|
|
206
|
+
</div>
|
|
207
|
+
<div class="flex items-center gap-3">
|
|
208
|
+
{#if run.status === "running"}
|
|
209
|
+
<div class="text-info-surface flex items-center gap-1.5">
|
|
210
|
+
<div class="bg-info-surface h-1.5 w-1.5 animate-pulse rounded-full"></div>
|
|
211
|
+
Live
|
|
212
|
+
</div>
|
|
213
|
+
{:else if run.status === "pending"}
|
|
214
|
+
<div class="flex items-center gap-1.5 text-warning-surface">
|
|
215
|
+
<div
|
|
216
|
+
class="h-1.5 w-1.5 animate-pulse rounded-full bg-warning-surface"
|
|
217
|
+
></div>
|
|
218
|
+
Pending
|
|
219
|
+
</div>
|
|
220
|
+
{/if}
|
|
221
|
+
<a
|
|
222
|
+
href="/api/tasks/{encodeURIComponent(run.task_name)}/runs/{encodeURIComponent(
|
|
223
|
+
run.id,
|
|
224
|
+
)}/log/raw"
|
|
225
|
+
download="{run.task_name}-{run.id}.log"
|
|
226
|
+
class="flex items-center gap-1.5 rounded border border-slate-700 px-2 py-1 text-slate-300 transition-colors hover:border-slate-500 hover:bg-slate-800 hover:text-slate-100"
|
|
227
|
+
title="Download the full log (rotated and current parts as one file)"
|
|
228
|
+
>
|
|
229
|
+
<Download size={12} />
|
|
230
|
+
Download
|
|
231
|
+
</a>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
{#key run.id}
|
|
235
|
+
<LogConsole
|
|
236
|
+
bind:this={logConsole}
|
|
237
|
+
fetchLogs={(f: number, t: number) => fetchLogs(run.id, f, t)}
|
|
238
|
+
class="min-h-0 flex-1"
|
|
239
|
+
/>
|
|
240
|
+
{/key}
|
|
241
|
+
</div>
|
|
242
|
+
{:else}
|
|
243
|
+
<div
|
|
244
|
+
class="flex flex-1 flex-col items-center justify-center bg-surface-sunken/30 text-on-surface-faint"
|
|
245
|
+
>
|
|
246
|
+
<div class="mb-4 rounded-full border border-outline bg-surface-raised p-4 shadow-sm">
|
|
247
|
+
<Server size={32} class="text-outline-faint" />
|
|
248
|
+
</div>
|
|
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>
|
|
251
|
+
</div>
|
|
252
|
+
{/if}
|