Repository navigation
feat(app): add an alert UI extension point for detection modes #3623
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@hyperdx/app': patch | ||
| --- | ||
|
|
||
| feat: add an internal extension point for alert detection modes in the chart editor; tile alerts edit, preview and save as before |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import { | ||
| Control, | ||
| Controller, | ||
| UseFormSetValue, | ||
| useWatch, | ||
| } from 'react-hook-form'; | ||
| import { | ||
| AlertThresholdType, | ||
| isRangeThresholdType, | ||
| } from '@hyperdx/common-utils/dist/types'; | ||
| import { NativeSelect, NumberInput, Text } from '@mantine/core'; | ||
|
|
||
| import { ChartEditorFormState } from '@/components/ChartEditor/types'; | ||
| import { optionsToSelectData } from '@/utils'; | ||
| import { TILE_ALERT_THRESHOLD_TYPE_OPTIONS } from '@/utils/alerts'; | ||
|
|
||
| /** The comparison, threshold and (for ranges) upper bound of a tile alert. */ | ||
| export function ThresholdConditionFields({ | ||
| control, | ||
| setValue, | ||
| }: { | ||
| control: Control<ChartEditorFormState>; | ||
| setValue: UseFormSetValue<ChartEditorFormState>; | ||
| }) { | ||
| const alertThresholdType = useWatch({ control, name: 'alert.thresholdType' }); | ||
| const alertThreshold = useWatch({ control, name: 'alert.threshold' }); | ||
| const alertThresholdMax = useWatch({ control, name: 'alert.thresholdMax' }); | ||
|
|
||
| return ( | ||
| <> | ||
| <Controller | ||
| control={control} | ||
| name="alert.thresholdType" | ||
| render={({ field }) => ( | ||
| <NativeSelect | ||
| data={optionsToSelectData(TILE_ALERT_THRESHOLD_TYPE_OPTIONS)} | ||
| size="xs" | ||
| {...field} | ||
| onChange={e => { | ||
| field.onChange(e); | ||
| if ( | ||
| isRangeThresholdType(e.currentTarget.value) && | ||
| alertThresholdMax == null | ||
| ) { | ||
| setValue('alert.thresholdMax', (alertThreshold ?? 0) + 1); | ||
| } | ||
| }} | ||
| /> | ||
| )} | ||
| /> | ||
| <Controller | ||
| control={control} | ||
| name="alert.threshold" | ||
| render={({ field }) => <NumberInput size="xs" w={80} {...field} />} | ||
| /> | ||
| {isRangeThresholdType(alertThresholdType as AlertThresholdType) && ( | ||
| <> | ||
| <Text size="sm" opacity={0.7}> | ||
| and | ||
| </Text> | ||
| <Controller | ||
| control={control} | ||
| name="alert.thresholdMax" | ||
| render={({ field, fieldState }) => ( | ||
| <NumberInput | ||
| size="xs" | ||
| w={80} | ||
| {...field} | ||
| error={fieldState.error?.message} | ||
| /> | ||
| )} | ||
| /> | ||
| </> | ||
| )} | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,17 +6,13 @@ import { | |
| } from 'react-hook-form'; | ||
| import { Badge } from '@clickhouse/click-ui'; | ||
| import { formatTileAlertDisplayName } from '@hyperdx/common-utils/dist/alerts'; | ||
| import { | ||
| AlertThresholdType, | ||
| isRangeThresholdType, | ||
| } from '@hyperdx/common-utils/dist/types'; | ||
| import { AlertThresholdType } from '@hyperdx/common-utils/dist/types'; | ||
| import { | ||
| ActionIcon, | ||
| Alert, | ||
| Box, | ||
| Group, | ||
| NativeSelect, | ||
| NumberInput, | ||
| Paper, | ||
| Text, | ||
| Tooltip, | ||
|
|
@@ -39,6 +35,7 @@ import { | |
| import { AlertChannelForm } from '@/components/Alerts'; | ||
| import { AckAlert } from '@/components/alerts/AckAlert'; | ||
| import { AlertHistoryCardList } from '@/components/alerts/AlertHistoryCards'; | ||
| import { useAlertConditionUI } from '@/components/alerts/alertUIExtension'; | ||
| import { AlertScheduleFields } from '@/components/AlertScheduleFields'; | ||
| import { ChartEditorFormState } from '@/components/ChartEditor/types'; | ||
| import { useDashboards } from '@/dashboard'; | ||
|
|
@@ -48,9 +45,10 @@ import { | |
| ALERT_CHANNEL_OPTIONS, | ||
| intervalToMinutes, | ||
| TILE_ALERT_INTERVAL_OPTIONS, | ||
| TILE_ALERT_THRESHOLD_TYPE_OPTIONS, | ||
| } from '@/utils/alerts'; | ||
|
|
||
| import { ThresholdConditionFields } from './ThresholdConditionFields'; | ||
|
|
||
| export function TileAlertEditor({ | ||
| control, | ||
| setValue, | ||
|
|
@@ -87,8 +85,7 @@ export function TileAlertEditor({ | |
| }; | ||
|
|
||
| const alertThresholdType = useWatch({ control, name: 'alert.thresholdType' }); | ||
| const alertThreshold = useWatch({ control, name: 'alert.threshold' }); | ||
| const alertThresholdMax = useWatch({ control, name: 'alert.thresholdMax' }); | ||
| const conditionUI = useAlertConditionUI({ control, setValue, alert }); | ||
| const alertScheduleOffsetMinutes = useWatch({ | ||
| control, | ||
| name: 'alert.scheduleOffsetMinutes', | ||
|
|
@@ -188,63 +185,23 @@ export function TileAlertEditor({ | |
| </Group> | ||
| </Group> | ||
| <Box px="sm" pb="sm"> | ||
| {conditionUI.header} | ||
| <Group gap="xs"> | ||
| <Text size="sm" opacity={0.7}> | ||
| Trigger when the value | ||
| {conditionUI.conditionLabel ?? 'Trigger when the value'} | ||
| </Text> | ||
| <Controller | ||
| control={control} | ||
| name="alert.thresholdType" | ||
| render={({ field }) => ( | ||
| <NativeSelect | ||
| data={optionsToSelectData(TILE_ALERT_THRESHOLD_TYPE_OPTIONS)} | ||
| size="xs" | ||
| {...field} | ||
| onChange={e => { | ||
| field.onChange(e); | ||
| if ( | ||
| isRangeThresholdType(e.currentTarget.value) && | ||
| alertThresholdMax == null | ||
| ) { | ||
| setValue('alert.thresholdMax', (alertThreshold ?? 0) + 1); | ||
| } | ||
| }} | ||
| /> | ||
| )} | ||
| /> | ||
| <Controller | ||
| control={control} | ||
| name="alert.threshold" | ||
| render={({ field }) => ( | ||
| <NumberInput size="xs" w={80} {...field} /> | ||
| )} | ||
| /> | ||
| {isRangeThresholdType(alertThresholdType as AlertThresholdType) && ( | ||
| <> | ||
| <Text size="sm" opacity={0.7}> | ||
| and | ||
| </Text> | ||
| <Controller | ||
| control={control} | ||
| name="alert.thresholdMax" | ||
| render={({ field, fieldState }) => ( | ||
| <NumberInput | ||
| size="xs" | ||
| w={80} | ||
| {...field} | ||
| error={fieldState.error?.message} | ||
| /> | ||
| )} | ||
| /> | ||
| </> | ||
| {conditionUI.conditionFields ?? ( | ||
| <ThresholdConditionFields control={control} setValue={setValue} /> | ||
| )} | ||
| over | ||
| {conditionUI.intervalLabel ?? 'over'} | ||
| <Controller | ||
| control={control} | ||
| name="alert.interval" | ||
| render={({ field }) => ( | ||
| <NativeSelect | ||
| data={optionsToSelectData(TILE_ALERT_INTERVAL_OPTIONS)} | ||
| data={optionsToSelectData( | ||
| conditionUI.intervalOptions ?? TILE_ALERT_INTERVAL_OPTIONS, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Narrowed If the extension passes Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| )} | ||
| size="xs" | ||
| {...field} | ||
| /> | ||
|
|
@@ -265,6 +222,7 @@ export function TileAlertEditor({ | |
| )} | ||
| /> | ||
| </Group> | ||
| {conditionUI.footer} | ||
| {alert?.createdBy && ( | ||
| <Text size="xs" opacity={0.6} mt="xs"> | ||
| Created by {alert.createdBy.name || alert.createdBy.email} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
potentially blocking here, seems
handleSaveAlertdoesn't callbeforeSaveThis check only runs in
handleSave. The edit-alert modal saves throughhandleSaveAlerta little further down, so an extension that blocks a save there would still see the alert saved. Would something like this make sense?