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

Skip to content

feat(site): implement notification ui #14175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fc282e7
Add base notification settings page to deployment
BrunoQuaresma Jul 29, 2024
997e0d3
Add base notifications components
BrunoQuaresma Jul 29, 2024
73f8471
Bind notifications into the user account notifications page
BrunoQuaresma Aug 2, 2024
33c9ab0
Remove deployment notifications page
BrunoQuaresma Aug 2, 2024
bbc8cbb
Add test for toggling notifications
BrunoQuaresma Aug 2, 2024
7227a42
Update migration
BrunoQuaresma Aug 5, 2024
788de88
Update template notification methods
BrunoQuaresma Aug 5, 2024
1262f7b
Fix types
BrunoQuaresma Aug 5, 2024
e449e3d
Fix remaining type issues
BrunoQuaresma Aug 5, 2024
0154b94
Experience improvements
BrunoQuaresma Aug 5, 2024
aedf159
Fix validation
BrunoQuaresma Aug 5, 2024
1a2efae
Remove BE changes
BrunoQuaresma Aug 6, 2024
a1f363c
Fix FE types
BrunoQuaresma Aug 6, 2024
7412eb9
Fix notifications permissions
BrunoQuaresma Aug 6, 2024
1ff0973
Display webhook info
BrunoQuaresma Aug 6, 2024
2acde04
Merge branch 'main' of https://github.com/coder/coder into bq/user-no…
BrunoQuaresma Aug 6, 2024
7cc7bdb
Add tests to the notifications page
BrunoQuaresma Aug 6, 2024
4956409
Remove unecessary migration
BrunoQuaresma Aug 6, 2024
1c62242
Don't show deployment wide method
BrunoQuaresma Aug 6, 2024
a020619
Fix templates sorting
BrunoQuaresma Aug 6, 2024
0efc40d
Add nav tabs
BrunoQuaresma Aug 6, 2024
1aedb92
Update titles
BrunoQuaresma Aug 6, 2024
786b005
Add tests
BrunoQuaresma Aug 6, 2024
2ecbe5f
Improve product copy
BrunoQuaresma Aug 7, 2024
ec7ab40
Fix notifications visibility
BrunoQuaresma Aug 7, 2024
c986e51
Minor improvements
BrunoQuaresma Aug 7, 2024
e037423
Remove alerts
BrunoQuaresma Aug 8, 2024
341f550
Add alerts when SMTP or Webhook config are enabled but not set
BrunoQuaresma Aug 8, 2024
d97dd82
Apply a few Michaels suggestions
BrunoQuaresma Aug 8, 2024
4399cd6
Merge branch 'main' of https://github.com/coder/coder into bq/user-no…
BrunoQuaresma Aug 9, 2024
d403eed
Simplify state logic for the switch component
BrunoQuaresma Aug 9, 2024
fb02aec
Update copy
BrunoQuaresma Aug 9, 2024
013ccff
Apply PR comments
BrunoQuaresma Aug 9, 2024
7858a5a
Add docs
BrunoQuaresma Aug 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion coderd/database/queries/notifications.sql
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,5 @@ WHERE id = @id::uuid;
-- name: GetNotificationTemplatesByKind :many
SELECT *
FROM notification_templates
WHERE kind = @kind::notification_template_kind;
WHERE kind = @kind::notification_template_kind
ORDER BY name ASC;
13 changes: 12 additions & 1 deletion site/src/@types/storybook.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import * as _storybook_types from "@storybook/react";
import type { QueryKey } from "react-query";
import type { Experiments, FeatureName } from "api/typesGenerated";
import type {
Experiments,
FeatureName,
SerpentOption,
User,
DeploymentValues,
} from "api/typesGenerated";
import type { Permissions } from "contexts/auth/permissions";

declare module "@storybook/react" {
type WebSocketEvent =
Expand All @@ -11,5 +18,9 @@ declare module "@storybook/react" {
experiments?: Experiments;
queries?: { key: QueryKey; data: unknown }[];
webSocket?: WebSocketEvent[];
user?: User;
permissions?: Partial<Permissions>;
deploymentValues?: DeploymentValues;
deploymentOptions?: SerpentOption[];
}
}
43 changes: 43 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2036,6 +2036,49 @@ class ApiMethods {

return response.data;
};

getUserNotificationPreferences = async (userId: string) => {
const res = await this.axios.get<TypesGen.NotificationPreference[] | null>(
`/api/v2/users/${userId}/notifications/preferences`,
);
return res.data ?? [];
};

putUserNotificationPreferences = async (
userId: string,
req: TypesGen.UpdateUserNotificationPreferences,
) => {
const res = await this.axios.put<TypesGen.NotificationPreference[]>(
`/api/v2/users/${userId}/notifications/preferences`,
req,
);
return res.data;
};

getSystemNotificationTemplates = async () => {
const res = await this.axios.get<TypesGen.NotificationTemplate[]>(
`/api/v2/notifications/templates/system`,
);
return res.data;
};

getNotificationDispatchMethods = async () => {
const res = await this.axios.get<TypesGen.NotificationMethodsResponse>(
`/api/v2/notifications/dispatch-methods`,
);
return res.data;
};

updateNotificationTemplateMethod = async (
templateId: string,
req: TypesGen.UpdateNotificationTemplateMethod,
) => {
const res = await this.axios.put<void>(
`/api/v2/notifications/templates/${templateId}/method`,
req,
);
return res.data;
};
}

// This is a hard coded CSRF token/cookie pair for local development. In prod,
Expand Down
138 changes: 138 additions & 0 deletions site/src/api/queries/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { QueryClient, UseMutationOptions } from "react-query";
import { API } from "api/api";
import type {
NotificationPreference,
NotificationTemplate,
UpdateNotificationTemplateMethod,
UpdateUserNotificationPreferences,
} from "api/typesGenerated";

export const userNotificationPreferencesKey = (userId: string) => [
"users",
userId,
"notifications",
"preferences",
];

export const userNotificationPreferences = (userId: string) => {
return {
queryKey: userNotificationPreferencesKey(userId),
queryFn: () => API.getUserNotificationPreferences(userId),
};
};

export const updateUserNotificationPreferences = (
userId: string,
queryClient: QueryClient,
) => {
return {
mutationFn: (req) => {
return API.putUserNotificationPreferences(userId, req);
},
onMutate: (data) => {
queryClient.setQueryData(
userNotificationPreferencesKey(userId),
Object.entries(data.template_disabled_map).map(
([id, disabled]) =>
({
id,
disabled,
updated_at: new Date().toISOString(),
}) satisfies NotificationPreference,
),
);
},
} satisfies UseMutationOptions<
NotificationPreference[],
unknown,
UpdateUserNotificationPreferences
>;
};

export const systemNotificationTemplatesKey = [
"notifications",
"templates",
"system",
];

export const systemNotificationTemplates = () => {
return {
queryKey: systemNotificationTemplatesKey,
queryFn: () => API.getSystemNotificationTemplates(),
};
};

export function selectTemplatesByGroup(
data: NotificationTemplate[],
): Record<string, NotificationTemplate[]> {
const grouped = data.reduce(
(acc, tpl) => {
if (!acc[tpl.group]) {
acc[tpl.group] = [];
}
acc[tpl.group].push(tpl);
return acc;
},
{} as Record<string, NotificationTemplate[]>,
);

// Sort templates within each group
for (const group in grouped) {
grouped[group].sort((a, b) => a.name.localeCompare(b.name));
}

// Sort groups by name
const sortedGroups = Object.keys(grouped).sort((a, b) => a.localeCompare(b));
const sortedGrouped: Record<string, NotificationTemplate[]> = {};
for (const group of sortedGroups) {
sortedGrouped[group] = grouped[group];
}

return sortedGrouped;
}

export const notificationDispatchMethodsKey = [
"notifications",
"dispatchMethods",
];

export const notificationDispatchMethods = () => {
return {
staleTime: Infinity,
queryKey: notificationDispatchMethodsKey,
queryFn: () => API.getNotificationDispatchMethods(),
};
};

export const updateNotificationTemplateMethod = (
templateId: string,
queryClient: QueryClient,
) => {
return {
mutationFn: (req: UpdateNotificationTemplateMethod) =>
API.updateNotificationTemplateMethod(templateId, req),
onMutate: (data) => {
const prevData = queryClient.getQueryData<NotificationTemplate[]>(
systemNotificationTemplatesKey,
);
if (!prevData) {
return;
}
queryClient.setQueryData(
systemNotificationTemplatesKey,
prevData.map((tpl) =>
tpl.id === templateId
? {
...tpl,
method: data.method,
}
: tpl,
),
);
},
} satisfies UseMutationOptions<
void,
unknown,
UpdateNotificationTemplateMethod
>;
};
4 changes: 3 additions & 1 deletion site/src/api/queries/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,12 @@ export function apiKey(): UseQueryOptions<GenerateAPIKeyResponse> {
};
}

export const hasFirstUserKey = ["hasFirstUser"];

export const hasFirstUser = (userMetadata: MetadataState<User>) => {
return cachedQuery({
metadata: userMetadata,
queryKey: ["hasFirstUser"],
queryKey: hasFirstUserKey,
queryFn: API.hasFirstUser,
});
};
Expand Down
29 changes: 29 additions & 0 deletions site/src/modules/notifications/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import EmailIcon from "@mui/icons-material/EmailOutlined";
import WebhookIcon from "@mui/icons-material/WebhookOutlined";

// TODO: This should be provided by the auto generated types from codersdk
const notificationMethods = ["smtp", "webhook"] as const;

export type NotificationMethod = (typeof notificationMethods)[number];

export const methodIcons: Record<NotificationMethod, typeof EmailIcon> = {
smtp: EmailIcon,
webhook: WebhookIcon,
};

export const methodLabels: Record<NotificationMethod, string> = {
smtp: "SMTP",
webhook: "Webhook",
};

export const castNotificationMethod = (value: string) => {
if (notificationMethods.includes(value as NotificationMethod)) {
return value as NotificationMethod;
}

throw new Error(
`Invalid notification method: ${value}. Accepted values: ${notificationMethods.join(
", ",
)}`,
);
};
Comment on lines +19 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tagging this for now while I look through the other files. I don't fully understand the point of this function, and at the very least, I think that it should be redefined as a type predicate

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just easier to use IMO.

Type predicate:

const isNotificationMethod = (v: string): v is NotificationMethod => {}
// Usage
values.map(v => {
   if(!isNotificationMethod(v)) {
      throw new Error("v is not valid") 
   }
   return <Component method={method} />
})

Cast function

const castNotificationMethod = (v: string): NotificationMethod => {}
// Usage
values.map(v => {
   const method = castNotificationMethod(v)
   return <Component method={method} />
})

2 changes: 1 addition & 1 deletion site/src/pages/DeploySettingsPage/DeploySettingsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const useDeploySettings = (): DeploySettingsContextValue => {
const context = useContext(DeploySettingsContext);
if (!context) {
throw new Error(
"useDeploySettings should be used inside of DeploySettingsLayout",
"useDeploySettings should be used inside of DeploySettingsContext or DeploySettingsLayout",
);
}
return context;
Expand Down
Loading
Loading