mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): add agent connection timings (#15276)
Local preview: <img width="1260" alt="Screenshot 2024-10-29 at 16 16 01" src="https://github.com/user-attachments/assets/10fdb20d-1f2a-4b0a-a8a1-171050ee620d"> Close https://github.com/coder/internal/issues/116 --------- Co-authored-by: Danny Kopping <danny@coder.com>
This commit is contained in:
co-authored by
Danny Kopping
parent
18ef954a03
commit
e232aee011
@@ -57,9 +57,18 @@ export const infiniteWorkspaceBuilds = (
|
||||
};
|
||||
};
|
||||
|
||||
export const workspaceBuildTimings = (workspaceBuildId: string) => {
|
||||
// We use readyAgentsCount to invalidate the query when an agent connects
|
||||
export const workspaceBuildTimings = (
|
||||
workspaceBuildId: string,
|
||||
readyAgentsCount: number,
|
||||
) => {
|
||||
return {
|
||||
queryKey: ["workspaceBuilds", workspaceBuildId, "timings"],
|
||||
queryKey: [
|
||||
"workspaceBuilds",
|
||||
workspaceBuildId,
|
||||
"timings",
|
||||
{ readyAgentsCount },
|
||||
],
|
||||
queryFn: () => API.workspaceBuildTimings(workspaceBuildId),
|
||||
};
|
||||
};
|
||||
|
||||
Generated
+18
-2
@@ -32,14 +32,25 @@ export interface AddLicenseRequest {
|
||||
readonly license: string;
|
||||
}
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
export interface AgentConnectionTiming {
|
||||
readonly started_at: string;
|
||||
readonly ended_at: string;
|
||||
readonly stage: TimingStage;
|
||||
readonly workspace_agent_id: string;
|
||||
readonly workspace_agent_name: string;
|
||||
}
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
export interface AgentScriptTiming {
|
||||
readonly started_at: string;
|
||||
readonly ended_at: string;
|
||||
readonly exit_code: number;
|
||||
readonly stage: string;
|
||||
readonly stage: TimingStage;
|
||||
readonly status: string;
|
||||
readonly display_name: string;
|
||||
readonly workspace_agent_id: string;
|
||||
readonly workspace_agent_name: string;
|
||||
}
|
||||
|
||||
// From codersdk/templates.go
|
||||
@@ -1104,7 +1115,7 @@ export interface ProvisionerTiming {
|
||||
readonly job_id: string;
|
||||
readonly started_at: string;
|
||||
readonly ended_at: string;
|
||||
readonly stage: string;
|
||||
readonly stage: TimingStage;
|
||||
readonly source: string;
|
||||
readonly action: string;
|
||||
readonly resource: string;
|
||||
@@ -1986,6 +1997,7 @@ export interface WorkspaceBuildParameter {
|
||||
export interface WorkspaceBuildTimings {
|
||||
readonly provisioner_timings: Readonly<Array<ProvisionerTiming>>;
|
||||
readonly agent_script_timings: Readonly<Array<AgentScriptTiming>>;
|
||||
readonly agent_connection_timings: Readonly<Array<AgentConnectionTiming>>;
|
||||
}
|
||||
|
||||
// From codersdk/workspaces.go
|
||||
@@ -2226,6 +2238,10 @@ export const TemplateRoles: TemplateRole[] = ["", "admin", "use"]
|
||||
export type TemplateVersionWarning = "UNSUPPORTED_WORKSPACES"
|
||||
export const TemplateVersionWarnings: TemplateVersionWarning[] = ["UNSUPPORTED_WORKSPACES"]
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
export type TimingStage = "apply" | "connect" | "cron" | "graph" | "init" | "plan" | "start" | "stop"
|
||||
export const TimingStages: TimingStage[] = ["apply", "connect", "cron", "graph", "init", "plan", "start", "stop"]
|
||||
|
||||
// From codersdk/workspaces.go
|
||||
export type UsageAppName = "jetbrains" | "reconnecting-pty" | "ssh" | "vscode"
|
||||
export const UsageAppNames: UsageAppName[] = ["jetbrains", "reconnecting-pty", "ssh", "vscode"]
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
|
||||
import { type Theme, useTheme } from "@emotion/react";
|
||||
import { type FC, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Bar } from "./Chart/Bar";
|
||||
import {
|
||||
Chart,
|
||||
@@ -30,7 +27,7 @@ import {
|
||||
makeTicks,
|
||||
mergeTimeRanges,
|
||||
} from "./Chart/utils";
|
||||
import type { StageCategory } from "./StagesChart";
|
||||
import type { Stage } from "./StagesChart";
|
||||
|
||||
type ResourceTiming = {
|
||||
name: string;
|
||||
@@ -40,14 +37,12 @@ type ResourceTiming = {
|
||||
};
|
||||
|
||||
export type ResourcesChartProps = {
|
||||
category: StageCategory;
|
||||
stage: string;
|
||||
stage: Stage;
|
||||
timings: ResourceTiming[];
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const ResourcesChart: FC<ResourcesChartProps> = ({
|
||||
category,
|
||||
stage,
|
||||
timings,
|
||||
onBack,
|
||||
@@ -71,11 +66,11 @@ export const ResourcesChart: FC<ResourcesChartProps> = ({
|
||||
<ChartBreadcrumbs
|
||||
breadcrumbs={[
|
||||
{
|
||||
label: category.name,
|
||||
label: stage.section,
|
||||
onClick: onBack,
|
||||
},
|
||||
{
|
||||
label: stage,
|
||||
label: stage.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -89,7 +84,7 @@ export const ResourcesChart: FC<ResourcesChartProps> = ({
|
||||
<ChartContent>
|
||||
<YAxis>
|
||||
<YAxisSection>
|
||||
<YAxisHeader>{stage} stage</YAxisHeader>
|
||||
<YAxisHeader>{stage.name} stage</YAxisHeader>
|
||||
<YAxisLabels>
|
||||
{visibleTimings.map((t) => (
|
||||
<YAxisLabel key={t.name} id={encodeURIComponent(t.name)}>
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
makeTicks,
|
||||
mergeTimeRanges,
|
||||
} from "./Chart/utils";
|
||||
import type { StageCategory } from "./StagesChart";
|
||||
import type { Stage } from "./StagesChart";
|
||||
|
||||
type ScriptTiming = {
|
||||
name: string;
|
||||
@@ -37,14 +37,12 @@ type ScriptTiming = {
|
||||
};
|
||||
|
||||
export type ScriptsChartProps = {
|
||||
category: StageCategory;
|
||||
stage: string;
|
||||
stage: Stage;
|
||||
timings: ScriptTiming[];
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const ScriptsChart: FC<ScriptsChartProps> = ({
|
||||
category,
|
||||
stage,
|
||||
timings,
|
||||
onBack,
|
||||
@@ -66,11 +64,11 @@ export const ScriptsChart: FC<ScriptsChartProps> = ({
|
||||
<ChartBreadcrumbs
|
||||
breadcrumbs={[
|
||||
{
|
||||
label: category.name,
|
||||
label: stage.section,
|
||||
onClick: onBack,
|
||||
},
|
||||
{
|
||||
label: stage,
|
||||
label: stage.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -84,7 +82,7 @@ export const ScriptsChart: FC<ScriptsChartProps> = ({
|
||||
<ChartContent>
|
||||
<YAxis>
|
||||
<YAxisSection>
|
||||
<YAxisHeader>{stage} stage</YAxisHeader>
|
||||
<YAxisHeader>{stage.name} stage</YAxisHeader>
|
||||
<YAxisLabels>
|
||||
{visibleTimings.map((t) => (
|
||||
<YAxisLabel key={t.name} id={encodeURIComponent(t.name)}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import ErrorSharp from "@mui/icons-material/ErrorSharp";
|
||||
import InfoOutlined from "@mui/icons-material/InfoOutlined";
|
||||
import type { TimingStage } from "api/typesGenerated";
|
||||
import type { FC } from "react";
|
||||
import { Bar, ClickableBar } from "./Chart/Bar";
|
||||
import { Blocks } from "./Chart/Blocks";
|
||||
@@ -28,118 +29,34 @@ import {
|
||||
mergeTimeRanges,
|
||||
} from "./Chart/utils";
|
||||
|
||||
export type StageCategory = {
|
||||
name: string;
|
||||
id: "provisioning" | "workspaceBoot";
|
||||
};
|
||||
|
||||
const stageCategories: StageCategory[] = [
|
||||
{
|
||||
name: "provisioning",
|
||||
id: "provisioning",
|
||||
},
|
||||
{
|
||||
name: "workspace boot",
|
||||
id: "workspaceBoot",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type Stage = {
|
||||
name: string;
|
||||
categoryID: StageCategory["id"];
|
||||
/**
|
||||
* The name is used to identify the stage.
|
||||
*/
|
||||
name: TimingStage;
|
||||
/**
|
||||
* The value to display in the stage label. This can differ from the stage
|
||||
* name to provide more context or clarity.
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* The section is used to group stages together.
|
||||
*/
|
||||
section: string;
|
||||
/**
|
||||
* The tooltip is used to provide additional information about the stage.
|
||||
*/
|
||||
tooltip: Omit<TooltipProps, "children">;
|
||||
};
|
||||
|
||||
export const stages: Stage[] = [
|
||||
{
|
||||
name: "init",
|
||||
categoryID: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform initialization</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Download providers & modules.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plan",
|
||||
categoryID: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform plan</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Compare state of desired vs actual resources and compute changes to
|
||||
be made.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "graph",
|
||||
categoryID: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform graph</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
List all resources in plan, used to update coderd database.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "apply",
|
||||
categoryID: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform apply</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Execute Terraform plan to create/modify/delete resources into
|
||||
desired states.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "start",
|
||||
categoryID: "workspaceBoot",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Start</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Scripts executed when the agent is starting.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type StageTiming = {
|
||||
name: string;
|
||||
/**
|
||||
stage: Stage;
|
||||
/**
|
||||
* Represents the number of resources included in this stage that can be
|
||||
* inspected. This value is used to display individual blocks within the bar,
|
||||
* indicating that the stage consists of multiple resource time blocks.
|
||||
*/
|
||||
visibleResources: number;
|
||||
/**
|
||||
* Represents the category of the stage. This value is used to group stages
|
||||
* together in the chart. For example, all provisioning stages are grouped
|
||||
* together.
|
||||
*/
|
||||
categoryID: StageCategory["id"];
|
||||
/**
|
||||
* Represents the time range of the stage. This value is used to calculate the
|
||||
* duration of the stage and to position the stage within the chart. This can
|
||||
@@ -155,7 +72,7 @@ type StageTiming = {
|
||||
|
||||
export type StagesChartProps = {
|
||||
timings: StageTiming[];
|
||||
onSelectStage: (timing: StageTiming, category: StageCategory) => void;
|
||||
onSelectStage: (stage: Stage) => void;
|
||||
};
|
||||
|
||||
export const StagesChart: FC<StagesChartProps> = ({
|
||||
@@ -167,27 +84,28 @@ export const StagesChart: FC<StagesChartProps> = ({
|
||||
);
|
||||
const totalTime = calcDuration(totalRange);
|
||||
const [ticks, scale] = makeTicks(totalTime);
|
||||
const sections = Array.from(new Set(timings.map((t) => t.stage.section)));
|
||||
|
||||
return (
|
||||
<Chart>
|
||||
<ChartContent>
|
||||
<YAxis>
|
||||
{stageCategories.map((c) => {
|
||||
const stagesInCategory = stages.filter(
|
||||
(s) => s.categoryID === c.id,
|
||||
);
|
||||
{sections.map((section) => {
|
||||
const stages = timings
|
||||
.filter((t) => t.stage.section === section)
|
||||
.map((t) => t.stage);
|
||||
|
||||
return (
|
||||
<YAxisSection key={c.id}>
|
||||
<YAxisHeader>{c.name}</YAxisHeader>
|
||||
<YAxisSection key={section}>
|
||||
<YAxisHeader>{section}</YAxisHeader>
|
||||
<YAxisLabels>
|
||||
{stagesInCategory.map((stage) => (
|
||||
{stages.map((stage) => (
|
||||
<YAxisLabel
|
||||
key={stage.name}
|
||||
id={encodeURIComponent(stage.name)}
|
||||
>
|
||||
<span css={styles.stageLabel}>
|
||||
{stage.name}
|
||||
{stage.label}
|
||||
<Tooltip {...stage.tooltip}>
|
||||
<InfoOutlined css={styles.info} />
|
||||
</Tooltip>
|
||||
@@ -201,19 +119,19 @@ export const StagesChart: FC<StagesChartProps> = ({
|
||||
</YAxis>
|
||||
|
||||
<XAxis ticks={ticks} scale={scale}>
|
||||
{stageCategories.map((category) => {
|
||||
{sections.map((section) => {
|
||||
const stageTimings = timings.filter(
|
||||
(t) => t.categoryID === category.id,
|
||||
(t) => t.stage.section === section,
|
||||
);
|
||||
return (
|
||||
<XAxisSection key={category.id}>
|
||||
<XAxisSection key={section}>
|
||||
{stageTimings.map((t) => {
|
||||
// If the stage has no timing data, we just want to render an empty row
|
||||
if (t.range === undefined) {
|
||||
return (
|
||||
<XAxisRow
|
||||
key={t.name}
|
||||
yAxisLabelId={encodeURIComponent(t.name)}
|
||||
key={t.stage.name}
|
||||
yAxisLabelId={encodeURIComponent(t.stage.name)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -223,18 +141,18 @@ export const StagesChart: FC<StagesChartProps> = ({
|
||||
|
||||
return (
|
||||
<XAxisRow
|
||||
key={t.name}
|
||||
yAxisLabelId={encodeURIComponent(t.name)}
|
||||
key={t.stage.name}
|
||||
yAxisLabelId={encodeURIComponent(t.stage.name)}
|
||||
>
|
||||
{/** We only want to expand stages with more than one resource */}
|
||||
{t.visibleResources > 1 ? (
|
||||
<ClickableBar
|
||||
aria-label={`View ${t.name} details`}
|
||||
aria-label={`View ${t.stage.label} details`}
|
||||
scale={scale}
|
||||
value={value}
|
||||
offset={offset}
|
||||
onClick={() => {
|
||||
onSelectStage(t, category);
|
||||
onSelectStage(t.stage);
|
||||
}}
|
||||
>
|
||||
{t.error && (
|
||||
@@ -281,3 +199,103 @@ const styles = {
|
||||
cursor: "pointer",
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export const provisioningStages: Stage[] = [
|
||||
{
|
||||
name: "init",
|
||||
label: "init",
|
||||
section: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform initialization</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Download providers & modules.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plan",
|
||||
label: "plan",
|
||||
section: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform plan</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Compare state of desired vs actual resources and compute changes to
|
||||
be made.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "graph",
|
||||
label: "graph",
|
||||
section: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform graph</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
List all resources in plan, used to update coderd database.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "apply",
|
||||
label: "apply",
|
||||
section: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform apply</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Execute Terraform plan to create/modify/delete resources into
|
||||
desired states.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const agentStages = (section: string): Stage[] => {
|
||||
return [
|
||||
{
|
||||
name: "connect",
|
||||
label: "connect",
|
||||
section,
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Connect</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Establish an RPC connection with the control plane.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "start",
|
||||
label: "run startup scripts",
|
||||
section,
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Run startup scripts</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Execute each agent startup script.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ const meta: Meta<typeof WorkspaceTimings> = {
|
||||
defaultIsOpen: true,
|
||||
provisionerTimings: WorkspaceTimingsResponse.provisioner_timings,
|
||||
agentScriptTimings: WorkspaceTimingsResponse.agent_script_timings,
|
||||
agentConnectionTimings: WorkspaceTimingsResponse.agent_connection_timings,
|
||||
},
|
||||
parameters: {
|
||||
chromatic,
|
||||
@@ -32,6 +33,7 @@ export const Loading: Story = {
|
||||
args: {
|
||||
provisionerTimings: undefined,
|
||||
agentScriptTimings: undefined,
|
||||
agentConnectionTimings: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -45,7 +47,7 @@ export const ClickToOpen: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const user = userEvent.setup();
|
||||
const canvas = within(canvasElement);
|
||||
await user.click(canvas.getByRole("button"));
|
||||
await user.click(canvas.getByText("Build timeline", { exact: false }));
|
||||
await canvas.findByText("provisioning");
|
||||
},
|
||||
};
|
||||
@@ -58,9 +60,9 @@ export const ClickToClose: Story = {
|
||||
const user = userEvent.setup();
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText("provisioning");
|
||||
await user.click(canvas.getByText("Provisioning time", { exact: false }));
|
||||
await user.click(canvas.getByText("Build timeline", { exact: false }));
|
||||
await waitFor(() =>
|
||||
expect(canvas.getByText("workspace boot")).not.toBeVisible(),
|
||||
expect(canvas.queryByText("workspace boot")).not.toBeInTheDocument(),
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -96,7 +98,7 @@ export const NavigateToStartStage: Story = {
|
||||
const user = userEvent.setup();
|
||||
const canvas = within(canvasElement);
|
||||
const detailsButton = canvas.getByRole("button", {
|
||||
name: "View start details",
|
||||
name: "View run startup scripts details",
|
||||
});
|
||||
await user.click(detailsButton);
|
||||
await canvas.findByText("Startup Script");
|
||||
|
||||
@@ -4,19 +4,27 @@ import KeyboardArrowUp from "@mui/icons-material/KeyboardArrowUp";
|
||||
import Button from "@mui/material/Button";
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import type { AgentScriptTiming, ProvisionerTiming } from "api/typesGenerated";
|
||||
import type {
|
||||
AgentConnectionTiming,
|
||||
AgentScriptTiming,
|
||||
ProvisionerTiming,
|
||||
} from "api/typesGenerated";
|
||||
import { type FC, useState } from "react";
|
||||
import { type TimeRange, calcDuration, mergeTimeRanges } from "./Chart/utils";
|
||||
import { ResourcesChart, isCoderResource } from "./ResourcesChart";
|
||||
import { ScriptsChart } from "./ScriptsChart";
|
||||
import { type StageCategory, StagesChart, stages } from "./StagesChart";
|
||||
import {
|
||||
type Stage,
|
||||
StagesChart,
|
||||
agentStages,
|
||||
provisioningStages,
|
||||
} from "./StagesChart";
|
||||
|
||||
type TimingView =
|
||||
| { name: "default" }
|
||||
| {
|
||||
name: "detailed";
|
||||
stage: string;
|
||||
category: StageCategory;
|
||||
stage: Stage;
|
||||
filter: string;
|
||||
};
|
||||
|
||||
@@ -24,20 +32,37 @@ type WorkspaceTimingsProps = {
|
||||
defaultIsOpen?: boolean;
|
||||
provisionerTimings: readonly ProvisionerTiming[] | undefined;
|
||||
agentScriptTimings: readonly AgentScriptTiming[] | undefined;
|
||||
agentConnectionTimings: readonly AgentConnectionTiming[] | undefined;
|
||||
};
|
||||
|
||||
export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
|
||||
provisionerTimings = [],
|
||||
agentScriptTimings = [],
|
||||
agentConnectionTimings = [],
|
||||
defaultIsOpen = false,
|
||||
}) => {
|
||||
const [view, setView] = useState<TimingView>({ name: "default" });
|
||||
const timings = [...provisionerTimings, ...agentScriptTimings];
|
||||
const timings = [
|
||||
...provisionerTimings,
|
||||
...agentScriptTimings,
|
||||
...agentConnectionTimings,
|
||||
];
|
||||
const [isOpen, setIsOpen] = useState(defaultIsOpen);
|
||||
const isLoading = timings.length === 0;
|
||||
|
||||
// All stages
|
||||
const agentStageLabels = Array.from(
|
||||
new Set(
|
||||
agentConnectionTimings.map((t) => `agent (${t.workspace_agent_name})`),
|
||||
),
|
||||
);
|
||||
const stages = [
|
||||
...provisioningStages,
|
||||
...agentStageLabels.flatMap((a) => agentStages(a)),
|
||||
];
|
||||
|
||||
const displayProvisioningTime = () => {
|
||||
const totalRange = mergeTimeRanges(timings.map(extractRange));
|
||||
const totalRange = mergeTimeRanges(timings.map(toTimeRange));
|
||||
const totalDuration = calcDuration(totalRange);
|
||||
return humanizeDuration(totalDuration);
|
||||
};
|
||||
@@ -81,7 +106,7 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
|
||||
const stageRange =
|
||||
stageTimings.length === 0
|
||||
? undefined
|
||||
: mergeTimeRanges(stageTimings.map(extractRange));
|
||||
: mergeTimeRanges(stageTimings.map(toTimeRange));
|
||||
|
||||
// Prevent users from inspecting internal coder resources in
|
||||
// provisioner timings.
|
||||
@@ -93,67 +118,63 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
|
||||
});
|
||||
|
||||
return {
|
||||
stage: s,
|
||||
range: stageRange,
|
||||
name: s.name,
|
||||
categoryID: s.categoryID,
|
||||
visibleResources: visibleResources.length,
|
||||
error: stageTimings.some(
|
||||
(t) => "status" in t && t.status === "exit_failure",
|
||||
),
|
||||
};
|
||||
})}
|
||||
onSelectStage={(t, category) => {
|
||||
onSelectStage={(stage) => {
|
||||
setView({
|
||||
stage,
|
||||
name: "detailed",
|
||||
stage: t.name,
|
||||
category,
|
||||
filter: "",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{view.name === "detailed" &&
|
||||
view.category.id === "provisioning" && (
|
||||
<ResourcesChart
|
||||
timings={provisionerTimings
|
||||
.filter((t) => t.stage === view.stage)
|
||||
.map((t) => {
|
||||
return {
|
||||
range: extractRange(t),
|
||||
{view.name === "detailed" && (
|
||||
<>
|
||||
{view.stage.section === "provisioning" && (
|
||||
<ResourcesChart
|
||||
timings={provisionerTimings
|
||||
.filter((t) => t.stage === view.stage.name)
|
||||
.map((t) => ({
|
||||
range: toTimeRange(t),
|
||||
name: t.resource,
|
||||
source: t.source,
|
||||
action: t.action,
|
||||
};
|
||||
})}
|
||||
category={view.category}
|
||||
stage={view.stage}
|
||||
onBack={() => {
|
||||
setView({ name: "default" });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
}))}
|
||||
stage={view.stage}
|
||||
onBack={() => {
|
||||
setView({ name: "default" });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{view.name === "detailed" &&
|
||||
view.category.id === "workspaceBoot" && (
|
||||
<ScriptsChart
|
||||
timings={agentScriptTimings
|
||||
.filter((t) => t.stage === view.stage)
|
||||
.map((t) => {
|
||||
return {
|
||||
range: extractRange(t),
|
||||
name: t.display_name,
|
||||
status: t.status,
|
||||
exitCode: t.exit_code,
|
||||
};
|
||||
})}
|
||||
category={view.category}
|
||||
stage={view.stage}
|
||||
onBack={() => {
|
||||
setView({ name: "default" });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{view.stage.name === "start" && (
|
||||
<ScriptsChart
|
||||
timings={agentScriptTimings
|
||||
.filter((t) => t.stage === view.stage.name)
|
||||
.map((t) => {
|
||||
return {
|
||||
range: toTimeRange(t),
|
||||
name: t.display_name,
|
||||
status: t.status,
|
||||
exitCode: t.exit_code,
|
||||
};
|
||||
})}
|
||||
stage={view.stage}
|
||||
onBack={() => {
|
||||
setView({ name: "default" });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Collapse>
|
||||
)}
|
||||
@@ -161,9 +182,10 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const extractRange = (
|
||||
timing: ProvisionerTiming | AgentScriptTiming,
|
||||
): TimeRange => {
|
||||
const toTimeRange = (timing: {
|
||||
started_at: string;
|
||||
ended_at: string;
|
||||
}): TimeRange => {
|
||||
return {
|
||||
startedAt: new Date(timing.started_at),
|
||||
endedAt: new Date(timing.ended_at),
|
||||
|
||||
@@ -355,6 +355,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "Startup Script",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.650915Z",
|
||||
@@ -363,6 +365,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "Dotfiles",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.650715Z",
|
||||
@@ -371,6 +375,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "Personalize",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.650512Z",
|
||||
@@ -379,6 +385,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "install_slackme",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.650659Z",
|
||||
@@ -387,6 +395,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "Coder Login",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.650666Z",
|
||||
@@ -395,6 +405,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "File Browser",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.652425Z",
|
||||
@@ -403,6 +415,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "code-server",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.650423Z",
|
||||
@@ -411,6 +425,17 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "Git Clone",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
],
|
||||
agent_connection_timings: [
|
||||
{
|
||||
started_at: "2024-10-14T11:30:55.650423Z",
|
||||
ended_at: "2024-10-14T11:30:56.650423Z",
|
||||
stage: "connect",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -267,8 +267,9 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
)}
|
||||
|
||||
<WorkspaceTimings
|
||||
agentScriptTimings={timings?.agent_script_timings}
|
||||
provisionerTimings={timings?.provisioner_timings}
|
||||
agentScriptTimings={timings?.agent_script_timings}
|
||||
agentConnectionTimings={timings?.agent_connection_timings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -158,8 +158,11 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient));
|
||||
|
||||
// Build Timings. Fetch build timings only when the build job is completed.
|
||||
const readyAgents = workspace.latest_build.resources
|
||||
.flatMap((r) => r.agents)
|
||||
.filter((a) => a && a.lifecycle_state !== "starting");
|
||||
const timingsQuery = useQuery({
|
||||
...workspaceBuildTimings(workspace.latest_build.id),
|
||||
...workspaceBuildTimings(workspace.latest_build.id, readyAgents.length),
|
||||
enabled: Boolean(workspace.latest_build.job.completed_at),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user