mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: handle invalid provisioning timings in ui (#18058)
Relates to https://github.com/coder/coder/issues/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.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export type TimeRange = {
|
||||
startedAt: Date;
|
||||
endedAt: Date;
|
||||
@@ -11,12 +13,26 @@ export const mergeTimeRanges = (ranges: TimeRange[]): TimeRange => {
|
||||
const sortedDurations = ranges
|
||||
.slice()
|
||||
.sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
|
||||
const start = sortedDurations[0].startedAt;
|
||||
|
||||
const sortedEndDurations = [...ranges].sort(
|
||||
(a, b) => a.endedAt.getTime() - b.endedAt.getTime(),
|
||||
);
|
||||
const end = sortedEndDurations[sortedEndDurations.length - 1].endedAt;
|
||||
|
||||
// Ref: #15432: if there start time is the 'zero' value, default
|
||||
// to the end time. This will result in an 'instant'.
|
||||
let start: Date = end;
|
||||
for (const r of sortedDurations) {
|
||||
if (
|
||||
Number.isNaN(r.startedAt.getTime()) ||
|
||||
r.startedAt.getUTCFullYear() <= 1
|
||||
) {
|
||||
continue; // Skip invalid start dates.
|
||||
}
|
||||
start = r.startedAt;
|
||||
break;
|
||||
}
|
||||
|
||||
return { startedAt: start, endedAt: end };
|
||||
};
|
||||
|
||||
@@ -33,7 +49,12 @@ const second = 1_000;
|
||||
const minute = 60 * second;
|
||||
const hour = 60 * minute;
|
||||
const day = 24 * hour;
|
||||
const week = 7 * day;
|
||||
const year = 365 * day; // Unlikely, and leap years won't matter here.
|
||||
|
||||
const scales = [
|
||||
year,
|
||||
week,
|
||||
day,
|
||||
hour,
|
||||
5 * minute,
|
||||
@@ -44,6 +65,8 @@ const scales = [
|
||||
100,
|
||||
];
|
||||
|
||||
const zeroTime: Date = dayjs("0001-01-01T00:00:00Z").toDate();
|
||||
|
||||
const pickScale = (totalTime: number): number => {
|
||||
for (const s of scales) {
|
||||
if (totalTime > s) {
|
||||
@@ -64,6 +87,7 @@ export const formatTime = (time: number): string => {
|
||||
const absTime = Math.abs(time);
|
||||
let unit = "";
|
||||
let value = 0;
|
||||
let frac = 2;
|
||||
|
||||
if (absTime < second) {
|
||||
value = time;
|
||||
@@ -74,15 +98,26 @@ export const formatTime = (time: number): string => {
|
||||
} else if (absTime < hour) {
|
||||
value = time / minute;
|
||||
unit = "m";
|
||||
frac = 1;
|
||||
} else if (absTime < day) {
|
||||
value = time / hour;
|
||||
unit = "h";
|
||||
} else {
|
||||
frac = 0;
|
||||
} else if (absTime < week) {
|
||||
value = time / day;
|
||||
unit = "d";
|
||||
frac = 0;
|
||||
} else if (absTime < year) {
|
||||
value = time / week;
|
||||
unit = "w";
|
||||
frac = 0;
|
||||
} else {
|
||||
value = time / year;
|
||||
unit = "y";
|
||||
frac = 0;
|
||||
}
|
||||
return `${value.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 2,
|
||||
maximumFractionDigits: frac,
|
||||
})}${unit}`;
|
||||
};
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ export const StagesChart: FC<StagesChartProps> = ({
|
||||
|
||||
const value = calcDuration(t.range);
|
||||
const offset = calcOffset(t.range, totalRange);
|
||||
const validDuration = value > 0 && !Number.isNaN(value);
|
||||
|
||||
return (
|
||||
<XAxisRow
|
||||
@@ -172,7 +173,17 @@ export const StagesChart: FC<StagesChartProps> = ({
|
||||
) : (
|
||||
<Bar scale={scale} value={value} offset={offset} />
|
||||
)}
|
||||
{formatTime(calcDuration(t.range))}
|
||||
{validDuration ? (
|
||||
<span>{formatTime(value)}</span>
|
||||
) : (
|
||||
<span
|
||||
css={(theme) => ({
|
||||
color: theme.palette.error.main,
|
||||
})}
|
||||
>
|
||||
Invalid
|
||||
</span>
|
||||
)}
|
||||
</XAxisRow>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -228,3 +228,52 @@ export const MissedAction: Story = {
|
||||
await canvas.findByText("missed action");
|
||||
},
|
||||
};
|
||||
|
||||
// Ref: #15432
|
||||
export const InvalidTimeRange: Story = {
|
||||
args: {
|
||||
provisionerTimings: [
|
||||
{
|
||||
...WorkspaceTimingsResponse.provisioner_timings[0],
|
||||
stage: "init",
|
||||
started_at: "2025-01-01T00:00:00Z",
|
||||
ended_at: "2025-01-01T00:01:00Z",
|
||||
},
|
||||
{
|
||||
...WorkspaceTimingsResponse.provisioner_timings[0],
|
||||
stage: "plan",
|
||||
started_at: "2025-01-01T00:01:00Z",
|
||||
ended_at: "0001-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
...WorkspaceTimingsResponse.provisioner_timings[0],
|
||||
stage: "graph",
|
||||
started_at: "0001-01-01T00:00:00Z",
|
||||
ended_at: "2025-01-01T00:03:00Z",
|
||||
},
|
||||
{
|
||||
...WorkspaceTimingsResponse.provisioner_timings[0],
|
||||
stage: "apply",
|
||||
started_at: "2025-01-01T00:03:00Z",
|
||||
ended_at: "2025-01-01T00:04:00Z",
|
||||
},
|
||||
],
|
||||
agentConnectionTimings: [
|
||||
{
|
||||
started_at: "2025-01-01T00:05:00Z",
|
||||
ended_at: "2025-01-01T00:06:00Z",
|
||||
stage: "connect",
|
||||
workspace_agent_id: "67e37a9d-ccac-497e-8f48-4093bcc4f3e7",
|
||||
workspace_agent_name: "main",
|
||||
},
|
||||
],
|
||||
agentScriptTimings: [
|
||||
{
|
||||
...WorkspaceTimingsResponse.agent_script_timings[0],
|
||||
display_name: "Startup Script 1",
|
||||
started_at: "0001-01-01T00:00:00Z",
|
||||
ended_at: "2025-01-01T00:10:00Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user