@unofficialbox/box-open-elements 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/components/collections/table.d.ts +57 -3
- package/dist/components/collections/table.js +339 -20
- package/dist/components/feedback/due-badge.d.ts +38 -0
- package/dist/components/feedback/due-badge.js +168 -0
- package/dist/components/feedback/due-types.d.ts +32 -0
- package/dist/components/feedback/due-types.js +91 -0
- package/dist/components/feedback/progress-steps.d.ts +51 -5
- package/dist/components/feedback/progress-steps.js +200 -22
- package/dist/components/feedback/stage-path.d.ts +44 -0
- package/dist/components/feedback/stage-path.js +235 -0
- package/dist/components/forms/select.d.ts +21 -0
- package/dist/components/forms/select.js +100 -3
- package/dist/components/forms/text-field.d.ts +21 -0
- package/dist/components/forms/text-field.js +100 -1
- package/dist/components/layout/nav-sidebar.d.ts +14 -0
- package/dist/components/layout/nav-sidebar.js +49 -0
- package/dist/components/layout/split-view.d.ts +14 -0
- package/dist/components/layout/split-view.js +80 -1
- package/dist/components/overlays/command-palette.d.ts +62 -0
- package/dist/components/overlays/command-palette.js +537 -0
- package/dist/components/overlays/command-types.d.ts +90 -0
- package/dist/components/overlays/command-types.js +259 -0
- package/dist/components/overlays/drawer.d.ts +18 -0
- package/dist/components/overlays/drawer.js +135 -9
- package/dist/components/overlays/shortcuts-overlay.d.ts +48 -0
- package/dist/components/overlays/shortcuts-overlay.js +385 -0
- package/dist/components/overlays/tooltip.js +1 -1
- package/dist/element-maps.d.ts +292 -0
- package/dist/element-maps.js +1 -0
- package/dist/entries/activity-density.d.ts +1 -0
- package/dist/entries/activity-density.js +1 -0
- package/dist/entries/agent-chat.d.ts +1 -0
- package/dist/entries/agent-chat.js +1 -0
- package/dist/entries/audit-log.d.ts +1 -0
- package/dist/entries/audit-log.js +1 -0
- package/dist/entries/command-palette.d.ts +1 -0
- package/dist/entries/command-palette.js +1 -0
- package/dist/entries/compare-view.d.ts +1 -0
- package/dist/entries/compare-view.js +1 -0
- package/dist/entries/diff-viewer.d.ts +1 -0
- package/dist/entries/diff-viewer.js +1 -0
- package/dist/entries/due-badge.d.ts +1 -0
- package/dist/entries/due-badge.js +1 -0
- package/dist/entries/form-wizard.d.ts +1 -0
- package/dist/entries/form-wizard.js +1 -0
- package/dist/entries/lineage-graph.d.ts +1 -0
- package/dist/entries/lineage-graph.js +1 -0
- package/dist/entries/notification-bell.d.ts +1 -0
- package/dist/entries/notification-bell.js +1 -0
- package/dist/entries/notification-inbox.d.ts +1 -0
- package/dist/entries/notification-inbox.js +1 -0
- package/dist/entries/provenance-strip.d.ts +1 -0
- package/dist/entries/provenance-strip.js +1 -0
- package/dist/entries/run-trace.d.ts +1 -0
- package/dist/entries/run-trace.js +1 -0
- package/dist/entries/shortcuts-overlay.d.ts +1 -0
- package/dist/entries/shortcuts-overlay.js +1 -0
- package/dist/entries/signature-ceremony.d.ts +1 -0
- package/dist/entries/signature-ceremony.js +1 -0
- package/dist/entries/stage-path.d.ts +1 -0
- package/dist/entries/stage-path.js +1 -0
- package/dist/entries/timeline.d.ts +1 -0
- package/dist/entries/timeline.js +1 -0
- package/dist/entries/version-graph.d.ts +1 -0
- package/dist/entries/version-graph.js +1 -0
- package/dist/entries/version-list.d.ts +1 -0
- package/dist/entries/version-list.js +1 -0
- package/dist/entries/wizard-summary.d.ts +1 -0
- package/dist/entries/wizard-summary.js +1 -0
- package/dist/entries/work-queue.d.ts +1 -0
- package/dist/entries/work-queue.js +1 -0
- package/dist/entries/workload-board.d.ts +1 -0
- package/dist/entries/workload-board.js +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/patterns/agent-chat/agent-chat.d.ts +75 -0
- package/dist/patterns/agent-chat/agent-chat.js +820 -0
- package/dist/patterns/agent-chat/controller.d.ts +27 -0
- package/dist/patterns/agent-chat/controller.js +182 -0
- package/dist/patterns/agent-chat/index.d.ts +3 -0
- package/dist/patterns/agent-chat/index.js +6 -0
- package/dist/patterns/agent-chat/types.d.ts +109 -0
- package/dist/patterns/agent-chat/types.js +1 -0
- package/dist/patterns/audit/activity-density.d.ts +68 -0
- package/dist/patterns/audit/activity-density.js +428 -0
- package/dist/patterns/audit/audit-log.d.ts +81 -0
- package/dist/patterns/audit/audit-log.js +794 -0
- package/dist/patterns/audit/index.d.ts +3 -0
- package/dist/patterns/audit/index.js +7 -0
- package/dist/patterns/audit/shared.d.ts +10 -0
- package/dist/patterns/audit/shared.js +50 -0
- package/dist/patterns/audit/types.d.ts +143 -0
- package/dist/patterns/audit/types.js +327 -0
- package/dist/patterns/content-explorer/actions/controller.js +12 -2
- package/dist/patterns/content-explorer/box-transport.js +117 -25
- package/dist/patterns/content-explorer/content-explorer.d.ts +13 -1
- package/dist/patterns/content-explorer/content-explorer.js +36 -3
- package/dist/patterns/content-explorer/contracts.d.ts +24 -1
- package/dist/patterns/content-explorer/contracts.js +68 -11
- package/dist/patterns/content-explorer/controller.d.ts +16 -1
- package/dist/patterns/content-explorer/controller.js +130 -4
- package/dist/patterns/content-explorer/navigation/controller.js +11 -9
- package/dist/patterns/content-explorer/types.d.ts +58 -1
- package/dist/patterns/content-picker/content-picker.d.ts +71 -0
- package/dist/patterns/content-picker/content-picker.js +747 -0
- package/dist/patterns/content-picker/controller.d.ts +38 -0
- package/dist/patterns/content-picker/controller.js +141 -0
- package/dist/patterns/content-picker/index.d.ts +3 -0
- package/dist/patterns/content-picker/index.js +6 -0
- package/dist/patterns/content-picker/types.d.ts +52 -0
- package/dist/patterns/content-picker/types.js +26 -0
- package/dist/patterns/content-sidebar/content-sidebar.d.ts +43 -0
- package/dist/patterns/content-sidebar/content-sidebar.js +293 -0
- package/dist/patterns/content-sidebar/index.d.ts +2 -0
- package/dist/patterns/content-sidebar/index.js +4 -0
- package/dist/patterns/content-sidebar/types.d.ts +18 -0
- package/dist/patterns/content-sidebar/types.js +20 -0
- package/dist/patterns/content-uploader/box-transport.d.ts +14 -0
- package/dist/patterns/content-uploader/box-transport.js +54 -0
- package/dist/patterns/content-uploader/content-uploader.d.ts +62 -0
- package/dist/patterns/content-uploader/content-uploader.js +559 -0
- package/dist/patterns/content-uploader/controller.d.ts +39 -0
- package/dist/patterns/content-uploader/controller.js +227 -0
- package/dist/patterns/content-uploader/index.d.ts +4 -0
- package/dist/patterns/content-uploader/index.js +8 -0
- package/dist/patterns/content-uploader/types.d.ts +120 -0
- package/dist/patterns/content-uploader/types.js +38 -0
- package/dist/patterns/diff/compare-view.d.ts +47 -0
- package/dist/patterns/diff/compare-view.js +276 -0
- package/dist/patterns/diff/diff-viewer.d.ts +42 -0
- package/dist/patterns/diff/diff-viewer.js +434 -0
- package/dist/patterns/diff/engine.d.ts +22 -0
- package/dist/patterns/diff/engine.js +276 -0
- package/dist/patterns/diff/index.d.ts +4 -0
- package/dist/patterns/diff/index.js +8 -0
- package/dist/patterns/diff/types.d.ts +69 -0
- package/dist/patterns/diff/types.js +24 -0
- package/dist/patterns/form-wizard/controller.d.ts +39 -0
- package/dist/patterns/form-wizard/controller.js +157 -0
- package/dist/patterns/form-wizard/form-wizard.d.ts +63 -0
- package/dist/patterns/form-wizard/form-wizard.js +468 -0
- package/dist/patterns/form-wizard/index.d.ts +4 -0
- package/dist/patterns/form-wizard/index.js +8 -0
- package/dist/patterns/form-wizard/types.d.ts +109 -0
- package/dist/patterns/form-wizard/types.js +83 -0
- package/dist/patterns/form-wizard/wizard-summary.d.ts +62 -0
- package/dist/patterns/form-wizard/wizard-summary.js +324 -0
- package/dist/patterns/internal/safe-href.d.ts +17 -0
- package/dist/patterns/internal/safe-href.js +17 -0
- package/dist/patterns/lineage/index.d.ts +3 -0
- package/dist/patterns/lineage/index.js +6 -0
- package/dist/patterns/lineage/lineage-graph.d.ts +30 -0
- package/dist/patterns/lineage/lineage-graph.js +434 -0
- package/dist/patterns/lineage/provenance-strip.d.ts +22 -0
- package/dist/patterns/lineage/provenance-strip.js +194 -0
- package/dist/patterns/lineage/types.d.ts +53 -0
- package/dist/patterns/lineage/types.js +49 -0
- package/dist/patterns/notifications/index.d.ts +3 -0
- package/dist/patterns/notifications/index.js +6 -0
- package/dist/patterns/notifications/notification-bell.d.ts +38 -0
- package/dist/patterns/notifications/notification-bell.js +198 -0
- package/dist/patterns/notifications/notification-inbox.d.ts +46 -0
- package/dist/patterns/notifications/notification-inbox.js +537 -0
- package/dist/patterns/notifications/types.d.ts +61 -0
- package/dist/patterns/notifications/types.js +108 -0
- package/dist/patterns/preview/annotation-inspector.d.ts +25 -6
- package/dist/patterns/preview/annotation-inspector.js +239 -94
- package/dist/patterns/preview/annotation-thread.d.ts +25 -5
- package/dist/patterns/preview/annotation-thread.js +233 -82
- package/dist/patterns/preview/annotation-toolbar.d.ts +20 -6
- package/dist/patterns/preview/annotation-toolbar.js +162 -96
- package/dist/patterns/preview/content-preview-adapter.d.ts +10 -1
- package/dist/patterns/preview/content-preview-adapter.js +26 -1
- package/dist/patterns/preview/preview-element.d.ts +15 -2
- package/dist/patterns/preview/preview-element.js +223 -52
- package/dist/patterns/preview/provider-adapter.d.ts +47 -0
- package/dist/patterns/preview/provider-adapter.js +17 -0
- package/dist/patterns/run/index.d.ts +2 -0
- package/dist/patterns/run/index.js +4 -0
- package/dist/patterns/run/run-trace.d.ts +40 -0
- package/dist/patterns/run/run-trace.js +441 -0
- package/dist/patterns/run/types.d.ts +66 -0
- package/dist/patterns/run/types.js +152 -0
- package/dist/patterns/signature/index.d.ts +2 -0
- package/dist/patterns/signature/index.js +4 -0
- package/dist/patterns/signature/signature-ceremony.d.ts +33 -0
- package/dist/patterns/signature/signature-ceremony.js +317 -0
- package/dist/patterns/signature/types.d.ts +68 -0
- package/dist/patterns/signature/types.js +89 -0
- package/dist/patterns/timeline/index.d.ts +2 -0
- package/dist/patterns/timeline/index.js +4 -0
- package/dist/patterns/timeline/timeline.d.ts +27 -0
- package/dist/patterns/timeline/timeline.js +430 -0
- package/dist/patterns/timeline/types.d.ts +36 -0
- package/dist/patterns/timeline/types.js +27 -0
- package/dist/patterns/versions/graph-layout.d.ts +43 -0
- package/dist/patterns/versions/graph-layout.js +159 -0
- package/dist/patterns/versions/index.d.ts +4 -0
- package/dist/patterns/versions/index.js +8 -0
- package/dist/patterns/versions/shared.d.ts +4 -0
- package/dist/patterns/versions/shared.js +13 -0
- package/dist/patterns/versions/types.d.ts +37 -0
- package/dist/patterns/versions/types.js +39 -0
- package/dist/patterns/versions/version-graph.d.ts +36 -0
- package/dist/patterns/versions/version-graph.js +438 -0
- package/dist/patterns/versions/version-list.d.ts +39 -0
- package/dist/patterns/versions/version-list.js +407 -0
- package/dist/patterns/work-queue/controller.d.ts +26 -0
- package/dist/patterns/work-queue/controller.js +150 -0
- package/dist/patterns/work-queue/index.d.ts +4 -0
- package/dist/patterns/work-queue/index.js +7 -0
- package/dist/patterns/work-queue/types.d.ts +128 -0
- package/dist/patterns/work-queue/types.js +45 -0
- package/dist/patterns/work-queue/work-queue.d.ts +48 -0
- package/dist/patterns/work-queue/work-queue.js +534 -0
- package/dist/patterns/work-queue/workload-board.d.ts +53 -0
- package/dist/patterns/work-queue/workload-board.js +608 -0
- package/package.json +89 -5
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { formatRunDuration, formatRunSummary, isRunStepRecord, resolveRunSteps, } from "./types.js";
|
|
2
|
+
import { ProgressBar } from "../../components/feedback/progress-bar.js";
|
|
3
|
+
import { BaseElement } from "../../core/index.js";
|
|
4
|
+
import { boePanel } from "../../foundations/geometry/index.js";
|
|
5
|
+
const DEFAULT_TAG_NAME = "box-run-trace";
|
|
6
|
+
/** Escape a value for use inside a double-quoted attribute selector. */
|
|
7
|
+
const cssAttrValue = (value) => value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
8
|
+
const escapeHtml = (value) => value
|
|
9
|
+
.replaceAll("&", "&")
|
|
10
|
+
.replaceAll("<", "<")
|
|
11
|
+
.replaceAll(">", ">")
|
|
12
|
+
.replaceAll('"', """)
|
|
13
|
+
.replaceAll("'", "'");
|
|
14
|
+
/** Every status in words — colour never carries the meaning alone. */
|
|
15
|
+
export const RUN_STEP_STATUS_LABEL = {
|
|
16
|
+
pending: "Queued",
|
|
17
|
+
running: "Running",
|
|
18
|
+
succeeded: "Succeeded",
|
|
19
|
+
warning: "Completed with warnings",
|
|
20
|
+
failed: "Failed",
|
|
21
|
+
skipped: "Skipped",
|
|
22
|
+
};
|
|
23
|
+
const STATUS_TONE = {
|
|
24
|
+
pending: "var(--boe-token-text-text-secondary, #6f6f6f)",
|
|
25
|
+
running: "var(--boe-token-surface-surface-brand, #0061d5)",
|
|
26
|
+
succeeded: "var(--boe-token-surface-status-surface-success, #26a27b)",
|
|
27
|
+
warning: "var(--boe-token-surface-status-surface-warning, #f5b31b)",
|
|
28
|
+
failed: "var(--boe-token-surface-status-surface-error, #ed3757)",
|
|
29
|
+
skipped: "var(--boe-token-text-text-secondary, #6f6f6f)",
|
|
30
|
+
};
|
|
31
|
+
const elementStyles = `
|
|
32
|
+
:host {
|
|
33
|
+
display: block;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[part="panel"] {
|
|
37
|
+
border: ${boePanel.border};
|
|
38
|
+
border-radius: ${boePanel.radius};
|
|
39
|
+
background: ${boePanel.background};
|
|
40
|
+
padding: 0.8rem 1rem;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
[part="header"] {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: baseline;
|
|
46
|
+
gap: 0.6rem;
|
|
47
|
+
margin-block-end: 0.4rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[part="heading"] {
|
|
51
|
+
margin: 0;
|
|
52
|
+
font: inherit;
|
|
53
|
+
font-size: 0.95rem;
|
|
54
|
+
font-weight: 700;
|
|
55
|
+
color: var(--boe-token-text-text, #1f1e1b);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
[part="summary"] {
|
|
59
|
+
margin-inline-start: auto;
|
|
60
|
+
font-size: 0.78rem;
|
|
61
|
+
font-weight: 600;
|
|
62
|
+
padding: 0.14rem 0.5rem;
|
|
63
|
+
border-radius: 999px;
|
|
64
|
+
border: 1px solid transparent;
|
|
65
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
66
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface-hover, #f4f4f4) 80%, transparent);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
[part="summary"][data-status="completed"] {
|
|
70
|
+
background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26a27b) 14%, transparent);
|
|
71
|
+
color: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26a27b) 74%, black 26%);
|
|
72
|
+
border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26a27b) 32%, transparent);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
[part="summary"][data-status="failed"] {
|
|
76
|
+
background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 14%, transparent);
|
|
77
|
+
color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 76%, black 24%);
|
|
78
|
+
border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 32%, transparent);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[part="summary"][data-status="running"] {
|
|
82
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 12%, transparent);
|
|
83
|
+
color: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 80%, black 20%);
|
|
84
|
+
border-color: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 30%, transparent);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
[part="steps"] {
|
|
88
|
+
margin: 0;
|
|
89
|
+
padding: 0;
|
|
90
|
+
list-style: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Marker column + body; the spine hangs off the marker column so the
|
|
94
|
+
nodes stay aligned at every width — long titles wrap in the body. */
|
|
95
|
+
[part="step"] {
|
|
96
|
+
position: relative;
|
|
97
|
+
display: grid;
|
|
98
|
+
grid-template-columns: auto 1fr;
|
|
99
|
+
gap: 0.65rem;
|
|
100
|
+
padding: 0.55rem 0 0.55rem 0.15rem;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
[part="step"]:not(:last-child)::after {
|
|
104
|
+
content: "";
|
|
105
|
+
position: absolute;
|
|
106
|
+
left: calc(0.15rem + 0.55rem - 1px);
|
|
107
|
+
top: 1.9rem;
|
|
108
|
+
bottom: -0.35rem;
|
|
109
|
+
width: 2px;
|
|
110
|
+
background: color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 72%, transparent);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
[part="marker"] {
|
|
114
|
+
inline-size: 1.1rem;
|
|
115
|
+
block-size: 1.1rem;
|
|
116
|
+
margin-top: 0.2rem;
|
|
117
|
+
border-radius: 999px;
|
|
118
|
+
border: 2px solid var(--run-tone, var(--boe-token-text-text-secondary, #6f6f6f));
|
|
119
|
+
background: color-mix(in srgb, var(--run-tone, var(--boe-token-text-text-secondary, #6f6f6f)) 14%, var(--boe-token-surface-surface, #ffffff) 86%);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
[part="step"][data-status="running"] [part="marker"] {
|
|
123
|
+
background: var(--run-tone, var(--boe-token-surface-surface-brand, #0061d5));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
[part="step"][data-status="pending"] [part="marker"],
|
|
127
|
+
[part="step"][data-status="skipped"] [part="marker"] {
|
|
128
|
+
border-style: dashed;
|
|
129
|
+
background: transparent;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
[part="step-body"] {
|
|
133
|
+
display: grid;
|
|
134
|
+
gap: 0.25rem;
|
|
135
|
+
min-inline-size: 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
[part="topline"] {
|
|
139
|
+
display: flex;
|
|
140
|
+
flex-wrap: wrap;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 0.4rem;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
[part="step-title"] {
|
|
146
|
+
font-weight: 700;
|
|
147
|
+
font-size: 0.88rem;
|
|
148
|
+
color: var(--boe-token-text-text, #1f1e1b);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
[part="step"][data-status="pending"] [part="step-title"],
|
|
152
|
+
[part="step"][data-status="skipped"] [part="step-title"] {
|
|
153
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
154
|
+
font-weight: 600;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
[part="status"] {
|
|
158
|
+
font-size: 0.76rem;
|
|
159
|
+
font-weight: 700;
|
|
160
|
+
color: color-mix(in srgb, var(--run-tone, var(--boe-token-text-text-secondary, #6f6f6f)) 78%, black 22%);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
[part="timestamp"],
|
|
164
|
+
[part="duration"] {
|
|
165
|
+
font-size: 0.76rem;
|
|
166
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
[part="duration"] {
|
|
170
|
+
font-variant-numeric: tabular-nums;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
[part="toggle"] {
|
|
174
|
+
appearance: none;
|
|
175
|
+
border: 0;
|
|
176
|
+
background: transparent;
|
|
177
|
+
padding: 0.1rem 0.35rem;
|
|
178
|
+
border-radius: 4px;
|
|
179
|
+
font: inherit;
|
|
180
|
+
font-size: 0.76rem;
|
|
181
|
+
font-weight: 700;
|
|
182
|
+
color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
[part="toggle"]:hover {
|
|
187
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 8%, transparent);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
[part="toggle"]:focus-visible {
|
|
191
|
+
outline: 2px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 34%, transparent);
|
|
192
|
+
outline-offset: 1px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
[part="detail"] {
|
|
196
|
+
display: grid;
|
|
197
|
+
gap: 0.45rem;
|
|
198
|
+
font-size: 0.82rem;
|
|
199
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
200
|
+
padding: 0.2rem 0 0.1rem;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
[part="detail"][hidden] {
|
|
204
|
+
display: none;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
[part="children"] {
|
|
208
|
+
margin: 0;
|
|
209
|
+
padding: 0;
|
|
210
|
+
list-style: none;
|
|
211
|
+
display: grid;
|
|
212
|
+
gap: 0.4rem;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
[part="child"] {
|
|
216
|
+
display: grid;
|
|
217
|
+
grid-template-columns: minmax(7rem, auto) 1fr auto;
|
|
218
|
+
align-items: center;
|
|
219
|
+
gap: 0.55rem;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
[part="child-label"] {
|
|
223
|
+
font-size: 0.8rem;
|
|
224
|
+
color: var(--boe-token-text-text, #1f1e1b);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* The row already carries the label and status in words; the bar's
|
|
228
|
+
own meta line would say it twice in half the space. */
|
|
229
|
+
[part="child"] box-progress-bar::part(meta) {
|
|
230
|
+
display: none;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
[part="child-status"] {
|
|
234
|
+
font-size: 0.74rem;
|
|
235
|
+
font-weight: 700;
|
|
236
|
+
color: color-mix(in srgb, var(--run-tone, var(--boe-token-text-text-secondary, #6f6f6f)) 78%, black 22%);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
[part="empty"] {
|
|
240
|
+
padding: 0.8rem 0;
|
|
241
|
+
text-align: center;
|
|
242
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.boe-sr-only {
|
|
246
|
+
position: absolute;
|
|
247
|
+
width: 1px;
|
|
248
|
+
height: 1px;
|
|
249
|
+
margin: -1px;
|
|
250
|
+
padding: 0;
|
|
251
|
+
border: 0;
|
|
252
|
+
clip: rect(0, 0, 0, 0);
|
|
253
|
+
overflow: hidden;
|
|
254
|
+
white-space: nowrap;
|
|
255
|
+
}
|
|
256
|
+
`;
|
|
257
|
+
/**
|
|
258
|
+
* Machine execution trace: a job, pipeline, or agent run, top-down. The
|
|
259
|
+
* rules live in `resolveRunSteps` (pure), the same way `resolveCeremony`
|
|
260
|
+
* backs the signature card: an explicit status wins, a failure shadows the
|
|
261
|
+
* queue behind it as skipped, timestamps derive running/succeeded, and the
|
|
262
|
+
* host can drive its notifications from the identical function.
|
|
263
|
+
*
|
|
264
|
+
* Deliberately not `box-timeline` (the human activity feed): a run reads
|
|
265
|
+
* forward, has one step in flight, and takes no comments.
|
|
266
|
+
*
|
|
267
|
+
* The summary chip doubles as a polite status region, so a run driven by
|
|
268
|
+
* attribute updates announces "Running Deploy — step 3 of 5" → "Completed"
|
|
269
|
+
* without extra wiring. Per-step detail expands in place: `description`
|
|
270
|
+
* plus a `detail-<id>` slot for rich host content, plus child tasks with
|
|
271
|
+
* live `box-progress-bar` rows.
|
|
272
|
+
*/
|
|
273
|
+
export class RunTrace extends BaseElement {
|
|
274
|
+
static tagName = DEFAULT_TAG_NAME;
|
|
275
|
+
static get observedAttributes() {
|
|
276
|
+
return ["heading", "steps"];
|
|
277
|
+
}
|
|
278
|
+
hostEl;
|
|
279
|
+
stepsRaw = null;
|
|
280
|
+
stepsCache = [];
|
|
281
|
+
expandedIds = new Set();
|
|
282
|
+
get heading() {
|
|
283
|
+
return this.getAttribute("heading") ?? "Run";
|
|
284
|
+
}
|
|
285
|
+
set heading(value) {
|
|
286
|
+
this.setAttribute("heading", value);
|
|
287
|
+
}
|
|
288
|
+
get steps() {
|
|
289
|
+
const raw = this.getAttribute("steps");
|
|
290
|
+
if (!raw) {
|
|
291
|
+
return [...this.stepsCache];
|
|
292
|
+
}
|
|
293
|
+
if (raw !== this.stepsRaw) {
|
|
294
|
+
this.stepsRaw = raw;
|
|
295
|
+
try {
|
|
296
|
+
const parsed = JSON.parse(raw);
|
|
297
|
+
this.stepsCache =
|
|
298
|
+
Array.isArray(parsed) && parsed.every(isRunStepRecord) ? parsed : [];
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
this.stepsCache = [];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return [...this.stepsCache];
|
|
305
|
+
}
|
|
306
|
+
set steps(value) {
|
|
307
|
+
if (value.length) {
|
|
308
|
+
this.setAttribute("steps", JSON.stringify(value));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// Clear the cache too: the no-attribute branch of the getter serves from
|
|
312
|
+
// it, and a stale cache would make an emptied run un-clearable.
|
|
313
|
+
this.stepsCache = [];
|
|
314
|
+
this.stepsRaw = null;
|
|
315
|
+
this.removeAttribute("steps");
|
|
316
|
+
}
|
|
317
|
+
/** The resolved run — same object the render uses. */
|
|
318
|
+
get resolution() {
|
|
319
|
+
return resolveRunSteps(this.steps);
|
|
320
|
+
}
|
|
321
|
+
/** Ids of the steps whose detail is expanded (host-readable). */
|
|
322
|
+
get expandedSteps() {
|
|
323
|
+
return [...this.expandedIds];
|
|
324
|
+
}
|
|
325
|
+
renderTemplate() {
|
|
326
|
+
if (!this.shadowRoot) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
this.shadowRoot.innerHTML = `<style>${elementStyles}</style><div part="host"></div>`;
|
|
330
|
+
this.hostEl = this.shadowRoot.querySelector('[part="host"]');
|
|
331
|
+
}
|
|
332
|
+
setupListeners() {
|
|
333
|
+
// Slot-only detail can be appended after render; watch the light DOM so
|
|
334
|
+
// the step's toggle appears without the host having to touch `steps`.
|
|
335
|
+
// Shallow, and update() never mutates the light DOM, so this cannot loop.
|
|
336
|
+
new MutationObserver(() => {
|
|
337
|
+
this.update();
|
|
338
|
+
}).observe(this, { childList: true });
|
|
339
|
+
this.hostEl.addEventListener("click", event => {
|
|
340
|
+
const toggle = event.target.closest('[part="toggle"]');
|
|
341
|
+
if (!toggle || !this.hostEl.contains(toggle)) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const stepId = toggle.getAttribute("data-step-id") ?? "";
|
|
345
|
+
if (!stepId) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const expanded = !this.expandedIds.has(stepId);
|
|
349
|
+
if (expanded) {
|
|
350
|
+
this.expandedIds.add(stepId);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
this.expandedIds.delete(stepId);
|
|
354
|
+
}
|
|
355
|
+
this.dispatchEvent(new CustomEvent("step-toggled", {
|
|
356
|
+
bubbles: true,
|
|
357
|
+
composed: true,
|
|
358
|
+
detail: { stepId, expanded },
|
|
359
|
+
}));
|
|
360
|
+
this.update();
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
childrenMarkup(step) {
|
|
364
|
+
if (!step.children?.length) {
|
|
365
|
+
return "";
|
|
366
|
+
}
|
|
367
|
+
const rows = step.children
|
|
368
|
+
.map(child => {
|
|
369
|
+
const status = child.status ?? "pending";
|
|
370
|
+
const clamped = child.progress === undefined ? null : Math.max(0, Math.min(100, child.progress));
|
|
371
|
+
return `
|
|
372
|
+
<li part="child" style="--run-tone:${STATUS_TONE[status]};">
|
|
373
|
+
<span part="child-label">${escapeHtml(child.label)}</span>
|
|
374
|
+
${clamped === null
|
|
375
|
+
? "<span></span>"
|
|
376
|
+
: `<box-progress-bar label="${escapeHtml(child.label)} progress" value="${String(clamped)}"></box-progress-bar>`}
|
|
377
|
+
<span part="child-status">${RUN_STEP_STATUS_LABEL[status]}</span>
|
|
378
|
+
</li>
|
|
379
|
+
`;
|
|
380
|
+
})
|
|
381
|
+
.join("");
|
|
382
|
+
return `<ul part="children">${rows}</ul>`;
|
|
383
|
+
}
|
|
384
|
+
update() {
|
|
385
|
+
if (!this.hostEl) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const resolution = this.resolution;
|
|
389
|
+
const rows = resolution.steps
|
|
390
|
+
.map(entry => {
|
|
391
|
+
const { step, status, position } = entry;
|
|
392
|
+
const duration = formatRunDuration(step.startedAt, step.finishedAt);
|
|
393
|
+
// Slot-only detail counts: a host projecting logs for a step with no
|
|
394
|
+
// JSON description still needs the toggle that reveals them.
|
|
395
|
+
const hasSlotted = this.querySelector(`[slot="detail-${cssAttrValue(step.id)}"]`) !== null;
|
|
396
|
+
const hasDetail = Boolean(step.description) || Boolean(step.children?.length) || hasSlotted;
|
|
397
|
+
const expanded = this.expandedIds.has(step.id);
|
|
398
|
+
// The detail slot renders even without JSON detail so a host can
|
|
399
|
+
// project rich content (logs, links) for any step by id.
|
|
400
|
+
const detail = `
|
|
401
|
+
<div part="detail" data-step-id="${escapeHtml(step.id)}" ${expanded ? "" : "hidden"}>
|
|
402
|
+
${step.description ? `<p part="description">${escapeHtml(step.description)}</p>` : ""}
|
|
403
|
+
${this.childrenMarkup(step)}
|
|
404
|
+
<slot name="detail-${escapeHtml(step.id)}"></slot>
|
|
405
|
+
</div>
|
|
406
|
+
`;
|
|
407
|
+
return `
|
|
408
|
+
<li part="step" data-status="${status}" data-step-id="${escapeHtml(step.id)}" style="--run-tone:${STATUS_TONE[status]};">
|
|
409
|
+
<span part="marker" aria-hidden="true"></span>
|
|
410
|
+
<div part="step-body">
|
|
411
|
+
<div part="topline">
|
|
412
|
+
<span part="step-title">${escapeHtml(step.title)}</span>
|
|
413
|
+
<span part="status">${RUN_STEP_STATUS_LABEL[status]}</span>
|
|
414
|
+
<span class="boe-sr-only">, step ${String(position)} of ${String(resolution.total)}</span>
|
|
415
|
+
${duration ? `<span part="duration">${duration}</span>` : ""}
|
|
416
|
+
${hasDetail
|
|
417
|
+
? `<button type="button" part="toggle" data-step-id="${escapeHtml(step.id)}" aria-expanded="${String(expanded)}">${expanded ? "Hide details" : "Details"}</button>`
|
|
418
|
+
: ""}
|
|
419
|
+
</div>
|
|
420
|
+
${detail}
|
|
421
|
+
</div>
|
|
422
|
+
</li>
|
|
423
|
+
`;
|
|
424
|
+
})
|
|
425
|
+
.join("");
|
|
426
|
+
const summary = formatRunSummary(resolution);
|
|
427
|
+
this.hostEl.innerHTML = `
|
|
428
|
+
<section part="panel" aria-label="${escapeHtml(this.heading)}">
|
|
429
|
+
<div part="header">
|
|
430
|
+
<h3 part="heading">${escapeHtml(this.heading)}</h3>
|
|
431
|
+
<span part="summary" role="status" data-status="${resolution.status}">${escapeHtml(summary)}</span>
|
|
432
|
+
</div>
|
|
433
|
+
${resolution.total
|
|
434
|
+
? `<ol part="steps">${rows}</ol>`
|
|
435
|
+
: `<div part="empty">No steps yet.</div>`}
|
|
436
|
+
</section>
|
|
437
|
+
`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
ProgressBar.register();
|
|
441
|
+
RunTrace.register();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A run is a machine execution trace — a job, pipeline, or agent run —
|
|
3
|
+
* rendered forward-chronologically. This is deliberately not
|
|
4
|
+
* `box-timeline`: that surface is an append-only *human* activity feed
|
|
5
|
+
* (newest first, actors, comments); a run reads top-down, has exactly one
|
|
6
|
+
* step in flight, and a failure shadows everything queued behind it.
|
|
7
|
+
*/
|
|
8
|
+
export type RunStepStatus = "pending" | "running" | "succeeded" | "warning" | "failed" | "skipped";
|
|
9
|
+
export type RunStatus = "pending" | "running" | "completed" | "failed";
|
|
10
|
+
/** A nested unit of work under a step, with optional live progress. */
|
|
11
|
+
export interface RunChildTask {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
/** 0–100; omit for indeterminate work that only has a status. */
|
|
15
|
+
progress?: number;
|
|
16
|
+
status?: RunStepStatus;
|
|
17
|
+
}
|
|
18
|
+
export interface RunStep {
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
/** Expandable detail under the title. */
|
|
22
|
+
description?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Explicit status wins. Absent, the status derives from timestamps and
|
|
25
|
+
* the failure shadow — see `resolveRunSteps`.
|
|
26
|
+
*/
|
|
27
|
+
status?: RunStepStatus;
|
|
28
|
+
/** ISO timestamps; duration renders when both are present. */
|
|
29
|
+
startedAt?: string;
|
|
30
|
+
finishedAt?: string;
|
|
31
|
+
children?: RunChildTask[];
|
|
32
|
+
}
|
|
33
|
+
export interface ResolvedRunStep {
|
|
34
|
+
step: RunStep;
|
|
35
|
+
status: RunStepStatus;
|
|
36
|
+
/** 1-based position, for "step 2 of 5" phrasing. */
|
|
37
|
+
position: number;
|
|
38
|
+
}
|
|
39
|
+
export interface RunResolution {
|
|
40
|
+
steps: ResolvedRunStep[];
|
|
41
|
+
status: RunStatus;
|
|
42
|
+
total: number;
|
|
43
|
+
/** Steps in a terminal state (succeeded, warning, failed, or skipped). */
|
|
44
|
+
settled: number;
|
|
45
|
+
}
|
|
46
|
+
/** Attribute payloads are author input — validate every record. */
|
|
47
|
+
export declare const isRunStepRecord: (value: unknown) => value is RunStep;
|
|
48
|
+
/**
|
|
49
|
+
* The run rules, pure and DOM-free, so a host can drive notifications or a
|
|
50
|
+
* "retry from here" affordance from the same function the surface renders.
|
|
51
|
+
*
|
|
52
|
+
* Per step, in priority order:
|
|
53
|
+
* 1. An explicit `status` wins.
|
|
54
|
+
* 2. A failure shadows what is queued behind it: steps after a failed step
|
|
55
|
+
* are `skipped` — a dead run must not show work as still coming, the
|
|
56
|
+
* same rule `resolveCeremony` applies after a decline.
|
|
57
|
+
* 3. `finishedAt` ⇒ succeeded; `startedAt` alone ⇒ running; neither ⇒ pending.
|
|
58
|
+
*/
|
|
59
|
+
export declare const resolveRunSteps: (steps: readonly RunStep[]) => RunResolution;
|
|
60
|
+
/** One line a host can put in a toast or heading — same rules as the surface. */
|
|
61
|
+
export declare const formatRunSummary: (resolution: RunResolution) => string;
|
|
62
|
+
/**
|
|
63
|
+
* "4m 12s"-style duration between two ISO timestamps; empty when either is
|
|
64
|
+
* missing or unparseable, so callers can simply omit the segment.
|
|
65
|
+
*/
|
|
66
|
+
export declare const formatRunDuration: (startedAt?: string, finishedAt?: string) => string;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A run is a machine execution trace — a job, pipeline, or agent run —
|
|
3
|
+
* rendered forward-chronologically. This is deliberately not
|
|
4
|
+
* `box-timeline`: that surface is an append-only *human* activity feed
|
|
5
|
+
* (newest first, actors, comments); a run reads top-down, has exactly one
|
|
6
|
+
* step in flight, and a failure shadows everything queued behind it.
|
|
7
|
+
*/
|
|
8
|
+
const STEP_STATUSES = new Set([
|
|
9
|
+
"pending",
|
|
10
|
+
"running",
|
|
11
|
+
"succeeded",
|
|
12
|
+
"warning",
|
|
13
|
+
"failed",
|
|
14
|
+
"skipped",
|
|
15
|
+
]);
|
|
16
|
+
/** Attribute payloads are author input — validate every record. */
|
|
17
|
+
export const isRunStepRecord = (value) => {
|
|
18
|
+
if (typeof value !== "object" || value === null) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const step = value;
|
|
22
|
+
if (typeof step.id !== "string" ||
|
|
23
|
+
step.id.length === 0 ||
|
|
24
|
+
typeof step.title !== "string" ||
|
|
25
|
+
step.title.length === 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (step.status !== undefined && !STEP_STATUSES.has(step.status)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
// A non-string description would reach escapeHtml and throw mid-render.
|
|
32
|
+
if (step.description !== undefined && typeof step.description !== "string") {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (step.children !== undefined) {
|
|
36
|
+
if (!Array.isArray(step.children)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
for (const entry of step.children) {
|
|
40
|
+
const child = entry;
|
|
41
|
+
if (typeof child !== "object" ||
|
|
42
|
+
child === null ||
|
|
43
|
+
typeof child.id !== "string" ||
|
|
44
|
+
typeof child.label !== "string" ||
|
|
45
|
+
(child.progress !== undefined && typeof child.progress !== "number") ||
|
|
46
|
+
(child.status !== undefined && !STEP_STATUSES.has(child.status))) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
const TERMINAL = new Set(["succeeded", "warning", "failed", "skipped"]);
|
|
54
|
+
/**
|
|
55
|
+
* The run rules, pure and DOM-free, so a host can drive notifications or a
|
|
56
|
+
* "retry from here" affordance from the same function the surface renders.
|
|
57
|
+
*
|
|
58
|
+
* Per step, in priority order:
|
|
59
|
+
* 1. An explicit `status` wins.
|
|
60
|
+
* 2. A failure shadows what is queued behind it: steps after a failed step
|
|
61
|
+
* are `skipped` — a dead run must not show work as still coming, the
|
|
62
|
+
* same rule `resolveCeremony` applies after a decline.
|
|
63
|
+
* 3. `finishedAt` ⇒ succeeded; `startedAt` alone ⇒ running; neither ⇒ pending.
|
|
64
|
+
*/
|
|
65
|
+
export const resolveRunSteps = (steps) => {
|
|
66
|
+
let failed = false;
|
|
67
|
+
const resolved = steps.map((step, index) => {
|
|
68
|
+
let status;
|
|
69
|
+
if (step.status) {
|
|
70
|
+
status = step.status;
|
|
71
|
+
}
|
|
72
|
+
else if (failed) {
|
|
73
|
+
status = "skipped";
|
|
74
|
+
}
|
|
75
|
+
else if (step.finishedAt) {
|
|
76
|
+
status = "succeeded";
|
|
77
|
+
}
|
|
78
|
+
else if (step.startedAt) {
|
|
79
|
+
status = "running";
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
status = "pending";
|
|
83
|
+
}
|
|
84
|
+
// An explicit failed also shadows what follows; an explicit status on a
|
|
85
|
+
// later step still overrides the shadow (rule 1 beats rule 2).
|
|
86
|
+
if (status === "failed") {
|
|
87
|
+
failed = true;
|
|
88
|
+
}
|
|
89
|
+
return { step, status, position: index + 1 };
|
|
90
|
+
});
|
|
91
|
+
const settled = resolved.filter(entry => TERMINAL.has(entry.status)).length;
|
|
92
|
+
const anyRunning = resolved.some(entry => entry.status === "running");
|
|
93
|
+
const status = failed
|
|
94
|
+
? "failed"
|
|
95
|
+
: anyRunning
|
|
96
|
+
? "running"
|
|
97
|
+
: resolved.length > 0 && settled === resolved.length
|
|
98
|
+
? "completed"
|
|
99
|
+
: settled > 0
|
|
100
|
+
? "running"
|
|
101
|
+
: "pending";
|
|
102
|
+
return { steps: resolved, status, total: resolved.length, settled };
|
|
103
|
+
};
|
|
104
|
+
/** One line a host can put in a toast or heading — same rules as the surface. */
|
|
105
|
+
export const formatRunSummary = (resolution) => {
|
|
106
|
+
if (resolution.total === 0) {
|
|
107
|
+
return "No steps";
|
|
108
|
+
}
|
|
109
|
+
if (resolution.status === "failed") {
|
|
110
|
+
const failedStep = resolution.steps.find(entry => entry.status === "failed");
|
|
111
|
+
return failedStep ? `Failed at ${failedStep.step.title}` : "Failed";
|
|
112
|
+
}
|
|
113
|
+
if (resolution.status === "completed") {
|
|
114
|
+
const warnings = resolution.steps.filter(entry => entry.status === "warning").length;
|
|
115
|
+
return warnings > 0
|
|
116
|
+
? `Completed with ${String(warnings)} warning${warnings === 1 ? "" : "s"}`
|
|
117
|
+
: "Completed";
|
|
118
|
+
}
|
|
119
|
+
if (resolution.status === "running") {
|
|
120
|
+
const runningStep = resolution.steps.find(entry => entry.status === "running");
|
|
121
|
+
return runningStep
|
|
122
|
+
? `Running ${runningStep.step.title} — step ${String(runningStep.position)} of ${String(resolution.total)}`
|
|
123
|
+
: `${String(resolution.settled)} of ${String(resolution.total)} steps done`;
|
|
124
|
+
}
|
|
125
|
+
return "Not started";
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* "4m 12s"-style duration between two ISO timestamps; empty when either is
|
|
129
|
+
* missing or unparseable, so callers can simply omit the segment.
|
|
130
|
+
*/
|
|
131
|
+
export const formatRunDuration = (startedAt, finishedAt) => {
|
|
132
|
+
if (!startedAt || !finishedAt) {
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
const start = Date.parse(startedAt);
|
|
136
|
+
const finish = Date.parse(finishedAt);
|
|
137
|
+
if (Number.isNaN(start) || Number.isNaN(finish) || finish < start) {
|
|
138
|
+
return "";
|
|
139
|
+
}
|
|
140
|
+
const totalSeconds = Math.round((finish - start) / 1000);
|
|
141
|
+
if (totalSeconds < 60) {
|
|
142
|
+
return `${String(totalSeconds)}s`;
|
|
143
|
+
}
|
|
144
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
145
|
+
const seconds = totalSeconds % 60;
|
|
146
|
+
if (minutes < 60) {
|
|
147
|
+
return seconds ? `${String(minutes)}m ${String(seconds)}s` : `${String(minutes)}m`;
|
|
148
|
+
}
|
|
149
|
+
const hours = Math.floor(minutes / 60);
|
|
150
|
+
const rest = minutes % 60;
|
|
151
|
+
return rest ? `${String(hours)}h ${String(rest)}m` : `${String(hours)}h`;
|
|
152
|
+
};
|