fix: hide empty startup scripts row in build timeline UI

This PR fixes issue #15464 by:
1. Filtering out the 'start' stage from agent stages in WorkspaceTimings.tsx when there are no startup scripts configured
2. Adding additional filtering in StagesChart.tsx to ignore empty 'start' stages
3. Adding tests to verify the behavior

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Kyle Carberry
2025-03-13 16:58:38 +00:00
parent 30179aeaac
commit da5451b928
4 changed files with 207 additions and 11 deletions

View File

@ -0,0 +1,81 @@
import { render, screen } from "@testing-library/react";
import { StagesChart, agentStages, type Stage } from "./StagesChart";
describe("StagesChart", () => {
const onSelectStage = jest.fn();
// Mock the stage timings
const mockStageWithTimings = {
stage: {
name: "connect",
label: "connect",
section: "agent (test)",
tooltip: { title: <div>Connect</div> },
} as Stage,
visibleResources: 1,
range: {
startedAt: new Date("2023-01-01T12:00:00Z"),
endedAt: new Date("2023-01-01T12:01:00Z"),
},
};
// Mock a stage with no timings
const mockStageWithoutTimings = {
stage: {
name: "start",
label: "run startup scripts",
section: "agent (test)",
tooltip: { title: <div>Run startup scripts</div> },
} as Stage,
visibleResources: 0,
range: undefined,
};
it("should render stages with timing data", () => {
render(
<StagesChart
timings={[mockStageWithTimings]}
onSelectStage={onSelectStage}
/>
);
// Should display the section header
expect(screen.getByText("agent (test)")).toBeInTheDocument();
// Should display the stage label
expect(screen.getByText("connect")).toBeInTheDocument();
});
it("should NOT render empty startup scripts stage with no visible resources", () => {
render(
<StagesChart
timings={[mockStageWithoutTimings]}
onSelectStage={onSelectStage}
/>
);
// Should display the section header
expect(screen.getByText("agent (test)")).toBeInTheDocument();
// Should NOT display the "run startup scripts" label as it has no timing data and no resources
expect(screen.queryByText("run startup scripts")).not.toBeInTheDocument();
});
it("should render both stages when the startup script stage has resources", () => {
const mockStartStageWithResources = {
...mockStageWithoutTimings,
visibleResources: 1, // Has one script
};
render(
<StagesChart
timings={[mockStageWithTimings, mockStartStageWithResources]}
onSelectStage={onSelectStage}
/>
);
// Should display both stage labels
expect(screen.getByText("connect")).toBeInTheDocument();
expect(screen.getByText("run startup scripts")).toBeInTheDocument();
});
});

View File

@ -91,9 +91,12 @@ export const StagesChart: FC<StagesChartProps> = ({
<ChartContent>
<YAxis>
{sections.map((section) => {
const stages = timings
// Filter out stages without timing data if it's the "start" stage with no visible resources
const filteredTimings = timings
.filter((t) => t.stage.section === section)
.map((t) => t.stage);
.filter((t) => !(t.stage.name === "start" && t.visibleResources === 0 && t.range === undefined));
const stages = filteredTimings.map((t) => t.stage);
return (
<YAxisSection key={section}>
@ -126,8 +129,13 @@ export const StagesChart: FC<StagesChartProps> = ({
return (
<XAxisSection key={section}>
{stageTimings.map((t) => {
// If the stage has no timing data, we just want to render an empty row
// If the stage has no timing data, we need to handle it specially
if (t.range === undefined) {
// Skip rendering empty "run startup scripts" rows when no scripts are configured
if (t.stage.name === "start" && t.visibleResources === 0) {
return null;
}
return (
<XAxisRow
key={t.stage.name}

View File

@ -0,0 +1,102 @@
import { render, screen } from "@testing-library/react";
import type {
AgentConnectionTiming,
AgentScriptTiming,
ProvisionerTiming
} from "api/typesGenerated";
import { WorkspaceTimings } from "./WorkspaceTimings";
describe("WorkspaceTimings", () => {
const mockProvisionerTimings: ProvisionerTiming[] = [
{
action: "create",
applied_at: "2023-01-01T12:00:00Z",
created_at: "2023-01-01T12:00:00Z",
ended_at: "2023-01-01T12:01:00Z",
log_source_id: "1",
log_url: "",
resource: "aws_instance.test",
source: "terraform",
stage: "apply",
started_at: "2023-01-01T12:00:00Z",
status: "ok",
workspace_build_id: "1",
workspace_transition: "start",
},
];
const mockAgentConnectionTimings: AgentConnectionTiming[] = [
{
created_at: "2023-01-01T12:01:00Z",
ended_at: "2023-01-01T12:02:00Z",
started_at: "2023-01-01T12:01:00Z",
stage: "connect",
status: "ok",
workspace_agent_id: "1",
workspace_agent_name: "test",
workspace_build_id: "1",
workspace_transition: "start",
},
];
const mockAgentScriptTimings: AgentScriptTiming[] = [
{
created_at: "2023-01-01T12:02:00Z",
display_name: "test script",
ended_at: "2023-01-01T12:03:00Z",
exit_code: 0,
script_id: "1",
started_at: "2023-01-01T12:02:00Z",
stage: "start",
status: "ok",
workspace_agent_id: "1",
workspace_build_id: "1",
workspace_transition: "start",
},
];
it("renders with all timings", () => {
render(
<WorkspaceTimings
provisionerTimings={mockProvisionerTimings}
agentConnectionTimings={mockAgentConnectionTimings}
agentScriptTimings={mockAgentScriptTimings}
defaultIsOpen={true}
/>
);
expect(screen.getByText("Build timeline")).toBeInTheDocument();
});
it("renders correctly with empty agent script timings", () => {
render(
<WorkspaceTimings
provisionerTimings={mockProvisionerTimings}
agentConnectionTimings={mockAgentConnectionTimings}
agentScriptTimings={[]} // No startup scripts configured
defaultIsOpen={true}
/>
);
expect(screen.getByText("Build timeline")).toBeInTheDocument();
// Should not show loading state with Skeleton component
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
// Should not show "run startup scripts" stage
expect(screen.queryByText("run startup scripts")).not.toBeInTheDocument();
});
it("shows loading state when provisioner timings are missing", () => {
render(
<WorkspaceTimings
provisionerTimings={[]} // Missing provisioner timings
agentConnectionTimings={mockAgentConnectionTimings}
agentScriptTimings={mockAgentScriptTimings}
defaultIsOpen={true}
/>
);
expect(screen.getByText("Build timeline")).toBeInTheDocument();
// Should be in loading state
expect(screen.getByRole("progressbar")).toBeInTheDocument();
});
});

View File

@ -61,13 +61,12 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
const [isOpen, setIsOpen] = useState(defaultIsOpen);
// If any of the timings are empty, we are still loading the data. They can be
// filled in different moments.
const isLoading = [
provisionerTimings,
agentScriptTimings,
agentConnectionTimings,
].some((t) => t.length === 0);
// If any of the required timing arrays are empty (except agentScriptTimings which
// can be empty if no scripts are configured), we are still loading the data.
// They can be filled in different moments.
const isLoading =
provisionerTimings.length === 0 ||
agentConnectionTimings.length === 0; // agentScriptTimings can be empty if no scripts are configured
// Each agent connection timing is a stage in the timeline to make it easier
// to users to see the timing for connection and the other scripts.
@ -77,9 +76,15 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
),
);
// Check if there are any startup scripts configured
const hasStartupScripts = uniqScriptTimings.some(t => t.stage === "start");
const stages = [
...provisioningStages,
...agentStageLabels.flatMap((a) => agentStages(a)),
...agentStageLabels.flatMap((a) =>
// Filter out the "start" stage if no startup scripts are configured
agentStages(a).filter(stage => hasStartupScripts || stage.name !== "start")
),
];
const displayProvisioningTime = () => {