mirror of https://github.com/grafana/grafana
Alerting: Choose a previous valid AM configuration in case of error (#65746)
* Add new property to AlertmanagerConfig type * Implement fetching successfully applied configurations Added method to fetch them from the API and its corresponding action and reducer * Extract ConfigEditor as component to avoid code duplication * Display dropdown with valid configs upon error and allow to save them * Fix tests * Refactor to call new endpoint using RTK * Improve texts * Apply suggested refactor * Change constant casing * Only show config selector for Grafana AM * Remove ts-ignore * Move code together for simplicity * Remove invalid mock * Update endpoint and types based on backend changes * Rename property * Rename alermanager config property from backend changes * Disable editing old configurations Due to the latest backend changes, we no longer will provide the option to edit previous AM configurations in a textearea. Instead users will only be allowed to reset to a specific one with the same content. For this reason the textearea for old conf igurations is disabled and a different form action (not submit) is executed on the "reset config" button. The updateAlertManage rConfigAction is reset to its old functionality due to these changes. * Add id to AlertManagerCortexConfig type We'll need it to pass as a parameter to the new reset endpoint * Add new endpoint for resetting AM configs to an old version * Move the "Reset to selected configuration" button next to the drop-down * Add relative offset to configurationspull/66059/head
parent
85f738cdf9
commit
f27326f7d9
@ -0,0 +1,110 @@ |
||||
import { css } from '@emotion/css'; |
||||
import React, { useMemo } from 'react'; |
||||
|
||||
import { dateTime, GrafanaTheme2, SelectableValue } from '@grafana/data'; |
||||
import { Button, HorizontalGroup, Select, useStyles2 } from '@grafana/ui'; |
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; |
||||
|
||||
import { alertmanagerApi } from '../../api/alertmanagerApi'; |
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; |
||||
|
||||
import { FormValues } from './AlertmanagerConfig'; |
||||
import { ConfigEditor } from './ConfigEditor'; |
||||
|
||||
export interface ValidAmConfigOption { |
||||
label?: string; |
||||
value?: AlertManagerCortexConfig; |
||||
} |
||||
|
||||
interface AlertmanagerConfigSelectorProps { |
||||
onChange: (selectedOption: ValidAmConfigOption) => void; |
||||
selectedAmConfig?: ValidAmConfigOption; |
||||
defaultValues: FormValues; |
||||
onSubmit: (values: FormValues, oldConfig?: AlertManagerCortexConfig) => void; |
||||
readOnly: boolean; |
||||
loading: boolean; |
||||
} |
||||
|
||||
export default function AlertmanagerConfigSelector({ |
||||
onChange, |
||||
selectedAmConfig, |
||||
defaultValues, |
||||
onSubmit, |
||||
readOnly, |
||||
loading, |
||||
}: AlertmanagerConfigSelectorProps): JSX.Element { |
||||
const { useGetValidAlertManagersConfigQuery, useResetAlertManagerConfigToOldVersionMutation } = alertmanagerApi; |
||||
|
||||
const styles = useStyles2(getStyles); |
||||
|
||||
const { currentData: validAmConfigs, isLoading: isFetchingValidAmConfigs } = useGetValidAlertManagersConfigQuery(); |
||||
|
||||
const [resetAlertManagerConfigToOldVersion] = useResetAlertManagerConfigToOldVersionMutation(); |
||||
|
||||
const validAmConfigsOptions = useMemo(() => { |
||||
if (!validAmConfigs?.length) { |
||||
return []; |
||||
} |
||||
|
||||
const configs: ValidAmConfigOption[] = validAmConfigs.map((config) => { |
||||
const date = new Date(config.last_applied!); |
||||
return { |
||||
label: config.last_applied |
||||
? `Config from ${date.toLocaleString()} (${dateTime(date).locale('en').fromNow(true)} ago)` |
||||
: 'Previous config', |
||||
value: config, |
||||
}; |
||||
}); |
||||
onChange(configs[0]); |
||||
return configs; |
||||
}, [validAmConfigs, onChange]); |
||||
|
||||
const onResetClick = async () => { |
||||
const id = selectedAmConfig?.value?.id; |
||||
if (id === undefined) { |
||||
return; |
||||
} |
||||
|
||||
resetAlertManagerConfigToOldVersion({ id }); |
||||
}; |
||||
|
||||
return ( |
||||
<> |
||||
{!isFetchingValidAmConfigs && validAmConfigs && validAmConfigs.length > 0 ? ( |
||||
<> |
||||
<div>Select a previous working configuration until you fix this error:</div> |
||||
|
||||
<div className={styles.container}> |
||||
<HorizontalGroup align="flex-start" spacing="md"> |
||||
<Select |
||||
options={validAmConfigsOptions} |
||||
value={selectedAmConfig} |
||||
onChange={(value: SelectableValue) => { |
||||
onChange(value); |
||||
}} |
||||
/> |
||||
|
||||
<Button variant="primary" disabled={loading} onClick={onResetClick}> |
||||
Reset to selected configuration |
||||
</Button> |
||||
</HorizontalGroup> |
||||
</div> |
||||
|
||||
<ConfigEditor |
||||
defaultValues={defaultValues} |
||||
onSubmit={(values) => onSubmit(values)} |
||||
readOnly={readOnly} |
||||
loading={loading} |
||||
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME} |
||||
/> |
||||
</> |
||||
) : null} |
||||
</> |
||||
); |
||||
} |
||||
const getStyles = (theme: GrafanaTheme2) => ({ |
||||
container: css` |
||||
margin-top: ${theme.spacing(2)}; |
||||
margin-bottom: ${theme.spacing(2)}; |
||||
`,
|
||||
}); |
@ -0,0 +1,96 @@ |
||||
import React from 'react'; |
||||
|
||||
import { Button, ConfirmModal, TextArea, HorizontalGroup, Field, Form } from '@grafana/ui'; |
||||
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; |
||||
|
||||
import { FormValues } from './AlertmanagerConfig'; |
||||
|
||||
interface ConfigEditorProps { |
||||
defaultValues: { configJSON: string }; |
||||
readOnly: boolean; |
||||
loading: boolean; |
||||
alertManagerSourceName?: string; |
||||
onSubmit: (values: FormValues) => void; |
||||
showConfirmDeleteAMConfig?: boolean; |
||||
onReset?: () => void; |
||||
onConfirmReset?: () => void; |
||||
onDismiss?: () => void; |
||||
} |
||||
|
||||
export const ConfigEditor = ({ |
||||
defaultValues, |
||||
readOnly, |
||||
loading, |
||||
alertManagerSourceName, |
||||
showConfirmDeleteAMConfig, |
||||
onSubmit, |
||||
onReset, |
||||
onConfirmReset, |
||||
onDismiss, |
||||
}: ConfigEditorProps) => { |
||||
return ( |
||||
<Form defaultValues={defaultValues} onSubmit={onSubmit} key={defaultValues.configJSON}> |
||||
{({ register, errors }) => ( |
||||
<> |
||||
{!readOnly && ( |
||||
<> |
||||
<Field |
||||
disabled={loading} |
||||
label="Configuration" |
||||
invalid={!!errors.configJSON} |
||||
error={errors.configJSON?.message} |
||||
> |
||||
<TextArea |
||||
{...register('configJSON', { |
||||
required: { value: true, message: 'Required.' }, |
||||
validate: (v) => { |
||||
try { |
||||
JSON.parse(v); |
||||
return true; |
||||
} catch (e) { |
||||
return e instanceof Error ? e.message : 'Invalid JSON.'; |
||||
} |
||||
}, |
||||
})} |
||||
id="configuration" |
||||
rows={25} |
||||
/> |
||||
</Field> |
||||
|
||||
<HorizontalGroup> |
||||
<Button type="submit" variant="primary" disabled={loading}> |
||||
Save |
||||
</Button> |
||||
{onReset && ( |
||||
<Button type="button" disabled={loading} variant="destructive" onClick={onReset}> |
||||
Reset configuration |
||||
</Button> |
||||
)} |
||||
</HorizontalGroup> |
||||
</> |
||||
)} |
||||
{readOnly && ( |
||||
<Field label="Configuration"> |
||||
<pre data-testid="readonly-config">{defaultValues.configJSON}</pre> |
||||
</Field> |
||||
)} |
||||
{Boolean(showConfirmDeleteAMConfig) && onConfirmReset && onDismiss && ( |
||||
<ConfirmModal |
||||
isOpen={true} |
||||
title="Reset Alertmanager configuration" |
||||
body={`Are you sure you want to reset configuration ${ |
||||
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME |
||||
? 'for the Grafana Alertmanager' |
||||
: `for "${alertManagerSourceName}"` |
||||
}? Contact points and notification policies will be reset to their defaults.`}
|
||||
confirmText="Yes, reset configuration" |
||||
onConfirm={onConfirmReset} |
||||
onDismiss={onDismiss} |
||||
/> |
||||
)} |
||||
</> |
||||
)} |
||||
</Form> |
||||
); |
||||
}; |
Loading…
Reference in new issue