diff --git a/site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts b/site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts index 2790701db5265..f125b8d1387f1 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts +++ b/site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts @@ -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}`; }; diff --git a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx index 32e87f9ea5b76..c9e9f8d3a71b2 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx @@ -141,6 +141,7 @@ export const StagesChart: FC = ({ const value = calcDuration(t.range); const offset = calcOffset(t.range, totalRange); + const validDuration = value > 0 && !Number.isNaN(value); return ( = ({ ) : ( )} - {formatTime(calcDuration(t.range))} + {validDuration ? ( + {formatTime(value)} + ) : ( + ({ + color: theme.palette.error.main, + })} + > + Invalid + + )} ); })} diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx index c2d1193d37fc1..36f08b36c0ca0 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx @@ -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", + }, + ], + }, +};