mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
Chart alerts: Add UI to Chart Builder (#98)
Chart alerts: Add UI to Chart Builder Should only show up in dev env. 
This commit is contained in:
parent
f62fca8ef1
commit
2fcd167540
5 changed files with 192 additions and 9 deletions
5
.changeset/sharp-meals-cheat.md
Normal file
5
.changeset/sharp-meals-cheat.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hyperdx/app': patch
|
||||
---
|
||||
|
||||
Chart alerts: Add UI to chart builder
|
||||
|
|
@ -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' ? (
|
||||
|
|
|
|||
130
packages/app/src/EditChartFormAlerts.tsx
Normal file
130
packages/app/src/EditChartFormAlerts.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue