Skip to content

Commit ce134bc

Browse files
authored
fix: handle invalid provisioning timings in ui (#18058)
Relates to #15432 * Adds a storybook entry for zero values in provisioner timings. * Coerces a 'zero' start time to an 'instant'. * Improves timing chart handling for large timeframes. Previously, this would have caused the tab to run out of memory when encountering a `time.Time{}`. * Render 'instants' as 'invalid' in timing chart.
1 parent 565fad5 commit ce134bc

File tree

3 files changed

+99
-4
lines changed

3 files changed

+99
-4
lines changed

site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import dayjs from "dayjs";
2+
13
export type TimeRange = {
24
startedAt: Date;
35
endedAt: Date;
@@ -11,12 +13,26 @@ export const mergeTimeRanges = (ranges: TimeRange[]): TimeRange => {
1113
const sortedDurations = ranges
1214
.slice()
1315
.sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
14-
const start = sortedDurations[0].startedAt;
1516

1617
const sortedEndDurations = [...ranges].sort(
1718
(a, b) => a.endedAt.getTime() - b.endedAt.getTime(),
1819
);
1920
const end = sortedEndDurations[sortedEndDurations.length - 1].endedAt;
21+
22+
// Ref: #15432: if there start time is the 'zero' value, default
23+
// to the end time. This will result in an 'instant'.
24+
let start: Date = end;
25+
for (const r of sortedDurations) {
26+
if (
27+
Number.isNaN(r.startedAt.getTime()) ||
28+
r.startedAt.getUTCFullYear() <= 1
29+
) {
30+
continue; // Skip invalid start dates.
31+
}
32+
start = r.startedAt;
33+
break;
34+
}
35+
2036
return { startedAt: start, endedAt: end };
2137
};
2238

@@ -33,7 +49,12 @@ const second = 1_000;
3349
const minute = 60 * second;
3450
const hour = 60 * minute;
3551
const day = 24 * hour;
52+
const week = 7 * day;
53+
const year = 365 * day; // Unlikely, and leap years won't matter here.
54+
3655
const scales = [
56+
year,
57+
week,
3758
day,
3859
hour,
3960
5 * minute,
@@ -44,6 +65,8 @@ const scales = [
4465
100,
4566
];
4667

68+
const zeroTime: Date = dayjs("0001-01-01T00:00:00Z").toDate();
69+
4770
const pickScale = (totalTime: number): number => {
4871
for (const s of scales) {
4972
if (totalTime > s) {
@@ -64,6 +87,7 @@ export const formatTime = (time: number): string => {
6487
const absTime = Math.abs(time);
6588
let unit = "";
6689
let value = 0;
90+
let frac = 2;
6791

6892
if (absTime < second) {
6993
value = time;
@@ -74,15 +98,26 @@ export const formatTime = (time: number): string => {
7498
} else if (absTime < hour) {
7599
value = time / minute;
76100
unit = "m";
101+
frac = 1;
77102
} else if (absTime < day) {
78103
value = time / hour;
79104
unit = "h";
80-
} else {
105+
frac = 0;
106+
} else if (absTime < week) {
81107
value = time / day;
82108
unit = "d";
109+
frac = 0;
110+
} else if (absTime < year) {
111+
value = time / week;
112+
unit = "w";
113+
frac = 0;
114+
} else {
115+
value = time / year;
116+
unit = "y";
117+
frac = 0;
83118
}
84119
return `${value.toLocaleString(undefined, {
85-
maximumFractionDigits: 2,
120+
maximumFractionDigits: frac,
86121
})}${unit}`;
87122
};
88123

site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export const StagesChart: FC<StagesChartProps> = ({
141141

142142
const value = calcDuration(t.range);
143143
const offset = calcOffset(t.range, totalRange);
144+
const validDuration = value > 0 && !Number.isNaN(value);
144145

145146
return (
146147
<XAxisRow
@@ -172,7 +173,17 @@ export const StagesChart: FC<StagesChartProps> = ({
172173
) : (
173174
<Bar scale={scale} value={value} offset={offset} />
174175
)}
175-
{formatTime(calcDuration(t.range))}
176+
{validDuration ? (
177+
<span>{formatTime(value)}</span>
178+
) : (
179+
<span
180+
css={(theme) => ({
181+
color: theme.palette.error.main,
182+
})}
183+
>
184+
Invalid
185+
</span>
186+
)}
176187
</XAxisRow>
177188
);
178189
})}

site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,52 @@ export const MissedAction: Story = {
228228
await canvas.findByText("missed action");
229229
},
230230
};
231+
232+
// Ref: #15432
233+
export const InvalidTimeRange: Story = {
234+
args: {
235+
provisionerTimings: [
236+
{
237+
...WorkspaceTimingsResponse.provisioner_timings[0],
238+
stage: "init",
239+
started_at: "2025-01-01T00:00:00Z",
240+
ended_at: "2025-01-01T00:01:00Z",
241+
},
242+
{
243+
...WorkspaceTimingsResponse.provisioner_timings[0],
244+
stage: "plan",
245+
started_at: "2025-01-01T00:01:00Z",
246+
ended_at: "0001-01-01T00:00:00Z",
247+
},
248+
{
249+
...WorkspaceTimingsResponse.provisioner_timings[0],
250+
stage: "graph",
251+
started_at: "0001-01-01T00:00:00Z",
252+
ended_at: "2025-01-01T00:03:00Z",
253+
},
254+
{
255+
...WorkspaceTimingsResponse.provisioner_timings[0],
256+
stage: "apply",
257+
started_at: "2025-01-01T00:03:00Z",
258+
ended_at: "2025-01-01T00:04:00Z",
259+
},
260+
],
261+
agentConnectionTimings: [
262+
{
263+
started_at: "2025-01-01T00:05:00Z",
264+
ended_at: "2025-01-01T00:06:00Z",
265+
stage: "connect",
266+
workspace_agent_id: "67e37a9d-ccac-497e-8f48-4093bcc4f3e7",
267+
workspace_agent_name: "main",
268+
},
269+
],
270+
agentScriptTimings: [
271+
{
272+
...WorkspaceTimingsResponse.agent_script_timings[0],
273+
display_name: "Startup Script 1",
274+
started_at: "0001-01-01T00:00:00Z",
275+
ended_at: "2025-01-01T00:10:00Z",
276+
},
277+
],
278+
},
279+
};

0 commit comments

Comments
 (0)