Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 38e2712

Browse files
committed
refactor: rollback provisioners page to its previous version
1 parent 763921b commit 38e2712

File tree

4 files changed

+339
-4
lines changed

4 files changed

+339
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { buildInfo } from "api/queries/buildInfo";
2+
import { provisionerDaemonGroups } from "api/queries/organizations";
3+
import { EmptyState } from "components/EmptyState/EmptyState";
4+
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
5+
import { useDashboard } from "modules/dashboard/useDashboard";
6+
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
7+
import type { FC } from "react";
8+
import { Helmet } from "react-helmet-async";
9+
import { useQuery } from "react-query";
10+
import { useParams } from "react-router-dom";
11+
import { pageTitle } from "utils/page";
12+
import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView";
13+
14+
const OrganizationProvisionersPage: FC = () => {
15+
const { organization: organizationName } = useParams() as {
16+
organization: string;
17+
};
18+
const { organization } = useOrganizationSettings();
19+
const { entitlements } = useDashboard();
20+
const { metadata } = useEmbeddedMetadata();
21+
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
22+
const provisionersQuery = useQuery(provisionerDaemonGroups(organizationName));
23+
24+
if (!organization) {
25+
return <EmptyState message="Organization not found" />;
26+
}
27+
28+
return (
29+
<>
30+
<Helmet>
31+
<title>
32+
{pageTitle(
33+
"Provisioners",
34+
organization.display_name || organization.name,
35+
)}
36+
</title>
37+
</Helmet>
38+
<OrganizationProvisionersPageView
39+
showPaywall={!entitlements.features.multiple_organizations.enabled}
40+
error={provisionersQuery.error}
41+
buildInfo={buildInfoQuery.data}
42+
provisioners={provisionersQuery.data}
43+
/>
44+
</>
45+
);
46+
};
47+
48+
export default OrganizationProvisionersPage;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { screen, userEvent } from "@storybook/test";
3+
import {
4+
MockBuildInfo,
5+
MockProvisioner,
6+
MockProvisioner2,
7+
MockProvisionerBuiltinKey,
8+
MockProvisionerKey,
9+
MockProvisionerPskKey,
10+
MockProvisionerUserAuthKey,
11+
MockProvisionerWithTags,
12+
MockUserProvisioner,
13+
mockApiError,
14+
} from "testHelpers/entities";
15+
import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView";
16+
17+
const meta: Meta<typeof OrganizationProvisionersPageView> = {
18+
title: "pages/OrganizationProvisionersPage",
19+
component: OrganizationProvisionersPageView,
20+
args: {
21+
buildInfo: MockBuildInfo,
22+
},
23+
};
24+
25+
export default meta;
26+
type Story = StoryObj<typeof OrganizationProvisionersPageView>;
27+
28+
export const Provisioners: Story = {
29+
args: {
30+
provisioners: [
31+
{
32+
key: MockProvisionerBuiltinKey,
33+
daemons: [MockProvisioner, MockProvisioner2],
34+
},
35+
{
36+
key: MockProvisionerPskKey,
37+
daemons: [
38+
MockProvisioner,
39+
MockUserProvisioner,
40+
MockProvisionerWithTags,
41+
],
42+
},
43+
{
44+
key: MockProvisionerPskKey,
45+
daemons: [MockProvisioner, MockProvisioner2],
46+
},
47+
{
48+
key: { ...MockProvisionerKey, id: "ジェイデン", name: "ジェイデン" },
49+
daemons: [
50+
MockProvisioner,
51+
{ ...MockProvisioner2, tags: { scope: "organization", owner: "" } },
52+
],
53+
},
54+
{
55+
key: { ...MockProvisionerKey, id: "ベン", name: "ベン" },
56+
daemons: [
57+
MockProvisioner,
58+
{
59+
...MockProvisioner2,
60+
version: "2.0.0",
61+
api_version: "1.0",
62+
},
63+
],
64+
},
65+
{
66+
key: {
67+
...MockProvisionerKey,
68+
id: "ケイラ",
69+
name: "ケイラ",
70+
tags: {
71+
...MockProvisioner.tags,
72+
都市: "ユタ",
73+
きっぷ: "yes",
74+
ちいさい: "no",
75+
},
76+
},
77+
daemons: Array.from({ length: 117 }, (_, i) => ({
78+
...MockProvisioner,
79+
id: `ケイラ-${i}`,
80+
name: `ケイラ-${i}`,
81+
})),
82+
},
83+
{
84+
key: MockProvisionerUserAuthKey,
85+
daemons: [
86+
MockUserProvisioner,
87+
{
88+
...MockUserProvisioner,
89+
id: "mock-user-provisioner-2",
90+
name: "Test User Provisioner 2",
91+
},
92+
],
93+
},
94+
],
95+
},
96+
play: async ({ step }) => {
97+
await step("open all details", async () => {
98+
const expandButtons = await screen.findAllByRole("button", {
99+
name: "Show provisioner details",
100+
});
101+
for (const it of expandButtons) {
102+
await userEvent.click(it);
103+
}
104+
});
105+
106+
await step("close uninteresting/large details", async () => {
107+
const collapseButtons = await screen.findAllByRole("button", {
108+
name: "Hide provisioner details",
109+
});
110+
111+
await userEvent.click(collapseButtons[2]);
112+
await userEvent.click(collapseButtons[3]);
113+
await userEvent.click(collapseButtons[5]);
114+
});
115+
116+
await step("show version popover", async () => {
117+
const outOfDate = await screen.findByText("Out of date");
118+
await userEvent.hover(outOfDate);
119+
});
120+
},
121+
};
122+
123+
export const Empty: Story = {
124+
args: {
125+
provisioners: [],
126+
},
127+
};
128+
129+
export const WithError: Story = {
130+
args: {
131+
error: mockApiError({
132+
message: "Fern is mad",
133+
detail: "Frieren slept in and didn't get groceries",
134+
}),
135+
},
136+
};
137+
138+
export const Paywall: Story = {
139+
args: {
140+
showPaywall: true,
141+
},
142+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
2+
import Button from "@mui/material/Button";
3+
import type {
4+
BuildInfoResponse,
5+
ProvisionerKey,
6+
ProvisionerKeyDaemons,
7+
} from "api/typesGenerated";
8+
import { ErrorAlert } from "components/Alert/ErrorAlert";
9+
import { EmptyState } from "components/EmptyState/EmptyState";
10+
import { Loader } from "components/Loader/Loader";
11+
import { Paywall } from "components/Paywall/Paywall";
12+
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
13+
import { Stack } from "components/Stack/Stack";
14+
import { ProvisionerGroup } from "modules/provisioners/ProvisionerGroup";
15+
import type { FC } from "react";
16+
import { docs } from "utils/docs";
17+
18+
interface OrganizationProvisionersPageViewProps {
19+
/** Determines if the paywall will be shown or not */
20+
showPaywall?: boolean;
21+
22+
/** An error to display instead of the page content */
23+
error?: unknown;
24+
25+
/** Info about the version of coderd */
26+
buildInfo?: BuildInfoResponse;
27+
28+
/** Groups of provisioners, along with their key information */
29+
provisioners?: readonly ProvisionerKeyDaemons[];
30+
}
31+
32+
export const OrganizationProvisionersPageView: FC<
33+
OrganizationProvisionersPageViewProps
34+
> = ({ showPaywall, error, buildInfo, provisioners }) => {
35+
return (
36+
<div>
37+
<Stack
38+
alignItems="baseline"
39+
direction="row"
40+
justifyContent="space-between"
41+
>
42+
<SettingsHeader title="Provisioners" />
43+
{!showPaywall && (
44+
<Button
45+
endIcon={<OpenInNewIcon />}
46+
target="_blank"
47+
href={docs("/admin/provisioners")}
48+
>
49+
Create a provisioner
50+
</Button>
51+
)}
52+
</Stack>
53+
{showPaywall ? (
54+
<Paywall
55+
message="Provisioners"
56+
description="Provisioners run your Terraform to create templates and workspaces. You need a Premium license to use this feature for multiple organizations."
57+
documentationLink={docs("/")}
58+
/>
59+
) : error ? (
60+
<ErrorAlert error={error} />
61+
) : !buildInfo || !provisioners ? (
62+
<Loader />
63+
) : (
64+
<ViewContent buildInfo={buildInfo} provisioners={provisioners} />
65+
)}
66+
</div>
67+
);
68+
};
69+
70+
type ViewContentProps = Required<
71+
Pick<OrganizationProvisionersPageViewProps, "buildInfo" | "provisioners">
72+
>;
73+
74+
const ViewContent: FC<ViewContentProps> = ({ buildInfo, provisioners }) => {
75+
const isEmpty = provisioners.every((group) => group.daemons.length === 0);
76+
77+
const provisionerGroupsCount = provisioners.length;
78+
const provisionersCount = provisioners.reduce(
79+
(a, group) => a + group.daemons.length,
80+
0,
81+
);
82+
83+
return (
84+
<>
85+
{isEmpty ? (
86+
<EmptyState
87+
message="No provisioners"
88+
description="A provisioner is required before you can create templates and workspaces. You can connect your first provisioner by following our documentation."
89+
cta={
90+
<Button
91+
endIcon={<OpenInNewIcon />}
92+
target="_blank"
93+
href={docs("/admin/provisioners")}
94+
>
95+
Create a provisioner
96+
</Button>
97+
}
98+
/>
99+
) : (
100+
<div
101+
css={(theme) => ({
102+
margin: 0,
103+
fontSize: 12,
104+
paddingBottom: 18,
105+
color: theme.palette.text.secondary,
106+
})}
107+
>
108+
Showing {provisionerGroupsCount} groups and {provisionersCount}{" "}
109+
provisioners
110+
</div>
111+
)}
112+
<Stack spacing={4.5}>
113+
{provisioners.map((group) => (
114+
<ProvisionerGroup
115+
key={group.key.id}
116+
buildInfo={buildInfo}
117+
keyName={group.key.name}
118+
keyTags={group.key.tags}
119+
type={getGroupType(group.key)}
120+
provisioners={group.daemons}
121+
/>
122+
))}
123+
</Stack>
124+
</>
125+
);
126+
};
127+
128+
// Ideally these would be generated and appear in typesGenerated.ts, but that is
129+
// not currently the case. In the meantime, these are taken from verbatim from
130+
// the corresponding codersdk declarations. The names remain unchanged to keep
131+
// usage of these special values "grep-able".
132+
// https://github.com/coder/coder/blob/7c77a3cc832fb35d9da4ca27df163c740f786137/codersdk/provisionerdaemons.go#L291-L295
133+
const ProvisionerKeyIDBuiltIn = "00000000-0000-0000-0000-000000000001";
134+
const ProvisionerKeyIDUserAuth = "00000000-0000-0000-0000-000000000002";
135+
const ProvisionerKeyIDPSK = "00000000-0000-0000-0000-000000000003";
136+
137+
function getGroupType(key: ProvisionerKey) {
138+
switch (key.id) {
139+
case ProvisionerKeyIDBuiltIn:
140+
return "builtin";
141+
case ProvisionerKeyIDUserAuth:
142+
return "userAuth";
143+
case ProvisionerKeyIDPSK:
144+
return "psk";
145+
default:
146+
return "key";
147+
}
148+
}

site/src/router.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,7 @@ const CreateEditRolePage = lazy(
267267
),
268268
);
269269
const ProvisionersPage = lazy(
270-
() =>
271-
import(
272-
"./pages/OrganizationSettingsPage/ProvisionersPage/ProvisionersPage"
273-
),
270+
() => import("./pages/OrganizationSettingsPage/OrganizationProvisionersPage"),
274271
);
275272
const TemplateEmbedPage = lazy(
276273
() => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"),

0 commit comments

Comments
 (0)