Skip to content

Commit b3f3151

Browse files
authored
feat: Team Page Slack Webhooks section (#558)
Team Page Slack Webhooks section – in preparation for the Alerts UI. ![Screenshot 2025-01-18 at 1 18 58 PM](https://github.com/user-attachments/assets/f96db07d-40f0-4ead-9d77-7787553e8227)
1 parent a70080e commit b3f3151

File tree

5 files changed

+255
-24
lines changed

5 files changed

+255
-24
lines changed

.changeset/famous-poets-rush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
Allow to create Slack Webhooks from Team Settings page

packages/app/pages/_app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,9 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
132132
<QueryClientProvider client={queryClient}>
133133
<ThemeWrapper fontFamily={userPreferences.font}>
134134
{getLayout(<Component {...pageProps} />)}
135+
{confirmModal}
135136
</ThemeWrapper>
136137
<ReactQueryDevtools initialIsOpen={true} />
137-
{confirmModal}
138138
{background}
139139
</QueryClientProvider>
140140
</QueryParamProvider>

packages/app/src/TeamPage.tsx

Lines changed: 218 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { useState } from 'react';
1+
import { Fragment, useMemo, useState } from 'react';
22
import Head from 'next/head';
33
import { HTTPError } from 'ky';
44
import { Button as BSButton, Modal as BSModal, Spinner } from 'react-bootstrap';
55
import { CopyToClipboard } from 'react-copy-to-clipboard';
6+
import { SubmitHandler, useForm } from 'react-hook-form';
67
import {
78
Badge,
89
Box,
@@ -18,6 +19,7 @@ import {
1819
Text,
1920
TextInput,
2021
} from '@mantine/core';
22+
import { useDisclosure } from '@mantine/hooks';
2123
import { notifications } from '@mantine/notifications';
2224

2325
import { ConnectionForm } from '@/components/ConnectionForm';
@@ -28,6 +30,7 @@ import api from './api';
2830
import { useConnections } from './connection';
2931
import { withAppNav } from './layout';
3032
import { useSources } from './source';
33+
import { useConfirm } from './useConfirm';
3134

3235
import styles from '../styles/TeamPage.module.scss';
3336

@@ -643,6 +646,219 @@ function TeamMembersSection() {
643646
);
644647
}
645648

649+
type WebhookForm = {
650+
name: string;
651+
url: string;
652+
description?: string;
653+
};
654+
655+
function CreateWebhookForm({
656+
service,
657+
onClose,
658+
onSuccess,
659+
}: {
660+
service: 'slack' | 'generic';
661+
onClose: VoidFunction;
662+
onSuccess: VoidFunction;
663+
}) {
664+
const saveWebhook = api.useSaveWebhook();
665+
666+
const form = useForm<WebhookForm>({
667+
defaultValues: {},
668+
});
669+
670+
const onSubmit: SubmitHandler<WebhookForm> = async values => {
671+
try {
672+
await saveWebhook.mutateAsync({
673+
service,
674+
name: values.name,
675+
url: values.url,
676+
description: values.description || '',
677+
});
678+
notifications.show({
679+
color: 'green',
680+
message: `Webhook created successfully`,
681+
});
682+
onSuccess();
683+
onClose();
684+
} catch (e) {
685+
console.error(e);
686+
const message =
687+
(e instanceof HTTPError ? (await e.response.json())?.message : null) ||
688+
'Something went wrong. Please contact HyperDX team.';
689+
notifications.show({
690+
message,
691+
color: 'red',
692+
autoClose: 5000,
693+
});
694+
}
695+
};
696+
697+
return (
698+
<form onSubmit={form.handleSubmit(onSubmit)}>
699+
<Stack mt="sm">
700+
<Text>Create Webhook</Text>
701+
<TextInput
702+
label="Webhook Name"
703+
placeholder="Post to #dev-alerts"
704+
required
705+
error={form.formState.errors.name?.message}
706+
{...form.register('name', { required: true })}
707+
/>
708+
<TextInput
709+
label="Webhook URL"
710+
placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
711+
type="url"
712+
required
713+
error={form.formState.errors.url?.message}
714+
{...form.register('url', { required: true })}
715+
/>
716+
<TextInput
717+
label="Webhook Description (optional)"
718+
placeholder="To be used for dev alerts"
719+
error={form.formState.errors.description?.message}
720+
{...form.register('description')}
721+
/>
722+
<Group justify="space-between">
723+
<Button
724+
variant="outline"
725+
type="submit"
726+
loading={saveWebhook.isPending}
727+
>
728+
Add Webhook
729+
</Button>
730+
<Button variant="outline" color="gray" onClick={onClose} type="reset">
731+
Cancel
732+
</Button>
733+
</Group>
734+
</Stack>
735+
</form>
736+
);
737+
}
738+
739+
function DeleteWebhookButton({
740+
webhookId,
741+
webhookName,
742+
onSuccess,
743+
}: {
744+
webhookId: string;
745+
webhookName: string;
746+
onSuccess: VoidFunction;
747+
}) {
748+
const confirm = useConfirm();
749+
const deleteWebhook = api.useDeleteWebhook();
750+
751+
const handleDelete = async () => {
752+
if (
753+
await confirm(
754+
`Are you sure you want to delete ${webhookName} webhook?`,
755+
'Delete',
756+
)
757+
) {
758+
try {
759+
await deleteWebhook.mutateAsync({ id: webhookId });
760+
notifications.show({
761+
color: 'green',
762+
message: 'Webhook deleted successfully',
763+
});
764+
onSuccess();
765+
} catch (e) {
766+
console.error(e);
767+
const message =
768+
(e instanceof HTTPError
769+
? (await e.response.json())?.message
770+
: null) || 'Something went wrong. Please contact HyperDX team.';
771+
notifications.show({
772+
message,
773+
color: 'red',
774+
autoClose: 5000,
775+
});
776+
}
777+
}
778+
};
779+
780+
return (
781+
<Button
782+
color="red"
783+
size="compact-xs"
784+
variant="outline"
785+
onClick={handleDelete}
786+
loading={deleteWebhook.isPending}
787+
>
788+
Delete
789+
</Button>
790+
);
791+
}
792+
793+
function IntegrationsSection() {
794+
const { data: slackWebhooksData, refetch: refetchSlackWebhooks } =
795+
api.useWebhooks(['slack']);
796+
797+
const slackWebhooks = useMemo(() => {
798+
return Array.isArray(slackWebhooksData?.data)
799+
? slackWebhooksData?.data
800+
: [];
801+
}, [slackWebhooksData]);
802+
803+
const [
804+
isAddSlackModalOpen,
805+
{ open: openSlackModal, close: closeSlackModal },
806+
] = useDisclosure();
807+
808+
return (
809+
<Box>
810+
<Text size="md" c="gray.4">
811+
Integrations
812+
</Text>
813+
<Divider my="md" />
814+
<Card>
815+
<Text mb="xs">Slack Webhooks</Text>
816+
817+
<Stack>
818+
{slackWebhooks.map((webhook: any) => (
819+
<Fragment key={webhook._id}>
820+
<Group justify="space-between">
821+
<Stack gap={0}>
822+
<Text size="sm">{webhook.name}</Text>
823+
<Text size="xs" opacity={0.7}>
824+
{webhook.url}
825+
</Text>
826+
{webhook.description && (
827+
<Text size="xxs" opacity={0.7}>
828+
{webhook.description}
829+
</Text>
830+
)}
831+
</Stack>
832+
<DeleteWebhookButton
833+
webhookId={webhook._id}
834+
webhookName={webhook.name}
835+
onSuccess={refetchSlackWebhooks}
836+
/>
837+
</Group>
838+
<Divider />
839+
</Fragment>
840+
))}
841+
</Stack>
842+
843+
{!isAddSlackModalOpen ? (
844+
<Button variant="outline" color="gray.4" onClick={openSlackModal}>
845+
Add Slack Webhook
846+
</Button>
847+
) : (
848+
<CreateWebhookForm
849+
service="slack"
850+
onClose={closeSlackModal}
851+
onSuccess={() => {
852+
refetchSlackWebhooks();
853+
closeSlackModal();
854+
}}
855+
/>
856+
)}
857+
</Card>
858+
</Box>
859+
);
860+
}
861+
646862
export default function TeamPage() {
647863
const { data: team, isLoading } = api.useTeam();
648864
const hasAllowedAuthMethods =
@@ -667,6 +883,7 @@ export default function TeamPage() {
667883
<Stack my={20} gap="xl">
668884
<SourcesSection />
669885
<ConnectionsSection />
886+
<IntegrationsSection />
670887

671888
{hasAllowedAuthMethods && (
672889
<>

packages/app/src/api.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -663,8 +663,8 @@ const api = {
663663
url: string;
664664
name: string;
665665
description: string;
666-
queryParams?: string;
667-
headers: string;
666+
queryParams?: Record<string, string>;
667+
headers?: Record<string, string>;
668668
body?: string;
669669
}
670670
>({
@@ -681,8 +681,8 @@ const api = {
681681
url: string;
682682
name: string;
683683
description: string;
684-
queryParams?: string;
685-
headers: string;
684+
queryParams?: Record<string, string>;
685+
headers?: Record<string, string>;
686686
body?: string;
687687
}) =>
688688
hdxServer(`webhooks`, {
@@ -692,8 +692,8 @@ const api = {
692692
service,
693693
url,
694694
description,
695-
queryParams,
696-
headers,
695+
queryParams: queryParams || {},
696+
headers: headers || {},
697697
body,
698698
},
699699
}).json(),

packages/app/src/useConfirm.tsx

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as React from 'react';
22
import { atom, useAtomValue, useSetAtom } from 'jotai';
3-
import Button from 'react-bootstrap/Button';
4-
import Modal from 'react-bootstrap/Modal';
3+
import { Button, Group, Modal, Text } from '@mantine/core';
54

65
type ConfirmAtom = {
76
message: string;
@@ -45,19 +44,29 @@ export const useConfirmModal = () => {
4544
setConfirm(null);
4645
}, [confirm, setConfirm]);
4746

48-
return confirm ? (
49-
<Modal show onHide={handleClose}>
50-
<Modal.Body className="bg-hdx-dark">
51-
{confirm.message}
52-
<div className="mt-3 d-flex justify-content-end gap-2">
53-
<Button variant="secondary" onClick={handleClose} size="sm">
54-
Cancel
55-
</Button>
56-
<Button variant="success" onClick={confirm.onConfirm} size="sm">
57-
{confirm.confirmLabel || 'OK'}
58-
</Button>
59-
</div>
60-
</Modal.Body>
47+
return (
48+
<Modal
49+
opened={!!confirm}
50+
onClose={handleClose}
51+
centered
52+
withCloseButton={false}
53+
>
54+
<Text size="sm" opacity={0.7}>
55+
{confirm?.message}
56+
</Text>
57+
<Group justify="flex-end" mt="md" gap="xs">
58+
<Button size="xs" variant="outline" onClick={handleClose} color="Gray">
59+
Cancel
60+
</Button>
61+
<Button
62+
size="xs"
63+
variant="outline"
64+
onClick={confirm?.onConfirm}
65+
color="red"
66+
>
67+
{confirm?.confirmLabel || 'Confirm'}
68+
</Button>
69+
</Group>
6170
</Modal>
62-
) : null;
71+
);
6372
};

0 commit comments

Comments
 (0)