mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
Refactor: Extract shared alert logic into a separate component (#97)
A small refactor in preparation for adding alert settings to charts. Before – after: 
This commit is contained in:
parent
bbda6696bb
commit
e904ec3bd6
4 changed files with 140 additions and 70 deletions
5
.changeset/tall-lizards-care.md
Normal file
5
.changeset/tall-lizards-care.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hyperdx/app': patch
|
||||
---
|
||||
|
||||
Refactor: Extract shared alert logic into a separate component
|
||||
93
packages/app/src/Alert.tsx
Normal file
93
packages/app/src/Alert.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { sub } from 'date-fns';
|
||||
import { Form, FormSelectProps } from 'react-bootstrap';
|
||||
import api from './api';
|
||||
import type { AlertInterval, AlertChannelType } from './types';
|
||||
|
||||
export function intervalToGranularity(interval: AlertInterval) {
|
||||
if (interval === '1m') return '1 minute' as const;
|
||||
if (interval === '5m') return '5 minute' as const;
|
||||
if (interval === '15m') return '15 minute' as const;
|
||||
if (interval === '30m') return '30 minute' as const;
|
||||
if (interval === '1h') return '1 hour' as const;
|
||||
if (interval === '6h') return '6 hour' as const;
|
||||
if (interval === '12h') return '12 hour' as const;
|
||||
if (interval === '1d') return '1 day' as const;
|
||||
return '1 day';
|
||||
}
|
||||
|
||||
export function intervalToDateRange(interval: AlertInterval): [Date, Date] {
|
||||
const now = new Date();
|
||||
if (interval === '1m') return [sub(now, { minutes: 15 }), now];
|
||||
if (interval === '5m') return [sub(now, { hours: 1 }), now];
|
||||
if (interval === '15m') return [sub(now, { hours: 4 }), now];
|
||||
if (interval === '30m') return [sub(now, { hours: 8 }), now];
|
||||
if (interval === '1h') return [sub(now, { hours: 16 }), now];
|
||||
if (interval === '6h') return [sub(now, { days: 4 }), now];
|
||||
if (interval === '12h') return [sub(now, { days: 7 }), now];
|
||||
if (interval === '1d') return [sub(now, { days: 7 }), now];
|
||||
return [now, now];
|
||||
}
|
||||
|
||||
export const ALERT_INTERVAL_OPTIONS: Record<AlertInterval, string> = {
|
||||
'1m': '1 minute',
|
||||
'5m': '5 minute',
|
||||
'15m': '15 minute',
|
||||
'30m': '30 minute',
|
||||
'1h': '1 hour',
|
||||
'6h': '6 hour',
|
||||
'12h': '12 hour',
|
||||
'1d': '1 day',
|
||||
};
|
||||
|
||||
export const ALERT_CHANNEL_OPTIONS: Record<AlertChannelType, string> = {
|
||||
webhook: 'Slack Webhook',
|
||||
};
|
||||
|
||||
export const SlackChannelForm = ({
|
||||
webhookSelectProps,
|
||||
}: {
|
||||
webhookSelectProps: FormSelectProps;
|
||||
}) => {
|
||||
const { data: slackWebhooks } = api.useWebhooks('slack');
|
||||
|
||||
const hasSlackWebhooks =
|
||||
Array.isArray(slackWebhooks?.data) && slackWebhooks.data.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasSlackWebhooks && (
|
||||
<div className="mt-3">
|
||||
<Form.Label className="text-muted">Slack Webhook</Form.Label>
|
||||
<Form.Select
|
||||
className="bg-black border-0 mb-1 px-3"
|
||||
required
|
||||
id="webhookId"
|
||||
size="sm"
|
||||
{...webhookSelectProps}
|
||||
>
|
||||
{/* Ensure user selects a slack webhook before submitting form */}
|
||||
<option value="" disabled>
|
||||
Select a Slack Webhook
|
||||
</option>
|
||||
{slackWebhooks.data.map((sw: any) => (
|
||||
<option key={sw._id} value={sw._id}>
|
||||
{sw.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-2">
|
||||
<a
|
||||
href="/team"
|
||||
target="_blank"
|
||||
className="text-muted-hover d-flex align-items-center gap-1 fs-8"
|
||||
>
|
||||
<i className="bi bi-plus fs-5" />
|
||||
Add New Slack Incoming Webhook
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { Button, Form, Modal } from 'react-bootstrap';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { sub } from 'date-fns';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
|
|
@ -11,32 +10,19 @@ import { FieldSelect } from './ChartUtils';
|
|||
import { capitalizeFirstLetter } from './utils';
|
||||
import { genEnglishExplanation } from './queryv2';
|
||||
|
||||
import type { AlertChannelType, AlertInterval, LogView } from './types';
|
||||
|
||||
function intervalToGranularity(interval: AlertInterval) {
|
||||
if (interval === '1m') return '1 minute' as const;
|
||||
if (interval === '5m') return '5 minute' as const;
|
||||
if (interval === '15m') return '15 minute' as const;
|
||||
if (interval === '30m') return '30 minute' as const;
|
||||
if (interval === '1h') return '1 hour' as const;
|
||||
if (interval === '6h') return '6 hour' as const;
|
||||
if (interval === '12h') return '12 hour' as const;
|
||||
if (interval === '1d') return '1 day' as const;
|
||||
return '1 day';
|
||||
}
|
||||
|
||||
function intervalToDateRange(interval: AlertInterval): [Date, Date] {
|
||||
const now = new Date();
|
||||
if (interval === '1m') return [sub(now, { minutes: 15 }), now];
|
||||
if (interval === '5m') return [sub(now, { hours: 1 }), now];
|
||||
if (interval === '15m') return [sub(now, { hours: 4 }), now];
|
||||
if (interval === '30m') return [sub(now, { hours: 8 }), now];
|
||||
if (interval === '1h') return [sub(now, { hours: 16 }), now];
|
||||
if (interval === '6h') return [sub(now, { days: 4 }), now];
|
||||
if (interval === '12h') return [sub(now, { days: 7 }), now];
|
||||
if (interval === '1d') return [sub(now, { days: 7 }), now];
|
||||
return [now, now];
|
||||
}
|
||||
import type {
|
||||
AlertChannelType,
|
||||
AlertInterval,
|
||||
AlertType,
|
||||
LogView,
|
||||
} from './types';
|
||||
import {
|
||||
intervalToGranularity,
|
||||
intervalToDateRange,
|
||||
ALERT_INTERVAL_OPTIONS,
|
||||
ALERT_CHANNEL_OPTIONS,
|
||||
SlackChannelForm,
|
||||
} from './Alert';
|
||||
|
||||
function AlertForm({
|
||||
alertId,
|
||||
|
|
@ -47,11 +33,11 @@ function AlertForm({
|
|||
}: {
|
||||
defaultValues:
|
||||
| {
|
||||
channelType: AlertChannelType;
|
||||
groupBy: string | undefined;
|
||||
interval: AlertInterval;
|
||||
threshold: number;
|
||||
type: string;
|
||||
type: AlertType;
|
||||
channelType: AlertChannelType;
|
||||
webhookId: string | undefined;
|
||||
}
|
||||
| undefined;
|
||||
|
|
@ -67,6 +53,8 @@ function AlertForm({
|
|||
onDeleteClick: () => void;
|
||||
query: string;
|
||||
}) {
|
||||
const { data: team } = api.useTeam();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
|
|
@ -86,8 +74,6 @@ function AlertForm({
|
|||
}
|
||||
: undefined,
|
||||
});
|
||||
const { data: slackWebhooks } = api.useWebhooks('slack');
|
||||
const { data: team } = api.useTeam();
|
||||
|
||||
const channel = watch('channelType');
|
||||
const interval = watch('interval');
|
||||
|
|
@ -113,8 +99,12 @@ function AlertForm({
|
|||
<div className="me-2 mb-2">Alert when</div>
|
||||
<div className="me-2 mb-2">
|
||||
<Form.Select id="type" size="sm" {...register('type')}>
|
||||
<option value="presence">More Than</option>
|
||||
<option value="absence">Less Than</option>
|
||||
<option key="presence" value="presence">
|
||||
more than
|
||||
</option>
|
||||
<option key="absence" value="absence">
|
||||
less than
|
||||
</option>
|
||||
</Form.Select>
|
||||
</div>
|
||||
<Form.Control
|
||||
|
|
@ -129,14 +119,11 @@ function AlertForm({
|
|||
<div className="me-2 mb-2">lines appear within</div>
|
||||
<div className="me-2 mb-2">
|
||||
<Form.Select id="interval" size="sm" {...register('interval')}>
|
||||
<option value="1m">1 minute</option>
|
||||
<option value="5m">5 minutes</option>
|
||||
<option value="15m">15 minutes</option>
|
||||
<option value="30m">30 minutes</option>
|
||||
<option value="1h">1 hour</option>
|
||||
<option value="6h">6 hours</option>
|
||||
<option value="12h">12 hours</option>
|
||||
<option value="1d">1 day</option>
|
||||
{Object.entries(ALERT_INTERVAL_OPTIONS).map(([value, text]) => (
|
||||
<option key={value} value={value}>
|
||||
{text}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</div>
|
||||
<div className="d-flex align-items-center">
|
||||
|
|
@ -160,39 +147,21 @@ function AlertForm({
|
|||
<div className="me-2 mb-2">via</div>
|
||||
<div className="me-2 mb-2">
|
||||
<Form.Select id="channel" size="sm" {...register('channelType')}>
|
||||
<option value="webhook">Slack Webhook</option>
|
||||
{Object.entries(ALERT_CHANNEL_OPTIONS).map(([value, text]) => (
|
||||
<option key={value} value={value}>
|
||||
{text}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex align-items-center mb-2"></div>
|
||||
{channel === 'webhook' &&
|
||||
Array.isArray(slackWebhooks?.data) &&
|
||||
slackWebhooks.data.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<Form.Label>Slack Webhook</Form.Label>
|
||||
<Form.Select
|
||||
className="bg-black border-0 mb-4 px-3"
|
||||
id="webhookId"
|
||||
size="sm"
|
||||
{...register('webhookId')}
|
||||
>
|
||||
{slackWebhooks.data.map((sw: any) => (
|
||||
<option key={sw._id} value={sw._id}>
|
||||
{sw.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{channel === 'webhook' && (
|
||||
<div className="mb-4">
|
||||
<a href="/team" target="_blank">
|
||||
<i className="bi bi-plus me-1" />
|
||||
Add New Slack Incoming Webhook
|
||||
</a>
|
||||
</div>
|
||||
<SlackChannelForm webhookSelectProps={register('webhookId')} />
|
||||
)}
|
||||
|
||||
<div className="d-flex justify-content-between mt-4">
|
||||
<Button
|
||||
variant="outline-success"
|
||||
|
|
@ -207,6 +176,7 @@ function AlertForm({
|
|||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="mb-3 text-muted ps-2 fs-7">Alert Threshold Preview</div>
|
||||
<div style={{ height: 400 }}>
|
||||
|
|
@ -287,7 +257,7 @@ export default function CreateLogAlertModal({
|
|||
size="xl"
|
||||
>
|
||||
<Modal.Body className="bg-hdx-dark rounded">
|
||||
<div className="d-flex align-items-center mt-3 mb-2">
|
||||
<div className="d-flex align-items-center mt-3 flex-wrap mb-4">
|
||||
<h5 className="text-nowrap me-3 my-0">Alerts for</h5>
|
||||
{savedSearch == null ? (
|
||||
<Form.Control
|
||||
|
|
@ -422,7 +392,7 @@ export default function CreateLogAlertModal({
|
|||
threshold,
|
||||
interval,
|
||||
groupBy,
|
||||
channel: { type: 'webhook', webhookId },
|
||||
channel: { type: channelType, webhookId },
|
||||
logViewId: savedSearch._id,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ export type LogView = {
|
|||
alerts?: Alert[];
|
||||
};
|
||||
|
||||
export type AlertType = 'presence' | 'absence';
|
||||
|
||||
export type AlertInterval =
|
||||
| '1m'
|
||||
| '5m'
|
||||
|
|
@ -71,7 +73,7 @@ export type Alert = {
|
|||
state: 'ALERT' | 'OK';
|
||||
threshold: number;
|
||||
timezone: string;
|
||||
type: 'presence' | 'absence';
|
||||
type: AlertType;
|
||||
source: 'LOG' | 'CHART';
|
||||
|
||||
// Log alerts
|
||||
|
|
|
|||
Loading…
Reference in a new issue