Refactor: Extract shared alert logic into a separate component (#97)

A small refactor in preparation for adding alert settings to charts.

Before – after:
![Screenshot 2023-11-11 at 7 22 21 PM](https://github.com/hyperdxio/hyperdx/assets/149748269/99c6518f-340f-49f6-a0c1-83dfc403df60)
This commit is contained in:
Shorpo 2023-11-11 23:20:08 -07:00 committed by GitHub
parent bbda6696bb
commit e904ec3bd6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 140 additions and 70 deletions

View file

@ -0,0 +1,5 @@
---
'@hyperdx/app': patch
---
Refactor: Extract shared alert logic into a separate component

View 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>
</>
);
};

View file

@ -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,
},
{

View file

@ -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