Skip to content

Commit 3981788

Browse files
committed
WIP: Integrations page
1 parent 0bc04e2 commit 3981788

File tree

13 files changed

+829
-19
lines changed

13 files changed

+829
-19
lines changed
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export interface DropDownItem<T> {
2+
description?: string;
23
label: string;
34
value: T;
45
}

src/Exceptionless.Web/ClientApp/src/lib/features/tokens/components/table/token-actions-cell.svelte

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,15 @@
109109
</DropdownMenu.Content>
110110
</DropdownMenu.Root>
111111

112-
<EnableTokenDialog bind:open={showEnableTokenDialog} id={token.id} notes={token.notes} enable={() => updateDisabled(false)} />
113-
<DisableTokenDialog bind:open={showDisableTokenDialog} id={token.id} notes={token.notes} disable={() => updateDisabled(true)} />
114-
<UpdateTokenNotesDialog bind:open={showUpdateNotesDialog} save={updateNotes} />
115-
<RemoveTokenDialog bind:open={showRemoveTokenDialog} id={token.id} notes={token.notes} {remove} />
112+
{#if showEnableTokenDialog}
113+
<EnableTokenDialog bind:open={showEnableTokenDialog} id={token.id} notes={token.notes} enable={() => updateDisabled(false)} />
114+
{/if}
115+
{#if showDisableTokenDialog}
116+
<DisableTokenDialog bind:open={showDisableTokenDialog} id={token.id} notes={token.notes} disable={() => updateDisabled(true)} />
117+
{/if}
118+
{#if showUpdateNotesDialog}
119+
<UpdateTokenNotesDialog bind:open={showUpdateNotesDialog} save={updateNotes} />
120+
{/if}
121+
{#if showRemoveTokenDialog}
122+
<RemoveTokenDialog bind:open={showRemoveTokenDialog} id={token.id} notes={token.notes} {remove} />
123+
{/if}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type { NewWebhook, Webhook } from '$features/webhooks/models';
2+
import type { WebSocketMessageValue } from '$features/websockets/models';
3+
4+
import { accessToken } from '$features/auth/index.svelte';
5+
import { DEFAULT_LIMIT } from '$features/shared/api/api.svelte';
6+
import { type FetchClientResponse, type ProblemDetails, useFetchClient } from '@exceptionless/fetchclient';
7+
import { createMutation, createQuery, QueryClient, useQueryClient } from '@tanstack/svelte-query';
8+
9+
export async function invalidateWebhookQueries(queryClient: QueryClient, message: WebSocketMessageValue<'WebhookChanged'>) {
10+
const { id, organization_id, project_id } = message;
11+
console.log('invalidateWebhookQueries', message);
12+
if (id) {
13+
await queryClient.invalidateQueries({ queryKey: queryKeys.id(id) });
14+
}
15+
16+
// if (organization_id) {
17+
// await queryClient.invalidateQueries({ queryKey: queryKeys.organization(organization_id) });
18+
// }
19+
20+
if (project_id) {
21+
await queryClient.invalidateQueries({ queryKey: queryKeys.project(project_id) });
22+
}
23+
24+
if (!id && !organization_id && !project_id) {
25+
await queryClient.invalidateQueries({ queryKey: queryKeys.type });
26+
}
27+
}
28+
29+
// TODO: Do we need to scope these all by organization?
30+
export const queryKeys = {
31+
deleteWebhook: (ids: string[] | undefined) => [...queryKeys.ids(ids), 'delete'] as const,
32+
id: (id: string | undefined) => [...queryKeys.type, id] as const,
33+
ids: (ids: string[] | undefined) => [...queryKeys.type, ...(ids ?? [])] as const,
34+
postWebhook: () => [...queryKeys.type, 'post'] as const,
35+
project: (id: string | undefined) => [...queryKeys.type, 'project', id] as const,
36+
type: ['Webhook'] as const
37+
};
38+
39+
export interface DeleteWebhookRequest {
40+
route: {
41+
ids: string[];
42+
};
43+
}
44+
45+
export interface GetProjectWebhooksParams {
46+
limit?: number;
47+
page?: number;
48+
}
49+
50+
export interface GetProjectWebhooksRequest {
51+
params: GetProjectWebhooksParams;
52+
route: {
53+
projectId: string | undefined;
54+
};
55+
}
56+
57+
export function deleteWebhook(request: DeleteWebhookRequest) {
58+
const queryClient = useQueryClient();
59+
60+
return createMutation<FetchClientResponse<unknown>, ProblemDetails, void>(() => ({
61+
enabled: () => !!accessToken.current && !!request.route.ids?.length,
62+
mutationFn: async () => {
63+
const client = useFetchClient();
64+
const response = await client.delete(`webhooks/${request.route.ids?.join(',')}`, {
65+
expectedStatusCodes: [202]
66+
});
67+
68+
return response;
69+
},
70+
mutationKey: queryKeys.deleteWebhook(request.route.ids),
71+
onError: () => {
72+
request.route.ids?.forEach((id) => queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }));
73+
},
74+
onSuccess: () => {
75+
request.route.ids?.forEach((id) => queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }));
76+
}
77+
}));
78+
}
79+
80+
export function getProjectWebhooksQuery(request: GetProjectWebhooksRequest) {
81+
const queryClient = useQueryClient();
82+
83+
return createQuery<FetchClientResponse<Webhook[]>, ProblemDetails>(() => ({
84+
enabled: () => !!accessToken.current && !!request.route.projectId,
85+
onSuccess: (data: Webhook[]) => {
86+
data.forEach((webhook) => {
87+
queryClient.setQueryData(queryKeys.id(webhook.id!), webhook);
88+
});
89+
},
90+
queryClient,
91+
queryFn: async ({ signal }: { signal: AbortSignal }) => {
92+
const client = useFetchClient();
93+
const response = await client.getJSON<Webhook[]>(`projects/${request.route.projectId}/webhooks`, {
94+
params: {
95+
...request.params,
96+
limit: request.params?.limit ?? DEFAULT_LIMIT
97+
},
98+
signal
99+
});
100+
101+
return response;
102+
},
103+
queryKey: queryKeys.project(request.route.projectId)
104+
}));
105+
}
106+
107+
export function postWebhook() {
108+
const queryClient = useQueryClient();
109+
110+
return createMutation<Webhook, ProblemDetails, NewWebhook>(() => ({
111+
enabled: () => !!accessToken.current,
112+
mutationFn: async (webhook: NewWebhook) => {
113+
const client = useFetchClient();
114+
const response = await client.postJSON<Webhook>('webhooks', webhook);
115+
return response.data!;
116+
},
117+
mutationKey: queryKeys.postWebhook(),
118+
onSuccess: (webhook: Webhook) => {
119+
queryClient.invalidateQueries({ queryKey: queryKeys.type });
120+
queryClient.setQueryData(queryKeys.id(webhook.id), webhook);
121+
}
122+
}));
123+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<script lang="ts">
2+
import { P } from '$comp/typography';
3+
import Muted from '$comp/typography/muted.svelte';
4+
import * as AlertDialog from '$comp/ui/alert-dialog';
5+
import { Checkbox } from '$comp/ui/checkbox';
6+
import * as Form from '$comp/ui/form';
7+
import { Input } from '$comp/ui/input';
8+
import { webhookEventTypes } from '$features/webhooks/options';
9+
import { defaults, superForm } from 'sveltekit-superforms';
10+
import { classvalidatorClient } from 'sveltekit-superforms/adapters';
11+
12+
import { NewWebhook, type WebhookKnownEventTypes } from '../../models';
13+
14+
interface Props {
15+
open: boolean;
16+
organizationId: string;
17+
projectId: string;
18+
save: (setting: NewWebhook) => Promise<void>;
19+
}
20+
let { open = $bindable(), organizationId, projectId, save }: Props = $props();
21+
22+
const defaultValue = new NewWebhook();
23+
defaultValue.organization_id = organizationId;
24+
defaultValue.project_id = projectId;
25+
26+
const form = superForm(defaults(defaultValue, classvalidatorClient(NewWebhook)), {
27+
dataType: 'json',
28+
async onUpdate({ form }) {
29+
if (!form.valid) {
30+
return;
31+
}
32+
33+
await save(form.data);
34+
open = false;
35+
},
36+
SPA: true,
37+
validators: classvalidatorClient(NewWebhook)
38+
});
39+
40+
const { enhance, form: formData } = form;
41+
42+
function addItem(eventType: WebhookKnownEventTypes) {
43+
$formData.event_types = [...$formData.event_types, eventType];
44+
}
45+
46+
function removeItem(eventType: WebhookKnownEventTypes) {
47+
$formData.event_types = $formData.event_types.filter((i) => i !== eventType);
48+
}
49+
</script>
50+
51+
<AlertDialog.Root bind:open>
52+
<AlertDialog.Content class="sm:max-w-[425px]">
53+
<form method="POST" use:enhance>
54+
<AlertDialog.Header>
55+
<AlertDialog.Title>Add New Webhook</AlertDialog.Title>
56+
<AlertDialog.Description>
57+
Webhooks allow external services to be notified when specific events occur. Enter a URL that will be called when your selected event types
58+
happen.
59+
</AlertDialog.Description>
60+
</AlertDialog.Header>
61+
62+
<P class="pb-4">
63+
<Form.Field {form} name="url">
64+
<Form.Control>
65+
{#snippet children({ props })}
66+
<Form.Label>URL</Form.Label>
67+
<Input
68+
{...props}
69+
bind:value={$formData.url}
70+
type="url"
71+
placeholder="Please enter a valid URL to call"
72+
autocomplete="url"
73+
required
74+
/>
75+
{/snippet}
76+
</Form.Control>
77+
<Form.Description />
78+
<Form.FieldErrors />
79+
</Form.Field>
80+
81+
<Form.Fieldset {form} name="event_types" class="space-y-0">
82+
<div class="mb-4">
83+
<Form.Legend class="text-base">Event Types</Form.Legend>
84+
<Form.Description>Control when the web hook is called by choosing the event types below.</Form.Description>
85+
</div>
86+
<div class="space-y-2">
87+
{#each webhookEventTypes as type (type.value)}
88+
{@const checked = $formData.event_types.includes(type.value)}
89+
<div class="flex flex-row items-start space-x-3">
90+
<Form.Control>
91+
{#snippet children({ props })}
92+
<Checkbox
93+
{...props}
94+
{checked}
95+
value={type.value}
96+
onCheckedChange={(v) => {
97+
if (v) {
98+
addItem(type.value);
99+
} else {
100+
removeItem(type.value);
101+
}
102+
}}
103+
/>
104+
<div class="grid gap-1.5 leading-none">
105+
<Form.Label class="font-normal">
106+
{type.label}
107+
</Form.Label>
108+
<Muted>{type.description}</Muted>
109+
</div>
110+
{/snippet}
111+
</Form.Control>
112+
</div>
113+
{/each}
114+
<Form.FieldErrors />
115+
</div>
116+
</Form.Fieldset>
117+
</P>
118+
119+
<AlertDialog.Footer>
120+
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
121+
<AlertDialog.Action>Add Webhook</AlertDialog.Action>
122+
</AlertDialog.Footer>
123+
</form>
124+
</AlertDialog.Content>
125+
</AlertDialog.Root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script lang="ts">
2+
import * as AlertDialog from '$comp/ui/alert-dialog';
3+
import { buttonVariants } from '$comp/ui/button';
4+
5+
interface Props {
6+
open: boolean;
7+
remove: () => Promise<void>;
8+
url: string;
9+
}
10+
11+
let { open = $bindable(false), remove, url }: Props = $props();
12+
13+
async function onSubmit() {
14+
await remove();
15+
open = false;
16+
}
17+
</script>
18+
19+
<AlertDialog.Root bind:open>
20+
<AlertDialog.Content>
21+
<AlertDialog.Header>
22+
<AlertDialog.Title>Delete Webhook</AlertDialog.Title>
23+
<AlertDialog.Description>
24+
Are you sure you want to delete "{url}"?
25+
</AlertDialog.Description>
26+
</AlertDialog.Header>
27+
<AlertDialog.Footer>
28+
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
29+
<AlertDialog.Action class={buttonVariants({ variant: 'destructive' })} onclick={onSubmit}>Delete Webhook</AlertDialog.Action>
30+
</AlertDialog.Footer>
31+
</AlertDialog.Content>
32+
</AlertDialog.Root>

0 commit comments

Comments
 (0)