mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: Team Page Slack Webhooks section (#558)
Team Page Slack Webhooks section – in preparation for the Alerts UI. 
This commit is contained in:
parent
a70080e533
commit
b3f3151a12
5 changed files with 255 additions and 24 deletions
5
.changeset/famous-poets-rush.md
Normal file
5
.changeset/famous-poets-rush.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
Allow to create Slack Webhooks from Team Settings page
|
||||
|
|
@ -132,9 +132,9 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeWrapper fontFamily={userPreferences.font}>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
{confirmModal}
|
||||
</ThemeWrapper>
|
||||
<ReactQueryDevtools initialIsOpen={true} />
|
||||
{confirmModal}
|
||||
{background}
|
||||
</QueryClientProvider>
|
||||
</QueryParamProvider>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useState } from 'react';
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { HTTPError } from 'ky';
|
||||
import { Button as BSButton, Modal as BSModal, Spinner } from 'react-bootstrap';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
|
|
@ -18,6 +19,7 @@ import {
|
|||
Text,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
import { ConnectionForm } from '@/components/ConnectionForm';
|
||||
|
|
@ -28,6 +30,7 @@ import api from './api';
|
|||
import { useConnections } from './connection';
|
||||
import { withAppNav } from './layout';
|
||||
import { useSources } from './source';
|
||||
import { useConfirm } from './useConfirm';
|
||||
|
||||
import styles from '../styles/TeamPage.module.scss';
|
||||
|
||||
|
|
@ -643,6 +646,219 @@ function TeamMembersSection() {
|
|||
);
|
||||
}
|
||||
|
||||
type WebhookForm = {
|
||||
name: string;
|
||||
url: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
function CreateWebhookForm({
|
||||
service,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: {
|
||||
service: 'slack' | 'generic';
|
||||
onClose: VoidFunction;
|
||||
onSuccess: VoidFunction;
|
||||
}) {
|
||||
const saveWebhook = api.useSaveWebhook();
|
||||
|
||||
const form = useForm<WebhookForm>({
|
||||
defaultValues: {},
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<WebhookForm> = async values => {
|
||||
try {
|
||||
await saveWebhook.mutateAsync({
|
||||
service,
|
||||
name: values.name,
|
||||
url: values.url,
|
||||
description: values.description || '',
|
||||
});
|
||||
notifications.show({
|
||||
color: 'green',
|
||||
message: `Webhook created successfully`,
|
||||
});
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const message =
|
||||
(e instanceof HTTPError ? (await e.response.json())?.message : null) ||
|
||||
'Something went wrong. Please contact HyperDX team.';
|
||||
notifications.show({
|
||||
message,
|
||||
color: 'red',
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Stack mt="sm">
|
||||
<Text>Create Webhook</Text>
|
||||
<TextInput
|
||||
label="Webhook Name"
|
||||
placeholder="Post to #dev-alerts"
|
||||
required
|
||||
error={form.formState.errors.name?.message}
|
||||
{...form.register('name', { required: true })}
|
||||
/>
|
||||
<TextInput
|
||||
label="Webhook URL"
|
||||
placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
type="url"
|
||||
required
|
||||
error={form.formState.errors.url?.message}
|
||||
{...form.register('url', { required: true })}
|
||||
/>
|
||||
<TextInput
|
||||
label="Webhook Description (optional)"
|
||||
placeholder="To be used for dev alerts"
|
||||
error={form.formState.errors.description?.message}
|
||||
{...form.register('description')}
|
||||
/>
|
||||
<Group justify="space-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
type="submit"
|
||||
loading={saveWebhook.isPending}
|
||||
>
|
||||
Add Webhook
|
||||
</Button>
|
||||
<Button variant="outline" color="gray" onClick={onClose} type="reset">
|
||||
Cancel
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteWebhookButton({
|
||||
webhookId,
|
||||
webhookName,
|
||||
onSuccess,
|
||||
}: {
|
||||
webhookId: string;
|
||||
webhookName: string;
|
||||
onSuccess: VoidFunction;
|
||||
}) {
|
||||
const confirm = useConfirm();
|
||||
const deleteWebhook = api.useDeleteWebhook();
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (
|
||||
await confirm(
|
||||
`Are you sure you want to delete ${webhookName} webhook?`,
|
||||
'Delete',
|
||||
)
|
||||
) {
|
||||
try {
|
||||
await deleteWebhook.mutateAsync({ id: webhookId });
|
||||
notifications.show({
|
||||
color: 'green',
|
||||
message: 'Webhook deleted successfully',
|
||||
});
|
||||
onSuccess();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const message =
|
||||
(e instanceof HTTPError
|
||||
? (await e.response.json())?.message
|
||||
: null) || 'Something went wrong. Please contact HyperDX team.';
|
||||
notifications.show({
|
||||
message,
|
||||
color: 'red',
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
color="red"
|
||||
size="compact-xs"
|
||||
variant="outline"
|
||||
onClick={handleDelete}
|
||||
loading={deleteWebhook.isPending}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function IntegrationsSection() {
|
||||
const { data: slackWebhooksData, refetch: refetchSlackWebhooks } =
|
||||
api.useWebhooks(['slack']);
|
||||
|
||||
const slackWebhooks = useMemo(() => {
|
||||
return Array.isArray(slackWebhooksData?.data)
|
||||
? slackWebhooksData?.data
|
||||
: [];
|
||||
}, [slackWebhooksData]);
|
||||
|
||||
const [
|
||||
isAddSlackModalOpen,
|
||||
{ open: openSlackModal, close: closeSlackModal },
|
||||
] = useDisclosure();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text size="md" c="gray.4">
|
||||
Integrations
|
||||
</Text>
|
||||
<Divider my="md" />
|
||||
<Card>
|
||||
<Text mb="xs">Slack Webhooks</Text>
|
||||
|
||||
<Stack>
|
||||
{slackWebhooks.map((webhook: any) => (
|
||||
<Fragment key={webhook._id}>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={0}>
|
||||
<Text size="sm">{webhook.name}</Text>
|
||||
<Text size="xs" opacity={0.7}>
|
||||
{webhook.url}
|
||||
</Text>
|
||||
{webhook.description && (
|
||||
<Text size="xxs" opacity={0.7}>
|
||||
{webhook.description}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<DeleteWebhookButton
|
||||
webhookId={webhook._id}
|
||||
webhookName={webhook.name}
|
||||
onSuccess={refetchSlackWebhooks}
|
||||
/>
|
||||
</Group>
|
||||
<Divider />
|
||||
</Fragment>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{!isAddSlackModalOpen ? (
|
||||
<Button variant="outline" color="gray.4" onClick={openSlackModal}>
|
||||
Add Slack Webhook
|
||||
</Button>
|
||||
) : (
|
||||
<CreateWebhookForm
|
||||
service="slack"
|
||||
onClose={closeSlackModal}
|
||||
onSuccess={() => {
|
||||
refetchSlackWebhooks();
|
||||
closeSlackModal();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TeamPage() {
|
||||
const { data: team, isLoading } = api.useTeam();
|
||||
const hasAllowedAuthMethods =
|
||||
|
|
@ -667,6 +883,7 @@ export default function TeamPage() {
|
|||
<Stack my={20} gap="xl">
|
||||
<SourcesSection />
|
||||
<ConnectionsSection />
|
||||
<IntegrationsSection />
|
||||
|
||||
{hasAllowedAuthMethods && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -663,8 +663,8 @@ const api = {
|
|||
url: string;
|
||||
name: string;
|
||||
description: string;
|
||||
queryParams?: string;
|
||||
headers: string;
|
||||
queryParams?: Record<string, string>;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
}
|
||||
>({
|
||||
|
|
@ -681,8 +681,8 @@ const api = {
|
|||
url: string;
|
||||
name: string;
|
||||
description: string;
|
||||
queryParams?: string;
|
||||
headers: string;
|
||||
queryParams?: Record<string, string>;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
}) =>
|
||||
hdxServer(`webhooks`, {
|
||||
|
|
@ -692,8 +692,8 @@ const api = {
|
|||
service,
|
||||
url,
|
||||
description,
|
||||
queryParams,
|
||||
headers,
|
||||
queryParams: queryParams || {},
|
||||
headers: headers || {},
|
||||
body,
|
||||
},
|
||||
}).json(),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import { atom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
import { Button, Group, Modal, Text } from '@mantine/core';
|
||||
|
||||
type ConfirmAtom = {
|
||||
message: string;
|
||||
|
|
@ -45,19 +44,29 @@ export const useConfirmModal = () => {
|
|||
setConfirm(null);
|
||||
}, [confirm, setConfirm]);
|
||||
|
||||
return confirm ? (
|
||||
<Modal show onHide={handleClose}>
|
||||
<Modal.Body className="bg-hdx-dark">
|
||||
{confirm.message}
|
||||
<div className="mt-3 d-flex justify-content-end gap-2">
|
||||
<Button variant="secondary" onClick={handleClose} size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="success" onClick={confirm.onConfirm} size="sm">
|
||||
{confirm.confirmLabel || 'OK'}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
return (
|
||||
<Modal
|
||||
opened={!!confirm}
|
||||
onClose={handleClose}
|
||||
centered
|
||||
withCloseButton={false}
|
||||
>
|
||||
<Text size="sm" opacity={0.7}>
|
||||
{confirm?.message}
|
||||
</Text>
|
||||
<Group justify="flex-end" mt="md" gap="xs">
|
||||
<Button size="xs" variant="outline" onClick={handleClose} color="Gray">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline"
|
||||
onClick={confirm?.onConfirm}
|
||||
color="red"
|
||||
>
|
||||
{confirm?.confirmLabel || 'Confirm'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue