Skip to content

Commit 96e9a4f

Browse files
authored
feat(site): add warnings and status indicator to provisioner groups (#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

Lines changed: 4 additions & 4 deletions
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

Lines changed: 12 additions & 0 deletions
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

Lines changed: 13 additions & 0 deletions
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
*
Lines changed: 63 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 94 additions & 60 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy