Skip to content

Commit 96e9a4f

Browse files
authored
feat(site): add warnings and status indicator to provisioner groups (coder#14708)
1 parent 86f68b2 commit 96e9a4f

File tree

10 files changed

+330
-182
lines changed

10 files changed

+330
-182
lines changed

cli/organizationsettings.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,13 @@ func (r *RootCmd) setOrganizationSettings(orgContext *OrganizationContext, setti
125125

126126
settingJSON, err := json.Marshal(output)
127127
if err != nil {
128-
return fmt.Errorf("failed to marshal organization setting %s: %w", inv.Args[0], err)
128+
return xerrors.Errorf("failed to marshal organization setting %s: %w", inv.Args[0], err)
129129
}
130130

131131
var dst bytes.Buffer
132132
err = json.Indent(&dst, settingJSON, "", "\t")
133133
if err != nil {
134-
return fmt.Errorf("failed to indent organization setting as json %s: %w", inv.Args[0], err)
134+
return xerrors.Errorf("failed to indent organization setting as json %s: %w", inv.Args[0], err)
135135
}
136136

137137
_, err = fmt.Fprintln(inv.Stdout, dst.String())
@@ -190,13 +190,13 @@ func (r *RootCmd) printOrganizationSetting(orgContext *OrganizationContext, sett
190190

191191
settingJSON, err := json.Marshal(output)
192192
if err != nil {
193-
return fmt.Errorf("failed to marshal organization setting %s: %w", inv.Args[0], err)
193+
return xerrors.Errorf("failed to marshal organization setting %s: %w", inv.Args[0], err)
194194
}
195195

196196
var dst bytes.Buffer
197197
err = json.Indent(&dst, settingJSON, "", "\t")
198198
if err != nil {
199-
return fmt.Errorf("failed to indent organization setting as json %s: %w", inv.Args[0], err)
199+
return xerrors.Errorf("failed to indent organization setting as json %s: %w", inv.Args[0], err)
200200
}
201201

202202
_, err = fmt.Fprintln(inv.Stdout, dst.String())

site/src/api/api.ts

+12
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,18 @@ class ApiMethods {
692692
return response.data;
693693
};
694694

695+
/**
696+
* @param organization Can be the organization's ID or name
697+
*/
698+
getProvisionerDaemonGroupsByOrganization = async (
699+
organization: string,
700+
): Promise<TypesGen.ProvisionerKeyDaemons[]> => {
701+
const response = await this.axios.get<TypesGen.ProvisionerKeyDaemons[]>(
702+
`/api/v2/organizations/${organization}/provisionerkeys/daemons`,
703+
);
704+
return response.data;
705+
};
706+
695707
getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
696708
const response = await this.axios.get<TypesGen.Template>(
697709
`/api/v2/templates/${templateId}`,

site/src/api/queries/organizations.ts

+13
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,19 @@ export const provisionerDaemons = (organization: string) => {
128128
};
129129
};
130130

131+
export const getProvisionerDaemonGroupsKey = (organization: string) => [
132+
"organization",
133+
organization,
134+
"provisionerDaemons",
135+
];
136+
137+
export const provisionerDaemonGroups = (organization: string) => {
138+
return {
139+
queryKey: getProvisionerDaemonGroupsKey(organization),
140+
queryFn: () => API.getProvisionerDaemonGroupsByOrganization(organization),
141+
};
142+
};
143+
131144
/**
132145
* Fetch permissions for a single organization.
133146
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { StatusIndicator } from "./StatusIndicator";
3+
4+
const meta: Meta<typeof StatusIndicator> = {
5+
title: "components/StatusIndicator",
6+
component: StatusIndicator,
7+
args: {},
8+
};
9+
10+
export default meta;
11+
type Story = StoryObj<typeof StatusIndicator>;
12+
13+
export const Success: Story = {
14+
args: {
15+
color: "success",
16+
},
17+
};
18+
19+
export const SuccessOutline: Story = {
20+
args: {
21+
color: "success",
22+
variant: "outlined",
23+
},
24+
};
25+
26+
export const Warning: Story = {
27+
args: {
28+
color: "warning",
29+
},
30+
};
31+
32+
export const WarningOutline: Story = {
33+
args: {
34+
color: "warning",
35+
variant: "outlined",
36+
},
37+
};
38+
39+
export const Danger: Story = {
40+
args: {
41+
color: "danger",
42+
},
43+
};
44+
45+
export const DangerOutline: Story = {
46+
args: {
47+
color: "danger",
48+
variant: "outlined",
49+
},
50+
};
51+
52+
export const Inactive: Story = {
53+
args: {
54+
color: "inactive",
55+
},
56+
};
57+
58+
export const InactiveOutline: Story = {
59+
args: {
60+
color: "inactive",
61+
variant: "outlined",
62+
},
63+
};

site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const Example: Story = {
3131
await step("click to open", async () => {
3232
await userEvent.click(canvas.getByRole("button"));
3333
await waitFor(() =>
34-
expect(screen.getByText(/v2\.99\.99/i)).toBeInTheDocument(),
34+
expect(screen.getByText(/v2\.\d+\.\d+/i)).toBeInTheDocument(),
3535
);
3636
});
3737
},

site/src/modules/provisioners/ProvisionerGroup.tsx

+94-60
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
PopoverTrigger,
2121
} from "components/Popover/Popover";
2222
import { Stack } from "components/Stack/Stack";
23+
import { StatusIndicator } from "components/StatusIndicator/StatusIndicator";
2324
import { type FC, useState } from "react";
2425
import { createDayString } from "utils/createDayString";
2526
import { docs } from "utils/docs";
@@ -31,7 +32,7 @@ interface ProvisionerGroupProps {
3132
readonly buildInfo?: BuildInfoResponse;
3233
readonly keyName?: string;
3334
readonly type: ProvisionerGroupType;
34-
readonly provisioners: ProvisionerDaemon[];
35+
readonly provisioners: readonly ProvisionerDaemon[];
3536
}
3637

3738
export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
@@ -40,36 +41,65 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
4041
type,
4142
provisioners,
4243
}) => {
43-
const [provisioner] = provisioners;
4444
const theme = useTheme();
4545

4646
const [showDetails, setShowDetails] = useState(false);
4747

48-
const daemonScope = provisioner.tags.scope || "organization";
49-
const iconScope = daemonScope === "organization" ? <Business /> : <Person />;
48+
const firstProvisioner = provisioners[0];
49+
if (!firstProvisioner) {
50+
return null;
51+
}
5052

51-
const provisionerVersion = provisioner.version;
53+
const daemonScope = firstProvisioner.tags.scope || "organization";
5254
const allProvisionersAreSameVersion = provisioners.every(
53-
(provisioner) => provisioner.version === provisionerVersion,
55+
(it) => it.version === firstProvisioner.version,
5456
);
55-
const upToDate =
56-
allProvisionersAreSameVersion && buildInfo?.version === provisioner.version;
57+
const provisionerVersion = allProvisionersAreSameVersion
58+
? firstProvisioner.version
59+
: null;
5760
const provisionerCount =
5861
provisioners.length === 1
5962
? "1 provisioner"
6063
: `${provisioners.length} provisioners`;
61-
62-
const extraTags = Object.entries(provisioner.tags).filter(
64+
const extraTags = Object.entries(firstProvisioner.tags).filter(
6365
([key]) => key !== "scope" && key !== "owner",
6466
);
6567

68+
let warnings = 0;
69+
let provisionersWithWarnings = 0;
70+
const provisionersWithWarningInfo = provisioners.map((it) => {
71+
const outOfDate = Boolean(buildInfo) && it.version !== buildInfo?.version;
72+
const warningCount = outOfDate ? 1 : 0;
73+
warnings += warningCount;
74+
if (warnings > 0) {
75+
provisionersWithWarnings++;
76+
}
77+
78+
return { ...it, warningCount, outOfDate };
79+
});
80+
81+
const hasWarning = warnings > 0;
82+
const warningsCount =
83+
warnings === 0
84+
? "No warnings"
85+
: warnings === 1
86+
? "1 warning"
87+
: `${warnings} warnings`;
88+
const provisionersWithWarningsCount =
89+
provisionersWithWarnings === 1
90+
? "1 provisioner"
91+
: `${provisionersWithWarnings} provisioners`;
92+
6693
return (
6794
<div
68-
css={{
69-
borderRadius: 8,
70-
border: `1px solid ${theme.palette.divider}`,
71-
fontSize: 14,
72-
}}
95+
css={[
96+
{
97+
borderRadius: 8,
98+
border: `1px solid ${theme.palette.divider}`,
99+
fontSize: 14,
100+
},
101+
hasWarning && styles.warningBorder,
102+
]}
73103
>
74104
<header
75105
css={{
@@ -80,48 +110,39 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
80110
gap: 24,
81111
}}
82112
>
83-
<div
84-
css={{
85-
display: "flex",
86-
alignItems: "center",
87-
gap: 24,
88-
objectFit: "fill",
89-
}}
90-
>
91-
{type === "builtin" && (
92-
<div css={{ lineHeight: "160%" }}>
93-
<BuiltinProvisionerTitle />
94-
<span css={{ color: theme.palette.text.secondary }}>
95-
{provisionerCount} &mdash; Built-in
96-
</span>
97-
</div>
98-
)}
99-
{type === "psk" && (
100-
<div css={{ lineHeight: "160%" }}>
101-
<PskProvisionerTitle />
102-
<span css={{ color: theme.palette.text.secondary }}>
103-
{provisionerCount} &mdash;{" "}
104-
{allProvisionersAreSameVersion ? (
105-
<code>{provisionerVersion}</code>
106-
) : (
107-
<span>Multiple versions</span>
108-
)}
109-
</span>
110-
</div>
111-
)}
112-
{type === "key" && (
113-
<div css={{ lineHeight: "160%" }}>
113+
<div css={{ display: "flex", alignItems: "center", gap: 16 }}>
114+
<StatusIndicator color={hasWarning ? "warning" : "success"} />
115+
<div
116+
css={{
117+
display: "flex",
118+
flexDirection: "column",
119+
lineHeight: 1.5,
120+
}}
121+
>
122+
{type === "builtin" && (
123+
<>
124+
<BuiltinProvisionerTitle />
125+
<span css={{ color: theme.palette.text.secondary }}>
126+
{provisionerCount} &mdash; Built-in
127+
</span>
128+
</>
129+
)}
130+
131+
{type === "psk" && <PskProvisionerTitle />}
132+
{type === "key" && (
114133
<h4 css={styles.groupTitle}>Key group &ndash; {keyName}</h4>
134+
)}
135+
{type !== "builtin" && (
115136
<span css={{ color: theme.palette.text.secondary }}>
116137
{provisionerCount} &mdash;{" "}
117-
{allProvisionersAreSameVersion ? (
138+
{provisionerVersion ? (
118139
<code>{provisionerVersion}</code>
119140
) : (
120141
<span>Multiple versions</span>
121142
)}
122143
</span>
123-
</div>
124-
)}
144+
)}
145+
</div>
125146
</div>
126147
<div
127148
css={{
@@ -133,7 +154,10 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
133154
}}
134155
>
135156
<Tooltip title="Scope">
136-
<Pill size="lg" icon={iconScope}>
157+
<Pill
158+
size="lg"
159+
icon={daemonScope === "organization" ? <Business /> : <Person />}
160+
>
137161
<span css={{ textTransform: "capitalize" }}>{daemonScope}</span>
138162
</Pill>
139163
</Tooltip>
@@ -153,16 +177,19 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
153177
flexWrap: "wrap",
154178
}}
155179
>
156-
{provisioners.map((provisioner) => (
180+
{provisionersWithWarningInfo.map((provisioner) => (
157181
<div
158182
key={provisioner.id}
159-
css={{
160-
borderRadius: 8,
161-
border: `1px solid ${theme.palette.divider}`,
162-
fontSize: 14,
163-
padding: "14px 18px",
164-
width: 375,
165-
}}
183+
css={[
184+
{
185+
borderRadius: 8,
186+
border: `1px solid ${theme.palette.divider}`,
187+
fontSize: 14,
188+
padding: "14px 18px",
189+
width: 375,
190+
},
191+
provisioner.warningCount > 0 && styles.warningBorder,
192+
]}
166193
>
167194
<Stack
168195
direction="row"
@@ -215,7 +242,10 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
215242
color: theme.palette.text.secondary,
216243
}}
217244
>
218-
<span>No warnings from {provisionerCount}</span>
245+
<span>
246+
{warningsCount} from{" "}
247+
{hasWarning ? provisionersWithWarningsCount : provisionerCount}
248+
</span>
219249
<Button
220250
variant="text"
221251
css={{
@@ -379,6 +409,10 @@ const PskProvisionerTitle: FC = () => {
379409
};
380410

381411
const styles = {
412+
warningBorder: (theme) => ({
413+
borderColor: theme.roles.warning.fill.outline,
414+
}),
415+
382416
groupTitle: {
383417
fontWeight: 500,
384418
margin: 0,
@@ -389,7 +423,7 @@ const styles = {
389423
marginBottom: 0,
390424
color: theme.palette.text.primary,
391425
fontSize: 14,
392-
lineHeight: "150%",
426+
lineHeight: 1.5,
393427
fontWeight: 600,
394428
}),
395429

0 commit comments

Comments
 (0)