mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 21:37:41 +00:00
## Summary This PR adds new types of alert thresholds: >, <=, =, and !=. ### Screenshots or video https://github.com/user-attachments/assets/159bffc4-87e5-41af-b59b-51d4bc88d6ed ### How to test locally or on Vercel This must be tested locally, since alerts are not supported in the preview environment. To see the notification content, run an echo server locally and create a webhook that targets it (http://localhost:3000): ```bash npx http-echo-server ``` ### References - Linear Issue: Closes HDX-3988 - Related PRs:
233 lines
6.9 KiB
TypeScript
233 lines
6.9 KiB
TypeScript
import {
|
|
Control,
|
|
Controller,
|
|
UseFormSetValue,
|
|
useWatch,
|
|
} from 'react-hook-form';
|
|
import { AlertThresholdType } from '@hyperdx/common-utils/dist/types';
|
|
import {
|
|
ActionIcon,
|
|
Alert,
|
|
Badge,
|
|
Box,
|
|
Collapse,
|
|
Group,
|
|
NativeSelect,
|
|
NumberInput,
|
|
Paper,
|
|
Text,
|
|
Tooltip,
|
|
UnstyledButton,
|
|
} from '@mantine/core';
|
|
import { useDisclosure } from '@mantine/hooks';
|
|
import {
|
|
IconChevronDown,
|
|
IconHelpCircle,
|
|
IconInfoCircleFilled,
|
|
IconTrash,
|
|
} from '@tabler/icons-react';
|
|
|
|
import api from '@/api';
|
|
import { AlertChannelForm } from '@/components/Alerts';
|
|
import { AckAlert } from '@/components/alerts/AckAlert';
|
|
import { AlertHistoryCardList } from '@/components/alerts/AlertHistoryCards';
|
|
import { AlertScheduleFields } from '@/components/AlertScheduleFields';
|
|
import { ChartEditorFormState } from '@/components/ChartEditor/types';
|
|
import { optionsToSelectData } from '@/utils';
|
|
import {
|
|
ALERT_CHANNEL_OPTIONS,
|
|
intervalToMinutes,
|
|
TILE_ALERT_INTERVAL_OPTIONS,
|
|
TILE_ALERT_THRESHOLD_TYPE_OPTIONS,
|
|
} from '@/utils/alerts';
|
|
|
|
export function TileAlertEditor({
|
|
control,
|
|
setValue,
|
|
alert,
|
|
onRemove,
|
|
error,
|
|
warning,
|
|
tooltip,
|
|
}: {
|
|
control: Control<ChartEditorFormState>;
|
|
setValue: UseFormSetValue<ChartEditorFormState>;
|
|
alert: NonNullable<ChartEditorFormState['alert']>;
|
|
onRemove: () => void;
|
|
error?: string;
|
|
warning?: string;
|
|
tooltip?: string;
|
|
}) {
|
|
const [opened, { toggle }] = useDisclosure(true);
|
|
|
|
const alertChannelType = useWatch({ control, name: 'alert.channel.type' });
|
|
const alertThresholdType = useWatch({ control, name: 'alert.thresholdType' });
|
|
const alertScheduleOffsetMinutes = useWatch({
|
|
control,
|
|
name: 'alert.scheduleOffsetMinutes',
|
|
});
|
|
const maxAlertScheduleOffsetMinutes = alert?.interval
|
|
? Math.max(intervalToMinutes(alert.interval) - 1, 0)
|
|
: 0;
|
|
const alertIntervalLabel = alert?.interval
|
|
? TILE_ALERT_INTERVAL_OPTIONS[alert.interval]
|
|
: undefined;
|
|
|
|
const { data: alertData } = api.useAlert(alert.id);
|
|
const alertItem = alertData?.data;
|
|
|
|
return (
|
|
<Paper data-testid="alert-details">
|
|
<Group justify="space-between" px="sm" pt="sm" pb="sm">
|
|
<UnstyledButton onClick={toggle}>
|
|
<Group gap="xs">
|
|
<IconChevronDown
|
|
size={14}
|
|
style={{
|
|
transform: opened ? 'rotate(0deg)' : 'rotate(-90deg)',
|
|
transition: 'transform 200ms',
|
|
}}
|
|
/>
|
|
<Group gap={4} align="center">
|
|
<Text size="sm" fw={500} mt={2}>
|
|
Alert
|
|
</Text>
|
|
{tooltip && (
|
|
<Tooltip label={tooltip} withArrow>
|
|
<IconHelpCircle size={16} opacity={0.5} />
|
|
</Tooltip>
|
|
)}
|
|
{error && (
|
|
<Tooltip label={error} withArrow>
|
|
<Badge
|
|
color="var(--color-text-danger)"
|
|
size="xs"
|
|
variant="light"
|
|
ml="xs"
|
|
>
|
|
Invalid Query
|
|
</Badge>
|
|
</Tooltip>
|
|
)}
|
|
{warning && (
|
|
<Tooltip label={warning} withArrow>
|
|
<Badge color="yellow" size="xs" variant="light" ml="xs">
|
|
Warning
|
|
</Badge>
|
|
</Tooltip>
|
|
)}
|
|
</Group>
|
|
</Group>
|
|
</UnstyledButton>
|
|
<Group gap="xs">
|
|
{alertItem && alertItem.history.length > 0 && (
|
|
<AlertHistoryCardList history={alertItem.history} />
|
|
)}
|
|
{alertItem && <AckAlert alert={alertItem} />}
|
|
<Tooltip label="Remove alert">
|
|
<ActionIcon
|
|
variant="danger"
|
|
color="red"
|
|
size="sm"
|
|
onClick={onRemove}
|
|
data-testid="remove-alert-button"
|
|
>
|
|
<IconTrash size={14} />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
</Group>
|
|
</Group>
|
|
<Collapse expanded={opened}>
|
|
<Box px="sm" pb="sm">
|
|
<Group gap="xs">
|
|
<Text size="sm" opacity={0.7}>
|
|
Trigger when the value
|
|
</Text>
|
|
<Controller
|
|
control={control}
|
|
name="alert.thresholdType"
|
|
render={({ field }) => (
|
|
<NativeSelect
|
|
data={optionsToSelectData(TILE_ALERT_THRESHOLD_TYPE_OPTIONS)}
|
|
size="xs"
|
|
{...field}
|
|
/>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="alert.threshold"
|
|
render={({ field }) => (
|
|
<NumberInput size="xs" w={80} {...field} />
|
|
)}
|
|
/>
|
|
over
|
|
<Controller
|
|
control={control}
|
|
name="alert.interval"
|
|
render={({ field }) => (
|
|
<NativeSelect
|
|
data={optionsToSelectData(TILE_ALERT_INTERVAL_OPTIONS)}
|
|
size="xs"
|
|
{...field}
|
|
/>
|
|
)}
|
|
/>
|
|
<Text size="sm" opacity={0.7}>
|
|
window via
|
|
</Text>
|
|
<Controller
|
|
control={control}
|
|
name="alert.channel.type"
|
|
render={({ field }) => (
|
|
<NativeSelect
|
|
data={optionsToSelectData(ALERT_CHANNEL_OPTIONS)}
|
|
size="xs"
|
|
{...field}
|
|
/>
|
|
)}
|
|
/>
|
|
</Group>
|
|
{alert?.createdBy && (
|
|
<Text size="xs" opacity={0.6} mt="xs">
|
|
Created by {alert.createdBy.name || alert.createdBy.email}
|
|
</Text>
|
|
)}
|
|
<AlertScheduleFields
|
|
control={control}
|
|
setValue={setValue}
|
|
scheduleOffsetName="alert.scheduleOffsetMinutes"
|
|
scheduleStartAtName="alert.scheduleStartAt"
|
|
scheduleOffsetMinutes={alertScheduleOffsetMinutes}
|
|
maxScheduleOffsetMinutes={maxAlertScheduleOffsetMinutes}
|
|
offsetWindowLabel={
|
|
alertIntervalLabel
|
|
? `from each ${alertIntervalLabel} window`
|
|
: 'from each alert window'
|
|
}
|
|
/>
|
|
<Text size="xxs" opacity={0.5} mb={4} mt="sm">
|
|
Send to
|
|
</Text>
|
|
<AlertChannelForm
|
|
control={control}
|
|
type={alertChannelType}
|
|
namePrefix="alert."
|
|
/>
|
|
{(alertThresholdType === AlertThresholdType.EQUAL ||
|
|
alertThresholdType === AlertThresholdType.NOT_EQUAL) && (
|
|
<Alert
|
|
icon={<IconInfoCircleFilled size={16} />}
|
|
color="gray"
|
|
py="xs"
|
|
mt="md"
|
|
>
|
|
Note: Floating-point query results are not rounded during equality
|
|
comparison.
|
|
</Alert>
|
|
)}
|
|
</Box>
|
|
</Collapse>
|
|
</Paper>
|
|
);
|
|
}
|