Alerting: Refactor Alertmanager picker (#70720)

This commit is contained in:
Gilles De Mey
2023-07-04 13:07:05 +03:00
committed by GitHub
parent f1338cee60
commit df3888ba60
23 changed files with 514 additions and 523 deletions
+15 -10
View File
@@ -1,22 +1,27 @@
import React from 'react';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import AlertmanagerConfig from './components/admin/AlertmanagerConfig';
import { ExternalAlertmanagers } from './components/admin/ExternalAlertmanagers';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from './hooks/useAlertManagerSources';
import { useAlertmanager } from './state/AlertmanagerContext';
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
export default function Admin(): JSX.Element {
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const isGrafanaAmSelected = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME;
return (
<AlertingPageWrapper pageId="alerting-admin">
<AlertmanagerPageWrapper pageId="alerting-admin" accessType="notification">
<AdminPageContents />
</AlertmanagerPageWrapper>
);
}
function AdminPageContents() {
const { selectedAlertmanager } = useAlertmanager();
const isGrafanaAmSelected = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME;
return (
<>
<AlertmanagerConfig test-id="admin-alertmanagerconfig" />
{isGrafanaAmSelected && <ExternalAlertmanagers test-id="admin-externalalertmanagers" />}
</AlertingPageWrapper>
</>
);
}
@@ -9,15 +9,13 @@ import { useDispatch } from 'app/types';
import { AlertmanagerChoice } from '../../../plugins/datasource/alertmanager/types';
import { alertmanagerApi } from './api/alertmanagerApi';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import { AlertGroup } from './components/alert-groups/AlertGroup';
import { AlertGroupFilter } from './components/alert-groups/AlertGroupFilter';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from './hooks/useAlertManagerSources';
import { useFilteredAmGroups } from './hooks/useFilteredAmGroups';
import { useGroupedAlerts } from './hooks/useGroupedAlerts';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { useAlertmanager } from './state/AlertmanagerContext';
import { fetchAlertGroupsAction } from './state/actions';
import { NOTIFICATIONS_POLL_INTERVAL_MS } from './utils/constants';
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
@@ -27,8 +25,7 @@ import { initialAsyncRequestState } from './utils/redux';
const AlertGroups = () => {
const { useGetAlertmanagerChoiceStatusQuery } = alertmanagerApi;
const alertManagers = useAlertManagersByPermission('instance');
const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const { selectedAlertmanager } = useAlertmanager();
const dispatch = useDispatch();
const [queryParams] = useQueryParams();
const { groupBy = [] } = getFiltersFromUrlParams(queryParams);
@@ -37,23 +34,19 @@ const AlertGroups = () => {
const { currentData: amConfigStatus } = useGetAlertmanagerChoiceStatusQuery();
const alertGroups = useUnifiedAlertingSelector((state) => state.amAlertGroups);
const {
loading,
error,
result: results = [],
} = alertGroups[alertManagerSourceName || ''] ?? initialAsyncRequestState;
const { loading, error, result: results = [] } = alertGroups[selectedAlertmanager || ''] ?? initialAsyncRequestState;
const groupedAlerts = useGroupedAlerts(results, groupBy);
const filteredAlertGroups = useFilteredAmGroups(groupedAlerts);
const grafanaAmDeliveryDisabled =
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME &&
amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.External;
useEffect(() => {
function fetchNotifications() {
if (alertManagerSourceName) {
dispatch(fetchAlertGroupsAction(alertManagerSourceName));
if (selectedAlertmanager) {
dispatch(fetchAlertGroupsAction(selectedAlertmanager));
}
}
fetchNotifications();
@@ -61,18 +54,10 @@ const AlertGroups = () => {
return () => {
clearInterval(interval);
};
}, [dispatch, alertManagerSourceName]);
if (!alertManagerSourceName) {
return (
<AlertingPageWrapper pageId="groups">
<NoAlertManagerWarning availableAlertManagers={alertManagers} />
</AlertingPageWrapper>
);
}
}, [dispatch, selectedAlertmanager]);
return (
<AlertingPageWrapper pageId="groups">
<>
<AlertGroupFilter groups={results} />
{loading && <LoadingPlaceholder text="Loading notifications" />}
{error && !loading && (
@@ -96,19 +81,25 @@ const AlertGroups = () => {
(index === 0 && Object.keys(group.labels).length > 0)) && (
<p className={styles.groupingBanner}>Grouped by: {Object.keys(group.labels).join(', ')}</p>
)}
<AlertGroup alertManagerSourceName={alertManagerSourceName || ''} group={group} />
<AlertGroup alertManagerSourceName={selectedAlertmanager || ''} group={group} />
</React.Fragment>
);
})}
{results && !filteredAlertGroups.length && <p>No results.</p>}
</AlertingPageWrapper>
</>
);
};
const AlertGroupsPage = () => (
<AlertmanagerPageWrapper pageId="groups" accessType="instance">
<AlertGroups />
</AlertmanagerPageWrapper>
);
const getStyles = (theme: GrafanaTheme2) => ({
groupingBanner: css`
margin: ${theme.spacing(2, 0)};
`,
});
export default AlertGroups;
export default AlertGroupsPage;
@@ -1,38 +1,38 @@
import React, { useCallback, useEffect } from 'react';
import { Route, Redirect, Switch } from 'react-router-dom';
import React, { useCallback, useEffect, useState } from 'react';
import { Route, Redirect, Switch, useRouteMatch } from 'react-router-dom';
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 { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from './hooks/useAlertManagerSources';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { useAlertmanager } from './state/AlertmanagerContext';
import { fetchAlertManagerConfigAction } from './state/actions';
import { initialAsyncRequestState } from './utils/redux';
const MuteTimings = () => {
const [queryParams] = useQueryParams();
const dispatch = useDispatch();
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const { selectedAlertmanager } = useAlertmanager();
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
const fetchConfig = useCallback(() => {
if (alertManagerSourceName) {
dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
if (selectedAlertmanager) {
dispatch(fetchAlertManagerConfigAction(selectedAlertmanager));
}
}, [alertManagerSourceName, dispatch]);
}, [selectedAlertmanager, dispatch]);
useEffect(() => {
fetchConfig();
}, [fetchConfig]);
const { result, error, loading } =
(alertManagerSourceName && amConfigs[alertManagerSourceName]) || initialAsyncRequestState;
(selectedAlertmanager && amConfigs[selectedAlertmanager]) || initialAsyncRequestState;
const config = result?.alertmanager_config;
@@ -57,7 +57,7 @@ const MuteTimings = () => {
return (
<>
{error && !loading && !result && (
<Alert severity="error" title={`Error loading Alertmanager config for ${alertManagerSourceName}`}>
<Alert severity="error" title={`Error loading Alertmanager config for ${selectedAlertmanager}`}>
{error.message || 'Unknown error.'}
</Alert>
)}
@@ -90,4 +90,35 @@ const MuteTimings = () => {
);
};
export default MuteTimings;
const MuteTimingsPage = () => {
const pageNav = useMuteTimingNavData();
return (
<AlertmanagerPageWrapper pageId="am-routes" pageNav={pageNav} accessType="notification">
<MuteTimings />
</AlertmanagerPageWrapper>
);
};
export function useMuteTimingNavData() {
const { isExact, path } = useRouteMatch();
const [pageNav, setPageNav] = useState<Pick<NavModelItem, 'id' | 'text' | 'icon'> | undefined>();
useEffect(() => {
if (path === '/alerting/routes/mute-timing/new') {
setPageNav({
id: 'alert-policy-new',
text: 'Add mute timing',
});
} else if (path === '/alerting/routes/mute-timing/edit') {
setPageNav({
id: 'alert-policy-edit',
text: 'Edit mute timing',
});
}
}, [path, isExact]);
return pageNav;
}
export default MuteTimingsPage;
@@ -14,10 +14,8 @@ import { useCleanup } from '../../../core/hooks/useCleanup';
import { alertmanagerApi } from './api/alertmanagerApi';
import { useGetContactPointsState } from './api/receiversApi';
import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
import { ProvisionedResource, ProvisioningAlert } from './components/Provisioning';
import { MuteTimingsTable } from './components/mute-timings/MuteTimingsTable';
import {
@@ -32,9 +30,8 @@ import {
useAlertGroupsModal,
} from './components/notification-policies/Modals';
import { Policy } from './components/notification-policies/Policy';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from './hooks/useAlertManagerSources';
import { useAlertmanagerConfig } from './hooks/useAlertmanagerConfig';
import { useAlertmanager } from './state/AlertmanagerContext';
import { updateAlertManagerConfigAction } from './state/actions';
import { FormAmRoute } from './types/amroutes';
import { useRouteGroupsMatcher } from './useRouteGroupsMatcher';
@@ -64,17 +61,15 @@ const AmRoutes = () => {
const [labelMatchersFilter, setLabelMatchersFilter] = useState<ObjectMatcher[]>([]);
const { getRouteGroupsMap } = useRouteGroupsMatcher();
const { selectedAlertmanager } = useAlertmanager();
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const contactPointsState = useGetContactPointsState(selectedAlertmanager ?? '');
const contactPointsState = useGetContactPointsState(alertManagerSourceName ?? '');
const { result, config, loading: resultLoading, error: resultError } = useAlertmanagerConfig(alertManagerSourceName);
const { result, config, loading: resultLoading, error: resultError } = useAlertmanagerConfig(selectedAlertmanager);
const { currentData: alertGroups, refetch: refetchAlertGroups } = useGetAlertmanagerAlertGroupsQuery(
{ amSourceName: alertManagerSourceName ?? '' },
{ skip: !alertManagerSourceName }
{ amSourceName: selectedAlertmanager ?? '' },
{ skip: !selectedAlertmanager }
);
const receivers = config?.receivers ?? [];
@@ -113,7 +108,7 @@ const AmRoutes = () => {
if (!rootRoute) {
return;
}
const newRouteTree = mergePartialAmRouteWithRouteTree(alertManagerSourceName ?? '', partialRoute, rootRoute);
const newRouteTree = mergePartialAmRouteWithRouteTree(selectedAlertmanager ?? '', partialRoute, rootRoute);
updateRouteTree(newRouteTree);
}
@@ -130,7 +125,7 @@ const AmRoutes = () => {
return;
}
const newRouteTree = addRouteToParentRoute(alertManagerSourceName ?? '', partialRoute, parentRoute, rootRoute);
const newRouteTree = addRouteToParentRoute(selectedAlertmanager ?? '', partialRoute, parentRoute, rootRoute);
updateRouteTree(newRouteTree);
}
@@ -151,14 +146,14 @@ const AmRoutes = () => {
},
},
oldConfig: result,
alertManagerSourceName: alertManagerSourceName!,
alertManagerSourceName: selectedAlertmanager!,
successMessage: 'Updated notification policies',
refetch: true,
})
)
.unwrap()
.then(() => {
if (alertManagerSourceName) {
if (selectedAlertmanager) {
refetchAlertGroups();
}
closeEditModal();
@@ -173,7 +168,7 @@ const AmRoutes = () => {
// edit, add, delete modals
const [addModal, openAddModal, closeAddModal] = useAddPolicyModal(receivers, handleAdd, updatingTree);
const [editModal, openEditModal, closeEditModal] = useEditPolicyModal(
alertManagerSourceName ?? '',
selectedAlertmanager ?? '',
receivers,
handleSave,
updatingTree
@@ -183,15 +178,11 @@ const AmRoutes = () => {
useCleanup((state) => (state.unifiedAlerting.saveAMConfig = initialAsyncRequestState));
if (!alertManagerSourceName) {
return (
<AlertingPageWrapper pageId="am-routes">
<NoAlertManagerWarning availableAlertManagers={alertManagers} />
</AlertingPageWrapper>
);
if (!selectedAlertmanager) {
return null;
}
const vanillaPrometheusAlertManager = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
const vanillaPrometheusAlertManager = isVanillaPrometheusAlertManagerDataSource(selectedAlertmanager);
const readOnlyPolicies = vanillaPrometheusAlertManager || isProvisioned;
const readOnlyMuteTimings = vanillaPrometheusAlertManager;
@@ -205,11 +196,6 @@ const AmRoutes = () => {
return (
<>
<AlertManagerPicker
current={alertManagerSourceName}
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
<TabsBar>
<Tab
label={'Notification Policies'}
@@ -240,7 +226,7 @@ const AmRoutes = () => {
<>
{policyTreeTabActive && (
<>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager} />
{isProvisioned && <ProvisioningAlert resource={ProvisionedResource.RootNotificationPolicy} />}
<Stack direction="column" gap={1}>
{rootRoute && (
@@ -258,7 +244,7 @@ const AmRoutes = () => {
alertGroups={alertGroups ?? []}
contactPointsState={contactPointsState.receivers}
readOnly={readOnlyPolicies}
alertManagerSourceName={alertManagerSourceName}
alertManagerSourceName={selectedAlertmanager}
onAddPolicy={openAddModal}
onEditPolicy={openEditModal}
onDeletePolicy={openDeleteModal}
@@ -275,7 +261,7 @@ const AmRoutes = () => {
</>
)}
{muteTimingsTabActive && (
<MuteTimingsTable alertManagerSourceName={alertManagerSourceName} hideActions={readOnlyMuteTimings} />
<MuteTimingsTable alertManagerSourceName={selectedAlertmanager} hideActions={readOnlyMuteTimings} />
)}
</>
)}
@@ -344,12 +330,10 @@ function getActiveTabFromUrl(queryParams: UrlQueryMap): QueryParamValues {
};
}
function NotificationPoliciesPage() {
return (
<AlertingPageWrapper pageId="am-routes">
<AmRoutes />
</AlertingPageWrapper>
);
}
const NotificationPoliciesPage = () => (
<AlertmanagerPageWrapper pageId="am-routes" accessType="notification">
<AmRoutes />
</AlertmanagerPageWrapper>
);
export default withErrorBoundary(NotificationPoliciesPage, { style: 'page' });
@@ -7,18 +7,18 @@ const ContactPointsV2 = SafeDynamicImport(() => import('./components/contact-poi
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import { AlertingFeature } from './features';
// TODO add pagenav back in – what are we missing if we don't specify it?
const ContactPoints = (props: GrafanaRouteComponentProps): JSX.Element => (
<AlertingPageWrapper pageId="receivers">
<AlertmanagerPageWrapper pageId="receivers" accessType="notification">
<Enable feature={AlertingFeature.ContactPointsV2}>
<ContactPointsV2 {...props} />
</Enable>
<Disable feature={AlertingFeature.ContactPointsV2}>
<ContactPointsV1 {...props} />
</Disable>
</AlertingPageWrapper>
</AlertmanagerPageWrapper>
);
export default withErrorBoundary(ContactPoints, { style: 'page' });
@@ -1,50 +1,42 @@
import React, { useCallback, useEffect } from 'react';
import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom';
import { Route, RouteChildrenProps, Switch } from 'react-router-dom';
import { Alert, withErrorBoundary } from '@grafana/ui';
import { Silence } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types';
import { featureDiscoveryApi } from './api/featureDiscoveryApi';
import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
import SilencesEditor from './components/silences/SilencesEditor';
import SilencesTable from './components/silences/SilencesTable';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from './hooks/useAlertManagerSources';
import { useSilenceNavData } from './hooks/useSilenceNavData';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { useAlertmanager } from './state/AlertmanagerContext';
import { fetchAmAlertsAction, fetchSilencesAction } from './state/actions';
import { SILENCES_POLL_INTERVAL_MS } from './utils/constants';
import { AsyncRequestState, initialAsyncRequestState } from './utils/redux';
const Silences = () => {
const alertManagers = useAlertManagersByPermission('instance');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const { selectedAlertmanager } = useAlertmanager();
const dispatch = useDispatch();
const silences = useUnifiedAlertingSelector((state) => state.silences);
const alertsRequests = useUnifiedAlertingSelector((state) => state.amAlerts);
const alertsRequest = alertManagerSourceName
? alertsRequests[alertManagerSourceName] || initialAsyncRequestState
const alertsRequest = selectedAlertmanager
? alertsRequests[selectedAlertmanager] || initialAsyncRequestState
: undefined;
const location = useLocation();
const pageNav = useSilenceNavData();
const isRoot = location.pathname.endsWith('/alerting/silences');
const { currentData: amFeatures } = featureDiscoveryApi.useDiscoverAmFeaturesQuery(
{ amSourceName: alertManagerSourceName ?? '' },
{ skip: !alertManagerSourceName }
{ amSourceName: selectedAlertmanager ?? '' },
{ skip: !selectedAlertmanager }
);
useEffect(() => {
function fetchAll() {
if (alertManagerSourceName) {
dispatch(fetchSilencesAction(alertManagerSourceName));
dispatch(fetchAmAlertsAction(alertManagerSourceName));
if (selectedAlertmanager) {
dispatch(fetchSilencesAction(selectedAlertmanager));
dispatch(fetchAmAlertsAction(selectedAlertmanager));
}
}
fetchAll();
@@ -52,35 +44,23 @@ const Silences = () => {
return () => {
clearInterval(interval);
};
}, [alertManagerSourceName, dispatch]);
}, [selectedAlertmanager, dispatch]);
const { result, loading, error }: AsyncRequestState<Silence[]> =
(alertManagerSourceName && silences[alertManagerSourceName]) || initialAsyncRequestState;
(selectedAlertmanager && silences[selectedAlertmanager]) || initialAsyncRequestState;
const getSilenceById = useCallback((id: string) => result && result.find((silence) => silence.id === id), [result]);
const mimirLazyInitError =
error?.message?.includes('the Alertmanager is not configured') && amFeatures?.lazyConfigInit;
if (!alertManagerSourceName) {
return isRoot ? (
<AlertingPageWrapper pageId="silences" pageNav={pageNav}>
<NoAlertManagerWarning availableAlertManagers={alertManagers} />
</AlertingPageWrapper>
) : (
<Redirect to="/alerting/silences" />
);
if (!selectedAlertmanager) {
return null;
}
return (
<AlertingPageWrapper pageId="silences" isLoading={loading} pageNav={pageNav}>
<AlertManagerPicker
disabled={!isRoot}
current={alertManagerSourceName}
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
<>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager} />
{mimirLazyInitError && (
<Alert title="The selected Alertmanager has no configuration" severity="warning">
@@ -104,11 +84,11 @@ const Silences = () => {
<SilencesTable
silences={result}
alertManagerAlerts={alertsRequest?.result ?? []}
alertManagerSourceName={alertManagerSourceName}
alertManagerSourceName={selectedAlertmanager}
/>
</Route>
<Route exact path="/alerting/silence/new">
<SilencesEditor alertManagerSourceName={alertManagerSourceName} />
<SilencesEditor alertManagerSourceName={selectedAlertmanager} />
</Route>
<Route exact path="/alerting/silence/:id/edit">
{({ match }: RouteChildrenProps<{ id: string }>) => {
@@ -116,7 +96,7 @@ const Silences = () => {
match?.params.id && (
<SilencesEditor
silence={getSilenceById(match.params.id)}
alertManagerSourceName={alertManagerSourceName}
alertManagerSourceName={selectedAlertmanager}
/>
)
);
@@ -124,8 +104,18 @@ const Silences = () => {
</Route>
</Switch>
)}
</AlertingPageWrapper>
</>
);
};
export default withErrorBoundary(Silences, { style: 'page' });
function SilencesPage() {
const pageNav = useSilenceNavData();
return (
<AlertmanagerPageWrapper pageId="silences" pageNav={pageNav} accessType="instance">
<Silences />
</AlertmanagerPageWrapper>
);
}
export default withErrorBoundary(SilencesPage, { style: 'page' });
@@ -2,35 +2,35 @@ import { css } from '@emotion/css';
import React, { useMemo } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Field, Select, useStyles2 } from '@grafana/ui';
import { InlineField, Select, useStyles2 } from '@grafana/ui';
import { useAlertmanager } from '../state/AlertmanagerContext';
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
interface Props {
onChange: (alertManagerSourceName: string) => void;
current?: string;
disabled?: boolean;
dataSources: AlertManagerDataSource[];
}
function getAlertManagerLabel(alertManager: AlertManagerDataSource) {
return alertManager.name === GRAFANA_RULES_SOURCE_NAME ? 'Grafana' : alertManager.name.slice(0, 37);
}
export const AlertManagerPicker = ({ onChange, current, dataSources, disabled = false }: Props) => {
export const AlertManagerPicker = ({ disabled = false }: Props) => {
const styles = useStyles2(getStyles);
const { selectedAlertmanager, availableAlertManagers, setSelectedAlertmanager } = useAlertmanager();
const options: Array<SelectableValue<string>> = useMemo(() => {
return dataSources.map((ds) => ({
return availableAlertManagers.map((ds) => ({
label: getAlertManagerLabel(ds),
value: ds.name,
imgUrl: ds.imgUrl,
meta: ds.meta,
}));
}, [dataSources]);
}, [availableAlertManagers]);
return (
<Field
<InlineField
className={styles.field}
label={disabled ? 'Alertmanager' : 'Choose Alertmanager'}
disabled={disabled || options.length === 1}
@@ -41,19 +41,23 @@ export const AlertManagerPicker = ({ onChange, current, dataSources, disabled =
width={29}
className="ds-picker select-container"
backspaceRemovesValue={false}
onChange={(value) => value.value && onChange(value.value)}
onChange={(value) => {
if (value?.value) {
setSelectedAlertmanager(value.value);
}
}}
options={options}
maxMenuHeight={500}
noOptionsMessage="No datasources found"
value={current}
value={selectedAlertmanager}
getOptionLabel={(o) => o.label}
/>
</Field>
</InlineField>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
field: css`
margin-bottom: ${theme.spacing(4)};
margin: 0;
`,
});
@@ -1,22 +1,30 @@
import Mousetrap from 'mousetrap';
import React, { useEffect, useState } from 'react';
import React, { PropsWithChildren, useEffect, useState } from 'react';
import { Features, ToggleFeatures } from 'react-enable';
import { useLocation } from 'react-use';
import { NavModelItem } from '@grafana/data';
import { Page } from 'app/core/components/Page/Page';
import FEATURES from '../features';
import { AlertmanagerProvider, useAlertmanager } from '../state/AlertmanagerContext';
interface Props {
pageId: string;
isLoading?: boolean;
pageNav?: NavModelItem;
}
import { AlertManagerPicker } from './AlertManagerPicker';
import { NoAlertManagerWarning } from './NoAlertManagerWarning';
const SHOW_TOGGLES_KEY_COMBO = 'ctrl+1';
const combokeys = new Mousetrap(document.body);
export const AlertingPageWrapper = ({ children, pageId, pageNav, isLoading }: React.PropsWithChildren<Props>) => {
/**
* This is the main alerting page wrapper, used by the alertmanager page wrapper and the alert rules list view
*/
interface AlertingPageWrapperProps extends PropsWithChildren {
pageId: string;
isLoading?: boolean;
pageNav?: NavModelItem;
actions?: React.ReactNode;
}
export const AlertingPageWrapper = ({ children, pageId, pageNav, actions, isLoading }: AlertingPageWrapperProps) => {
const [showFeatureToggle, setShowFeatureToggles] = useState(false);
useEffect(() => {
@@ -31,10 +39,53 @@ export const AlertingPageWrapper = ({ children, pageId, pageNav, isLoading }: Re
return (
<Features features={FEATURES}>
<Page pageNav={pageNav} navId={pageId}>
<Page pageNav={pageNav} navId={pageId} actions={actions}>
<Page.Contents isLoading={isLoading}>{children}</Page.Contents>
</Page>
{showFeatureToggle ? <ToggleFeatures defaultOpen={true} /> : null}
</Features>
);
};
/**
* This wrapper is for pages that use the Alertmanager API
*/
interface AlertmanagerPageWrapperProps extends AlertingPageWrapperProps {
accessType: 'instance' | 'notification';
}
export const AlertmanagerPageWrapper = ({ children, accessType, ...props }: AlertmanagerPageWrapperProps) => {
const disableAlertmanager = useIsDisabledAlertmanagerSelection();
return (
<AlertmanagerProvider accessType={accessType}>
<AlertingPageWrapper {...props} actions={<AlertManagerPicker disabled={disableAlertmanager} />}>
<AlertManagerPagePermissionsCheck>{children}</AlertManagerPagePermissionsCheck>
</AlertingPageWrapper>
</AlertmanagerProvider>
);
};
/**
* This function tells us when we want to disable the alertmanager picker
* It's not great...
*/
function useIsDisabledAlertmanagerSelection() {
const location = useLocation();
const disabledPathSegment = ['/edit', '/new'];
return disabledPathSegment.some((match) => location?.pathname?.includes(match));
}
/**
* This component will render an error message if the user doesn't have sufficient permissions or if the requested
* alertmanager doesn't exist
*/
const AlertManagerPagePermissionsCheck = ({ children }: PropsWithChildren) => {
const { availableAlertManagers, selectedAlertmanager } = useAlertmanager();
if (!selectedAlertmanager) {
return <NoAlertManagerWarning availableAlertManagers={availableAlertManagers} />;
}
return <>{children}</>;
};
@@ -2,11 +2,8 @@ import React from 'react';
import { Alert } from '@grafana/ui';
import { useAlertManagerSourceName } from '../hooks/useAlertManagerSourceName';
import { AlertManagerDataSource } from '../utils/datasource';
import { AlertManagerPicker } from './AlertManagerPicker';
interface Props {
availableAlertManagers: AlertManagerDataSource[];
}
@@ -18,25 +15,14 @@ const NoAlertManagersAvailable = () => (
);
const OtherAlertManagersAvailable = () => (
<Alert title="Selected Alertmanager not found. Select a different Alertmanager." severity="warning">
Selected Alertmanager no longer exists or you may not have permission to access it.
<Alert title="Selected Alertmanager not found." severity="warning">
The selected Alertmanager no longer exists or you may not have permission to access it. You can select a different
Alertmanager from the dropdown.
</Alert>
);
export const NoAlertManagerWarning = ({ availableAlertManagers }: Props) => {
const [_, setAlertManagerSourceName] = useAlertManagerSourceName(availableAlertManagers);
const hasOtherAMs = availableAlertManagers.length > 0;
return (
<div>
{hasOtherAMs ? (
<>
<AlertManagerPicker onChange={setAlertManagerSourceName} dataSources={availableAlertManagers} />
<OtherAlertManagersAvailable />
</>
) : (
<NoAlertManagersAvailable />
)}
</div>
);
return <div>{hasOtherAMs ? <OtherAlertManagersAvailable /> : <NoAlertManagersAvailable />}</div>;
};
@@ -26,6 +26,7 @@ import {
someCloudAlertManagerConfig,
someCloudAlertManagerStatus,
} from '../../mocks';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { getAllDataSources } from '../../utils/config';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../../utils/constants';
import { DataSourceType } from '../../utils/datasource';
@@ -55,7 +56,9 @@ const renderAdminPage = (alertManagerSourceName?: string) => {
return render(
<TestProvider>
<AlertmanagerConfig />
<AlertmanagerProvider accessType="instance">
<AlertmanagerConfig />
</AlertmanagerProvider>
</TestProvider>
);
};
@@ -5,9 +5,8 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Alert, useStyles2 } from '@grafana/ui';
import { useDispatch } from 'app/types';
import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from '../../hooks/useAlertManagerSources';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import {
deleteAlertManagerConfigAction,
fetchAlertManagerConfigAction,
@@ -15,7 +14,6 @@ import {
} from '../../state/actions';
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
import { initialAsyncRequestState } from '../../utils/redux';
import { AlertManagerPicker } from '../AlertManagerPicker';
import AlertmanagerConfigSelector, { ValidAmConfigOption } from './AlertmanagerConfigSelector';
import { ConfigEditor } from './ConfigEditor';
@@ -26,14 +24,13 @@ export interface FormValues {
export default function AlertmanagerConfig(): JSX.Element {
const dispatch = useDispatch();
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const [showConfirmDeleteAMConfig, setShowConfirmDeleteAMConfig] = useState(false);
const { loading: isDeleting } = useUnifiedAlertingSelector((state) => state.deleteAMConfig);
const { loading: isSaving } = useUnifiedAlertingSelector((state) => state.saveAMConfig);
const { selectedAlertmanager } = useAlertmanager();
const readOnly = alertManagerSourceName ? isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName) : false;
const readOnly = selectedAlertmanager ? isVanillaPrometheusAlertManagerDataSource(selectedAlertmanager) : false;
const styles = useStyles2(getStyles);
const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs);
@@ -44,17 +41,17 @@ export default function AlertmanagerConfig(): JSX.Element {
result: config,
loading: isLoadingConfig,
error: loadingError,
} = (alertManagerSourceName && configRequests[alertManagerSourceName]) || initialAsyncRequestState;
} = (selectedAlertmanager && configRequests[selectedAlertmanager]) || initialAsyncRequestState;
useEffect(() => {
if (alertManagerSourceName) {
dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
if (selectedAlertmanager) {
dispatch(fetchAlertManagerConfigAction(selectedAlertmanager));
}
}, [alertManagerSourceName, dispatch]);
}, [selectedAlertmanager, dispatch]);
const resetConfig = () => {
if (alertManagerSourceName) {
dispatch(deleteAlertManagerConfigAction(alertManagerSourceName));
if (selectedAlertmanager) {
dispatch(deleteAlertManagerConfigAction(selectedAlertmanager));
}
setShowConfirmDeleteAMConfig(false);
};
@@ -76,12 +73,12 @@ export default function AlertmanagerConfig(): JSX.Element {
const loading = isDeleting || isLoadingConfig || isSaving;
const onSubmit = (values: FormValues) => {
if (alertManagerSourceName && config) {
if (selectedAlertmanager && config) {
dispatch(
updateAlertManagerConfigAction({
newConfig: JSON.parse(values.configJSON),
oldConfig: config,
alertManagerSourceName,
alertManagerSourceName: selectedAlertmanager,
successMessage: 'Alertmanager configuration updated.',
refetch: true,
})
@@ -91,11 +88,6 @@ export default function AlertmanagerConfig(): JSX.Element {
return (
<div className={styles.container}>
<AlertManagerPicker
current={alertManagerSourceName}
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
{loadingError && !loading && (
<>
<Alert
@@ -105,7 +97,7 @@ export default function AlertmanagerConfig(): JSX.Element {
{loadingError.message || 'Unknown error.'}
</Alert>
{alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && (
{selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME && (
<AlertmanagerConfigSelector
onChange={setSelectedAmConfig}
selectedAmConfig={selectedAmConfig}
@@ -117,18 +109,18 @@ export default function AlertmanagerConfig(): JSX.Element {
)}
</>
)}
{isDeleting && alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME && (
{isDeleting && selectedAlertmanager !== GRAFANA_RULES_SOURCE_NAME && (
<Alert severity="info" title="Resetting Alertmanager configuration">
It might take a while...
</Alert>
)}
{alertManagerSourceName && config && (
{selectedAlertmanager && config && (
<ConfigEditor
defaultValues={defaultValues}
onSubmit={(values) => onSubmit(values)}
readOnly={readOnly}
loading={loading}
alertManagerSourceName={alertManagerSourceName}
alertManagerSourceName={selectedAlertmanager}
showConfirmDeleteAMConfig={showConfirmDeleteAMConfig}
onReset={() => setShowConfirmDeleteAMConfig(true)}
onConfirmReset={resetConfig}
@@ -6,10 +6,7 @@ import { Button, useStyles2 } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { AlertmanagerGroup, AlertState } from 'app/plugins/datasource/alertmanager/types';
import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from '../../hooks/useAlertManagerSources';
import { getFiltersFromUrlParams } from '../../utils/misc';
import { AlertManagerPicker } from '../AlertManagerPicker';
import { AlertStateFilter } from './AlertStateFilter';
import { GroupBy } from './GroupBy';
@@ -25,8 +22,6 @@ export const AlertGroupFilter = ({ groups }: Props) => {
const { groupBy = [], queryString, alertState } = getFiltersFromUrlParams(queryParams);
const matcherFilterKey = `matcher-${filterKey}`;
const alertManagers = useAlertManagersByPermission('instance');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const styles = useStyles2(getStyles);
const clearFilters = () => {
@@ -42,11 +37,6 @@ export const AlertGroupFilter = ({ groups }: Props) => {
return (
<div className={styles.wrapper}>
<AlertManagerPicker
current={alertManagerSourceName}
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
<div className={styles.filterSection}>
<MatcherFilter
className={styles.filterInput}
@@ -35,6 +35,7 @@ import {
} from '../../mocks';
import { mockAlertmanagerChoiceResponse } from '../../mocks/alertmanagerApi';
import { grafanaNotifiersMock } from '../../mocks/grafana-notifiers';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { getAllDataSources } from '../../utils/config';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../../utils/constants';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
@@ -69,19 +70,6 @@ const alertmanagerChoiceMockedResponse: AlertmanagersChoiceResponse = {
numExternalAlertmanagers: 0,
};
const renderReceivers = (alertManagerSourceName?: string) => {
locationService.push(
'/alerting/notifications' +
(alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : '')
);
return render(
<TestProvider>
<Receivers />
</TestProvider>
);
};
const dataSources = {
alertManager: mockDataSource({
name: 'CloudManager',
@@ -96,6 +84,21 @@ const dataSources = {
}),
};
const renderReceivers = (alertManagerSourceName?: string) => {
locationService.push(
'/alerting/notifications' +
(alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : '')
);
return render(
<TestProvider>
<AlertmanagerProvider accessType="notification">
<Receivers />
</AlertmanagerProvider>
</TestProvider>
);
};
const ui = {
newContactPointButton: byRole('link', { name: /add contact point/i }),
saveContactButton: byRole('button', { name: /save contact point/i }),
@@ -116,7 +119,6 @@ const ui = {
channelFormContainer: byTestId('item-container'),
notificationError: byTestId('receivers-notification-error'),
contactPointsCollapseToggle: byTestId('collapse-toggle'),
inputs: {
@@ -210,25 +212,6 @@ describe('Receivers', () => {
expect(mocks.api.fetchConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME);
expect(mocks.api.fetchNotifiers).toHaveBeenCalledTimes(1);
expect(locationService.getSearchObject()[ALERTMANAGER_NAME_QUERY_KEY]).toEqual(undefined);
// select external cloud alertmanager, check that data is retrieved and contents are rendered as appropriate
await clickSelectOption(ui.alertManagerPicker.get(), 'CloudManager');
await byText('cloud-receiver').find();
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(2);
expect(mocks.api.fetchConfig).toHaveBeenLastCalledWith('CloudManager');
await ui.receiversTable.find();
templatesTable = await ui.templatesTable.find();
templateRows = templatesTable.querySelectorAll('tbody tr');
expect(templateRows[0]).toHaveTextContent('foo template');
expect(templateRows).toHaveLength(1);
receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(receiverRows[0]).toHaveTextContent('cloud-receiver');
expect(receiverRows).toHaveLength(1);
expect(locationService.getSearchObject()[ALERTMANAGER_NAME_QUERY_KEY]).toEqual('CloudManager');
//should not render any notification error
expect(ui.notificationError.query()).not.toBeInTheDocument();
});
it('Grafana receiver can be tested', async () => {
@@ -593,9 +576,6 @@ describe('Receivers', () => {
//
await ui.receiversTable.find();
//should render notification error
expect(ui.notificationError.query()).toBeInTheDocument();
expect(ui.notificationError.get()).toHaveTextContent('1 error with contact points');
const receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(receiverRows[0]).toHaveTextContent('1 error');
@@ -665,9 +645,6 @@ describe('Receivers', () => {
//
await ui.receiversTable.find();
//should render notification error
expect(ui.notificationError.query()).toBeInTheDocument();
expect(ui.notificationError.get()).toHaveTextContent('1 error with contact points');
const receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(receiverRows[0]).toHaveTextContent('1 error');
@@ -700,8 +677,6 @@ describe('Receivers', () => {
renderReceivers();
await ui.receiversTable.find();
//should not render notification error
expect(ui.notificationError.query()).not.toBeInTheDocument();
//contact points are not expandable
expect(ui.contactPointsCollapseToggle.query()).not.toBeInTheDocument();
//should render receivers, only one dynamic table
@@ -723,8 +698,6 @@ describe('Receivers', () => {
renderReceivers();
await ui.receiversTable.find();
//should not render notification error
expect(ui.notificationError.query()).not.toBeInTheDocument();
//contact points are not expandable
expect(ui.contactPointsCollapseToggle.query()).not.toBeInTheDocument();
//should render receivers, only one dynamic table
@@ -1,23 +1,15 @@
import { css } from '@emotion/css';
import pluralize from 'pluralize';
import React, { useEffect } from 'react';
import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom';
import { Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { Alert, Icon, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { ContactPointsState, useDispatch } from 'app/types';
import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { useDispatch } from 'app/types';
import { useGetContactPointsState } from '../../api/receiversApi';
import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from '../../hooks/useAlertManagerSources';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { fetchAlertManagerConfigAction, fetchGrafanaNotifiersAction } from '../../state/actions';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { initialAsyncRequestState } from '../../utils/redux';
import { AlertManagerPicker } from '../AlertManagerPicker';
import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
import { NoAlertManagerWarning } from '../NoAlertManagerWarning';
import { DuplicateTemplateView } from '../receivers/DuplicateTemplateView';
import { EditReceiverView } from '../receivers/EditReceiverView';
import { EditTemplateView } from '../receivers/EditTemplateView';
@@ -30,27 +22,9 @@ export interface NotificationErrorProps {
errorCount: number;
}
function NotificationError({ errorCount }: NotificationErrorProps) {
const styles = useStyles2(getStyles);
return (
<div className={styles.error} data-testid="receivers-notification-error">
<Stack alignItems="flex-end" direction="column" gap={0}>
<Stack alignItems="center" gap={1}>
<Icon name="exclamation-circle" />
<div>{`${errorCount} ${pluralize('error', errorCount)} with contact points`}</div>
</Stack>
<div>{'Some alert notifications might not be delivered'}</div>
</Stack>
</div>
);
}
const Receivers = () => {
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const { selectedAlertmanager: alertManagerSourceName } = useAlertmanager();
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
const location = useLocation();
const isRoot = location.pathname.endsWith('/alerting/notifications');
@@ -65,7 +39,7 @@ const Receivers = () => {
const receiverTypes = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
const shouldLoadConfig = isRoot || !config;
const shouldRenderNotificationStatus = isRoot;
// const shouldRenderNotificationStatus = isRoot;
useEffect(() => {
if (alertManagerSourceName && shouldLoadConfig) {
@@ -82,32 +56,12 @@ const Receivers = () => {
}
}, [alertManagerSourceName, dispatch, receiverTypes]);
const contactPointsState: ContactPointsState = useGetContactPointsState(alertManagerSourceName ?? '');
const integrationsErrorCount = contactPointsState?.errorCount ?? 0;
const disableAmSelect = !isRoot;
if (!alertManagerSourceName) {
return isRoot ? (
<NoAlertManagerWarning availableAlertManagers={alertManagers} />
) : (
<Redirect to="/alerting/notifications" />
);
return null;
}
return (
<>
<div className={styles.headingContainer}>
<AlertManagerPicker
current={alertManagerSourceName}
disabled={disableAmSelect}
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
{shouldRenderNotificationStatus && integrationsErrorCount > 0 && (
<NotificationError errorCount={integrationsErrorCount} />
)}
</div>
{error && !loading && (
<Alert severity="error" title="Error loading Alertmanager config">
{error.message || 'Unknown error.'}
@@ -169,13 +123,3 @@ const Receivers = () => {
};
export default Receivers;
const getStyles = (theme: GrafanaTheme2) => ({
error: css`
color: ${theme.colors.error.text};
`,
headingContainer: css`
display: flex;
justify-content: space-between;
`,
});
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import React, { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, Button, Field, FieldSet, Input, LinkButton, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import {
AlertmanagerConfig,
@@ -11,17 +11,14 @@ import {
} from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types';
import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName';
import { useAlertManagersByPermission } from '../../hooks/useAlertManagerSources';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
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 { AlertManagerPicker } from '../AlertManagerPicker';
import { AlertingPageWrapper } from '../AlertingPageWrapper';
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
import { MuteTimingTimeInterval } from './MuteTimingTimeInterval';
@@ -58,14 +55,9 @@ const useDefaultValues = (muteTiming?: MuteTimeInterval): MuteTimingFields => {
};
};
const defaultPageNav: Partial<NavModelItem> = {
icon: 'sitemap',
};
const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) => {
const dispatch = useDispatch();
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const { selectedAlertmanager } = useAlertmanager();
const styles = useStyles2(getStyles);
const [updating, setUpdating] = useState(false);
@@ -73,7 +65,7 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
const defaultAmCortexConfig = { alertmanager_config: {}, template_files: {} };
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
const { result = defaultAmCortexConfig } =
(alertManagerSourceName && amConfigs[alertManagerSourceName]) || initialAsyncRequestState;
(selectedAlertmanager && amConfigs[selectedAlertmanager]) || initialAsyncRequestState;
const config: AlertmanagerConfig = result?.alertmanager_config ?? {};
const defaultValues = useDefaultValues(muteTiming);
@@ -102,7 +94,7 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
updateAlertManagerConfigAction({
newConfig,
oldConfig: result,
alertManagerSourceName: alertManagerSourceName!,
alertManagerSourceName: selectedAlertmanager!,
successMessage: 'Mute timing saved',
redirectPath: '/alerting/routes/',
redirectSearch: 'tab=mute_timings',
@@ -117,20 +109,7 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
};
return (
<AlertingPageWrapper
pageId="am-routes"
pageNav={{
...defaultPageNav,
id: muteTiming ? 'alert-policy-edit' : 'alert-policy-new',
text: muteTiming ? 'Edit mute timing' : 'Add mute timing',
}}
>
<AlertManagerPicker
current={alertManagerSourceName}
onChange={setAlertManagerSourceName}
disabled
dataSources={alertManagers}
/>
<>
{provenance && <ProvisioningAlert resource={ProvisionedResource.MuteTiming} />}
{loading && <LoadingPlaceholder text="Loading mute timing" />}
{showError && <Alert title="No matching mute timing found" />}
@@ -168,7 +147,7 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
type="button"
variant="secondary"
fill="outline"
href={makeAMLink('/alerting/routes/', alertManagerSourceName, { tab: 'mute_timings' })}
href={makeAMLink('/alerting/routes/', selectedAlertmanager, { tab: 'mute_timings' })}
disabled={updating}
>
Cancel
@@ -177,7 +156,7 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
</form>
</FormProvider>
)}
</AlertingPageWrapper>
</>
);
};
@@ -9,6 +9,7 @@ import { Button } from '@grafana/ui';
import { TestProvider } from '../../../../../../test/helpers/TestProvider';
import { RouteWithID } from '../../../../../plugins/datasource/alertmanager/types';
import * as grafanaApp from '../../components/receivers/grafanaAppReceivers/grafanaApp';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { FormAmRoute } from '../../types/amroutes';
import { AmRouteReceiver } from '../receivers/grafanaAppReceivers/types';
@@ -142,12 +143,14 @@ function renderRouteForm(
onSubmit: (route: Partial<FormAmRoute>) => void = noop
) {
render(
<AmRoutesExpandedForm
actionButtons={<Button type="submit">Update default policy</Button>}
onSubmit={onSubmit}
receivers={receivers}
route={route}
/>,
<AlertmanagerProvider accessType="instance">
<AmRoutesExpandedForm
actionButtons={<Button type="submit">Update default policy</Button>}
onSubmit={onSubmit}
receivers={receivers}
route={route}
/>
</AlertmanagerProvider>,
{ wrapper: TestProvider }
);
}
@@ -1,103 +0,0 @@
import { renderHook } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import React from 'react';
import { MemoryRouter, Router } from 'react-router-dom';
import store from 'app/core/store';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY } from '../utils/constants';
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { useAlertManagerSourceName } from './useAlertManagerSourceName';
const grafanaAm: AlertManagerDataSource = {
name: GRAFANA_RULES_SOURCE_NAME,
imgUrl: '',
};
const externalAmProm: AlertManagerDataSource = {
name: 'PrometheusAm',
imgUrl: '',
};
const externalAmMimir: AlertManagerDataSource = {
name: 'MimirAm',
imgUrl: '',
};
describe('useAlertManagerSourceName', () => {
it('Should return undefined alert manager name when there are no available alert managers', () => {
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <MemoryRouter>{children}</MemoryRouter>;
const { result } = renderHook(() => useAlertManagerSourceName([]), { wrapper });
const [alertManager] = result.current;
expect(alertManager).toBeUndefined();
});
it('Should return Grafana AM when it is available and no alert manager query param exists', () => {
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <MemoryRouter>{children}</MemoryRouter>;
const availableAMs = [grafanaAm, externalAmProm, externalAmMimir];
const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper });
const [alertManager] = result.current;
expect(alertManager).toBe(grafanaAm.name);
});
it('Should return alert manager included in the query param when available', () => {
const history = createMemoryHistory();
history.push({ search: `alertmanager=${externalAmProm.name}` });
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Router history={history}>{children}</Router>;
const availableAMs = [grafanaAm, externalAmProm, externalAmMimir];
const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper });
const [alertManager] = result.current;
expect(alertManager).toBe(externalAmProm.name);
});
it('Should return undefined if alert manager included in the query is not available', () => {
const history = createMemoryHistory();
history.push({ search: `alertmanager=Not available external AM` });
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Router history={history}>{children}</Router>;
const availableAMs = [grafanaAm, externalAmProm, externalAmMimir];
const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper });
const [alertManager] = result.current;
expect(alertManager).toBe(undefined);
});
it('Should return alert manager from store if available and query is empty', () => {
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <MemoryRouter>{children}</MemoryRouter>;
const availableAMs = [grafanaAm, externalAmProm, externalAmMimir];
store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmProm.name);
const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper });
const [alertManager] = result.current;
expect(alertManager).toBe(externalAmProm.name);
});
it('Should prioritize the alert manager from query over store', () => {
const history = createMemoryHistory();
history.push({ search: `alertmanager=${externalAmProm.name}` });
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Router history={history}>{children}</Router>;
const availableAMs = [grafanaAm, externalAmProm, externalAmMimir];
store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmMimir.name);
const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper });
const [alertManager] = result.current;
expect(alertManager).toBe(externalAmProm.name);
});
});
@@ -1,67 +0,0 @@
import { useCallback } from 'react';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import store from 'app/core/store';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../utils/constants';
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
function useIsAlertManagerAvailable(availableAlertManagers: AlertManagerDataSource[]) {
return useCallback(
(alertManagerName: string) => {
const availableAlertManagersNames = availableAlertManagers.map((am) => am.name);
return availableAlertManagersNames.includes(alertManagerName);
},
[availableAlertManagers]
);
}
/* This will return am name either from query params or from local storage or a default (grafana).
* Due to RBAC permissions Grafana Managed Alert manager or external alert managers may not be available
* In the worst case neither GMA nor external alert manager is available
*/
export function useAlertManagerSourceName(
availableAlertManagers: AlertManagerDataSource[]
): [string | undefined, (alertManagerSourceName: string) => void] {
const [queryParams, updateQueryParams] = useQueryParams();
const isAlertManagerAvailable = useIsAlertManagerAvailable(availableAlertManagers);
const update = useCallback(
(alertManagerSourceName: string) => {
if (!isAlertManagerAvailable(alertManagerSourceName)) {
return;
}
if (alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME) {
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
updateQueryParams({ [ALERTMANAGER_NAME_QUERY_KEY]: null });
} else {
store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, alertManagerSourceName);
updateQueryParams({ [ALERTMANAGER_NAME_QUERY_KEY]: alertManagerSourceName });
}
},
[updateQueryParams, isAlertManagerAvailable]
);
const querySource = queryParams[ALERTMANAGER_NAME_QUERY_KEY];
if (querySource && typeof querySource === 'string') {
if (isAlertManagerAvailable(querySource)) {
return [querySource, update];
} else {
// non existing alertmanager
return [undefined, update];
}
}
const storeSource = store.get(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
if (storeSource && typeof storeSource === 'string' && isAlertManagerAvailable(storeSource)) {
update(storeSource);
return [storeSource, update];
}
if (isAlertManagerAvailable(GRAFANA_RULES_SOURCE_NAME)) {
return [GRAFANA_RULES_SOURCE_NAME, update];
}
return [undefined, update];
}
@@ -3,20 +3,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 { useAlertManagerSourceName } from './useAlertManagerSourceName';
import { useAlertManagersByPermission } from './useAlertManagerSources';
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
export function useMuteTimingOptions(): Array<SelectableValue<string>> {
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const { selectedAlertmanager } = useAlertmanager();
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
return useMemo(() => {
const { result } = (alertManagerSourceName && amConfigs[alertManagerSourceName]) || initialAsyncRequestState;
const { result } = (selectedAlertmanager && amConfigs[selectedAlertmanager]) || initialAsyncRequestState;
const config: AlertmanagerConfig = result?.alertmanager_config ?? {};
const muteTimingsOptions: Array<SelectableValue<string>> =
@@ -27,5 +25,5 @@ export function useMuteTimingOptions(): Array<SelectableValue<string>> {
})) ?? [];
return muteTimingsOptions;
}, [alertManagerSourceName, amConfigs]);
}, [selectedAlertmanager, amConfigs]);
}
@@ -0,0 +1,120 @@
import { renderHook } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import React from 'react';
import { MemoryRouter, Router } from 'react-router-dom';
import store from 'app/core/store';
import * as useAlertManagerSources from '../hooks/useAlertManagerSources';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY } from '../utils/constants';
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { AlertmanagerProvider, useAlertmanager } from './AlertmanagerContext';
const grafanaAm: AlertManagerDataSource = {
name: GRAFANA_RULES_SOURCE_NAME,
imgUrl: '',
};
const externalAmProm: AlertManagerDataSource = {
name: 'PrometheusAm',
imgUrl: '',
};
const externalAmMimir: AlertManagerDataSource = {
name: 'MimirAm',
imgUrl: '',
};
describe('useAlertmanager', () => {
it('Should return undefined alert manager name when there are no available alert managers', () => {
jest.spyOn(useAlertManagerSources, 'useAlertManagersByPermission').mockReturnValueOnce([]);
const wrapper = ({ children }: React.PropsWithChildren) => (
<MemoryRouter>
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
</MemoryRouter>
);
const { result } = renderHook(() => useAlertmanager(), { wrapper });
expect(result.current.selectedAlertmanager).toBe(undefined);
});
it('Should return Grafana AM when it is available and no alert manager query param exists', () => {
jest.spyOn(useAlertManagerSources, 'useAlertManagersByPermission').mockReturnValueOnce([grafanaAm]);
const wrapper = ({ children }: React.PropsWithChildren) => (
<MemoryRouter>
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
</MemoryRouter>
);
const { result } = renderHook(() => useAlertmanager(), { wrapper });
expect(result.current.selectedAlertmanager).toBe(grafanaAm.name);
});
it('Should return alert manager included in the query param when available', () => {
jest.spyOn(useAlertManagerSources, 'useAlertManagersByPermission').mockReturnValueOnce([externalAmProm]);
const history = createMemoryHistory();
history.push({ search: `alertmanager=${externalAmProm.name}` });
const wrapper = ({ children }: React.PropsWithChildren) => (
<Router history={history}>
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
</Router>
);
const { result } = renderHook(() => useAlertmanager(), { wrapper });
expect(result.current.selectedAlertmanager).toBe(externalAmProm.name);
});
it('Should return undefined if alert manager included in the query is not available', () => {
jest.spyOn(useAlertManagerSources, 'useAlertManagersByPermission').mockReturnValueOnce([]);
const history = createMemoryHistory();
history.push({ search: `alertmanager=Not available external AM` });
const wrapper = ({ children }: React.PropsWithChildren) => (
<Router history={history}>
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
</Router>
);
const { result } = renderHook(() => useAlertmanager(), { wrapper });
expect(result.current.selectedAlertmanager).toBe(undefined);
});
it('Should return alert manager from store if available and query is empty', () => {
jest.spyOn(useAlertManagerSources, 'useAlertManagersByPermission').mockReturnValueOnce([externalAmProm]);
const wrapper = ({ children }: React.PropsWithChildren) => (
<MemoryRouter>
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
</MemoryRouter>
);
store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmProm.name);
const { result } = renderHook(() => useAlertmanager(), { wrapper });
expect(result.current.selectedAlertmanager).toBe(externalAmProm.name);
});
it('Should prioritize the alert manager from query over store', () => {
jest
.spyOn(useAlertManagerSources, 'useAlertManagersByPermission')
.mockReturnValueOnce([externalAmProm, externalAmMimir]);
const history = createMemoryHistory();
history.push({ search: `alertmanager=${externalAmProm.name}` });
const wrapper = ({ children }: React.PropsWithChildren) => (
<Router history={history}>
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
</Router>
);
store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmMimir.name);
const { result } = renderHook(() => useAlertmanager(), { wrapper });
expect(result.current.selectedAlertmanager).toBe(externalAmProm.name);
});
});
@@ -0,0 +1,77 @@
import * as React from 'react';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import store from 'app/core/store';
import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../utils/constants';
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
interface Context {
selectedAlertmanager: string | undefined;
availableAlertManagers: AlertManagerDataSource[];
setSelectedAlertmanager: (name: string) => void;
}
const AlertmanagerContext = React.createContext<Context | undefined>(undefined);
interface Props extends React.PropsWithChildren {
accessType: 'instance' | 'notification';
}
const AlertmanagerProvider = ({ children, accessType }: Props) => {
const [queryParams, updateQueryParams] = useQueryParams();
const availableAlertManagers = useAlertManagersByPermission(accessType);
const updateSelectedAlertmanager = React.useCallback(
(selectedAlertManager: string) => {
if (!isAlertManagerAvailable(availableAlertManagers, selectedAlertManager)) {
return;
}
if (selectedAlertManager === GRAFANA_RULES_SOURCE_NAME) {
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
updateQueryParams({ [ALERTMANAGER_NAME_QUERY_KEY]: null });
} else {
store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, selectedAlertManager);
updateQueryParams({ [ALERTMANAGER_NAME_QUERY_KEY]: selectedAlertManager });
}
},
[availableAlertManagers, updateQueryParams]
);
const sourceFromQuery = queryParams[ALERTMANAGER_NAME_QUERY_KEY];
const sourceFromStore = store.get(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
const defaultSource = GRAFANA_RULES_SOURCE_NAME;
// queryParam > localStorage > default
const desiredAlertmanager = sourceFromQuery ?? sourceFromStore ?? defaultSource;
const selectedAlertmanager = isAlertManagerAvailable(availableAlertManagers, desiredAlertmanager)
? desiredAlertmanager
: undefined;
const value: Context = {
selectedAlertmanager,
availableAlertManagers,
setSelectedAlertmanager: updateSelectedAlertmanager,
};
return <AlertmanagerContext.Provider value={value}>{children}</AlertmanagerContext.Provider>;
};
function useAlertmanager() {
const context = React.useContext(AlertmanagerContext);
if (context === undefined) {
throw new Error('useAlertmanager must be used within a AlertmanagerContext');
}
return context;
}
export { AlertmanagerProvider, useAlertmanager };
function isAlertManagerAvailable(availableAlertManagers: AlertManagerDataSource[], alertManagerName: string) {
const availableAlertManagersNames = availableAlertManagers.map((am) => am.name);
return availableAlertManagersNames.includes(alertManagerName);
}
@@ -0,0 +1,40 @@
import React, { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
interface Props {
onChange: (alertManagerSourceName: string) => void;
current?: string;
dataSources: AlertManagerDataSource[];
}
function getAlertManagerLabel(alertManager: AlertManagerDataSource) {
return alertManager.name === GRAFANA_RULES_SOURCE_NAME ? 'Grafana' : alertManager.name.slice(0, 37);
}
export const AlertManagerPicker = ({ onChange, current, dataSources }: Props) => {
const options: Array<SelectableValue<string>> = useMemo(() => {
return dataSources.map((ds) => ({
label: getAlertManagerLabel(ds),
value: ds.name,
imgUrl: ds.imgUrl,
meta: ds.meta,
}));
}, [dataSources]);
return (
<Select
aria-label={'Choose Alertmanager'}
width={29}
backspaceRemovesValue={false}
onChange={(value) => value.value && onChange(value.value)}
options={options}
maxMenuHeight={500}
noOptionsMessage="No datasources found"
value={current}
getOptionLabel={(o) => o.label}
/>
);
};
@@ -1,13 +1,13 @@
import React, { useMemo } from 'react';
import { PanelPlugin } from '@grafana/data';
import { AlertManagerPicker } from 'app/features/alerting/unified/components/AlertManagerPicker';
import {
getAllAlertManagerDataSources,
GRAFANA_RULES_SOURCE_NAME,
} from 'app/features/alerting/unified/utils/datasource';
import { AlertGroupsPanel } from './AlertGroupsPanel';
import { AlertManagerPicker } from './AlertmanagerPicker';
import { Options } from './panelcfg.gen';
export const plugin = new PanelPlugin<Options>(AlertGroupsPanel).setPanelOptions((builder) => {