mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
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
This commit is contained in:
parent
9103ad6328
commit
12cd6433b7
11 changed files with 370 additions and 495 deletions
5
.changeset/angry-pugs-drive.md
Normal file
5
.changeset/angry-pugs-drive.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
Improvements to Webhooks rendering (grouping, icons, etc)
|
||||
|
|
@ -37,6 +37,7 @@ import { ErrorBoundary } from '@/components/ErrorBoundary';
|
|||
import { PageHeader } from '@/components/PageHeader';
|
||||
|
||||
import { isAlertSilenceExpired } from './utils/alerts';
|
||||
import { getWebhookChannelIcon } from './utils/webhookIcons';
|
||||
import api from './api';
|
||||
import { withAppNav } from './layout';
|
||||
import type { AlertsPageItem } from './types';
|
||||
|
|
@ -367,13 +368,11 @@ function AlertDetails({ alert }: { alert: AlertsPageItem }) {
|
|||
}, [alert]);
|
||||
|
||||
const notificationMethod = React.useMemo(() => {
|
||||
if (alert.channel.type === 'webhook') {
|
||||
return (
|
||||
<Group gap={2}>
|
||||
Notify via <IconBrandSlack size={16} /> Webhook
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Group gap={5}>
|
||||
Notify via {getWebhookChannelIcon(alert.channel.type)} Webhook
|
||||
</Group>
|
||||
);
|
||||
}, [alert]);
|
||||
|
||||
const linkTitle = React.useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import {
|
|||
import { AlertPreviewChart } from './components/AlertPreviewChart';
|
||||
import { AlertChannelForm } from './components/Alerts';
|
||||
import { SQLInlineEditorControlled } from './components/SQLInlineEditor';
|
||||
import { getWebhookChannelIcon } from './utils/webhookIcons';
|
||||
import api from './api';
|
||||
import { AlertWithCreatedBy, SearchConfig } from './types';
|
||||
import { optionsToSelectData } from './utils';
|
||||
|
|
@ -61,10 +62,6 @@ const SavedSearchAlertFormSchema = z
|
|||
})
|
||||
.passthrough();
|
||||
|
||||
const CHANNEL_ICONS = {
|
||||
webhook: <IconBrandSlack size={14} />,
|
||||
};
|
||||
|
||||
const AlertForm = ({
|
||||
sourceId,
|
||||
where,
|
||||
|
|
@ -422,7 +419,7 @@ export const DBSearchPageAlertModal = ({
|
|||
{(savedSearch?.alerts || []).map((alert, index) => (
|
||||
<Tabs.Tab key={alert.id} value={`${index}`}>
|
||||
<Group gap="xs">
|
||||
{CHANNEL_ICONS[alert.channel.type]} Alert {index + 1}
|
||||
{getWebhookChannelIcon(alert.channel.type)} Alert {index + 1}
|
||||
</Group>
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -31,6 +31,7 @@ import {
|
|||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconChartHistogram,
|
||||
IconChartLine,
|
||||
IconFilter,
|
||||
IconFilterEdit,
|
||||
|
|
@ -67,7 +68,6 @@ import {
|
|||
useServiceDashboardExpressions,
|
||||
} from '@/serviceDashboard';
|
||||
import { useSource, useSources } from '@/source';
|
||||
import { Histogram } from '@/SVGIcons';
|
||||
import { parseTimeQuery, useNewTimeQuery } from '@/timeQuery';
|
||||
|
||||
import usePresetDashboardFilters from './hooks/usePresetDashboardFilters';
|
||||
|
|
@ -234,7 +234,7 @@ export function EndpointLatencyChart({
|
|||
title="Histogram"
|
||||
onClick={() => setLatencyChartType('histogram')}
|
||||
>
|
||||
<Histogram width={12} color="currentColor" />
|
||||
<IconChartHistogram size={14} />
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { Fragment, useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { HTTPError } from 'ky';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
|
||||
import { DEFAULT_METADATA_MAX_ROWS_TO_READ } from '@hyperdx/common-utils/dist/core/metadata';
|
||||
import {
|
||||
SourceKind,
|
||||
TeamClickHouseSettings,
|
||||
WebhookService,
|
||||
} from '@hyperdx/common-utils/dist/types';
|
||||
import {
|
||||
Box,
|
||||
|
|
@ -26,7 +24,6 @@ import {
|
|||
TextInput,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
IconCheck,
|
||||
|
|
@ -47,14 +44,12 @@ import { IS_LOCAL_MODE } from '@/config';
|
|||
|
||||
import { PageHeader } from './components/PageHeader';
|
||||
import TeamMembersSection from './components/TeamSettings/TeamMembersSection';
|
||||
import { WebhookForm } from './components/TeamSettings/WebhookForm';
|
||||
import WebhooksSection from './components/TeamSettings/WebhooksSection';
|
||||
import api from './api';
|
||||
import { useConnections } from './connection';
|
||||
import { DEFAULT_QUERY_TIMEOUT, DEFAULT_SEARCH_ROW_LIMIT } from './defaults';
|
||||
import { withAppNav } from './layout';
|
||||
import { useSources } from './source';
|
||||
import type { Webhook } from './types';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import { capitalizeFirstLetter } from './utils';
|
||||
|
||||
function ConnectionsSection() {
|
||||
|
|
@ -242,159 +237,15 @@ function SourcesSection() {
|
|||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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: webhookData, refetch: refetchWebhooks } = api.useWebhooks([
|
||||
WebhookService.Slack,
|
||||
WebhookService.Generic,
|
||||
WebhookService.IncidentIO,
|
||||
]);
|
||||
|
||||
const allWebhooks = useMemo<Webhook[]>(() => {
|
||||
return Array.isArray(webhookData?.data) ? webhookData.data : [];
|
||||
}, [webhookData]);
|
||||
|
||||
const [editedWebhookId, setEditedWebhookId] = useState<string | null>(null);
|
||||
const [
|
||||
isAddWebhookModalOpen,
|
||||
{ open: openWebhookModal, close: closeWebhookModal },
|
||||
] = useDisclosure();
|
||||
|
||||
return (
|
||||
<Box id="integrations">
|
||||
<Text size="md">Integrations</Text>
|
||||
<Divider my="md" />
|
||||
<Card variant="muted">
|
||||
<Text mb="xs">Webhooks</Text>
|
||||
|
||||
<Stack>
|
||||
{allWebhooks.map(webhook => (
|
||||
<Fragment key={webhook._id}>
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<Stack gap={0}>
|
||||
<Text size="sm">
|
||||
{webhook.name} ({webhook.service})
|
||||
</Text>
|
||||
<Text size="xs" opacity={0.7}>
|
||||
{webhook.url}
|
||||
</Text>
|
||||
{webhook.description && (
|
||||
<Text size="xxs" opacity={0.7}>
|
||||
{webhook.description}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<Group gap="xs">
|
||||
{editedWebhookId !== webhook._id && (
|
||||
<>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => setEditedWebhookId(webhook._id)}
|
||||
size="compact-xs"
|
||||
leftSection={<IconPencil size={14} />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<DeleteWebhookButton
|
||||
webhookId={webhook._id}
|
||||
webhookName={webhook.name}
|
||||
onSuccess={refetchWebhooks}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{editedWebhookId === webhook._id && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => setEditedWebhookId(null)}
|
||||
size="compact-xs"
|
||||
>
|
||||
<IconX size={14} className="me-2" /> Cancel
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
{editedWebhookId === webhook._id && (
|
||||
<WebhookForm
|
||||
webhook={webhook}
|
||||
onClose={() => setEditedWebhookId(null)}
|
||||
onSuccess={() => {
|
||||
setEditedWebhookId(null);
|
||||
refetchWebhooks();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Divider />
|
||||
</Fragment>
|
||||
))}
|
||||
<Stack gap="md">
|
||||
<WebhooksSection />
|
||||
</Stack>
|
||||
|
||||
{!isAddWebhookModalOpen ? (
|
||||
<Button variant="outline" onClick={openWebhookModal}>
|
||||
Add Webhook
|
||||
</Button>
|
||||
) : (
|
||||
<WebhookForm
|
||||
onClose={closeWebhookModal}
|
||||
onSuccess={() => {
|
||||
refetchWebhooks();
|
||||
closeWebhookModal();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ import { Controller, SubmitHandler, useForm, useWatch } from 'react-hook-form';
|
|||
import { ZodIssue } from 'zod';
|
||||
import { json, jsonParseLinter } from '@codemirror/lang-json';
|
||||
import { linter } from '@codemirror/lint';
|
||||
import { AlertState, WebhookService } from '@hyperdx/common-utils/dist/types';
|
||||
import {
|
||||
AlertState,
|
||||
WebhookApiData,
|
||||
WebhookService,
|
||||
} from '@hyperdx/common-utils/dist/types';
|
||||
import { isValidSlackUrl } from '@hyperdx/common-utils/dist/validation';
|
||||
import {
|
||||
Alert,
|
||||
|
|
@ -23,7 +27,6 @@ import ReactCodeMirror, {
|
|||
} from '@uiw/react-codemirror';
|
||||
|
||||
import api from '@/api';
|
||||
import { Webhook } from '@/types';
|
||||
import { isValidUrl } from '@/utils';
|
||||
|
||||
const DEFAULT_GENERIC_WEBHOOK_BODY = [
|
||||
|
|
@ -47,7 +50,7 @@ const jsonLinterWithEmptyCheck = () => (editorView: EditorView) => {
|
|||
type WebhookForm = {
|
||||
name: string;
|
||||
url: string;
|
||||
service: string;
|
||||
service: WebhookService;
|
||||
description?: string;
|
||||
body?: string;
|
||||
headers?: string;
|
||||
|
|
@ -58,7 +61,7 @@ export function WebhookForm({
|
|||
onClose,
|
||||
onSuccess,
|
||||
}: {
|
||||
webhook?: Webhook;
|
||||
webhook?: WebhookApiData;
|
||||
onClose: VoidFunction;
|
||||
onSuccess: (webhookId?: string) => void;
|
||||
}) {
|
||||
|
|
@ -280,14 +283,12 @@ export function WebhookForm({
|
|||
label="Service Type"
|
||||
required
|
||||
value={service}
|
||||
onChange={value => {
|
||||
form.setValue('service', value);
|
||||
}}
|
||||
onChange={value => form.setValue('service', value as WebhookService)}
|
||||
>
|
||||
<Group mt="xs">
|
||||
<Radio value={WebhookService.Slack} label="Slack" />
|
||||
<Radio value={WebhookService.IncidentIO} label="incident.io" />
|
||||
<Radio value={WebhookService.Generic} label="Generic" />
|
||||
<Radio value={WebhookService.IncidentIO} label="Incident.io" />
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
<TextInput
|
||||
|
|
@ -297,6 +298,7 @@ export function WebhookForm({
|
|||
error={form.formState.errors.name?.message}
|
||||
{...form.register('name', { required: true })}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Webhook URL"
|
||||
placeholder={
|
||||
|
|
@ -318,6 +320,7 @@ export function WebhookForm({
|
|||
: isValidUrl(value) || 'URL must be valid',
|
||||
})}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Webhook Description (optional)"
|
||||
placeholder="To be used for dev alerts"
|
||||
|
|
|
|||
204
packages/app/src/components/TeamSettings/WebhooksSection.tsx
Normal file
204
packages/app/src/components/TeamSettings/WebhooksSection.tsx
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { HTTPError } from 'ky';
|
||||
import {
|
||||
WebhookApiData,
|
||||
WebhookService,
|
||||
} from '@hyperdx/common-utils/dist/types';
|
||||
import { Button, Divider, Group, Stack, Text, Title } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconPencil, IconX } from '@tabler/icons-react';
|
||||
|
||||
import api from '../../api';
|
||||
import { useConfirm } from '../../useConfirm';
|
||||
import {
|
||||
getWebhookServiceConfig,
|
||||
getWebhookServiceName,
|
||||
groupWebhooksByService,
|
||||
} from '../../utils/webhookIcons';
|
||||
|
||||
import { WebhookForm } from './WebhookForm';
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WebhooksSection() {
|
||||
const { data: webhookData, refetch: refetchWebhooks } = api.useWebhooks([
|
||||
WebhookService.Slack,
|
||||
WebhookService.Generic,
|
||||
WebhookService.IncidentIO,
|
||||
]);
|
||||
|
||||
const [editedWebhookId, setEditedWebhookId] = useState<string | null>(null);
|
||||
|
||||
const allWebhooks = useMemo((): WebhookApiData[] => {
|
||||
return Array.isArray(webhookData?.data) ? webhookData?.data || [] : [];
|
||||
}, [webhookData]);
|
||||
|
||||
const groupedWebhooks = useMemo(() => {
|
||||
return groupWebhooksByService(allWebhooks);
|
||||
}, [allWebhooks]);
|
||||
|
||||
const [
|
||||
isAddWebhookModalOpen,
|
||||
{ open: openWebhookModal, close: closeWebhookModal },
|
||||
] = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text mb="xs">Webhooks</Text>
|
||||
|
||||
<Stack>
|
||||
{groupedWebhooks.length === 0 ? (
|
||||
<Text size="sm" c="dimmed" ta="center" py="xl">
|
||||
No webhooks configured yet
|
||||
</Text>
|
||||
) : (
|
||||
groupedWebhooks.map(([serviceType, webhooks]) => {
|
||||
const config = getWebhookServiceConfig(serviceType);
|
||||
return (
|
||||
<Stack key={serviceType} gap="xs">
|
||||
{/* Service type header with icon */}
|
||||
<Group gap="xs" mt="md" mb="xs">
|
||||
{config?.icon}
|
||||
<Title order={6} c="dimmed">
|
||||
{getWebhookServiceName(serviceType)}
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Webhooks in this service type */}
|
||||
<Stack>
|
||||
{webhooks.map(webhook => (
|
||||
<Fragment key={webhook._id}>
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<Stack gap={0}>
|
||||
<Text size="sm">
|
||||
{webhook.name} ({webhook.service})
|
||||
</Text>
|
||||
<Text size="xs" opacity={0.7}>
|
||||
{webhook.url}
|
||||
</Text>
|
||||
{webhook.description && (
|
||||
<Text size="xxs" opacity={0.7}>
|
||||
{webhook.description}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Group gap="xs">
|
||||
{editedWebhookId !== webhook._id ? (
|
||||
<>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray.4"
|
||||
onClick={() => setEditedWebhookId(webhook._id)}
|
||||
size="compact-xs"
|
||||
leftSection={<IconPencil size={14} />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<DeleteWebhookButton
|
||||
webhookId={webhook._id}
|
||||
webhookName={webhook.name}
|
||||
onSuccess={refetchWebhooks}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray.4"
|
||||
onClick={() => setEditedWebhookId(null)}
|
||||
size="compact-xs"
|
||||
>
|
||||
<IconX size={16} /> Cancel
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
{editedWebhookId === webhook._id && (
|
||||
<WebhookForm
|
||||
webhook={webhook}
|
||||
onClose={() => setEditedWebhookId(null)}
|
||||
onSuccess={() => {
|
||||
setEditedWebhookId(null);
|
||||
refetchWebhooks();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Divider />
|
||||
</Fragment>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{!isAddWebhookModalOpen ? (
|
||||
<Button variant="outline" color="gray.4" onClick={openWebhookModal}>
|
||||
Add Webhook
|
||||
</Button>
|
||||
) : (
|
||||
<WebhookForm
|
||||
onClose={closeWebhookModal}
|
||||
onSuccess={() => {
|
||||
refetchWebhooks();
|
||||
closeWebhookModal();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -251,19 +251,6 @@ export enum KubePhase {
|
|||
Unknown = 5,
|
||||
}
|
||||
|
||||
export type Webhook = {
|
||||
_id: string;
|
||||
name: string;
|
||||
service: WebhookService;
|
||||
url: string;
|
||||
description?: string;
|
||||
headers?: Record<string, string>;
|
||||
queryParams?: Record<string, string>;
|
||||
body?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type NextApiConfigResponseData = {
|
||||
apiKey: string;
|
||||
collectorUrl: string;
|
||||
|
|
|
|||
107
packages/app/src/utils/webhookIcons.tsx
Normal file
107
packages/app/src/utils/webhookIcons.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
WebhookApiData,
|
||||
WebhookService,
|
||||
} from '@hyperdx/common-utils/dist/types';
|
||||
import { IconBrandSlack, IconLink, IconMail } from '@tabler/icons-react';
|
||||
|
||||
import { IncidentIOIcon } from '@/SVGIcons';
|
||||
|
||||
export interface ServiceConfig {
|
||||
name: string;
|
||||
icon: React.ReactElement;
|
||||
order: number;
|
||||
}
|
||||
|
||||
// Service type configuration with icons and display names
|
||||
export const WEBHOOK_SERVICE_CONFIG: Record<WebhookService, ServiceConfig> = {
|
||||
[WebhookService.Slack]: {
|
||||
name: 'Slack',
|
||||
icon: <IconBrandSlack size={16} />,
|
||||
order: 1,
|
||||
},
|
||||
[WebhookService.IncidentIO]: {
|
||||
name: 'incident.io',
|
||||
icon: <IncidentIOIcon width={16} />,
|
||||
order: 4,
|
||||
},
|
||||
[WebhookService.Generic]: {
|
||||
name: 'Generic',
|
||||
icon: <IconLink size={16} />,
|
||||
order: 5,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Channel icons for alert display (smaller sizes)
|
||||
export const CHANNEL_ICONS: Record<WebhookService, React.ReactElement> = {
|
||||
[WebhookService.Generic]: <IconLink size={16} />,
|
||||
[WebhookService.Slack]: <IconBrandSlack size={16} />,
|
||||
[WebhookService.IncidentIO]: <IncidentIOIcon width={16} />,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Get webhook service configuration by service type
|
||||
*/
|
||||
export const getWebhookServiceConfig = (
|
||||
serviceType: string | undefined,
|
||||
): ServiceConfig | undefined => {
|
||||
if (!serviceType) return undefined;
|
||||
return WEBHOOK_SERVICE_CONFIG[serviceType as WebhookService];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get webhook service icon for display in lists/headers
|
||||
*/
|
||||
export const getWebhookServiceIcon = (
|
||||
serviceType: string | undefined,
|
||||
): React.ReactElement => {
|
||||
const config = getWebhookServiceConfig(serviceType);
|
||||
return config?.icon || WEBHOOK_SERVICE_CONFIG[WebhookService.Generic].icon;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get webhook channel icon for alert tabs/smaller displays
|
||||
*/
|
||||
export const getWebhookChannelIcon = (
|
||||
serviceType: string | undefined,
|
||||
): React.ReactElement => {
|
||||
if (!serviceType) return CHANNEL_ICONS[WebhookService.Generic];
|
||||
return (
|
||||
CHANNEL_ICONS[serviceType as keyof typeof CHANNEL_ICONS] ||
|
||||
CHANNEL_ICONS[WebhookService.Generic]
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get webhook service display name
|
||||
*/
|
||||
export const getWebhookServiceName = (
|
||||
serviceType: string | undefined,
|
||||
): string => {
|
||||
const config = getWebhookServiceConfig(serviceType);
|
||||
return config?.name || serviceType || 'Unknown';
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to group webhooks by service type
|
||||
*/
|
||||
export const groupWebhooksByService = (webhooks: WebhookApiData[]) => {
|
||||
const grouped = webhooks.reduce(
|
||||
(acc, webhook) => {
|
||||
const service = webhook.service;
|
||||
if (!acc[service]) {
|
||||
acc[service] = [];
|
||||
}
|
||||
acc[service].push(webhook);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, WebhookApiData[]>,
|
||||
);
|
||||
|
||||
// Sort groups by predefined order
|
||||
return Object.entries(grouped).sort(([a], [b]) => {
|
||||
const orderA = WEBHOOK_SERVICE_CONFIG[a as WebhookService]?.order || 999;
|
||||
const orderB = WEBHOOK_SERVICE_CONFIG[b as WebhookService]?.order || 999;
|
||||
return orderA - orderB;
|
||||
});
|
||||
};
|
||||
|
|
@ -244,6 +244,23 @@ export enum WebhookService {
|
|||
IncidentIO = 'incidentio',
|
||||
}
|
||||
|
||||
// Base webhook interface (matches backend IWebhook but with JSON-serialized types)
|
||||
export interface IWebhook {
|
||||
_id: string;
|
||||
createdAt: string;
|
||||
name: string;
|
||||
service: WebhookService;
|
||||
updatedAt: string;
|
||||
url?: string;
|
||||
description?: string;
|
||||
queryParams?: Record<string, string>;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
// Webhook API response type (excludes team field for security)
|
||||
export type WebhookApiData = Omit<IWebhook, 'team'>;
|
||||
|
||||
// -------------------------
|
||||
// ALERTS
|
||||
// -------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue