Skip to content

Commit a747efc

Browse files
committed
next: Update slack notification settings.
1 parent 17f9a1e commit a747efc

File tree

4 files changed

+158
-9
lines changed

4 files changed

+158
-9
lines changed

src/Exceptionless.Web/ClientApp/src/lib/features/projects/api.svelte.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ClientConfiguration, NewProject, UpdateProject, ViewProject } from '$features/projects/models';
1+
import type { ClientConfiguration, NewProject, NotificationSettings, UpdateProject, ViewProject } from '$features/projects/models';
22
import type { WebSocketMessageValue } from '$features/websockets/models';
33

44
import { accessToken } from '$features/auth/index.svelte';
@@ -30,11 +30,14 @@ export const queryKeys = {
3030
deleteSlack: (id: string | undefined) => [...queryKeys.id(id), 'delete-slack'] as const,
3131
id: (id: string | undefined) => [...queryKeys.type, id] as const,
3232
ids: (ids: string[] | undefined) => [...queryKeys.type, ...(ids ?? [])] as const,
33+
integrationNotificationSettings: (id: string | undefined, integration: string) => [...queryKeys.id(id), integration, 'notification-settings'] as const,
3334
organization: (id: string | undefined) => [...queryKeys.type, 'organization', id] as const,
3435
postConfig: (id: string | undefined) => [...queryKeys.id(id), 'post-config'] as const,
3536
postProject: () => [...queryKeys.type, 'post-project'] as const,
3637
postPromotedTab: (id: string | undefined) => [...queryKeys.id(id), 'promote-tab'] as const,
3738
postSlack: (id: string | undefined) => [...queryKeys.id(id), 'post-slack'] as const,
39+
putIntegrationNotificationSettings: (id: string | undefined, integration: string) =>
40+
[...queryKeys.id(id), integration, 'put-notification-settings'] as const,
3841
resetData: (id: string | undefined) => [...queryKeys.id(id), 'reset-data'] as const,
3942
type: ['Project'] as const
4043
};
@@ -91,6 +94,13 @@ export interface GetProjectConfigRequest {
9194
};
9295
}
9396

97+
export interface GetProjectIntegrationNotificationSettingsRequest {
98+
route: {
99+
id: string | undefined;
100+
integration: string;
101+
};
102+
}
103+
94104
export interface GetProjectRequest {
95105
route: {
96106
id: string | undefined;
@@ -130,6 +140,13 @@ export interface PostSlackRequest {
130140
};
131141
}
132142

143+
export interface PutProjectIntegrationNotificationSettingsRequest {
144+
route: {
145+
id: string | undefined;
146+
integration: string;
147+
};
148+
}
149+
133150
export interface ResetDataRequest {
134151
route: {
135152
id: string;
@@ -273,6 +290,21 @@ export function getProjectConfig(request: GetProjectConfigRequest) {
273290
}));
274291
}
275292

293+
export function getProjectIntegrationNotificationSettings(request: GetProjectIntegrationNotificationSettingsRequest) {
294+
return createQuery<NotificationSettings, ProblemDetails>(() => ({
295+
enabled: () => !!accessToken.current && !!request.route.id,
296+
queryFn: async ({ signal }: { signal: AbortSignal }) => {
297+
const client = useFetchClient();
298+
const response = await client.getJSON<NotificationSettings>(`projects/${request.route.id}/${request.route.integration}/notifications`, {
299+
signal
300+
});
301+
302+
return response.data!;
303+
},
304+
queryKey: queryKeys.integrationNotificationSettings(request.route.id, request.route.integration)
305+
}));
306+
}
307+
276308
export function getProjectQuery(request: GetProjectRequest) {
277309
return createQuery<ViewProject, ProblemDetails>(() => ({
278310
enabled: () => !!accessToken.current && !!request.route.id,
@@ -371,6 +403,23 @@ export function postSlack(request: PostSlackRequest) {
371403
}));
372404
}
373405

406+
export function putProjectIntegrationNotificationSettings(request: PutProjectIntegrationNotificationSettingsRequest) {
407+
const queryClient = useQueryClient();
408+
409+
return createMutation<boolean, ProblemDetails, NotificationSettings>(() => ({
410+
enabled: () => !!accessToken.current && !!request.route.id,
411+
mutationFn: async (settings: NotificationSettings) => {
412+
const client = useFetchClient();
413+
const response = await client.put(`projects/${request.route.id}/${request.route.integration}/notifications`, settings);
414+
return response.ok;
415+
},
416+
mutationKey: queryKeys.putIntegrationNotificationSettings(request.route.id, request.route.integration),
417+
onSuccess: () => {
418+
queryClient.invalidateQueries({ queryKey: queryKeys.integrationNotificationSettings(request.route.id, request.route.integration) });
419+
}
420+
}));
421+
}
422+
374423
export function resetData(request: ResetDataRequest) {
375424
return createMutation<void, ProblemDetails, void>(() => ({
376425
enabled: () => !!accessToken.current && !!request.route.id,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<script lang="ts">
2+
import type { NotificationSettings } from '$features/projects/models';
3+
4+
import { Label } from '$comp/ui/label';
5+
import { Skeleton } from '$comp/ui/skeleton';
6+
import { Switch } from '$comp/ui/switch';
7+
8+
interface Props {
9+
changed: (settings: NotificationSettings) => Promise<void>;
10+
settings?: NotificationSettings;
11+
}
12+
13+
// TODO: Clone settings?
14+
// TODO: Add Skeletons
15+
// TODO: Use switch primitive in shared?
16+
let { changed, settings }: Props = $props();
17+
</script>
18+
19+
{#if settings}
20+
<div class="flex items-center space-x-2">
21+
<Switch id="send_daily_summary" bind:checked={settings.send_daily_summary} onCheckedChange={async () => await changed(settings)} />
22+
<Label for="send_daily_summary">
23+
Send daily project summary <strong>(Coming soon!)</strong>
24+
</Label>
25+
</div>
26+
27+
<div class="flex items-center space-x-2">
28+
<Switch id="report_new_errors" bind:checked={settings.report_new_errors} onCheckedChange={async () => await changed(settings)} />
29+
<Label for="report_new_errors">Notify me on new errors</Label>
30+
</div>
31+
32+
<div class="flex items-center space-x-2">
33+
<Switch id="report_critical_errors" bind:checked={settings.report_critical_errors} onCheckedChange={async () => await changed(settings)} />
34+
<Label for="report_critical_errors">Notify me on critical errors</Label>
35+
</div>
36+
37+
<div class="flex items-center space-x-2">
38+
<Switch id="report_event_regressions" bind:checked={settings.report_event_regressions} onCheckedChange={async () => await changed(settings)} />
39+
<Label for="report_event_regressions">Notify me on error regressions</Label>
40+
</div>
41+
42+
<div class="flex items-center space-x-2">
43+
<Switch id="report_new_events" bind:checked={settings.report_new_events} onCheckedChange={async () => await changed(settings)} />
44+
<Label for="report_new_events">Notify me on new events</Label>
45+
</div>
46+
47+
<div class="flex items-center space-x-2">
48+
<Switch id="report_critical_events" bind:checked={settings.report_critical_events} onCheckedChange={async () => await changed(settings)} />
49+
<Label for="report_critical_events">Notify me on critical events</Label>
50+
</div>
51+
{:else}
52+
<Skeleton />
53+
{/if}

src/Exceptionless.Web/ClientApp/src/lib/features/projects/models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { ClientConfiguration, NewProject, ViewProject } from '$generated/api';
1+
export { ClientConfiguration, NewProject, NotificationSettings, ViewProject } from '$generated/api';
22

33
import { IsBoolean, IsOptional, IsString } from 'class-validator';
44

src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/integrations/+page.svelte

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script lang="ts">
2+
import type { NotificationSettings } from '$features/projects/models';
3+
24
import { page } from '$app/state';
35
import * as DataTable from '$comp/data-table';
46
import { H3, H4, Muted, P } from '$comp/typography';
@@ -7,8 +9,15 @@
79
import { env } from '$env/dynamic/public';
810
import { slackOAuthLogin } from '$features/auth/index.svelte';
911
import { organization } from '$features/organizations/context.svelte';
10-
import { deleteSlack, getProjectQuery, postSlack } from '$features/projects/api.svelte';
12+
import {
13+
deleteSlack,
14+
getProjectIntegrationNotificationSettings,
15+
getProjectQuery,
16+
postSlack,
17+
putProjectIntegrationNotificationSettings
18+
} from '$features/projects/api.svelte';
1119
import RemoveSlackDialog from '$features/projects/components/dialogs/remove-slack-dialog.svelte';
20+
import NotificationSettingsForm from '$features/projects/components/notification-settings-form.svelte';
1221
import { DEFAULT_LIMIT } from '$features/shared/api/api.svelte';
1322
import { postWebhook } from '$features/webhooks/api.svelte';
1423
import { getProjectWebhooksQuery } from '$features/webhooks/api.svelte';
@@ -24,6 +33,7 @@
2433
import { watch } from 'runed';
2534
import { toast } from 'svelte-sonner';
2635
36+
let toastId = $state<number | string>();
2737
let showAddWebhookDialog = $state(false);
2838
let showRemoveSlackDialog = $state(false);
2939
const projectId = page.params.projectId || '';
@@ -55,31 +65,66 @@
5565
}
5666
});
5767
68+
const slackNotificationSettingsResponse = getProjectIntegrationNotificationSettings({
69+
route: {
70+
get id() {
71+
return projectId;
72+
},
73+
integration: 'slack'
74+
}
75+
});
76+
77+
const updateSlackNotificationSettingsResponse = putProjectIntegrationNotificationSettings({
78+
route: {
79+
get id() {
80+
return projectId;
81+
},
82+
integration: 'slack'
83+
}
84+
});
85+
5886
async function addWebhook(webhook: NewWebhook) {
87+
toast.dismiss(toastId);
88+
5989
try {
6090
await newWebhook.mutateAsync(webhook);
61-
toast.success('Webhook added successfully');
91+
toastId = toast.success('Webhook added successfully');
6292
} catch {
63-
toast.error('Error adding webhook. Please try again.');
93+
toastId = toast.error('Error adding webhook. Please try again.');
6494
}
6595
}
6696
6797
async function addSlack() {
98+
toast.dismiss(toastId);
99+
68100
try {
69101
const code = await slackOAuthLogin();
70102
await addSlackMutation.mutateAsync({ code });
71-
toast.success('Successfully connected Slack integration.');
103+
toastId = toast.success('Successfully connected Slack integration.');
72104
} catch {
73-
toast.error('Error connecting Slack integration. Please try again.');
105+
toastId = toast.error('Error connecting Slack integration. Please try again.');
74106
}
75107
}
76108
77109
async function removeSlack() {
110+
toast.dismiss(toastId);
111+
78112
try {
79113
await removeSlackMutation.mutateAsync();
80-
toast.success('Successfully removed Slack integration.');
114+
toastId = toast.success('Successfully removed Slack integration.');
81115
} catch {
82-
toast.error('Error removing Slack integration. Please try again.');
116+
toastId = toast.error('Error removing Slack integration. Please try again.');
117+
}
118+
}
119+
120+
async function updateSlackNotificationSettings(settings: NotificationSettings) {
121+
toast.dismiss(toastId);
122+
123+
try {
124+
await updateSlackNotificationSettingsResponse.mutateAsync(settings);
125+
toastId = toast.success('Successfully updated Slack notification settings.');
126+
} catch {
127+
toastId = toast.error('Error updating Slack notification settings. Please try again.');
83128
}
84129
}
85130
@@ -143,6 +188,8 @@
143188
team's Slack channels. Keep your team informed and respond faster to issues without constantly checking the dashboard.</P
144189
>
145190

191+
<NotificationSettingsForm settings={slackNotificationSettingsResponse.data} changed={updateSlackNotificationSettings} />
192+
146193
{#if hasSlackIntegration}
147194
<Button onclick={() => (showRemoveSlackDialog = true)}><img class="text- mr-2 size-4" alt="Slack" src={Slack} /> Remove Slack</Button>
148195
{:else}

0 commit comments

Comments
 (0)