Alerting: Refactor Alertmanager picker (#70720)
This commit is contained in:
@@ -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}
|
||||
|
||||
+16
-43
@@ -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
|
||||
|
||||
+7
-63
@@ -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,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) => {
|
||||
|
||||
Reference in New Issue
Block a user