mirror of
https://github.com/grafana/grafana.git
synced 2025-01-09 23:53:25 -06:00
Alerting: refactor fetchAlertManagerConfigAction to use RTK Query (#71261)
This commit is contained in:
parent
49b7edfed4
commit
fc5d43e1bf
@ -1925,6 +1925,9 @@ exports[`better eslint`] = {
|
||||
"public/app/features/alerting/unified/components/silences/SilencesFilter.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/alerting/unified/hooks/useAlertmanagerConfig.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/alerting/unified/hooks/useControlledFieldArray.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
@ -5,43 +5,27 @@ import { NavModelItem } from '@grafana/data';
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
|
||||
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
|
||||
import MuteTimingForm from './components/mute-timings/MuteTimingForm';
|
||||
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
||||
import { useAlertmanagerConfig } from './hooks/useAlertmanagerConfig';
|
||||
import { useAlertmanager } from './state/AlertmanagerContext';
|
||||
import { fetchAlertManagerConfigAction } from './state/actions';
|
||||
import { initialAsyncRequestState } from './utils/redux';
|
||||
|
||||
const MuteTimings = () => {
|
||||
const [queryParams] = useQueryParams();
|
||||
const dispatch = useDispatch();
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
|
||||
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||
|
||||
const fetchConfig = useCallback(() => {
|
||||
if (selectedAlertmanager) {
|
||||
dispatch(fetchAlertManagerConfigAction(selectedAlertmanager));
|
||||
}
|
||||
}, [selectedAlertmanager, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
}, [fetchConfig]);
|
||||
|
||||
const { result, error, loading } =
|
||||
(selectedAlertmanager && amConfigs[selectedAlertmanager]) || initialAsyncRequestState;
|
||||
|
||||
const config = result?.alertmanager_config;
|
||||
const { currentData, isLoading, error } = useAlertmanagerConfig(selectedAlertmanager, {
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
});
|
||||
const config = currentData?.alertmanager_config;
|
||||
|
||||
const getMuteTimingByName = useCallback(
|
||||
(id: string): MuteTimeInterval | undefined => {
|
||||
const timing = config?.mute_time_intervals?.find(({ name }: MuteTimeInterval) => name === id);
|
||||
|
||||
if (timing) {
|
||||
const provenance = (config?.muteTimeProvenances ?? {})[timing.name];
|
||||
const provenance = config?.muteTimeProvenances?.[timing.name];
|
||||
|
||||
return {
|
||||
...timing,
|
||||
@ -56,15 +40,15 @@ const MuteTimings = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && !loading && !result && (
|
||||
{error && !isLoading && !currentData && (
|
||||
<Alert severity="error" title={`Error loading Alertmanager config for ${selectedAlertmanager}`}>
|
||||
{error.message || 'Unknown error.'}
|
||||
</Alert>
|
||||
)}
|
||||
{result && !error && (
|
||||
{currentData && !error && (
|
||||
<Switch>
|
||||
<Route exact path="/alerting/routes/mute-timing/new">
|
||||
<MuteTimingForm loading={loading} />
|
||||
<MuteTimingForm loading={isLoading} />
|
||||
</Route>
|
||||
<Route exact path="/alerting/routes/mute-timing/edit">
|
||||
{() => {
|
||||
@ -74,9 +58,9 @@ const MuteTimings = () => {
|
||||
|
||||
return (
|
||||
<MuteTimingForm
|
||||
loading={loading}
|
||||
loading={isLoading}
|
||||
muteTiming={muteTiming}
|
||||
showError={!muteTiming && !loading}
|
||||
showError={!muteTiming && !isLoading}
|
||||
provenance={provenance}
|
||||
/>
|
||||
);
|
||||
|
@ -64,7 +64,16 @@ const AmRoutes = () => {
|
||||
|
||||
const contactPointsState = useGetContactPointsState(selectedAlertmanager ?? '');
|
||||
|
||||
const { result, config, loading: resultLoading, error: resultError } = useAlertmanagerConfig(selectedAlertmanager);
|
||||
const {
|
||||
currentData: result,
|
||||
isLoading: resultLoading,
|
||||
error: resultError,
|
||||
} = useAlertmanagerConfig(selectedAlertmanager, {
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
});
|
||||
|
||||
const config = result?.alertmanager_config;
|
||||
|
||||
const { currentData: alertGroups, refetch: refetchAlertGroups } = useGetAlertmanagerAlertGroupsQuery(
|
||||
{ amSourceName: selectedAlertmanager ?? '' },
|
||||
@ -187,7 +196,7 @@ const AmRoutes = () => {
|
||||
|
||||
const numberOfMuteTimings = result?.alertmanager_config.mute_time_intervals?.length ?? 0;
|
||||
const haveData = result && !resultError && !resultLoading;
|
||||
const isLoading = !result && resultLoading;
|
||||
const isFetching = !result && resultLoading;
|
||||
const haveError = resultError && !resultLoading;
|
||||
|
||||
const muteTimingsTabActive = activeTab === ActiveTab.MuteTimings;
|
||||
@ -215,7 +224,7 @@ const AmRoutes = () => {
|
||||
/>
|
||||
</TabsBar>
|
||||
<TabContent className={styles.tabContent}>
|
||||
{isLoading && <LoadingPlaceholder text="Loading Alertmanager config..." />}
|
||||
{isFetching && <LoadingPlaceholder text="Loading Alertmanager config..." />}
|
||||
{haveError && (
|
||||
<Alert severity="error" title="Error loading Alertmanager config">
|
||||
{resultError.message || 'Unknown error.'}
|
||||
|
@ -27,6 +27,6 @@ export const backendSrvBaseQuery = (): BaseQueryFn<BackendSrvRequest> => async (
|
||||
export const alertingApi = createApi({
|
||||
reducerPath: 'alertingApi',
|
||||
baseQuery: backendSrvBaseQuery(),
|
||||
tagTypes: ['AlertmanagerChoice'],
|
||||
tagTypes: ['AlertmanagerChoice', 'AlertmanagerConfiguration'],
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
|
@ -1,3 +1,7 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
||||
import {
|
||||
AlertmanagerAlert,
|
||||
AlertmanagerChoice,
|
||||
@ -8,13 +12,22 @@ import {
|
||||
ExternalAlertmanagersResponse,
|
||||
Matcher,
|
||||
} from '../../../../plugins/datasource/alertmanager/types';
|
||||
import { withPerformanceLogging } from '../Analytics';
|
||||
import { matcherToOperator } from '../utils/alertmanager';
|
||||
import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
import { wrapWithQuotes } from '../utils/misc';
|
||||
import {
|
||||
getDatasourceAPIUid,
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
isVanillaPrometheusAlertManagerDataSource,
|
||||
} from '../utils/datasource';
|
||||
import { retryWhile, wrapWithQuotes } from '../utils/misc';
|
||||
import { messageFromError, withSerializedError } from '../utils/redux';
|
||||
|
||||
import { alertingApi } from './alertingApi';
|
||||
import { fetchAlertManagerConfig, fetchStatus } from './alertmanager';
|
||||
import { featureDiscoveryApi } from './featureDiscoveryApi';
|
||||
|
||||
const LIMIT_TO_SUCCESSFULLY_APPLIED_AMS = 10;
|
||||
const FETCH_CONFIG_RETRY_TIMEOUT = 30 * 1000;
|
||||
|
||||
export interface AlertmanagersChoiceResponse {
|
||||
alertmanagersChoice: AlertmanagerChoice;
|
||||
@ -107,5 +120,111 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
|
||||
method: 'POST',
|
||||
}),
|
||||
}),
|
||||
|
||||
// TODO we've sort of inherited the errors format here from the previous Redux actions, errors throw are of type "SerializedError"
|
||||
getAlertmanagerConfiguration: build.query<AlertManagerCortexConfig, string>({
|
||||
queryFn: async (alertmanagerSourceName: string) => {
|
||||
const isGrafanaManagedAlertmanager = alertmanagerSourceName === GRAFANA_RULES_SOURCE_NAME;
|
||||
const isVanillaPrometheusAlertmanager = isVanillaPrometheusAlertManagerDataSource(alertmanagerSourceName);
|
||||
|
||||
// for vanilla prometheus, there is no config endpoint. Only fetch config from status
|
||||
if (isVanillaPrometheusAlertmanager) {
|
||||
return withSerializedError(
|
||||
fetchStatus(alertmanagerSourceName).then((status) => ({
|
||||
data: {
|
||||
alertmanager_config: status.config,
|
||||
template_files: {},
|
||||
},
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// discover features, we want to know if Mimir has "lazyConfigInit" configured
|
||||
const { data: alertmanagerFeatures } = await dispatch(
|
||||
featureDiscoveryApi.endpoints.discoverAmFeatures.initiate({
|
||||
amSourceName: alertmanagerSourceName,
|
||||
})
|
||||
);
|
||||
|
||||
const defaultConfig = {
|
||||
alertmanager_config: {},
|
||||
template_files: {},
|
||||
template_file_provenances: {},
|
||||
};
|
||||
|
||||
const lazyConfigInitSupported = alertmanagerFeatures?.lazyConfigInit ?? false;
|
||||
|
||||
// wrap our fetchConfig function with some performance logging functions
|
||||
const fetchAMconfigWithLogging = withPerformanceLogging(
|
||||
fetchAlertManagerConfig,
|
||||
`[${alertmanagerSourceName}] Alertmanager config loaded`,
|
||||
{
|
||||
dataSourceName: alertmanagerSourceName,
|
||||
thunk: 'unifiedalerting/fetchAmConfig',
|
||||
}
|
||||
);
|
||||
|
||||
const tryFetchingConfiguration = retryWhile(
|
||||
() => fetchAMconfigWithLogging(alertmanagerSourceName),
|
||||
// if config has been recently deleted, it takes a while for cortex start returning the default one.
|
||||
// retry for a short while instead of failing
|
||||
(error) =>
|
||||
!!messageFromError(error)?.includes('alertmanager storage object not found') && !lazyConfigInitSupported,
|
||||
FETCH_CONFIG_RETRY_TIMEOUT
|
||||
)
|
||||
.then((result) => {
|
||||
if (isGrafanaManagedAlertmanager) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// if user config is empty for Mimir alertmanager, try to get config from status endpoint
|
||||
const emptyConfiguration = isEmpty(result.alertmanager_config) && isEmpty(result.template_files);
|
||||
|
||||
if (emptyConfiguration) {
|
||||
return fetchStatus(alertmanagerSourceName).then((status) => ({
|
||||
alertmanager_config: status.config,
|
||||
template_files: {},
|
||||
template_file_provenances: result.template_file_provenances,
|
||||
last_applied: result.last_applied,
|
||||
id: result.id,
|
||||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
.then((result) => result ?? defaultConfig)
|
||||
.then((result) => ({ data: result }))
|
||||
.catch((error) => {
|
||||
// When mimir doesn't have fallback AM url configured the default response will be as above
|
||||
// However it's fine, and it's possible to create AM configuration
|
||||
if (lazyConfigInitSupported && messageFromError(error)?.includes('alertmanager storage object not found')) {
|
||||
return {
|
||||
data: defaultConfig,
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
||||
return withSerializedError(tryFetchingConfiguration).catch((err) => ({
|
||||
error: err,
|
||||
data: undefined,
|
||||
}));
|
||||
},
|
||||
providesTags: ['AlertmanagerConfiguration'],
|
||||
}),
|
||||
|
||||
updateAlertmanagerConfiguration: build.mutation<
|
||||
void,
|
||||
{ selectedAlertmanager: string; config: AlertManagerCortexConfig }
|
||||
>({
|
||||
query: ({ selectedAlertmanager, config, ...rest }) => ({
|
||||
url: `/api/alertmanager/${getDatasourceAPIUid(selectedAlertmanager)}/config/api/v1/alerts`,
|
||||
method: 'POST',
|
||||
data: config,
|
||||
...rest,
|
||||
}),
|
||||
invalidatesTags: ['AlertmanagerConfiguration'],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
@ -132,7 +132,7 @@ describe('Admin config', () => {
|
||||
await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled());
|
||||
expect(mocks.api.updateAlertManagerConfig.mock.lastCall).toMatchSnapshot();
|
||||
|
||||
await waitFor(() => expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3));
|
||||
await waitFor(() => expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
|
||||
it('Read-only when using Prometheus Alertmanager', async () => {
|
||||
|
@ -1,19 +1,15 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, useStyles2 } from '@grafana/ui';
|
||||
import { useDispatch } from 'app/types';
|
||||
|
||||
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
import {
|
||||
deleteAlertManagerConfigAction,
|
||||
fetchAlertManagerConfigAction,
|
||||
updateAlertManagerConfigAction,
|
||||
} from '../../state/actions';
|
||||
import { deleteAlertManagerConfigAction, updateAlertManagerConfigAction } from '../../state/actions';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
||||
import { initialAsyncRequestState } from '../../utils/redux';
|
||||
|
||||
import AlertmanagerConfigSelector, { ValidAmConfigOption } from './AlertmanagerConfigSelector';
|
||||
import { ConfigEditor } from './ConfigEditor';
|
||||
@ -33,21 +29,13 @@ export default function AlertmanagerConfig(): JSX.Element {
|
||||
const readOnly = selectedAlertmanager ? isVanillaPrometheusAlertManagerDataSource(selectedAlertmanager) : false;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||
|
||||
const [selectedAmConfig, setSelectedAmConfig] = useState<ValidAmConfigOption | undefined>();
|
||||
|
||||
const {
|
||||
result: config,
|
||||
loading: isLoadingConfig,
|
||||
currentData: config,
|
||||
error: loadingError,
|
||||
} = (selectedAlertmanager && configRequests[selectedAlertmanager]) || initialAsyncRequestState;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedAlertmanager) {
|
||||
dispatch(fetchAlertManagerConfigAction(selectedAlertmanager));
|
||||
}
|
||||
}, [selectedAlertmanager, dispatch]);
|
||||
isLoading: isLoadingConfig,
|
||||
} = useAlertmanagerConfig(selectedAlertmanager);
|
||||
|
||||
const resetConfig = () => {
|
||||
if (selectedAlertmanager) {
|
||||
|
@ -306,7 +306,7 @@ describe('Receivers', () => {
|
||||
// see that we're back to main page and proper api calls have been made
|
||||
await ui.receiversTable.find();
|
||||
expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1);
|
||||
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications');
|
||||
expect(mocks.api.updateConfig).toHaveBeenLastCalledWith(GRAFANA_RULES_SOURCE_NAME, {
|
||||
...someGrafanaAlertManagerConfig,
|
||||
@ -400,7 +400,8 @@ describe('Receivers', () => {
|
||||
// see that we're back to main page and proper api calls have been made
|
||||
await ui.receiversTable.find();
|
||||
expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications');
|
||||
expect(mocks.api.updateConfig).toHaveBeenLastCalledWith('CloudManager', {
|
||||
...someCloudAlertManagerConfig,
|
||||
|
@ -1,14 +1,14 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom';
|
||||
import { Route, RouteChildrenProps, Switch } from 'react-router-dom';
|
||||
|
||||
import { Alert, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { useDispatch } from 'app/types';
|
||||
|
||||
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
import { fetchAlertManagerConfigAction, fetchGrafanaNotifiersAction } from '../../state/actions';
|
||||
import { fetchGrafanaNotifiersAction } from '../../state/actions';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { initialAsyncRequestState } from '../../utils/redux';
|
||||
import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
|
||||
import { DuplicateTemplateView } from '../receivers/DuplicateTemplateView';
|
||||
import { EditReceiverView } from '../receivers/EditReceiverView';
|
||||
@ -25,28 +25,10 @@ export interface NotificationErrorProps {
|
||||
const Receivers = () => {
|
||||
const { selectedAlertmanager: alertManagerSourceName } = useAlertmanager();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const location = useLocation();
|
||||
const isRoot = location.pathname.endsWith('/alerting/notifications');
|
||||
const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||
|
||||
const {
|
||||
result: config,
|
||||
loading,
|
||||
error,
|
||||
} = (alertManagerSourceName && configRequests[alertManagerSourceName]) || initialAsyncRequestState;
|
||||
const { currentData: config, isLoading: loading, error } = useAlertmanagerConfig(alertManagerSourceName);
|
||||
|
||||
const receiverTypes = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
|
||||
|
||||
const shouldLoadConfig = isRoot || !config;
|
||||
// const shouldRenderNotificationStatus = isRoot;
|
||||
|
||||
useEffect(() => {
|
||||
if (alertManagerSourceName && shouldLoadConfig) {
|
||||
dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
|
||||
}
|
||||
}, [alertManagerSourceName, dispatch, shouldLoadConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
|
||||
|
@ -4,21 +4,16 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Button, Field, FieldSet, Input, LinkButton, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
|
||||
import {
|
||||
AlertmanagerConfig,
|
||||
AlertManagerCortexConfig,
|
||||
MuteTimeInterval,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
import { updateAlertManagerConfigAction } from '../../state/actions';
|
||||
import { MuteTimingFields } from '../../types/mute-timing-form';
|
||||
import { renameMuteTimings } from '../../utils/alertmanager';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { createMuteTiming, defaultTimeInterval } from '../../utils/mute-timings';
|
||||
import { initialAsyncRequestState } from '../../utils/redux';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||
|
||||
import { MuteTimingTimeInterval } from './MuteTimingTimeInterval';
|
||||
@ -62,21 +57,22 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
|
||||
|
||||
const [updating, setUpdating] = useState(false);
|
||||
|
||||
const defaultAmCortexConfig = { alertmanager_config: {}, template_files: {} };
|
||||
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||
const { result = defaultAmCortexConfig } =
|
||||
(selectedAlertmanager && amConfigs[selectedAlertmanager]) || initialAsyncRequestState;
|
||||
const { currentData: result } = useAlertmanagerConfig(selectedAlertmanager);
|
||||
const config = result?.alertmanager_config;
|
||||
|
||||
const config: AlertmanagerConfig = result?.alertmanager_config ?? {};
|
||||
const defaultValues = useDefaultValues(muteTiming);
|
||||
const formApi = useForm({ defaultValues });
|
||||
|
||||
const onSubmit = (values: MuteTimingFields) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newMuteTiming = createMuteTiming(values);
|
||||
|
||||
const muteTimings = muteTiming
|
||||
? config?.mute_time_intervals?.filter(({ name }) => name !== muteTiming.name)
|
||||
: config.mute_time_intervals;
|
||||
: config?.mute_time_intervals;
|
||||
|
||||
const newConfig: AlertManagerCortexConfig = {
|
||||
...result,
|
||||
@ -84,8 +80,8 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
|
||||
...config,
|
||||
route:
|
||||
muteTiming && newMuteTiming.name !== muteTiming.name
|
||||
? renameMuteTimings(newMuteTiming.name, muteTiming.name, config.route ?? {})
|
||||
: config.route,
|
||||
? renameMuteTimings(newMuteTiming.name, muteTiming.name, config?.route ?? {})
|
||||
: config?.route,
|
||||
mute_time_intervals: [...(muteTimings || []), newMuteTiming],
|
||||
},
|
||||
};
|
||||
|
@ -5,15 +5,14 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { IconButton, LinkButton, Link, useStyles2, ConfirmModal } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
import { Authorize } from '../../components/Authorize';
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
|
||||
import { deleteMuteTimingAction } from '../../state/actions';
|
||||
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { AsyncRequestState, initialAsyncRequestState } from '../../utils/redux';
|
||||
import { DynamicTable, DynamicTableItemProps, DynamicTableColumnProps } from '../DynamicTable';
|
||||
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
|
||||
import { ProvisioningBadge } from '../Provisioning';
|
||||
@ -31,14 +30,18 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||
|
||||
const { currentData } = useAlertmanagerConfig(alertManagerSourceName, {
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
});
|
||||
const config = currentData?.alertmanager_config;
|
||||
|
||||
const [muteTimingName, setMuteTimingName] = useState<string>('');
|
||||
const { result }: AsyncRequestState<AlertManagerCortexConfig> =
|
||||
(alertManagerSourceName && amConfigs[alertManagerSourceName]) || initialAsyncRequestState;
|
||||
|
||||
const items = useMemo((): Array<DynamicTableItemProps<MuteTimeInterval>> => {
|
||||
const muteTimings = result?.alertmanager_config?.mute_time_intervals ?? [];
|
||||
const muteTimingsProvenances = result?.alertmanager_config?.muteTimeProvenances ?? {};
|
||||
const muteTimings = config?.mute_time_intervals ?? [];
|
||||
const muteTimingsProvenances = config?.muteTimeProvenances ?? {};
|
||||
|
||||
return muteTimings
|
||||
.filter(({ name }) => (muteTimingNames ? muteTimingNames.includes(name) : true))
|
||||
@ -51,11 +54,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [
|
||||
result?.alertmanager_config?.mute_time_intervals,
|
||||
result?.alertmanager_config?.muteTimeProvenances,
|
||||
muteTimingNames,
|
||||
]);
|
||||
}, [config?.mute_time_intervals, config?.muteTimeProvenances, muteTimingNames]);
|
||||
|
||||
const columns = useColumns(alertManagerSourceName, hideActions, setMuteTimingName);
|
||||
|
||||
@ -100,7 +99,10 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
|
||||
title="Delete mute timing"
|
||||
body={`Are you sure you would like to delete "${muteTimingName}"`}
|
||||
confirmText="Delete"
|
||||
onConfirm={() => dispatch(deleteMuteTimingAction(alertManagerSourceName, muteTimingName))}
|
||||
onConfirm={() => {
|
||||
dispatch(deleteMuteTimingAction(alertManagerSourceName, muteTimingName));
|
||||
setMuteTimingName('');
|
||||
}}
|
||||
onDismiss={() => setMuteTimingName('')}
|
||||
/>
|
||||
)}
|
||||
|
@ -15,17 +15,14 @@ export const useAlertmanagerNotificationRoutingPreview = (
|
||||
alertManagerSourceName: string,
|
||||
potentialInstances: Labels[]
|
||||
) => {
|
||||
const {
|
||||
config: AMConfig,
|
||||
loading: configLoading,
|
||||
error: configError,
|
||||
} = useAlertmanagerConfig(alertManagerSourceName);
|
||||
const { currentData, isLoading: configLoading, error: configError } = useAlertmanagerConfig(alertManagerSourceName);
|
||||
const config = currentData?.alertmanager_config;
|
||||
|
||||
const { matchInstancesToRoute } = useRouteGroupsMatcher();
|
||||
|
||||
// to create the list of matching contact points we need to first get the rootRoute
|
||||
const { rootRoute, receivers } = useMemo(() => {
|
||||
if (!AMConfig) {
|
||||
if (!config) {
|
||||
return {
|
||||
receivers: [],
|
||||
rootRoute: undefined,
|
||||
@ -33,10 +30,10 @@ export const useAlertmanagerNotificationRoutingPreview = (
|
||||
}
|
||||
|
||||
return {
|
||||
rootRoute: AMConfig.route ? normalizeRoute(addUniqueIdentifierToRoute(AMConfig.route)) : undefined,
|
||||
receivers: AMConfig.receivers ?? [],
|
||||
rootRoute: config.route ? normalizeRoute(addUniqueIdentifierToRoute(config.route)) : undefined,
|
||||
receivers: config.receivers ?? [],
|
||||
};
|
||||
}, [AMConfig]);
|
||||
}, [config]);
|
||||
|
||||
// create maps for routes to be get by id, this map also contains the path to the route
|
||||
// ⚠️ don't forget to compute the inherited tree before using this map
|
||||
|
@ -1,26 +1,23 @@
|
||||
import { useEffect } from 'react';
|
||||
import { SerializedError } from '@reduxjs/toolkit';
|
||||
|
||||
import { useDispatch } from 'app/types';
|
||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
|
||||
import { fetchAlertManagerConfigAction } from '../state/actions';
|
||||
import { initialAsyncRequestState } from '../utils/redux';
|
||||
type Options = {
|
||||
refetchOnFocus: boolean;
|
||||
refetchOnReconnect: boolean;
|
||||
};
|
||||
|
||||
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
||||
// TODO refactor this so we can just call "alertmanagerApi.endpoints.getAlertmanagerConfiguration" everywhere
|
||||
// and remove this hook since it adds little value
|
||||
export function useAlertmanagerConfig(amSourceName?: string, options?: Options) {
|
||||
const fetchConfig = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery(amSourceName ?? '', {
|
||||
...options,
|
||||
skip: !amSourceName,
|
||||
});
|
||||
|
||||
export function useAlertmanagerConfig(amSourceName?: string) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (amSourceName) {
|
||||
dispatch(fetchAlertManagerConfigAction(amSourceName));
|
||||
}
|
||||
}, [amSourceName, dispatch]);
|
||||
|
||||
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||
|
||||
const { result, loading, error } = (amSourceName && amConfigs[amSourceName]) || initialAsyncRequestState;
|
||||
|
||||
const config = result?.alertmanager_config;
|
||||
|
||||
return { result, config, loading, error };
|
||||
return {
|
||||
...fetchConfig,
|
||||
// TODO refactor to get rid of this type assertion
|
||||
error: fetchConfig.error as SerializedError,
|
||||
};
|
||||
}
|
||||
|
@ -1,22 +1,18 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { AlertmanagerConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { useAlertmanager } from '../state/AlertmanagerContext';
|
||||
import { timeIntervalToString } from '../utils/alertmanager';
|
||||
import { initialAsyncRequestState } from '../utils/redux';
|
||||
|
||||
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
||||
import { useAlertmanagerConfig } from './useAlertmanagerConfig';
|
||||
|
||||
export function useMuteTimingOptions(): Array<SelectableValue<string>> {
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||
const { currentData } = useAlertmanagerConfig(selectedAlertmanager);
|
||||
const config = currentData?.alertmanager_config;
|
||||
|
||||
return useMemo(() => {
|
||||
const { result } = (selectedAlertmanager && amConfigs[selectedAlertmanager]) || initialAsyncRequestState;
|
||||
const config: AlertmanagerConfig = result?.alertmanager_config ?? {};
|
||||
|
||||
const muteTimingsOptions: Array<SelectableValue<string>> =
|
||||
config?.mute_time_intervals?.map((value) => ({
|
||||
value: value.name,
|
||||
@ -25,5 +21,5 @@ export function useMuteTimingOptions(): Array<SelectableValue<string>> {
|
||||
})) ?? [];
|
||||
|
||||
return muteTimingsOptions;
|
||||
}, [selectedAlertmanager, amConfigs]);
|
||||
}, [config]);
|
||||
}
|
||||
|
@ -40,18 +40,16 @@ import {
|
||||
deleteAlertManagerConfig,
|
||||
expireSilence,
|
||||
fetchAlertGroups,
|
||||
fetchAlertManagerConfig,
|
||||
fetchAlerts,
|
||||
fetchExternalAlertmanagerConfig,
|
||||
fetchExternalAlertmanagers,
|
||||
fetchSilences,
|
||||
fetchStatus,
|
||||
testReceivers,
|
||||
updateAlertManagerConfig,
|
||||
} from '../api/alertmanager';
|
||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
import { fetchAnnotations } from '../api/annotations';
|
||||
import { discoverFeatures } from '../api/buildInfo';
|
||||
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
|
||||
import { fetchNotifiers } from '../api/grafana';
|
||||
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
|
||||
import {
|
||||
@ -68,17 +66,14 @@ import {
|
||||
getRulesDataSource,
|
||||
getRulesSourceName,
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
isVanillaPrometheusAlertManagerDataSource,
|
||||
} from '../utils/datasource';
|
||||
import { makeAMLink, retryWhile } from '../utils/misc';
|
||||
import { AsyncRequestMapSlice, messageFromError, withAppEvents, withSerializedError } from '../utils/redux';
|
||||
import { makeAMLink } from '../utils/misc';
|
||||
import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../utils/redux';
|
||||
import * as ruleId from '../utils/rule-id';
|
||||
import { getRulerClient } from '../utils/rulerClient';
|
||||
import { getAlertInfo, isRulerNotSupportedResponse } from '../utils/rules';
|
||||
import { safeParseDurationstr } from '../utils/time';
|
||||
|
||||
const FETCH_CONFIG_RETRY_TIMEOUT = 30 * 1000;
|
||||
|
||||
function getDataSourceConfig(getState: () => unknown, rulesSourceName: string) {
|
||||
const dataSources = (getState() as StoreState).unifiedAlerting.dataSources;
|
||||
const dsConfig = dataSources[rulesSourceName]?.result;
|
||||
@ -131,76 +126,6 @@ export const fetchPromRulesAction = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchAlertManagerConfigAction = createAsyncThunk(
|
||||
'unifiedalerting/fetchAmConfig',
|
||||
(alertManagerSourceName: string, thunkAPI): Promise<AlertManagerCortexConfig> =>
|
||||
withSerializedError(
|
||||
(async () => {
|
||||
// for vanilla prometheus, there is no config endpoint. Only fetch config from status
|
||||
if (isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName)) {
|
||||
return fetchStatus(alertManagerSourceName).then((status) => ({
|
||||
alertmanager_config: status.config,
|
||||
template_files: {},
|
||||
}));
|
||||
}
|
||||
|
||||
const { data: amFeatures } = await thunkAPI.dispatch(
|
||||
featureDiscoveryApi.endpoints.discoverAmFeatures.initiate({
|
||||
amSourceName: alertManagerSourceName,
|
||||
})
|
||||
);
|
||||
|
||||
const lazyConfigInitSupported = amFeatures?.lazyConfigInit ?? false;
|
||||
const fetchAMconfigWithLogging = withPerformanceLogging(
|
||||
fetchAlertManagerConfig,
|
||||
`[${alertManagerSourceName}] Alertmanager config loaded`,
|
||||
{
|
||||
dataSourceName: alertManagerSourceName,
|
||||
thunk: 'unifiedalerting/fetchAmConfig',
|
||||
}
|
||||
);
|
||||
|
||||
return retryWhile(
|
||||
() => fetchAMconfigWithLogging(alertManagerSourceName),
|
||||
// if config has been recently deleted, it takes a while for cortex start returning the default one.
|
||||
// retry for a short while instead of failing
|
||||
(e) => !!messageFromError(e)?.includes('alertmanager storage object not found') && !lazyConfigInitSupported,
|
||||
FETCH_CONFIG_RETRY_TIMEOUT
|
||||
)
|
||||
.then((result) => {
|
||||
// if user config is empty for cortex alertmanager, try to get config from status endpoint
|
||||
if (
|
||||
isEmpty(result.alertmanager_config) &&
|
||||
isEmpty(result.template_files) &&
|
||||
alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME
|
||||
) {
|
||||
return fetchStatus(alertManagerSourceName).then((status) => ({
|
||||
alertmanager_config: status.config,
|
||||
template_files: {},
|
||||
template_file_provenances: result.template_file_provenances,
|
||||
last_applied: result.last_applied,
|
||||
id: result.id,
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.catch((e) => {
|
||||
// When mimir doesn't have fallback AM url configured the default response will be as above
|
||||
// However it's fine, and it's possible to create AM configuration
|
||||
if (lazyConfigInitSupported && messageFromError(e)?.includes('alertmanager storage object not found')) {
|
||||
return Promise.resolve<AlertManagerCortexConfig>({
|
||||
alertmanager_config: {},
|
||||
template_files: {},
|
||||
template_file_provenances: {},
|
||||
});
|
||||
}
|
||||
|
||||
throw e;
|
||||
});
|
||||
})()
|
||||
)
|
||||
);
|
||||
|
||||
export const fetchExternalAlertmanagersAction = createAsyncThunk(
|
||||
'unifiedAlerting/fetchExternalAlertmanagers',
|
||||
(): Promise<ExternalAlertmanagersResponse> => {
|
||||
@ -585,8 +510,9 @@ export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlert
|
||||
withAppEvents(
|
||||
withSerializedError(
|
||||
(async () => {
|
||||
// TODO there must be a better way here than to dispatch another fetch as this causes re-rendering :(
|
||||
const latestConfig = await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)).unwrap();
|
||||
const latestConfig = await thunkAPI
|
||||
.dispatch(alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName))
|
||||
.unwrap();
|
||||
|
||||
const isLatestConfigEmpty = isEmpty(latestConfig.alertmanager_config) && isEmpty(latestConfig.template_files);
|
||||
const oldLastConfigsDiffer = JSON.stringify(latestConfig) !== JSON.stringify(oldConfig);
|
||||
@ -598,7 +524,7 @@ export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlert
|
||||
}
|
||||
await updateAlertManagerConfig(alertManagerSourceName, addDefaultsToAlertmanagerConfig(newConfig));
|
||||
if (refetch) {
|
||||
await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
|
||||
thunkAPI.dispatch(alertmanagerApi.util.invalidateTags(['AlertmanagerConfiguration']));
|
||||
}
|
||||
if (redirectPath) {
|
||||
const options = new URLSearchParams(redirectSearch ?? '');
|
||||
@ -654,8 +580,11 @@ export const createOrUpdateSilenceAction = createAsyncThunk<void, UpdateSilenceA
|
||||
);
|
||||
|
||||
export const deleteReceiverAction = (receiverName: string, alertManagerSourceName: string): ThunkResult<void> => {
|
||||
return (dispatch, getState) => {
|
||||
const config = getState().unifiedAlerting.amConfigs?.[alertManagerSourceName]?.result;
|
||||
return async (dispatch) => {
|
||||
const config = await dispatch(
|
||||
alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName)
|
||||
).unwrap();
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`Config for ${alertManagerSourceName} not found`);
|
||||
}
|
||||
@ -682,8 +611,11 @@ export const deleteReceiverAction = (receiverName: string, alertManagerSourceNam
|
||||
};
|
||||
|
||||
export const deleteTemplateAction = (templateName: string, alertManagerSourceName: string): ThunkResult<void> => {
|
||||
return (dispatch, getState) => {
|
||||
const config = getState().unifiedAlerting.amConfigs?.[alertManagerSourceName]?.result;
|
||||
return async (dispatch) => {
|
||||
const config = await dispatch(
|
||||
alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName)
|
||||
).unwrap();
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`Config for ${alertManagerSourceName} not found`);
|
||||
}
|
||||
@ -739,7 +671,7 @@ export const deleteAlertManagerConfigAction = createAsyncThunk(
|
||||
withSerializedError(
|
||||
(async () => {
|
||||
await deleteAlertManagerConfig(alertManagerSourceName);
|
||||
await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
|
||||
await thunkAPI.dispatch(alertmanagerApi.util.invalidateTags(['AlertmanagerConfiguration']));
|
||||
})()
|
||||
),
|
||||
{
|
||||
@ -751,8 +683,10 @@ export const deleteAlertManagerConfigAction = createAsyncThunk(
|
||||
);
|
||||
|
||||
export const deleteMuteTimingAction = (alertManagerSourceName: string, muteTimingName: string): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const config = getState().unifiedAlerting.amConfigs[alertManagerSourceName].result;
|
||||
return async (dispatch) => {
|
||||
const config = await dispatch(
|
||||
alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName)
|
||||
).unwrap();
|
||||
|
||||
const muteIntervals =
|
||||
config?.alertmanager_config?.mute_time_intervals?.filter(({ name }) => name !== muteTimingName) ?? [];
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
createOrUpdateSilenceAction,
|
||||
deleteAlertManagerConfigAction,
|
||||
fetchAlertGroupsAction,
|
||||
fetchAlertManagerConfigAction,
|
||||
fetchAmAlertsAction,
|
||||
fetchEditableRuleAction,
|
||||
fetchExternalAlertmanagersAction,
|
||||
@ -33,11 +32,6 @@ export const reducer = combineReducers({
|
||||
promRules: createAsyncMapSlice('promRules', fetchPromRulesAction, ({ rulesSourceName }) => rulesSourceName).reducer,
|
||||
rulerRules: createAsyncMapSlice('rulerRules', fetchRulerRulesAction, ({ rulesSourceName }) => rulesSourceName)
|
||||
.reducer,
|
||||
amConfigs: createAsyncMapSlice(
|
||||
'amConfigs',
|
||||
fetchAlertManagerConfigAction,
|
||||
(alertManagerSourceName) => alertManagerSourceName
|
||||
).reducer,
|
||||
silences: createAsyncMapSlice('silences', fetchSilencesAction, (alertManagerSourceName) => alertManagerSourceName)
|
||||
.reducer,
|
||||
ruleForm: combineReducers({
|
||||
|
Loading…
Reference in New Issue
Block a user