Skip to content

Commit dda6bdc

Browse files
authored
feat: group provisioners by authentication method (#14580)
1 parent d96adad commit dda6bdc

File tree

6 files changed

+382
-23
lines changed

6 files changed

+382
-23
lines changed

site/src/modules/provisioners/Provisioner.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export const Provisioner: FC<ProvisionerProps> = ({
6767
display: "flex",
6868
flexWrap: "wrap",
6969
gap: 12,
70+
justifyContent: "right",
7071
}}
7172
>
7273
<Tooltip title="Scope">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { useTheme } from "@emotion/react";
2+
import Business from "@mui/icons-material/Business";
3+
import Person from "@mui/icons-material/Person";
4+
import Button from "@mui/material/Button";
5+
import Tooltip from "@mui/material/Tooltip";
6+
import type { BuildInfoResponse, ProvisionerDaemon } from "api/typesGenerated";
7+
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
8+
import { Pill } from "components/Pill/Pill";
9+
import { type FC, useState } from "react";
10+
import { createDayString } from "utils/createDayString";
11+
import { ProvisionerTag } from "./ProvisionerTag";
12+
13+
type ProvisionerGroupType = "builtin" | "psk" | "key";
14+
15+
interface ProvisionerGroupProps {
16+
readonly buildInfo?: BuildInfoResponse;
17+
readonly keyName?: string;
18+
readonly type: ProvisionerGroupType;
19+
readonly provisioners: ProvisionerDaemon[];
20+
}
21+
22+
export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
23+
buildInfo,
24+
keyName,
25+
type,
26+
provisioners,
27+
}) => {
28+
const [provisioner] = provisioners;
29+
const theme = useTheme();
30+
31+
const [showDetails, setShowDetails] = useState(false);
32+
33+
const daemonScope = provisioner.tags.scope || "organization";
34+
const iconScope = daemonScope === "organization" ? <Business /> : <Person />;
35+
36+
const provisionerVersion = provisioner.version;
37+
const allProvisionersAreSameVersion = provisioners.every(
38+
(provisioner) => provisioner.version === provisionerVersion,
39+
);
40+
const upToDate =
41+
allProvisionersAreSameVersion && buildInfo?.version === provisioner.version;
42+
const provisionerCount =
43+
provisioners.length === 1
44+
? "1 provisioner"
45+
: `${provisioners.length} provisioners`;
46+
47+
const extraTags = Object.entries(provisioner.tags).filter(
48+
([key]) => key !== "scope" && key !== "owner",
49+
);
50+
51+
return (
52+
<div
53+
css={{
54+
borderRadius: 8,
55+
border: `1px solid ${theme.palette.divider}`,
56+
fontSize: 14,
57+
}}
58+
>
59+
<header
60+
css={{
61+
padding: 24,
62+
display: "flex",
63+
alignItems: "center",
64+
justifyContenxt: "space-between",
65+
gap: 24,
66+
}}
67+
>
68+
<div
69+
css={{
70+
display: "flex",
71+
alignItems: "center",
72+
gap: 24,
73+
objectFit: "fill",
74+
}}
75+
>
76+
{type === "builtin" && (
77+
<div css={{ lineHeight: "160%" }}>
78+
<h4 css={{ fontWeight: 500, margin: 0 }}>
79+
Built-in provisioners
80+
</h4>
81+
<span css={{ color: theme.palette.text.secondary }}>
82+
{provisionerCount} &mdash; Built-in
83+
</span>
84+
</div>
85+
)}
86+
{type === "psk" && (
87+
<div css={{ lineHeight: "160%" }}>
88+
<h4 css={{ fontWeight: 500, margin: 0 }}>PSK provisioners</h4>
89+
<span css={{ color: theme.palette.text.secondary }}>
90+
{provisionerCount} &mdash;{" "}
91+
{allProvisionersAreSameVersion ? (
92+
<code>{provisionerVersion}</code>
93+
) : (
94+
<span>Multiple versions</span>
95+
)}
96+
</span>
97+
</div>
98+
)}
99+
{type === "key" && (
100+
<div css={{ lineHeight: "160%" }}>
101+
<h4 css={{ fontWeight: 500, margin: 0 }}>
102+
Key group &ndash; {keyName}
103+
</h4>
104+
<span css={{ color: theme.palette.text.secondary }}>
105+
{provisionerCount} &mdash;{" "}
106+
{allProvisionersAreSameVersion ? (
107+
<code>{provisionerVersion}</code>
108+
) : (
109+
<span>Multiple versions</span>
110+
)}
111+
</span>
112+
</div>
113+
)}
114+
</div>
115+
<div
116+
css={{
117+
marginLeft: "auto",
118+
display: "flex",
119+
flexWrap: "wrap",
120+
gap: 12,
121+
justifyContent: "right",
122+
}}
123+
>
124+
<Tooltip title="Scope">
125+
<Pill size="lg" icon={iconScope}>
126+
<span
127+
css={{
128+
":first-letter": { textTransform: "uppercase" },
129+
}}
130+
>
131+
{daemonScope}
132+
</span>
133+
</Pill>
134+
</Tooltip>
135+
{type === "key" &&
136+
extraTags.map(([key, value]) => (
137+
<ProvisionerTag key={key} tagName={key} tagValue={value} />
138+
))}
139+
</div>
140+
</header>
141+
142+
{showDetails && (
143+
<div
144+
css={{
145+
padding: "0 24px 24px",
146+
display: "flex",
147+
gap: 12,
148+
flexWrap: "wrap",
149+
}}
150+
>
151+
{provisioners.map((provisioner) => (
152+
<div
153+
key={provisioner.id}
154+
css={{
155+
borderRadius: 8,
156+
border: `1px solid ${theme.palette.divider}`,
157+
fontSize: 14,
158+
padding: "12px 18px",
159+
width: 310,
160+
}}
161+
>
162+
<div css={{ lineHeight: "160%" }}>
163+
<h4 css={{ fontWeight: 500, margin: 0 }}>{provisioner.name}</h4>
164+
<span css={{ color: theme.palette.text.secondary }}>
165+
{type === "builtin" ? (
166+
<span>Built-in</span>
167+
) : (
168+
<>
169+
{upToDate ? "Up to date" : provisioner.version} &mdash;{" "}
170+
{provisioner.last_seen_at && (
171+
<span data-chromatic="ignore">
172+
Last seen {createDayString(provisioner.last_seen_at)}
173+
</span>
174+
)}
175+
</>
176+
)}
177+
</span>
178+
</div>
179+
</div>
180+
))}
181+
</div>
182+
)}
183+
184+
<div
185+
css={{
186+
borderTop: `1px solid ${theme.palette.divider}`,
187+
display: "flex",
188+
alignItems: "center",
189+
justifyContent: "space-between",
190+
padding: "8px 8px 8px 24px",
191+
fontSize: 12,
192+
color: theme.palette.text.secondary,
193+
}}
194+
>
195+
<span>No warnings from {provisionerCount}</span>
196+
<Button
197+
variant="text"
198+
css={{
199+
display: "flex",
200+
alignItems: "center",
201+
gap: 4,
202+
color: theme.roles.info.text,
203+
fontSize: "inherit",
204+
}}
205+
onClick={() => setShowDetails((it) => !it)}
206+
>
207+
{showDetails ? "Hide" : "Show"} provisioner details{" "}
208+
<DropdownArrow close={showDetails} />
209+
</Button>
210+
</div>
211+
</div>
212+
);
213+
};

site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx

+56-3
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,72 @@
1+
import { buildInfo } from "api/queries/buildInfo";
12
import {
23
organizationsPermissions,
34
provisionerDaemons,
45
} from "api/queries/organizations";
5-
import type { Organization } from "api/typesGenerated";
6+
import type { Organization, ProvisionerDaemon } from "api/typesGenerated";
67
import { ErrorAlert } from "components/Alert/ErrorAlert";
78
import { EmptyState } from "components/EmptyState/EmptyState";
89
import { Loader } from "components/Loader/Loader";
10+
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
911
import NotFoundPage from "pages/404Page/404Page";
1012
import type { FC } from "react";
1113
import { useQuery } from "react-query";
1214
import { useParams } from "react-router-dom";
1315
import { useOrganizationSettings } from "./ManagementSettingsLayout";
14-
import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView";
16+
import {
17+
OrganizationProvisionersPageView,
18+
type ProvisionersByGroup,
19+
} from "./OrganizationProvisionersPageView";
20+
21+
const ProvisionerKeyIDBuiltIn = "00000000-0000-0000-0000-000000000001";
22+
const ProvisionerKeyIDUserAuth = "00000000-0000-0000-0000-000000000002";
23+
const ProvisionerKeyIDPSK = "00000000-0000-0000-0000-000000000003";
24+
25+
function groupProvisioners(
26+
provisioners: readonly ProvisionerDaemon[],
27+
): ProvisionersByGroup {
28+
const groups: ProvisionersByGroup = {
29+
builtin: [],
30+
psk: [],
31+
userAuth: [],
32+
keys: new Map(),
33+
};
34+
// NOTE: I'll fix this at the end of the PR chain
35+
const keyName = "TODO";
36+
37+
for (const it of provisioners) {
38+
if (it.key_id === ProvisionerKeyIDBuiltIn) {
39+
groups.builtin.push(it);
40+
continue;
41+
}
42+
if (it.key_id === ProvisionerKeyIDPSK) {
43+
groups.psk.push(it);
44+
continue;
45+
}
46+
if (it.key_id === ProvisionerKeyIDUserAuth) {
47+
groups.userAuth.push(it);
48+
continue;
49+
}
50+
51+
const keyGroup = groups.keys.get(keyName) ?? [];
52+
if (!groups.keys.has(keyName)) {
53+
groups.keys.set(keyName, keyGroup);
54+
}
55+
keyGroup.push(it);
56+
}
57+
58+
return groups;
59+
}
1560

1661
const OrganizationProvisionersPage: FC = () => {
1762
const { organization: organizationName } = useParams() as {
1863
organization: string;
1964
};
2065
const { organizations } = useOrganizationSettings();
2166

67+
const { metadata } = useEmbeddedMetadata();
68+
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
69+
2270
const organization = organizations
2371
? getOrganizationByName(organizations, organizationName)
2472
: undefined;
@@ -54,7 +102,12 @@ const OrganizationProvisionersPage: FC = () => {
54102
return <NotFoundPage />;
55103
}
56104

57-
return <OrganizationProvisionersPageView provisioners={provisioners} />;
105+
return (
106+
<OrganizationProvisionersPageView
107+
buildInfo={buildInfoQuery.data}
108+
provisioners={groupProvisioners(provisioners)}
109+
/>
110+
);
58111
};
59112

60113
export default OrganizationProvisionersPage;
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,49 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2-
import { MockProvisioner, MockUserProvisioner } from "testHelpers/entities";
2+
import {
3+
MockBuildInfo,
4+
MockProvisioner,
5+
MockProvisioner2,
6+
MockProvisionerWithTags,
7+
MockUserProvisioner,
8+
} from "testHelpers/entities";
39
import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView";
410

511
const meta: Meta<typeof OrganizationProvisionersPageView> = {
612
title: "pages/OrganizationProvisionersPage",
713
component: OrganizationProvisionersPageView,
14+
args: {
15+
buildInfo: MockBuildInfo,
16+
},
817
};
918

1019
export default meta;
1120
type Story = StoryObj<typeof OrganizationProvisionersPageView>;
1221

1322
export const Provisioners: Story = {
1423
args: {
15-
provisioners: [
16-
MockProvisioner,
17-
MockUserProvisioner,
18-
{
19-
...MockProvisioner,
20-
tags: {
21-
...MockProvisioner.tags,
22-
都市: "ユタ",
23-
きっぷ: "yes",
24-
ちいさい: "no",
25-
},
26-
},
27-
],
24+
provisioners: {
25+
builtin: [MockProvisioner, MockProvisioner2],
26+
psk: [MockProvisioner, MockUserProvisioner, MockProvisionerWithTags],
27+
userAuth: [],
28+
keys: new Map([
29+
[
30+
"ケイラ",
31+
[
32+
{
33+
...MockProvisioner,
34+
tags: {
35+
...MockProvisioner.tags,
36+
都市: "ユタ",
37+
きっぷ: "yes",
38+
ちいさい: "no",
39+
},
40+
warnings: [
41+
{ code: "EUNKNOWN", message: "私は日本語が話せません" },
42+
],
43+
},
44+
],
45+
],
46+
]),
47+
},
2848
},
2949
};

0 commit comments

Comments
 (0)