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

Skip to content

Commit 84d312c

Browse files
fix(site): only show method warning if some template is using it (coder#14565)
Previously, we were showing the warning regardless of whether a template was using the misconfigured notification method or not. However, we realized this could be too noisy, so we decided to display the warning only when the user has a template configured to use the misconfigured method.
1 parent 92b81c4 commit 84d312c

File tree

5 files changed

+587
-451
lines changed

5 files changed

+587
-451
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { spyOn, userEvent, within } from "@storybook/test";
3+
import { API } from "api/api";
4+
import { selectTemplatesByGroup } from "api/queries/notifications";
5+
import type { DeploymentValues } from "api/typesGenerated";
6+
import { MockNotificationTemplates } from "testHelpers/entities";
7+
import { NotificationEvents } from "./NotificationEvents";
8+
import { baseMeta } from "./storybookUtils";
9+
10+
const meta: Meta<typeof NotificationEvents> = {
11+
title: "pages/DeploymentSettings/NotificationsPage/NotificationEvents",
12+
component: NotificationEvents,
13+
args: {
14+
defaultMethod: "smtp",
15+
availableMethods: ["smtp", "webhook"],
16+
templatesByGroup: selectTemplatesByGroup(MockNotificationTemplates),
17+
deploymentValues: baseMeta.parameters.deploymentValues,
18+
},
19+
...baseMeta,
20+
};
21+
22+
export default meta;
23+
24+
type Story = StoryObj<typeof NotificationEvents>;
25+
26+
export const SMTPNotConfigured: Story = {
27+
args: {
28+
deploymentValues: {
29+
notifications: {
30+
webhook: {
31+
endpoint: "https://example.com",
32+
},
33+
email: {
34+
smarthost: "",
35+
},
36+
},
37+
} as DeploymentValues,
38+
},
39+
};
40+
41+
export const WebhookNotConfigured: Story = {
42+
args: {
43+
deploymentValues: {
44+
notifications: {
45+
webhook: {
46+
endpoint: "",
47+
},
48+
email: {
49+
smarthost: "smtp.example.com",
50+
from: "bob@localhost",
51+
hello: "localhost",
52+
},
53+
},
54+
} as DeploymentValues,
55+
},
56+
};
57+
58+
export const Toggle: Story = {
59+
play: async ({ canvasElement }) => {
60+
spyOn(API, "updateNotificationTemplateMethod").mockResolvedValue();
61+
const user = userEvent.setup();
62+
const canvas = within(canvasElement);
63+
const tmpl = MockNotificationTemplates[4];
64+
const option = await canvas.findByText(tmpl.name);
65+
const li = option.closest("li");
66+
if (!li) {
67+
throw new Error("Could not find li");
68+
}
69+
const toggleButton = within(li).getByRole("button", {
70+
name: "Webhook",
71+
});
72+
await user.click(toggleButton);
73+
await within(document.body).findByText("Notification method updated");
74+
},
75+
};
76+
77+
export const ToggleError: Story = {
78+
play: async ({ canvasElement }) => {
79+
spyOn(API, "updateNotificationTemplateMethod").mockRejectedValue({});
80+
const user = userEvent.setup();
81+
const canvas = within(canvasElement);
82+
const tmpl = MockNotificationTemplates[4];
83+
const option = await canvas.findByText(tmpl.name);
84+
const li = option.closest("li");
85+
if (!li) {
86+
throw new Error("Could not find li");
87+
}
88+
const toggleButton = within(li).getByRole("button", {
89+
name: "Webhook",
90+
});
91+
await user.click(toggleButton);
92+
await within(document.body).findByText(
93+
"Failed to update notification method",
94+
);
95+
},
96+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import Button from "@mui/material/Button";
3+
import Card from "@mui/material/Card";
4+
import Divider from "@mui/material/Divider";
5+
import List from "@mui/material/List";
6+
import ListItem from "@mui/material/ListItem";
7+
import ListItemText, { listItemTextClasses } from "@mui/material/ListItemText";
8+
import ToggleButton from "@mui/material/ToggleButton";
9+
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
10+
import Tooltip from "@mui/material/Tooltip";
11+
import { getErrorMessage } from "api/errors";
12+
import {
13+
type selectTemplatesByGroup,
14+
updateNotificationTemplateMethod,
15+
} from "api/queries/notifications";
16+
import type { DeploymentValues } from "api/typesGenerated";
17+
import { Alert } from "components/Alert/Alert";
18+
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
19+
import { Stack } from "components/Stack/Stack";
20+
import {
21+
type NotificationMethod,
22+
castNotificationMethod,
23+
methodIcons,
24+
methodLabels,
25+
} from "modules/notifications/utils";
26+
import { type FC, Fragment } from "react";
27+
import { useMutation, useQueryClient } from "react-query";
28+
import { docs } from "utils/docs";
29+
30+
type NotificationEventsProps = {
31+
defaultMethod: NotificationMethod;
32+
availableMethods: NotificationMethod[];
33+
templatesByGroup: ReturnType<typeof selectTemplatesByGroup>;
34+
deploymentValues: DeploymentValues;
35+
};
36+
37+
export const NotificationEvents: FC<NotificationEventsProps> = ({
38+
defaultMethod,
39+
availableMethods,
40+
templatesByGroup,
41+
deploymentValues,
42+
}) => {
43+
// Webhook
44+
const hasWebhookNotifications = Object.values(templatesByGroup)
45+
.flat()
46+
.some((t) => t.method === "webhook");
47+
const webhookValues = deploymentValues.notifications?.webhook ?? {};
48+
const isWebhookConfigured = requiredFieldsArePresent(webhookValues, [
49+
"endpoint",
50+
]);
51+
52+
// SMTP
53+
const hasSMTPNotifications = Object.values(templatesByGroup)
54+
.flat()
55+
.some((t) => t.method === "smtp");
56+
const smtpValues = deploymentValues.notifications?.email ?? {};
57+
const isSMTPConfigured = requiredFieldsArePresent(smtpValues, [
58+
"smarthost",
59+
"from",
60+
"hello",
61+
]);
62+
63+
return (
64+
<Stack spacing={4}>
65+
{hasWebhookNotifications && !isWebhookConfigured && (
66+
<Alert
67+
severity="warning"
68+
actions={
69+
<Button
70+
variant="text"
71+
size="small"
72+
component="a"
73+
target="_blank"
74+
rel="noreferrer"
75+
href={docs("/admin/notifications#webhook")}
76+
>
77+
Read the docs
78+
</Button>
79+
}
80+
>
81+
Webhook notifications are enabled, but not properly configured.
82+
</Alert>
83+
)}
84+
85+
{hasSMTPNotifications && !isSMTPConfigured && (
86+
<Alert
87+
severity="warning"
88+
actions={
89+
<Button
90+
variant="text"
91+
size="small"
92+
component="a"
93+
target="_blank"
94+
rel="noreferrer"
95+
href={docs("/admin/notifications#smtp-email")}
96+
>
97+
Read the docs
98+
</Button>
99+
}
100+
>
101+
SMTP notifications are enabled but not properly configured.
102+
</Alert>
103+
)}
104+
105+
{Object.entries(templatesByGroup).map(([group, templates]) => (
106+
<Card
107+
key={group}
108+
variant="outlined"
109+
css={{ background: "transparent", width: "100%" }}
110+
>
111+
<List>
112+
<ListItem css={styles.listHeader}>
113+
<ListItemText css={styles.listItemText} primary={group} />
114+
</ListItem>
115+
116+
{templates.map((tpl, i) => {
117+
const value = castNotificationMethod(tpl.method || defaultMethod);
118+
const isLastItem = i === templates.length - 1;
119+
120+
return (
121+
<Fragment key={tpl.id}>
122+
<ListItem>
123+
<ListItemText
124+
css={styles.listItemText}
125+
primary={tpl.name}
126+
/>
127+
<MethodToggleGroup
128+
templateId={tpl.id}
129+
options={availableMethods}
130+
value={value}
131+
/>
132+
</ListItem>
133+
{!isLastItem && <Divider />}
134+
</Fragment>
135+
);
136+
})}
137+
</List>
138+
</Card>
139+
))}
140+
</Stack>
141+
);
142+
};
143+
144+
function requiredFieldsArePresent(
145+
obj: Record<string, string | undefined>,
146+
fields: string[],
147+
): boolean {
148+
return fields.every((field) => Boolean(obj[field]));
149+
}
150+
151+
type MethodToggleGroupProps = {
152+
templateId: string;
153+
options: NotificationMethod[];
154+
value: NotificationMethod;
155+
};
156+
157+
const MethodToggleGroup: FC<MethodToggleGroupProps> = ({
158+
value,
159+
options,
160+
templateId,
161+
}) => {
162+
const queryClient = useQueryClient();
163+
const updateMethodMutation = useMutation(
164+
updateNotificationTemplateMethod(templateId, queryClient),
165+
);
166+
167+
return (
168+
<ToggleButtonGroup
169+
exclusive
170+
value={value}
171+
size="small"
172+
aria-label="Notification method"
173+
css={styles.toggleGroup}
174+
onChange={async (_, method) => {
175+
try {
176+
await updateMethodMutation.mutateAsync({
177+
method,
178+
});
179+
displaySuccess("Notification method updated");
180+
} catch (error) {
181+
displayError(
182+
getErrorMessage(error, "Failed to update notification method"),
183+
);
184+
}
185+
}}
186+
>
187+
{options.map((method) => {
188+
const Icon = methodIcons[method];
189+
const label = methodLabels[method];
190+
return (
191+
<Tooltip key={method} title={label}>
192+
<ToggleButton
193+
value={method}
194+
css={styles.toggleButton}
195+
onClick={(e) => {
196+
// Retain the value if the user clicks the same button, ensuring
197+
// at least one value remains selected.
198+
if (method === value) {
199+
e.preventDefault();
200+
e.stopPropagation();
201+
return;
202+
}
203+
}}
204+
>
205+
<Icon aria-label={label} />
206+
</ToggleButton>
207+
</Tooltip>
208+
);
209+
})}
210+
</ToggleButtonGroup>
211+
);
212+
};
213+
214+
const styles = {
215+
listHeader: (theme) => ({
216+
background: theme.palette.background.paper,
217+
borderBottom: `1px solid ${theme.palette.divider}`,
218+
}),
219+
listItemText: {
220+
[`& .${listItemTextClasses.primary}`]: {
221+
fontSize: 14,
222+
fontWeight: 500,
223+
},
224+
[`& .${listItemTextClasses.secondary}`]: {
225+
fontSize: 14,
226+
},
227+
},
228+
toggleGroup: (theme) => ({
229+
border: `1px solid ${theme.palette.divider}`,
230+
borderRadius: 4,
231+
}),
232+
toggleButton: (theme) => ({
233+
border: 0,
234+
borderRadius: 4,
235+
fontSize: 16,
236+
padding: "4px 8px",
237+
color: theme.palette.text.disabled,
238+
239+
"&:hover": {
240+
color: theme.palette.text.primary,
241+
},
242+
243+
"& svg": {
244+
fontSize: "inherit",
245+
},
246+
}),
247+
} as Record<string, Interpolation<Theme>>;

0 commit comments

Comments
 (0)