grafana/public/app/features/alerting/unified/AmRoutes.tsx
gotjosh 6572017ec7
Alerting: Allow more characters in label names so notifications are sent (#38629)
Remove validation for labels to be accepted in the Alertmanager, This helps with datasources that produce non-compatible labels.

Adds an "object_matchers" to alert manager routers so we can support labels names with extended characters beyond prometheus/openmetrics. It only does this for the internal Grafana managed Alert Manager.

This requires a change to alert manager, so for now we use grafana/alertmanager which is a slight fork, with the intention of going back to upstream.

The frontend handles the migration of "matchers" -> "object_matchers" when the route is edited and saved. Once this is done, downgrades will not work old versions will not recognize the "object_matchers".

Co-authored-by: Kyle Brandt <kyle@grafana.com>
Co-authored-by: Nathan Rodman <nathanrodman@gmail.com>
2021-10-04 15:06:40 +02:00

138 lines
4.7 KiB
TypeScript

import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui';
import { useDispatch } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { Receiver } from 'app/plugins/datasource/alertmanager/types';
import { useCleanup } from '../../../core/hooks/useCleanup';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AmRootRoute } from './components/amroutes/AmRootRoute';
import { AmSpecificRouting } from './components/amroutes/AmSpecificRouting';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchAlertManagerConfigAction, updateAlertManagerConfigAction } from './state/actions';
import { AmRouteReceiver, FormAmRoute } from './types/amroutes';
import { amRouteToFormAmRoute, formAmRouteToAmRoute, stringsToSelectableValues } from './utils/amroutes';
import { initialAsyncRequestState } from './utils/redux';
import { isVanillaPrometheusAlertManagerDataSource } from './utils/datasource';
const AmRoutes: FC = () => {
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
const [isRootRouteEditMode, setIsRootRouteEditMode] = useState(false);
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
const readOnly = alertManagerSourceName ? isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName) : true;
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
const fetchConfig = useCallback(() => {
if (alertManagerSourceName) {
dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
}
}, [alertManagerSourceName, dispatch]);
useEffect(() => {
fetchConfig();
}, [fetchConfig]);
const { result, loading: resultLoading, error: resultError } =
(alertManagerSourceName && amConfigs[alertManagerSourceName]) || initialAsyncRequestState;
const config = result?.alertmanager_config;
const [rootRoute, id2ExistingRoute] = useMemo(() => amRouteToFormAmRoute(config?.route), [config?.route]);
const receivers = stringsToSelectableValues(
(config?.receivers ?? []).map((receiver: Receiver) => receiver.name)
) as AmRouteReceiver[];
const enterRootRouteEditMode = () => {
setIsRootRouteEditMode(true);
};
const exitRootRouteEditMode = () => {
setIsRootRouteEditMode(false);
};
useCleanup((state) => state.unifiedAlerting.saveAMConfig);
const handleSave = (data: Partial<FormAmRoute>) => {
const newData = formAmRouteToAmRoute(
alertManagerSourceName,
{
...rootRoute,
...data,
},
id2ExistingRoute
);
if (isRootRouteEditMode) {
exitRootRouteEditMode();
}
dispatch(
updateAlertManagerConfigAction({
newConfig: {
...result,
alertmanager_config: {
...result.alertmanager_config,
route: newData,
},
},
oldConfig: result,
alertManagerSourceName: alertManagerSourceName!,
successMessage: 'Saved',
refetch: true,
})
);
};
if (!alertManagerSourceName) {
return <Redirect to="/alerting/routes" />;
}
return (
<AlertingPageWrapper pageId="am-routes">
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
{resultError && !resultLoading && (
<Alert severity="error" title="Error loading Alertmanager config">
{resultError.message || 'Unknown error.'}
</Alert>
)}
{resultLoading && <LoadingPlaceholder text="Loading Alertmanager config..." />}
{result && !resultLoading && !resultError && (
<>
<AmRootRoute
alertManagerSourceName={alertManagerSourceName}
isEditMode={isRootRouteEditMode}
onSave={handleSave}
onEnterEditMode={enterRootRouteEditMode}
onExitEditMode={exitRootRouteEditMode}
receivers={receivers}
routes={rootRoute}
/>
<div className={styles.break} />
<AmSpecificRouting
onChange={handleSave}
readOnly={readOnly}
onRootRouteEdit={enterRootRouteEditMode}
receivers={receivers}
routes={rootRoute}
/>
</>
)}
</AlertingPageWrapper>
);
};
export default withErrorBoundary(AmRoutes, { style: 'page' });
const getStyles = (theme: GrafanaTheme2) => ({
break: css`
width: 100%;
height: 0;
margin-bottom: ${theme.spacing(2)};
`,
});