-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: define types for CheckAlert * feat: add AlertsPerCheck and AlertCard components in Check form * feat: add schema for new alert fields * feat: add useCheckAlert hook and datasource methods to fetch and update alerts * feat: fetch alerts when creating/updating checks and format payload for api * feat: fetch alerts when editing a check, and format response for form * chore: mock API response (only for testing purposes) * fix: filter alerts according to the supported checktypes * fix: make current tests pass - adjust schema validation to make it optional - add test handlers for new alerts requests * fix: remove mocked response for testing with dev API * fix: set correct alert id to payload - Also, invalidate cache for fetching fresh check alerts * fix: remove alert mocks as the API is available in dev * test: add tests for creating a check with alerts per check * fix: remove percentiles dropdown - Instead of grouping by alert type, I'm displaying all alerts as separate ones - Grouping by type didn't allow to specify threshold for different percentiles * refactor: introduce new CheckAlert types: Base, Draft and Published * test: add mocks back temporarily - since the alerts API is no longer in dev, I'm adding the mocks back to be able to test it - This should be improved by #850 * refactor: avoid passing unneeded props to AlertsPerCheck - Also, adding loading and error states * fix: remove emtpy response from listAlertsforCheck * refactor: move predefined alerts to external file - Add specific predefined alerts according to the check type in a single constants file - Display threshold units * fix: tests * fix: rebase conflicts * refactor: display new alerts in list format * chore: remove unneeded AlertCard component * fix: tests * fix: humanize alert labels * fix: tests * feat: add feature flag - To enable the feature, set sm-alerts-per-check=true * fix: review comments * fix: add comment on query key * fix: change layout * feat: add default values for check alerts * fix: address review comments * fix: setting submitting form state when alerts request fails * fix: allow to delete input values * fix: set default value when generating form values * fix: tests * chore: remove checkAlert id concept (#1041) The API is removing the id property for check alerts as they can be identified just by the name * fix: center header and tweak description text * chore: remove mocked data as API is available in dev * fix: avoid setting 0 when there's no threshold value * fix: remove success message when alerts are saved * fix: add validation on percentage values * fix: when alerts load, add them to form defaults This prevents show the unsaved changes warning when there are no changes * fix: address review comments * fix: review comments
- Loading branch information
Showing
25 changed files
with
1,038 additions
and
25 deletions.
There are no files selected for viewing
25 changes: 25 additions & 0 deletions
25
src/components/CheckEditor/transformations/toFormValues.alerts.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { CheckAlertFormRecord, CheckAlertType } from 'types'; | ||
import { CheckAlertsResponse } from 'datasource/responses.types'; | ||
import { ALL_PREDEFINED_ALERTS } from 'components/CheckForm/AlertsPerCheck/AlertsPerCheck.constants'; | ||
|
||
export function getAlertCheckFormValues(data: CheckAlertsResponse): CheckAlertFormRecord { | ||
return Object.keys(CheckAlertType).reduce<CheckAlertFormRecord>((acc, alertTypeKey) => { | ||
const alertType = CheckAlertType[alertTypeKey as keyof typeof CheckAlertType]; | ||
|
||
const existingAlert = data.alerts.find((alert) => alert.name.includes(alertType)); | ||
|
||
if (existingAlert) { | ||
acc[alertType] = { | ||
threshold: existingAlert.threshold, | ||
isSelected: true, | ||
}; | ||
} else { | ||
acc[alertType] = { | ||
threshold: ALL_PREDEFINED_ALERTS.find((alert) => alert.type === alertType)?.default || 0, | ||
isSelected: false, | ||
}; | ||
} | ||
|
||
return acc; | ||
}, {}); | ||
} |
17 changes: 17 additions & 0 deletions
17
src/components/CheckEditor/transformations/toPayload.alerts.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { CheckAlertDraft, CheckAlertFormRecord, CheckAlertType } from 'types'; | ||
|
||
export function getAlertsPayload(formValues?: CheckAlertFormRecord, checkId?: number): CheckAlertDraft[] { | ||
if (!checkId || !formValues) { | ||
return []; | ||
} | ||
|
||
return Object.entries(formValues).reduce<CheckAlertDraft[]>((alerts, [alertType, alert]) => { | ||
if (alert.isSelected) { | ||
alerts.push({ | ||
name: alertType as CheckAlertType, | ||
threshold: alert.threshold!!, | ||
}); | ||
} | ||
return alerts; | ||
}, []); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import React from 'react'; | ||
import { Controller, useFormContext } from 'react-hook-form'; | ||
import { GrafanaTheme2 } from '@grafana/data'; | ||
import { Checkbox, Field, Input, Label, useStyles2 } from '@grafana/ui'; | ||
import { css } from '@emotion/css'; | ||
|
||
import { CheckAlertType, CheckFormValues } from 'types'; | ||
|
||
import { useCheckFormContext } from '../CheckFormContext/CheckFormContext'; | ||
import { PredefinedAlertInterface } from './AlertsPerCheck.constants'; | ||
|
||
export const AlertItem = ({ | ||
alert, | ||
selected, | ||
onSelectionChange, | ||
}: { | ||
alert: PredefinedAlertInterface; | ||
selected: boolean; | ||
onSelectionChange: (type: CheckAlertType) => void; | ||
}) => { | ||
const styles = useStyles2(getStyles); | ||
|
||
const { control, formState, getValues } = useFormContext<CheckFormValues>(); | ||
const { isFormDisabled } = useCheckFormContext(); | ||
|
||
const handleToggleAlert = (type: CheckAlertType) => { | ||
onSelectionChange(type); | ||
}; | ||
|
||
const threshold: number = getValues(`alerts.${alert.type}.threshold`); | ||
const thresholdError = formState.errors?.alerts?.[alert.type]?.threshold?.message; | ||
|
||
return ( | ||
<div key={alert.type} className={styles.item}> | ||
<div className={styles.itemInfo}> | ||
<Checkbox id={`alert-${alert.type}`} onClick={() => handleToggleAlert(alert.type)} checked={selected} /> | ||
<Label htmlFor={`alert-${alert.type}`} className={styles.columnLabel}> | ||
{alert.name} | ||
</Label> | ||
</div> | ||
<div className={styles.thresholdInput}> | ||
<Field | ||
label="Threshold" | ||
htmlFor={`alert-threshold-${alert.type}`} | ||
invalid={!!thresholdError} | ||
error={thresholdError} | ||
> | ||
<Controller | ||
name={`alerts.${alert.type}.threshold`} | ||
control={control} | ||
render={({ field }) => ( | ||
<Input | ||
aria-disabled={!selected} | ||
suffix={alert.unit} | ||
type="number" | ||
step="any" | ||
id={`alert-threshold-${alert.type}`} | ||
value={field.value !== undefined ? field.value : threshold} | ||
onChange={(e) => { | ||
const value = e.currentTarget.value; | ||
return field.onChange(value !== '' ? Number(value) : undefined); | ||
}} | ||
width={10} | ||
disabled={!selected || isFormDisabled} | ||
/> | ||
)} | ||
/> | ||
</Field> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
const getStyles = (theme: GrafanaTheme2) => ({ | ||
item: css({ | ||
display: `flex`, | ||
gap: theme.spacing(1), | ||
marginLeft: theme.spacing(1), | ||
}), | ||
|
||
itemInfo: css({ | ||
display: 'flex', | ||
alignItems: 'center', | ||
gap: theme.spacing(1), | ||
width: '50%', | ||
textWrap: 'wrap', | ||
}), | ||
|
||
columnLabel: css({ | ||
fontWeight: theme.typography.fontWeightLight, | ||
fontSize: theme.typography.h6.fontSize, | ||
lineHeight: theme.typography.body.lineHeight, | ||
marginBottom: '0', | ||
}), | ||
|
||
thresholdInput: css({ | ||
marginLeft: '22px', | ||
}), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import React from 'react'; | ||
import { GrafanaTheme2 } from '@grafana/data'; | ||
import { Label, Stack, Text, useStyles2 } from '@grafana/ui'; | ||
import { css } from '@emotion/css'; | ||
|
||
import { CheckAlertFormValues, CheckAlertType } from 'types'; | ||
|
||
import { AlertItem } from './AlertItem'; | ||
import { PredefinedAlertInterface } from './AlertsPerCheck.constants'; | ||
|
||
export const AlertsList = ({ | ||
title, | ||
alerts, | ||
selectedAlerts, | ||
onSelectionChange, | ||
}: { | ||
title: string; | ||
alerts: PredefinedAlertInterface[]; | ||
selectedAlerts?: Partial<Record<CheckAlertType, CheckAlertFormValues>>; | ||
onSelectionChange: (type: CheckAlertType) => void; | ||
}) => { | ||
const styles = useStyles2(getStyles); | ||
|
||
const handleToggleAlert = (type: CheckAlertType) => { | ||
onSelectionChange(type); | ||
}; | ||
|
||
return ( | ||
<div className={styles.column}> | ||
<div className={styles.sectionHeader}> | ||
<Label htmlFor={`header-${title}`} className={styles.headerLabel}> | ||
<Stack> | ||
<Text>{title}</Text> | ||
</Stack> | ||
</Label> | ||
</div> | ||
<div className={styles.list}> | ||
{alerts.map((alert: PredefinedAlertInterface) => ( | ||
<AlertItem | ||
key={alert.type} | ||
alert={alert} | ||
selected={!!selectedAlerts?.[alert.type]?.isSelected} | ||
onSelectionChange={() => handleToggleAlert(alert.type)} | ||
/> | ||
))} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
const getStyles = (theme: GrafanaTheme2) => ({ | ||
column: css({ | ||
fontSize: theme.typography.h6.fontSize, | ||
fontWeight: theme.typography.fontWeightLight, | ||
flex: 1, | ||
}), | ||
|
||
list: css({ | ||
display: 'flex', | ||
flexDirection: 'column', | ||
whiteSpace: 'nowrap', | ||
overflowY: 'auto', | ||
}), | ||
|
||
sectionHeader: css({ | ||
display: 'flex', | ||
border: `1px solid ${theme.colors.border.weak}`, | ||
backgroundColor: `${theme.colors.background.secondary}`, | ||
padding: theme.spacing(1), | ||
marginTop: theme.spacing(1), | ||
marginBottom: theme.spacing(1), | ||
gap: theme.spacing(1), | ||
verticalAlign: 'middle', | ||
alignItems: 'center', | ||
}), | ||
|
||
headerLabel: css({ | ||
fontWeight: theme.typography.fontWeightLight, | ||
fontSize: theme.typography.h5.fontSize, | ||
color: theme.colors.text.primary, | ||
marginBottom: 0, | ||
}), | ||
}); |
105 changes: 105 additions & 0 deletions
105
src/components/CheckForm/AlertsPerCheck/AlertsPerCheck.constants.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { CheckAlertCategory, CheckAlertType, CheckType, ThresholdUnit } from 'types'; | ||
|
||
export interface PredefinedAlertInterface { | ||
type: CheckAlertType; | ||
name: string; | ||
unit: ThresholdUnit; | ||
category: CheckAlertCategory; | ||
default?: number; | ||
} | ||
|
||
const GLOBAL_PREDEFINED_ALERTS: PredefinedAlertInterface[] = [ | ||
{ | ||
type: CheckAlertType.ProbeFailedExecutionsTooHigh, | ||
name: 'Probe Failed Executions Too High', | ||
unit: '%', | ||
category: CheckAlertCategory.SystemHealth, | ||
default: 10, | ||
}, | ||
]; | ||
|
||
const HTTP_PREDEFINED_ALERTS: PredefinedAlertInterface[] = [ | ||
{ | ||
type: CheckAlertType.HTTPRequestDurationTooHighP50, | ||
name: 'HTTP Request Duration Too High (P50)', | ||
unit: 'ms', | ||
category: CheckAlertCategory.RequestDuration, | ||
default: 300, | ||
}, | ||
{ | ||
type: CheckAlertType.HTTPRequestDurationTooHighP90, | ||
name: 'HTTP Request Duration Too High (P90)', | ||
unit: 'ms', | ||
category: CheckAlertCategory.RequestDuration, | ||
default: 500, | ||
}, | ||
{ | ||
type: CheckAlertType.HTTPRequestDurationTooHighP95, | ||
name: 'HTTP Request Duration Too High (P95)', | ||
unit: 'ms', | ||
category: CheckAlertCategory.RequestDuration, | ||
default: 800, | ||
}, | ||
{ | ||
type: CheckAlertType.HTTPRequestDurationTooHighP99, | ||
name: 'HTTP Request Duration Too High (P99)', | ||
unit: 'ms', | ||
category: CheckAlertCategory.RequestDuration, | ||
default: 1500, | ||
}, | ||
{ | ||
type: CheckAlertType.HTTPTargetCertificateCloseToExpiring, | ||
name: 'HTTP Target Certificate Close To Expiring', | ||
unit: 'd', | ||
category: CheckAlertCategory.SystemHealth, | ||
default: 60, | ||
}, | ||
]; | ||
|
||
const PING_PREDEFINED_ALERTS: PredefinedAlertInterface[] = [ | ||
{ | ||
type: CheckAlertType.PingICMPDurationTooHighP50, | ||
name: 'Ping ICMP Duration Too High (P50)', | ||
unit: 'ms', | ||
category: CheckAlertCategory.RequestDuration, | ||
default: 50, | ||
}, | ||
{ | ||
type: CheckAlertType.PingICMPDurationTooHighP90, | ||
name: 'Ping ICMP Duration Too High (P90)', | ||
unit: 'ms', | ||
category: CheckAlertCategory.RequestDuration, | ||
default: 100, | ||
}, | ||
{ | ||
type: CheckAlertType.PingICMPDurationTooHighP95, | ||
name: 'Ping ICMP Duration Too High (P95)', | ||
unit: 'ms', | ||
category: CheckAlertCategory.RequestDuration, | ||
default: 200, | ||
}, | ||
{ | ||
type: CheckAlertType.PingICMPDurationTooHighP99, | ||
name: 'Ping ICMP Duration Too High (P99)', | ||
unit: 'ms', | ||
category: CheckAlertCategory.RequestDuration, | ||
default: 400, | ||
}, | ||
]; | ||
|
||
export const PREDEFINED_ALERTS: Record<CheckType, PredefinedAlertInterface[]> = Object.fromEntries( | ||
Object.values(CheckType).map((checkType) => [ | ||
checkType, | ||
[ | ||
...GLOBAL_PREDEFINED_ALERTS, | ||
...(checkType === CheckType.HTTP ? HTTP_PREDEFINED_ALERTS : []), | ||
...(checkType === CheckType.PING ? PING_PREDEFINED_ALERTS : []), | ||
], | ||
]) | ||
) as Record<CheckType, PredefinedAlertInterface[]>; | ||
|
||
export const ALL_PREDEFINED_ALERTS: PredefinedAlertInterface[] = [ | ||
...GLOBAL_PREDEFINED_ALERTS, | ||
...HTTP_PREDEFINED_ALERTS, | ||
...PING_PREDEFINED_ALERTS, | ||
]; |
Oops, something went wrong.