Skip to content

Commit 12cd643

Browse files
Improvements to Webhooks Section (#1522)
- Sync with Upstream to avoid future conflicts - Move WebhookSections to its own file - Group Webhooks by Type - Add Webhook Icons Support - Ensure Link is used instead of Slack to represent Webhooks Generically <img width="959" height="752" alt="Screenshot 2025-12-23 at 1 35 40 PM" src="https://github.com/user-attachments/assets/0df2d5a2-4396-415c-ba38-685d65d69836" /> Fixes HDX-2794
1 parent 9103ad6 commit 12cd643

File tree

11 files changed

+370
-495
lines changed

11 files changed

+370
-495
lines changed

.changeset/angry-pugs-drive.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+
Improvements to Webhooks rendering (grouping, icons, etc)

packages/app/src/AlertsPage.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { ErrorBoundary } from '@/components/ErrorBoundary';
3737
import { PageHeader } from '@/components/PageHeader';
3838

3939
import { isAlertSilenceExpired } from './utils/alerts';
40+
import { getWebhookChannelIcon } from './utils/webhookIcons';
4041
import api from './api';
4142
import { withAppNav } from './layout';
4243
import type { AlertsPageItem } from './types';
@@ -367,13 +368,11 @@ function AlertDetails({ alert }: { alert: AlertsPageItem }) {
367368
}, [alert]);
368369

369370
const notificationMethod = React.useMemo(() => {
370-
if (alert.channel.type === 'webhook') {
371-
return (
372-
<Group gap={2}>
373-
Notify via <IconBrandSlack size={16} /> Webhook
374-
</Group>
375-
);
376-
}
371+
return (
372+
<Group gap={5}>
373+
Notify via {getWebhookChannelIcon(alert.channel.type)} Webhook
374+
</Group>
375+
);
377376
}, [alert]);
378377

379378
const linkTitle = React.useMemo(() => {

packages/app/src/DBSearchPageAlertModal.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
import { AlertPreviewChart } from './components/AlertPreviewChart';
4949
import { AlertChannelForm } from './components/Alerts';
5050
import { SQLInlineEditorControlled } from './components/SQLInlineEditor';
51+
import { getWebhookChannelIcon } from './utils/webhookIcons';
5152
import api from './api';
5253
import { AlertWithCreatedBy, SearchConfig } from './types';
5354
import { optionsToSelectData } from './utils';
@@ -61,10 +62,6 @@ const SavedSearchAlertFormSchema = z
6162
})
6263
.passthrough();
6364

64-
const CHANNEL_ICONS = {
65-
webhook: <IconBrandSlack size={14} />,
66-
};
67-
6865
const AlertForm = ({
6966
sourceId,
7067
where,
@@ -422,7 +419,7 @@ export const DBSearchPageAlertModal = ({
422419
{(savedSearch?.alerts || []).map((alert, index) => (
423420
<Tabs.Tab key={alert.id} value={`${index}`}>
424421
<Group gap="xs">
425-
{CHANNEL_ICONS[alert.channel.type]} Alert {index + 1}
422+
{getWebhookChannelIcon(alert.channel.type)} Alert {index + 1}
426423
</Group>
427424
</Tabs.Tab>
428425
))}

packages/app/src/SVGIcons.tsx

Lines changed: 12 additions & 307 deletions
Large diffs are not rendered by default.

packages/app/src/ServicesDashboardPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
Tooltip,
3232
} from '@mantine/core';
3333
import {
34+
IconChartHistogram,
3435
IconChartLine,
3536
IconFilter,
3637
IconFilterEdit,
@@ -67,7 +68,6 @@ import {
6768
useServiceDashboardExpressions,
6869
} from '@/serviceDashboard';
6970
import { useSource, useSources } from '@/source';
70-
import { Histogram } from '@/SVGIcons';
7171
import { parseTimeQuery, useNewTimeQuery } from '@/timeQuery';
7272

7373
import usePresetDashboardFilters from './hooks/usePresetDashboardFilters';
@@ -234,7 +234,7 @@ export function EndpointLatencyChart({
234234
title="Histogram"
235235
onClick={() => setLatencyChartType('histogram')}
236236
>
237-
<Histogram width={12} color="currentColor" />
237+
<IconChartHistogram size={14} />
238238
</Button>
239239
</Button.Group>
240240
</Box>

packages/app/src/TeamPage.tsx

Lines changed: 4 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { Fragment, useCallback, useMemo, useState } from 'react';
1+
import { useCallback, useState } from 'react';
22
import Head from 'next/head';
3-
import { HTTPError } from 'ky';
43
import { CopyToClipboard } from 'react-copy-to-clipboard';
54
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
65
import { DEFAULT_METADATA_MAX_ROWS_TO_READ } from '@hyperdx/common-utils/dist/core/metadata';
76
import {
87
SourceKind,
98
TeamClickHouseSettings,
10-
WebhookService,
119
} from '@hyperdx/common-utils/dist/types';
1210
import {
1311
Box,
@@ -26,7 +24,6 @@ import {
2624
TextInput,
2725
Tooltip,
2826
} from '@mantine/core';
29-
import { useDisclosure } from '@mantine/hooks';
3027
import { notifications } from '@mantine/notifications';
3128
import {
3229
IconCheck,
@@ -47,14 +44,12 @@ import { IS_LOCAL_MODE } from '@/config';
4744

4845
import { PageHeader } from './components/PageHeader';
4946
import TeamMembersSection from './components/TeamSettings/TeamMembersSection';
50-
import { WebhookForm } from './components/TeamSettings/WebhookForm';
47+
import WebhooksSection from './components/TeamSettings/WebhooksSection';
5148
import api from './api';
5249
import { useConnections } from './connection';
5350
import { DEFAULT_QUERY_TIMEOUT, DEFAULT_SEARCH_ROW_LIMIT } from './defaults';
5451
import { withAppNav } from './layout';
5552
import { useSources } from './source';
56-
import type { Webhook } from './types';
57-
import { useConfirm } from './useConfirm';
5853
import { capitalizeFirstLetter } from './utils';
5954

6055
function ConnectionsSection() {
@@ -242,159 +237,15 @@ function SourcesSection() {
242237
</Box>
243238
);
244239
}
245-
246-
function DeleteWebhookButton({
247-
webhookId,
248-
webhookName,
249-
onSuccess,
250-
}: {
251-
webhookId: string;
252-
webhookName: string;
253-
onSuccess: VoidFunction;
254-
}) {
255-
const confirm = useConfirm();
256-
const deleteWebhook = api.useDeleteWebhook();
257-
258-
const handleDelete = async () => {
259-
if (
260-
await confirm(
261-
`Are you sure you want to delete ${webhookName} webhook?`,
262-
'Delete',
263-
)
264-
) {
265-
try {
266-
await deleteWebhook.mutateAsync({ id: webhookId });
267-
notifications.show({
268-
color: 'green',
269-
message: 'Webhook deleted successfully',
270-
});
271-
onSuccess();
272-
} catch (e) {
273-
console.error(e);
274-
const message =
275-
(e instanceof HTTPError
276-
? (await e.response.json())?.message
277-
: null) || 'Something went wrong. Please contact HyperDX team.';
278-
notifications.show({
279-
message,
280-
color: 'red',
281-
autoClose: 5000,
282-
});
283-
}
284-
}
285-
};
286-
287-
return (
288-
<Button
289-
color="red"
290-
size="compact-xs"
291-
variant="outline"
292-
onClick={handleDelete}
293-
loading={deleteWebhook.isPending}
294-
>
295-
Delete
296-
</Button>
297-
);
298-
}
299-
300240
function IntegrationsSection() {
301-
const { data: webhookData, refetch: refetchWebhooks } = api.useWebhooks([
302-
WebhookService.Slack,
303-
WebhookService.Generic,
304-
WebhookService.IncidentIO,
305-
]);
306-
307-
const allWebhooks = useMemo<Webhook[]>(() => {
308-
return Array.isArray(webhookData?.data) ? webhookData.data : [];
309-
}, [webhookData]);
310-
311-
const [editedWebhookId, setEditedWebhookId] = useState<string | null>(null);
312-
const [
313-
isAddWebhookModalOpen,
314-
{ open: openWebhookModal, close: closeWebhookModal },
315-
] = useDisclosure();
316-
317241
return (
318242
<Box id="integrations">
319243
<Text size="md">Integrations</Text>
320244
<Divider my="md" />
321245
<Card variant="muted">
322-
<Text mb="xs">Webhooks</Text>
323-
324-
<Stack>
325-
{allWebhooks.map(webhook => (
326-
<Fragment key={webhook._id}>
327-
<Group justify="space-between" align="flex-start">
328-
<Stack gap={0}>
329-
<Text size="sm">
330-
{webhook.name} ({webhook.service})
331-
</Text>
332-
<Text size="xs" opacity={0.7}>
333-
{webhook.url}
334-
</Text>
335-
{webhook.description && (
336-
<Text size="xxs" opacity={0.7}>
337-
{webhook.description}
338-
</Text>
339-
)}
340-
</Stack>
341-
<Group gap="xs">
342-
{editedWebhookId !== webhook._id && (
343-
<>
344-
<Button
345-
variant="subtle"
346-
onClick={() => setEditedWebhookId(webhook._id)}
347-
size="compact-xs"
348-
leftSection={<IconPencil size={14} />}
349-
>
350-
Edit
351-
</Button>
352-
<DeleteWebhookButton
353-
webhookId={webhook._id}
354-
webhookName={webhook.name}
355-
onSuccess={refetchWebhooks}
356-
/>
357-
</>
358-
)}
359-
{editedWebhookId === webhook._id && (
360-
<Button
361-
variant="subtle"
362-
onClick={() => setEditedWebhookId(null)}
363-
size="compact-xs"
364-
>
365-
<IconX size={14} className="me-2" /> Cancel
366-
</Button>
367-
)}
368-
</Group>
369-
</Group>
370-
{editedWebhookId === webhook._id && (
371-
<WebhookForm
372-
webhook={webhook}
373-
onClose={() => setEditedWebhookId(null)}
374-
onSuccess={() => {
375-
setEditedWebhookId(null);
376-
refetchWebhooks();
377-
}}
378-
/>
379-
)}
380-
<Divider />
381-
</Fragment>
382-
))}
246+
<Stack gap="md">
247+
<WebhooksSection />
383248
</Stack>
384-
385-
{!isAddWebhookModalOpen ? (
386-
<Button variant="outline" onClick={openWebhookModal}>
387-
Add Webhook
388-
</Button>
389-
) : (
390-
<WebhookForm
391-
onClose={closeWebhookModal}
392-
onSuccess={() => {
393-
refetchWebhooks();
394-
closeWebhookModal();
395-
}}
396-
/>
397-
)}
398249
</Card>
399250
</Box>
400251
);

packages/app/src/components/TeamSettings/WebhookForm.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { Controller, SubmitHandler, useForm, useWatch } from 'react-hook-form';
44
import { ZodIssue } from 'zod';
55
import { json, jsonParseLinter } from '@codemirror/lang-json';
66
import { linter } from '@codemirror/lint';
7-
import { AlertState, WebhookService } from '@hyperdx/common-utils/dist/types';
7+
import {
8+
AlertState,
9+
WebhookApiData,
10+
WebhookService,
11+
} from '@hyperdx/common-utils/dist/types';
812
import { isValidSlackUrl } from '@hyperdx/common-utils/dist/validation';
913
import {
1014
Alert,
@@ -23,7 +27,6 @@ import ReactCodeMirror, {
2327
} from '@uiw/react-codemirror';
2428

2529
import api from '@/api';
26-
import { Webhook } from '@/types';
2730
import { isValidUrl } from '@/utils';
2831

2932
const DEFAULT_GENERIC_WEBHOOK_BODY = [
@@ -47,7 +50,7 @@ const jsonLinterWithEmptyCheck = () => (editorView: EditorView) => {
4750
type WebhookForm = {
4851
name: string;
4952
url: string;
50-
service: string;
53+
service: WebhookService;
5154
description?: string;
5255
body?: string;
5356
headers?: string;
@@ -58,7 +61,7 @@ export function WebhookForm({
5861
onClose,
5962
onSuccess,
6063
}: {
61-
webhook?: Webhook;
64+
webhook?: WebhookApiData;
6265
onClose: VoidFunction;
6366
onSuccess: (webhookId?: string) => void;
6467
}) {
@@ -280,14 +283,12 @@ export function WebhookForm({
280283
label="Service Type"
281284
required
282285
value={service}
283-
onChange={value => {
284-
form.setValue('service', value);
285-
}}
286+
onChange={value => form.setValue('service', value as WebhookService)}
286287
>
287288
<Group mt="xs">
288289
<Radio value={WebhookService.Slack} label="Slack" />
290+
<Radio value={WebhookService.IncidentIO} label="incident.io" />
289291
<Radio value={WebhookService.Generic} label="Generic" />
290-
<Radio value={WebhookService.IncidentIO} label="Incident.io" />
291292
</Group>
292293
</Radio.Group>
293294
<TextInput
@@ -297,6 +298,7 @@ export function WebhookForm({
297298
error={form.formState.errors.name?.message}
298299
{...form.register('name', { required: true })}
299300
/>
301+
300302
<TextInput
301303
label="Webhook URL"
302304
placeholder={
@@ -318,6 +320,7 @@ export function WebhookForm({
318320
: isValidUrl(value) || 'URL must be valid',
319321
})}
320322
/>
323+
321324
<TextInput
322325
label="Webhook Description (optional)"
323326
placeholder="To be used for dev alerts"

0 commit comments

Comments
 (0)