Skip to content

Commit a8862be

Browse files
authored
feat(site): add tab to invalidate prebuilds (#20864)
Updates #17917
1 parent ce627bf commit a8862be

File tree

5 files changed

+168
-0
lines changed

5 files changed

+168
-0
lines changed

site/src/api/api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,15 @@ class ApiMethods {
11771177
return response.data;
11781178
};
11791179

1180+
invalidateTemplatePresets = async (
1181+
templateId: string,
1182+
): Promise<TypesGen.InvalidatePresetsResponse> => {
1183+
const response = await this.axios.post<TypesGen.InvalidatePresetsResponse>(
1184+
`/api/v2/templates/${templateId}/prebuilds/invalidate`,
1185+
);
1186+
return response.data;
1187+
};
1188+
11801189
getWorkspace = async (
11811190
workspaceId: string,
11821191
params?: TypesGen.WorkspaceOptions,

site/src/pages/TemplatePage/TemplateLayout.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Loader } from "components/Loader/Loader";
66
import { Margins } from "components/Margins/Margins";
77
import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs";
88
import { useAuthenticated } from "hooks";
9+
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
910
import {
1011
type WorkspacePermissions,
1112
workspacePermissionChecks,
@@ -105,6 +106,8 @@ export const TemplateLayout: FC<PropsWithChildren> = ({
105106
// have permission to update templates. Need both checks.
106107
const shouldShowInsights =
107108
data?.permissions?.canUpdateTemplate || data?.permissions?.canReadInsights;
109+
const { workspace_prebuilds: isWorkspacePrebuildsEnabled } =
110+
useFeatureVisibility();
108111

109112
if (error || workspacePermissionsQuery.error) {
110113
return (
@@ -157,6 +160,12 @@ export const TemplateLayout: FC<PropsWithChildren> = ({
157160
Insights
158161
</TabLink>
159162
)}
163+
{isWorkspacePrebuildsEnabled &&
164+
data.permissions.canUpdateTemplate && (
165+
<TabLink to="prebuilds" value="prebuilds">
166+
Prebuilds
167+
</TabLink>
168+
)}
160169
</TabsList>
161170
</Margins>
162171
</Tabs>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { API } from "api/api";
2+
import type { InvalidatePresetsResponse } from "api/typesGenerated";
3+
import { ErrorAlert } from "components/Alert/ErrorAlert";
4+
import { Button } from "components/Button/Button";
5+
import { displaySuccess } from "components/GlobalSnackbar/utils";
6+
import { RefreshCw } from "lucide-react";
7+
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
8+
import type { FC } from "react";
9+
import { useMutation } from "react-query";
10+
import { pageTitle } from "utils/page";
11+
12+
const TemplatePrebuildsPage: FC = () => {
13+
const { template } = useTemplateLayoutContext();
14+
15+
return (
16+
<>
17+
<title>{pageTitle(`${template.name} - Prebuilds`)}</title>
18+
<TemplatePrebuildsPageView templateId={template.id} />
19+
</>
20+
);
21+
};
22+
23+
interface TemplatePrebuildsPageViewProps {
24+
templateId: string;
25+
}
26+
27+
export const TemplatePrebuildsPageView: FC<TemplatePrebuildsPageViewProps> = ({
28+
templateId,
29+
}) => {
30+
const invalidateMutation = useMutation({
31+
mutationFn: () => API.invalidateTemplatePresets(templateId),
32+
onSuccess: (data: InvalidatePresetsResponse) => {
33+
if (data.invalidated.length === 0) {
34+
displaySuccess("No template presets required invalidation.");
35+
return;
36+
}
37+
38+
// They all have the same template version
39+
const { template_version_name } = data.invalidated[0];
40+
const count = data.invalidated.length;
41+
42+
displaySuccess(
43+
`Invalidated ${count} ${count === 1 ? "preset" : "presets"} for version ${template_version_name}.`,
44+
);
45+
},
46+
});
47+
48+
return (
49+
<div className="flex">
50+
<div className="max-w-xl space-y-6">
51+
{invalidateMutation.error && (
52+
<ErrorAlert error={invalidateMutation.error} />
53+
)}
54+
<div>
55+
<h3 className="text-xl text-content-primary m-0">
56+
Invalidate presets
57+
</h3>
58+
<p className="text-sm text-content-secondary">
59+
All prebuilt workspaces for the active template version are marked
60+
as invalid. This is useful when prebuilds become stale due to
61+
repository changes or infrastructure updates and need recycling.
62+
</p>
63+
</div>
64+
65+
<div>
66+
<Button
67+
onClick={() => invalidateMutation.mutate()}
68+
disabled={invalidateMutation.isPending}
69+
className="gap-2"
70+
>
71+
<RefreshCw className="size-4" />
72+
Invalidate now
73+
</Button>
74+
</div>
75+
</div>
76+
</div>
77+
);
78+
};
79+
80+
export default TemplatePrebuildsPage;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { MockTemplate } from "testHelpers/entities";
2+
import { withGlobalSnackbar } from "testHelpers/storybook";
3+
import type { Meta, StoryObj } from "@storybook/react-vite";
4+
import { API } from "api/api";
5+
import { spyOn, userEvent, within } from "storybook/test";
6+
import { TemplatePrebuildsPageView } from "./TemplatePrebuildsPage";
7+
8+
const meta: Meta<typeof TemplatePrebuildsPageView> = {
9+
title: "pages/TemplatePage/TemplatePrebuildsPageView",
10+
component: TemplatePrebuildsPageView,
11+
args: {
12+
templateId: MockTemplate.id,
13+
},
14+
decorators: [withGlobalSnackbar],
15+
};
16+
17+
export default meta;
18+
type Story = StoryObj<typeof TemplatePrebuildsPageView>;
19+
20+
export const NoPresetsInvalidated: Story = {
21+
play: async ({ canvasElement }) => {
22+
spyOn(API, "invalidateTemplatePresets").mockResolvedValue({
23+
invalidated: [],
24+
});
25+
const user = userEvent.setup();
26+
const canvas = within(canvasElement);
27+
const groupLabel = await canvas.findByText("Invalidate now");
28+
await user.click(groupLabel);
29+
},
30+
};
31+
32+
export const PresetsInvalidated: Story = {
33+
play: async ({ canvasElement }) => {
34+
spyOn(API, "invalidateTemplatePresets").mockResolvedValue({
35+
invalidated: [
36+
{
37+
preset_name: "First preset",
38+
template_name: "Super template",
39+
template_version_name: "abcdef",
40+
},
41+
{
42+
preset_name: "Second preset",
43+
template_name: "Super template",
44+
template_version_name: "abcdef",
45+
},
46+
],
47+
});
48+
const user = userEvent.setup();
49+
const canvas = within(canvasElement);
50+
const groupLabel = await canvas.findByText("Invalidate now");
51+
await user.click(groupLabel);
52+
},
53+
};
54+
55+
export const InvalidationFailed: Story = {
56+
play: async ({ canvasElement }) => {
57+
spyOn(API, "invalidateTemplatePresets").mockRejectedValue(
58+
new Error("Mocked error"),
59+
);
60+
const user = userEvent.setup();
61+
const canvas = within(canvasElement);
62+
const groupLabel = await canvas.findByText("Invalidate now");
63+
await user.click(groupLabel);
64+
},
65+
};

site/src/router.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@ const TemplateInsightsPage = lazy(
287287
() =>
288288
import("./pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage"),
289289
);
290+
const TemplatePrebuildsPage = lazy(
291+
() =>
292+
import("./pages/TemplatePage/TemplatePrebuildsPage/TemplatePrebuildsPage"),
293+
);
290294
const PremiumPage = lazy(
291295
() => import("./pages/DeploymentSettingsPage/PremiumPage/PremiumPage"),
292296
);
@@ -361,6 +365,7 @@ const templateRouter = () => {
361365
<Route path="versions" element={<TemplateVersionsPage />} />
362366
<Route path="embed" element={<TemplateEmbedExperimentRouter />} />
363367
<Route path="insights" element={<TemplateInsightsPage />} />
368+
<Route path="prebuilds" element={<TemplatePrebuildsPage />} />
364369
</Route>
365370

366371
<Route path="workspace" element={<CreateWorkspacePage />} />

0 commit comments

Comments
 (0)