Skip to content

Commit

Permalink
Chart alert: Connect UI to API (#105)
Browse files Browse the repository at this point in the history
Stacked on top of #104

Actual commit: b29369b

Testing:

- [x] Make sure log alerts work
- [x] - create
- [x] - update
- [x] - delete
- [x] Make sure chart alerts work
- [x] - create
- [x] - update
- [x] - delete
  • Loading branch information
svc-shorpo committed Nov 16, 2023
1 parent 956e5b5 commit 283f32a
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 75 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-falcons-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperdx/app': minor
---

Chart alerts: connect UI to API
2 changes: 1 addition & 1 deletion packages/app/src/CreateLogAlertModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function AlertForm({
groupBy: string | undefined;
interval: AlertInterval;
threshold: number;
type: string;
type: AlertType;
webhookId: string | undefined;
}) => void;
onDeleteClick: () => void;
Expand Down
120 changes: 99 additions & 21 deletions packages/app/src/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,15 +264,19 @@ const Tile = forwardRef(
);

const EditChartModal = ({
isLocalDashboard,
chart,
alerts,
dateRange,
onSave,
show,
onClose,
}: {
isLocalDashboard: boolean;
chart: Chart | undefined;
alerts: Alert[];
dateRange: [Date, Date];
onSave: (chart: Chart) => void;
onSave: (chart: Chart, alerts?: Alert[]) => void;
onClose: () => void;
show: boolean;
}) => {
Expand Down Expand Up @@ -356,9 +360,11 @@ const EditChartModal = ({
/>
{displayedTab === 'time' && chart != null && (
<EditLineChartForm
isLocalDashboard={isLocalDashboard}
chart={produce(chart, draft => {
draft.series[0].type = 'time';
})}
alerts={alerts}
onSave={onSave}
onClose={onClose}
dateRange={dateRange}
Expand Down Expand Up @@ -553,6 +559,9 @@ export default function DashboardPage() {
api.useDashboards();
const updateDashboard = api.useUpdateDashboard();
const createDashboard = api.useCreateDashboard();
const saveAlert = api.useSaveAlert();
const deleteAlert = api.useDeleteAlert();
const updateAlert = api.useUpdateAlert();
const router = useRouter();
const { dashboardId, config } = router.query;
const queryClient = useQueryClient();
Expand All @@ -563,6 +572,7 @@ export default function DashboardPage() {
id: '',
name: 'My New Dashboard',
charts: [],
alerts: [],
query: '',
}),
{ updateType: 'pushIn', enableBatching: true },
Expand Down Expand Up @@ -633,6 +643,10 @@ export default function DashboardPage() {
const deleteDashboard = api.useDeleteDashboard();

const [editedChart, setEditedChart] = useState<undefined | Chart>();
const editedChartAlerts = useMemo<Alert[]>(
() => dashboard?.alerts?.filter(a => a.chartId === editedChart?.id) || [],
[dashboard?.alerts, editedChart?.id],
);

const { searchedTimeRange, displayedTimeInputValue, onSearch } =
useNewTimeQuery({
Expand All @@ -648,7 +662,7 @@ export default function DashboardPage() {

const onAddChart = () => {
setEditedChart({
id: '',
id: Math.floor(100000000 * Math.random()).toString(36),
name: 'My New Chart',
x: 0,
y: 0,
Expand Down Expand Up @@ -706,6 +720,86 @@ export default function DashboardPage() {
],
);

const handleSaveChart = useCallback(
(newChart: Chart, newAlerts?: Alert[]) => {
if (dashboard == null) {
return;
}

setDashboard(
produce(dashboard, draft => {
const chartIndex = draft.charts.findIndex(
chart => chart.id === newChart.id,
);
// This is a new chart (probably?)
if (chartIndex === -1) {
draft.charts.push(newChart);
} else {
draft.charts[chartIndex] = newChart;
}
}),
);

// Using only the first alert for now
const [editedChartAlert] = editedChartAlerts;
const newAlert = newAlerts?.[0];

if (editedChartAlert?._id) {
// Update or delete
if (newAlert != null) {
updateAlert.mutate(
{
...newAlert,
id: editedChartAlert._id,
dashboardId: dashboardId as string,
chartId: editedChart?.id,
},
{
onError: err => {
console.error(err);
toast.error('Failed to update alert.');
},
},
);
} else {
deleteAlert.mutate(editedChartAlert._id, {
onError: err => {
console.error(err);
toast.error('Failed to delete alert.');
},
});
}
} else if (newAlert) {
// Create
saveAlert.mutate(
{
...newAlert,
dashboardId: dashboardId as string,
chartId: editedChart?.id,
},
{
onError: err => {
console.error(err);
toast.error('Failed to save alert.');
},
},
);
}

setEditedChart(undefined);
},
[
dashboard,
dashboardId,
deleteAlert,
editedChart?.id,
editedChartAlerts,
saveAlert,
setDashboard,
updateAlert,
],
);

const layout = (dashboard?.charts ?? []).map(chart => {
return {
i: chart.id,
Expand All @@ -726,30 +820,14 @@ export default function DashboardPage() {
<AppNav fixed />
{dashboard != null ? (
<EditChartModal
isLocalDashboard={isLocalDashboard}
dateRange={searchedTimeRange}
key={editedChart?.id}
chart={editedChart}
alerts={editedChartAlerts}
show={!!editedChart}
onClose={() => setEditedChart(undefined)}
onSave={newChart => {
setDashboard(
produce(dashboard, draft => {
const chartIndex = draft.charts.findIndex(
chart => chart.id === newChart.id,
);
if (chartIndex === -1) {
// This is a new chart (probably?)
draft.charts.push({
...newChart,
id: Math.floor(100000000 * Math.random()).toString(36),
});
} else {
draft.charts[chartIndex] = newChart;
}
}),
);
setEditedChart(undefined);
}}
onSave={handleSaveChart}
/>
) : null}
<div className="flex-grow-1">
Expand Down
49 changes: 32 additions & 17 deletions packages/app/src/EditChartForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useMemo, useState, useEffect } from 'react';
import produce from 'immer';
import HDXMarkdownChart from './HDXMarkdownChart';
import Select from 'react-select';
Expand Down Expand Up @@ -818,20 +818,24 @@ export const EditHistogramChartForm = ({
};

export const EditLineChartForm = ({
isLocalDashboard,
chart,
alerts,
onClose,
onSave,
dateRange,
}: {
isLocalDashboard: boolean;
chart: Chart | undefined;
alerts: Alert[];
dateRange: [Date, Date];
onSave: (chart: Chart) => void;
onSave: (chart: Chart, alerts?: Alert[]) => void;
onClose: () => void;
}) => {
const CHART_TYPE = 'time';

const [alert] = alerts; // TODO: Support multiple alerts eventually
const [editedChart, setEditedChart] = useState<Chart | undefined>(chart);
const [editedAlert, setEditedAlert] = useState<Alert | undefined>();
const [editedAlert, setEditedAlert] = useState<Alert | undefined>(alert);
const [alertEnabled, setAlertEnabled] = useState(editedAlert != null);

const chartConfig = useMemo(
Expand Down Expand Up @@ -867,7 +871,10 @@ export const EditLineChartForm = ({
<form
onSubmit={e => {
e.preventDefault();
onSave(editedChart);
onSave(
editedChart,
alertEnabled ? [editedAlert ?? DEFAULT_ALERT] : undefined,
);
}}
>
<div className="fs-5 mb-4">Line Chart Builder</div>
Expand Down Expand Up @@ -965,19 +972,27 @@ 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}
{isLocalDashboard ? (
<span className="text-gray-600 fs-8">
Alerts are not available in unsaved dashboards.
</span>
) : (
<>
<Checkbox
id="check"
label="Enable alerts"
checked={alertEnabled}
onChange={() => setAlertEnabled(!alertEnabled)}
/>
</div>
{alertEnabled && (
<div className="mt-2">
<EditChartFormAlerts
alert={editedAlert ?? DEFAULT_ALERT}
setAlert={setEditedAlert}
/>
</div>
)}
</>
)}
</div>
)}
Expand Down
10 changes: 7 additions & 3 deletions packages/app/src/EditChartFormAlerts.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import * as React from 'react';
import { Form } from 'react-bootstrap';
import type { Alert } from './types';
import { omit } from 'lodash';
import produce from 'immer';
import type { Alert } from './types';

import {
ALERT_INTERVAL_OPTIONS,
ALERT_CHANNEL_OPTIONS,
SlackChannelForm,
} from './Alert';

// Don't allow 1 minute alerts for charts
const CHART_ALERT_INTERVAL_OPTIONS = omit(ALERT_INTERVAL_OPTIONS, '1m');

type ChartAlertFormProps = {
alert: Alert;
setAlert: (alert?: Alert) => void;
Expand Down Expand Up @@ -77,7 +81,7 @@ export default function EditChartFormAlerts({
);
}}
>
{Object.entries(ALERT_INTERVAL_OPTIONS).map(([value, text]) => (
{Object.entries(CHART_ALERT_INTERVAL_OPTIONS).map(([value, text]) => (
<option key={value} value={value}>
{text}
</option>
Expand Down Expand Up @@ -110,7 +114,7 @@ export default function EditChartFormAlerts({
{alert?.channel?.type === 'webhook' && (
<SlackChannelForm
webhookSelectProps={{
value: alert?.channel?.webhookId,
value: alert?.channel?.webhookId || '',
onChange: e => {
setAlert(
produce(alert, draft => {
Expand Down
Loading

0 comments on commit 283f32a

Please sign in to comment.