Chart alerts: Add UI to Chart Builder (#98)

Chart alerts: Add UI to Chart Builder

Should only show up in dev env.

![Screenshot 2023-11-13 at 10 16 09 AM](https://github.com/hyperdxio/hyperdx/assets/149748269/e5ca0278-3ca0-4b31-8734-40d8fbb79a8c)
This commit is contained in:
Shorpo 2023-11-14 01:26:45 -07:00 committed by GitHub
parent f62fca8ef1
commit 2fcd167540
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 192 additions and 9 deletions

View file

@ -0,0 +1,5 @@
---
'@hyperdx/app': patch
---
Chart alerts: Add UI to chart builder

View file

@ -3,7 +3,9 @@ import produce from 'immer';
import HDXMarkdownChart from './HDXMarkdownChart';
import Select from 'react-select';
import { Button, Form, InputGroup, Modal } from 'react-bootstrap';
import * as config from './config';
import type { Alert } from './types';
import Checkbox from './Checkbox';
import HDXLineChart from './HDXLineChart';
import {
AGG_FNS,
@ -15,9 +17,10 @@ import {
import { hashCode, useDebounce } from './utils';
import HDXHistogramChart from './HDXHistogramChart';
import { LogTableWithSidePanel } from './LogTableWithSidePanel';
import EditChartFormAlerts from './EditChartFormAlerts';
import HDXNumberChart from './HDXNumberChart';
import HDXTableChart from './HDXTableChart';
import { intervalToGranularity } from './Alert';
export type Chart = {
id: string;
@ -69,6 +72,16 @@ export type Chart = {
)[];
};
const DEFAULT_ALERT: Alert = {
channel: {
type: 'webhook',
},
threshold: 1,
interval: '1m',
type: 'presence',
source: 'CHART',
};
export const EditMarkdownChartForm = ({
chart,
onClose,
@ -818,6 +831,8 @@ export const EditLineChartForm = ({
const CHART_TYPE = 'time';
const [editedChart, setEditedChart] = useState<Chart | undefined>(chart);
const [editedAlert, setEditedAlert] = useState<Alert | undefined>();
const [alertEnabled, setAlertEnabled] = useState(editedAlert != null);
const chartConfig = useMemo(
() =>
@ -828,11 +843,14 @@ export const EditLineChartForm = ({
field: editedChart.series[0].field ?? '', // TODO: Fix in definition
groupBy: editedChart.series[0].groupBy[0],
where: editedChart.series[0].where,
granularity: convertDateRangeToGranularityString(dateRange, 60),
granularity:
alertEnabled && editedAlert?.interval
? intervalToGranularity(editedAlert?.interval)
: convertDateRangeToGranularityString(dateRange, 60),
dateRange,
}
: null,
[editedChart, dateRange],
[editedChart, alertEnabled, editedAlert?.interval, dateRange],
);
const previewConfig = useDebounce(chartConfig, 500);
@ -944,6 +962,26 @@ export const EditLineChartForm = ({
);
}}
/>
{config.CHART_ALERTS_ENABLED && (
<div className="mt-4 border-top border-bottom border-grey p-2 py-3">
<Checkbox
id="check"
label="Enable alerts"
checked={alertEnabled}
onChange={() => setAlertEnabled(!alertEnabled)}
/>
{alertEnabled && (
<div className="mt-2">
<EditChartFormAlerts
alert={editedAlert ?? DEFAULT_ALERT}
setAlert={setEditedAlert}
/>
</div>
)}
</div>
)}
<div className="d-flex justify-content-between my-3 ps-2">
<Button
variant="outline-success"
@ -959,7 +997,14 @@ export const EditLineChartForm = ({
<div className="mt-4">
<div className="mb-3 text-muted ps-2 fs-7">Chart Preview</div>
<div style={{ height: 400 }}>
<HDXLineChart config={previewConfig} />
<HDXLineChart
config={previewConfig}
{...(alertEnabled && {
alertThreshold: editedAlert?.threshold,
alertThresholdType:
editedAlert?.type === 'presence' ? 'above' : 'below',
})}
/>
</div>
</div>
{editedChart.series[0].table === 'logs' ? (

View file

@ -0,0 +1,130 @@
import * as React from 'react';
import { Form } from 'react-bootstrap';
import type { Alert } from './types';
import produce from 'immer';
import {
ALERT_INTERVAL_OPTIONS,
ALERT_CHANNEL_OPTIONS,
SlackChannelForm,
} from './Alert';
type ChartAlertFormProps = {
alert: Alert;
setAlert: (alert?: Alert) => void;
};
export default function EditChartFormAlerts({
alert,
setAlert,
}: ChartAlertFormProps) {
return (
<>
<div className="d-flex align-items-center gap-3">
Alert when the value
<Form.Select
id="type"
size="sm"
style={{
width: 140,
}}
value={alert?.type}
onChange={e => {
setAlert(
produce(alert, draft => {
draft.type = e.target.value as 'presence' | 'absence';
}),
);
}}
>
<option key="presence" value="presence">
exceeds
</option>
<option key="absence" value="absence">
falls below
</option>
</Form.Select>
<Form.Control
style={{ width: 70 }}
type="number"
required
id="threshold"
size="sm"
defaultValue={1}
value={alert?.threshold}
onChange={e => {
setAlert(
produce(alert, draft => {
draft.threshold = parseFloat(e.target.value);
}),
);
}}
/>
over
<Form.Select
id="interval"
size="sm"
style={{
width: 140,
}}
value={alert?.interval}
onChange={e => {
setAlert(
produce(alert, draft => {
draft.interval = e.target
.value as keyof typeof ALERT_INTERVAL_OPTIONS;
}),
);
}}
>
{Object.entries(ALERT_INTERVAL_OPTIONS).map(([value, text]) => (
<option key={value} value={value}>
{text}
</option>
))}
</Form.Select>
window via
<Form.Select
id="channel"
size="sm"
style={{ width: 200 }}
value={alert?.channel?.type}
onChange={e => {
setAlert(
produce(alert, draft => {
draft.channel = {
type: e.target.value as keyof typeof ALERT_CHANNEL_OPTIONS,
};
}),
);
}}
>
{Object.entries(ALERT_CHANNEL_OPTIONS).map(([value, text]) => (
<option key={value} value={value}>
{text}
</option>
))}
</Form.Select>
</div>
<div className="mt-3">
{alert?.channel?.type === 'webhook' && (
<SlackChannelForm
webhookSelectProps={{
value: alert?.channel?.webhookId,
onChange: e => {
setAlert(
produce(alert, draft => {
draft.channel = {
type: 'webhook',
webhookId: e.target.value,
};
}),
);
},
}}
/>
)}
</div>
</>
);
}

View file

@ -9,3 +9,6 @@ export const HDX_COLLECTOR_URL =
'http://localhost:4318';
export const IS_OSS = process.env.NEXT_PUBLIC_IS_OSS ?? 'true' === 'true';
// Features in development
export const CHART_ALERTS_ENABLED = process.env.NODE_ENV === 'development';

View file

@ -66,13 +66,13 @@ export type AlertChannel = {
};
export type Alert = {
_id: string;
_id?: string;
channel: AlertChannel;
cron: string;
cron?: string;
interval: AlertInterval;
state: 'ALERT' | 'OK';
state?: 'ALERT' | 'OK';
threshold: number;
timezone: string;
timezone?: string;
type: AlertType;
source: 'LOG' | 'CHART';