Alerting: Split out components for notification policies (#97191)

This commit is contained in:
Tom Ratcliffe 2024-12-18 12:16:46 +00:00 committed by GitHub
parent 89774f3c8d
commit 6df7d1fbc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 256 additions and 257 deletions

View File

@ -1406,12 +1406,9 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "13"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "13"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "14"] [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "14"]
], ],
"public/app/features/alerting/unified/NotificationPolicies.tsx:5381": [ "public/app/features/alerting/unified/NotificationPoliciesPage.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"]
], ],
"public/app/features/alerting/unified/PanelAlertTabContent.tsx:5381": [ "public/app/features/alerting/unified/PanelAlertTabContent.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
@ -1711,10 +1708,8 @@ exports[`better eslint`] = {
], ],
"public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx:5381": [ "public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"]
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"]
], ],
"public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx:5381": [ "public/app/features/alerting/unified/components/notification-policies/ContactPointSelector.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
@ -1760,18 +1755,18 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "18"] [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "18"]
], ],
"public/app/features/alerting/unified/components/notification-policies/Filters.tsx:5381": [ "public/app/features/alerting/unified/components/notification-policies/Filters.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
], ],
"public/app/features/alerting/unified/components/notification-policies/Modals.tsx:5381": [ "public/app/features/alerting/unified/components/notification-policies/Modals.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"] [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"]
], ],
"public/app/features/alerting/unified/components/notification-policies/NotificationPoliciesList.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/features/alerting/unified/components/notification-policies/Policy.tsx:5381": [ "public/app/features/alerting/unified/components/notification-policies/Policy.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
@ -2579,9 +2574,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"]
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
], ],
"public/app/features/alerting/unified/components/silences/SilencesTable.tsx:5381": [ "public/app/features/alerting/unified/components/silences/SilencesTable.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"], [0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],

View File

@ -47,7 +47,10 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
...PERMISSIONS_TIME_INTERVALS_MODIFY, ...PERMISSIONS_TIME_INTERVALS_MODIFY,
]), ]),
component: importAlertingComponent( component: importAlertingComponent(
() => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/NotificationPolicies') () =>
import(
/* webpackChunkName: "NotificationPoliciesPage" */ 'app/features/alerting/unified/NotificationPoliciesPage'
)
), ),
}, },
{ {

View File

@ -32,7 +32,8 @@ import {
} from 'app/plugins/datasource/alertmanager/types'; } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import NotificationPolicies, { findRoutesMatchingFilters } from './NotificationPolicies'; import NotificationPolicies from './NotificationPoliciesPage';
import { findRoutesMatchingFilters } from './components/notification-policies/NotificationPoliciesList';
import { import {
grantUserPermissions, grantUserPermissions,
mockDataSource, mockDataSource,

View File

@ -0,0 +1,113 @@
import { css } from '@emotion/css';
import { useState } from 'react';
import { GrafanaTheme2, UrlQueryMap } from '@grafana/data';
import { Tab, TabContent, TabsBar, useStyles2, withErrorBoundary } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { useMuteTimings } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings';
import { NotificationPoliciesList } from 'app/features/alerting/unified/components/notification-policies/NotificationPoliciesList';
import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
import { MuteTimingsTable } from './components/mute-timings/MuteTimingsTable';
import { useAlertmanager } from './state/AlertmanagerContext';
enum ActiveTab {
NotificationPolicies = 'notification_policies',
MuteTimings = 'mute_timings',
}
const NotificationPoliciesTabs = () => {
const styles = useStyles2(getStyles);
// Alertmanager logic and data hooks
const { selectedAlertmanager = '' } = useAlertmanager();
const [policiesSupported, canSeePoliciesTab] = useAlertmanagerAbility(AlertmanagerAction.ViewNotificationPolicyTree);
const [timingsSupported, canSeeTimingsTab] = useAlertmanagerAbility(AlertmanagerAction.ViewMuteTiming);
const availableTabs = [
canSeePoliciesTab && ActiveTab.NotificationPolicies,
canSeeTimingsTab && ActiveTab.MuteTimings,
].filter((tab) => !!tab);
const { data: muteTimings = [] } = useMuteTimings({
alertmanager: selectedAlertmanager,
skip: !canSeeTimingsTab,
});
// Tab state management
const [queryParams, setQueryParams] = useQueryParams();
const { tab } = getActiveTabFromUrl(queryParams, availableTabs[0]);
const [activeTab, setActiveTab] = useState<ActiveTab>(tab);
const muteTimingsTabActive = activeTab === ActiveTab.MuteTimings;
const policyTreeTabActive = activeTab === ActiveTab.NotificationPolicies;
const numberOfMuteTimings = muteTimings.length;
return (
<>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager} />
<TabsBar>
{policiesSupported && canSeePoliciesTab && (
<Tab
label={'Notification Policies'}
active={policyTreeTabActive}
onChangeTab={() => {
setActiveTab(ActiveTab.NotificationPolicies);
setQueryParams({ tab: ActiveTab.NotificationPolicies });
}}
/>
)}
{timingsSupported && canSeeTimingsTab && (
<Tab
label={'Mute Timings'}
active={muteTimingsTabActive}
counter={numberOfMuteTimings}
onChangeTab={() => {
setActiveTab(ActiveTab.MuteTimings);
setQueryParams({ tab: ActiveTab.MuteTimings });
}}
/>
)}
</TabsBar>
<TabContent className={styles.tabContent}>
{policyTreeTabActive && <NotificationPoliciesList />}
{muteTimingsTabActive && <MuteTimingsTable />}
</TabContent>
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
tabContent: css({
marginTop: theme.spacing(2),
}),
});
interface QueryParamValues {
tab: ActiveTab;
}
function getActiveTabFromUrl(queryParams: UrlQueryMap, defaultTab: ActiveTab): QueryParamValues {
let tab = defaultTab;
if (queryParams.tab === ActiveTab.NotificationPolicies) {
tab = ActiveTab.NotificationPolicies;
}
if (queryParams.tab === ActiveTab.MuteTimings) {
tab = ActiveTab.MuteTimings;
}
return {
tab,
};
}
const NotificationPoliciesPage = () => (
<AlertmanagerPageWrapper navId="am-routes" accessType="notification">
<NotificationPoliciesTabs />
</AlertmanagerPageWrapper>
);
export default withErrorBoundary(NotificationPoliciesPage, { style: 'page' });

View File

@ -19,7 +19,7 @@ import { MuteTimingsTable } from './MuteTimingsTable';
const renderWithProvider = (alertManagerSource?: string) => { const renderWithProvider = (alertManagerSource?: string) => {
return render( return render(
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={alertManagerSource}> <AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={alertManagerSource}>
<MuteTimingsTable alertManagerSourceName={alertManagerSource ?? GRAFANA_RULES_SOURCE_NAME} /> <MuteTimingsTable />
</AlertmanagerProvider> </AlertmanagerProvider>
); );
}; };

View File

@ -9,6 +9,7 @@ import {
ALL_MUTE_TIMINGS, ALL_MUTE_TIMINGS,
useExportMuteTimingsDrawer, useExportMuteTimingsDrawer,
} from 'app/features/alerting/unified/components/mute-timings/useExportMuteTimingsDrawer'; } from 'app/features/alerting/unified/components/mute-timings/useExportMuteTimingsDrawer';
import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext';
import { PROVENANCE_ANNOTATION } from 'app/features/alerting/unified/utils/k8s/constants'; import { PROVENANCE_ANNOTATION } from 'app/features/alerting/unified/utils/k8s/constants';
import { Authorize } from '../../components/Authorize'; import { Authorize } from '../../components/Authorize';
@ -22,21 +23,18 @@ import { Spacer } from '../Spacer';
import { MuteTiming, useMuteTimings } from './useMuteTimings'; import { MuteTiming, useMuteTimings } from './useMuteTimings';
import { renderTimeIntervals } from './util'; import { renderTimeIntervals } from './util';
interface MuteTimingsTableProps {
alertManagerSourceName: string;
hideActions?: boolean;
}
type TableItem = { type TableItem = {
id: string; id: string;
data: MuteTiming; data: MuteTiming;
}; };
export const MuteTimingsTable = ({ alertManagerSourceName, hideActions }: MuteTimingsTableProps) => { export const MuteTimingsTable = () => {
const { selectedAlertmanager: alertManagerSourceName = '', hasConfigurationAPI } = useAlertmanager();
const hideActions = !hasConfigurationAPI;
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const [ExportAllDrawer, showExportAllDrawer] = useExportMuteTimingsDrawer(); const [ExportAllDrawer, showExportAllDrawer] = useExportMuteTimingsDrawer();
const { data, isLoading, error } = useMuteTimings({ alertmanager: alertManagerSourceName }); const { data, isLoading, error } = useMuteTimings({ alertmanager: alertManagerSourceName ?? '' });
const items = useMemo((): TableItem[] => { const items = useMemo((): TableItem[] => {
const muteTimings = data || []; const muteTimings = data || [];
@ -73,10 +71,10 @@ export const MuteTimingsTable = ({ alertManagerSourceName, hideActions }: MuteTi
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Stack direction="row" alignItems="center"> <Stack direction="row" alignItems="center">
<span> <Trans i18nKey="alerting.mute-timings.description">
Enter specific time intervals when not to send notifications or freeze notifications for recurring periods of Enter specific time intervals when not to send notifications or freeze notifications for recurring periods of
time. time.
</span> </Trans>
<Spacer /> <Spacer />
{!hideActions && items.length > 0 && ( {!hideActions && items.length > 0 && (
<Authorize actions={[AlertmanagerAction.CreateMuteTiming]}> <Authorize actions={[AlertmanagerAction.CreateMuteTiming]}>
@ -86,7 +84,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, hideActions }: MuteTi
variant="primary" variant="primary"
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)} href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
> >
Add mute timing <Trans i18nKey="alerting.mute-timings.add-mute-timing">Add mute timing</Trans>
</LinkButton> </LinkButton>
</Authorize> </Authorize>
)} )}

View File

@ -3,7 +3,9 @@ import { debounce, isEqual } from 'lodash';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { Button, Field, Icon, Input, Label, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui'; import { Button, Field, Icon, Input, Label, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { ContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector'; import { ContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector';
import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities';
import { ObjectMatcher, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; import { ObjectMatcher, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
import { useURLSearchParams } from '../../hooks/useURLSearchParams'; import { useURLSearchParams } from '../../hooks/useURLSearchParams';
@ -25,6 +27,7 @@ const NotificationPoliciesFilter = ({
onChangeMatchers, onChangeMatchers,
matchingCount, matchingCount,
}: NotificationPoliciesFilterProps) => { }: NotificationPoliciesFilterProps) => {
const [contactPointsSupported, canSeeContactPoints] = useAlertmanagerAbility(AlertmanagerAction.ViewContactPoint);
const [searchParams, setSearchParams] = useURLSearchParams(); const [searchParams, setSearchParams] = useURLSearchParams();
const searchInputRef = useRef<HTMLInputElement | null>(null); const searchInputRef = useRef<HTMLInputElement | null>(null);
const { queryString, contactPoint } = getNotificationPoliciesFilters(searchParams); const { queryString, contactPoint } = getNotificationPoliciesFilters(searchParams);
@ -68,13 +71,13 @@ const NotificationPoliciesFilter = ({
label={ label={
<Label> <Label>
<Stack gap={0.5}> <Stack gap={0.5}>
<span>Search by matchers</span> <Trans i18nKey="alerting.common.search-by-matchers">Search by matchers</Trans>
<Tooltip <Tooltip
content={ content={
<div> <Trans i18nKey="alerting.policies.filter-description">
Filter notification policies by using a comma separated list of matchers, e.g.: Filter notification policies by using a comma separated list of matchers, e.g.:
<pre>severity=critical, region=EMEA</pre> <pre>severity=critical, region=EMEA</pre>
</div> </Trans>
} }
> >
<Icon name="info-circle" size="sm" /> <Icon name="info-circle" size="sm" />
@ -97,6 +100,7 @@ const NotificationPoliciesFilter = ({
defaultValue={queryString} defaultValue={queryString}
/> />
</Field> </Field>
{contactPointsSupported && canSeeContactPoints && (
<Field label="Search by contact point" style={{ marginBottom: 0 }}> <Field label="Search by contact point" style={{ marginBottom: 0 }}>
<ContactPointSelector <ContactPointSelector
selectProps={{ selectProps={{
@ -111,10 +115,11 @@ const NotificationPoliciesFilter = ({
selectedContactPointName={searchParams.get('contactPoint') ?? undefined} selectedContactPointName={searchParams.get('contactPoint') ?? undefined}
/> />
</Field> </Field>
)}
{hasFilters && ( {hasFilters && (
<Stack alignItems="center"> <Stack alignItems="center">
<Button variant="secondary" icon="times" onClick={clearFilters}> <Button variant="secondary" icon="times" onClick={clearFilters}>
Clear filters <Trans i18nKey="alerting.common.clear-filters">Clear filters</Trans>
</Button> </Button>
<Text variant="bodySmall" color="secondary"> <Text variant="bodySmall" color="secondary">
{matchingCount === 0 && 'No policies matching filters.'} {matchingCount === 0 && 'No policies matching filters.'}

View File

@ -1,102 +1,50 @@
import { css } from '@emotion/css';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useAsyncFn } from 'react-use'; import { useAsyncFn } from 'react-use';
import { GrafanaTheme2, UrlQueryMap } from '@grafana/data'; import { Alert, Button, Stack } from '@grafana/ui';
import {
Alert,
Button,
LoadingPlaceholder,
Stack,
Tab,
TabContent,
TabsBar,
useStyles2,
withErrorBoundary,
} from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { Trans } from 'app/core/internationalization'; import { Trans } from 'app/core/internationalization';
import { useContactPointsWithStatus } from 'app/features/alerting/unified/components/contact-points/useContactPoints'; import { useContactPointsWithStatus } from 'app/features/alerting/unified/components/contact-points/useContactPoints';
import { useMuteTimings } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings';
import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities'; import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities';
import { FormAmRoute } from 'app/features/alerting/unified/types/amroutes';
import { addUniqueIdentifierToRoute } from 'app/features/alerting/unified/utils/amroutes';
import { ERROR_NEWER_CONFIGURATION } from 'app/features/alerting/unified/utils/k8s/errors';
import { isErrorMatchingCode, stringifyErrorLike } from 'app/features/alerting/unified/utils/misc';
import { computeInheritedTree } from 'app/features/alerting/unified/utils/notification-policies';
import { ObjectMatcher, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types'; import { ObjectMatcher, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
import { alertmanagerApi } from './api/alertmanagerApi'; import { useAlertmanager } from '../../state/AlertmanagerContext';
import { useGetContactPointsState } from './api/receiversApi';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper'; import { alertmanagerApi } from './../../api/alertmanagerApi';
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning'; import { useGetContactPointsState } from './../../api/receiversApi';
import { MuteTimingsTable } from './components/mute-timings/MuteTimingsTable'; import { isLoading as isPending, useAsync } from './../../hooks/useAsync';
import { import { useRouteGroupsMatcher } from './../../useRouteGroupsMatcher';
NotificationPoliciesFilter,
findRoutesByMatchers,
findRoutesMatchingPredicate,
} from './components/notification-policies/Filters';
import {
useAddPolicyModal,
useAlertGroupsModal,
useDeletePolicyModal,
useEditPolicyModal,
} from './components/notification-policies/Modals';
import { Policy } from './components/notification-policies/Policy';
import {
useNotificationPolicyRoute,
useUpdateNotificationPolicyRoute,
} from './components/notification-policies/useNotificationPolicyRoute';
import { isLoading as isPending, useAsync } from './hooks/useAsync';
import { useAlertmanager } from './state/AlertmanagerContext';
import { FormAmRoute } from './types/amroutes';
import { useRouteGroupsMatcher } from './useRouteGroupsMatcher';
import { addUniqueIdentifierToRoute } from './utils/amroutes';
import { ERROR_NEWER_CONFIGURATION } from './utils/k8s/errors';
import { isErrorMatchingCode, stringifyErrorLike } from './utils/misc';
import { computeInheritedTree } from './utils/notification-policies';
import { import {
InsertPosition, InsertPosition,
addRouteToReferenceRoute, addRouteToReferenceRoute,
cleanRouteIDs, cleanRouteIDs,
mergePartialAmRouteWithRouteTree, mergePartialAmRouteWithRouteTree,
omitRouteFromRouteTree, omitRouteFromRouteTree,
} from './utils/routeTree'; } from './../../utils/routeTree';
import { NotificationPoliciesFilter, findRoutesByMatchers, findRoutesMatchingPredicate } from './Filters';
import { useAddPolicyModal, useAlertGroupsModal, useDeletePolicyModal, useEditPolicyModal } from './Modals';
import { Policy } from './Policy';
import { useNotificationPolicyRoute, useUpdateNotificationPolicyRoute } from './useNotificationPolicyRoute';
enum ActiveTab { export const NotificationPoliciesList = () => {
NotificationPolicies = 'notification_policies',
MuteTimings = 'mute_timings',
}
const AmRoutes = () => {
const styles = useStyles2(getStyles);
const appNotification = useAppNotification(); const appNotification = useAppNotification();
const [policiesSupported, canSeePoliciesTab] = useAlertmanagerAbility(AlertmanagerAction.ViewNotificationPolicyTree); const [contactPointsSupported, canSeeContactPoints] = useAlertmanagerAbility(AlertmanagerAction.ViewContactPoint);
const [timingsSupported, canSeeTimingsTab] = useAlertmanagerAbility(AlertmanagerAction.ViewMuteTiming);
const [contactPointsSupported, canSeeContactPointsStatus] = useAlertmanagerAbility(
AlertmanagerAction.ViewContactPoint
);
const availableTabs = [
canSeePoliciesTab && ActiveTab.NotificationPolicies,
canSeeTimingsTab && ActiveTab.MuteTimings,
].filter((tab) => !!tab);
const [_, canSeeAlertGroups] = useAlertmanagerAbility(AlertmanagerAction.ViewAlertGroups); const [_, canSeeAlertGroups] = useAlertmanagerAbility(AlertmanagerAction.ViewAlertGroups);
const { useGetAlertmanagerAlertGroupsQuery } = alertmanagerApi; const { useGetAlertmanagerAlertGroupsQuery } = alertmanagerApi;
const [queryParams, setQueryParams] = useQueryParams();
const { tab } = getActiveTabFromUrl(queryParams, availableTabs[0]);
const [activeTab, setActiveTab] = useState<ActiveTab>(tab);
const [contactPointFilter, setContactPointFilter] = useState<string | undefined>(); const [contactPointFilter, setContactPointFilter] = useState<string | undefined>();
const [labelMatchersFilter, setLabelMatchersFilter] = useState<ObjectMatcher[]>([]); const [labelMatchersFilter, setLabelMatchersFilter] = useState<ObjectMatcher[]>([]);
const { selectedAlertmanager, hasConfigurationAPI, isGrafanaAlertmanager } = useAlertmanager(); const { selectedAlertmanager, hasConfigurationAPI, isGrafanaAlertmanager } = useAlertmanager();
const { getRouteGroupsMap } = useRouteGroupsMatcher(); const { getRouteGroupsMap } = useRouteGroupsMatcher();
const { data: muteTimings = [] } = useMuteTimings({
alertmanager: selectedAlertmanager ?? '',
skip: !canSeeTimingsTab,
});
const shouldFetchContactPoints =
policiesSupported && canSeePoliciesTab && contactPointsSupported && canSeeContactPointsStatus;
const shouldFetchContactPoints = contactPointsSupported && canSeeContactPoints;
const contactPointsState = useGetContactPointsState( const contactPointsState = useGetContactPointsState(
// Workaround to not try and call this API when we don't have access to the policies tab // Workaround to not try and call this API when we don't have access to the policies tab
shouldFetchContactPoints ? (selectedAlertmanager ?? '') : '' shouldFetchContactPoints ? (selectedAlertmanager ?? '') : ''
@ -107,7 +55,7 @@ const AmRoutes = () => {
isLoading, isLoading,
error: resultError, error: resultError,
refetch: refetchNotificationPolicyRoute, refetch: refetchNotificationPolicyRoute,
} = useNotificationPolicyRoute({ alertmanager: selectedAlertmanager ?? '' }, { skip: !canSeePoliciesTab }); } = useNotificationPolicyRoute({ alertmanager: selectedAlertmanager ?? '' });
// We make the assumption that the first policy is the default one // We make the assumption that the first policy is the default one
// At the time of writing, this will be always the case for the AM config response, and the K8S API // At the time of writing, this will be always the case for the AM config response, and the K8S API
@ -118,14 +66,14 @@ const AmRoutes = () => {
const { currentData: alertGroups, refetch: refetchAlertGroups } = useGetAlertmanagerAlertGroupsQuery( const { currentData: alertGroups, refetch: refetchAlertGroups } = useGetAlertmanagerAlertGroupsQuery(
{ amSourceName: selectedAlertmanager ?? '' }, { amSourceName: selectedAlertmanager ?? '' },
{ skip: !canSeePoliciesTab || !canSeeAlertGroups || !selectedAlertmanager } { skip: !canSeeAlertGroups || !selectedAlertmanager }
); );
const { contactPoints: receivers } = useContactPointsWithStatus({ const { contactPoints: receivers } = useContactPointsWithStatus({
alertmanager: selectedAlertmanager ?? '', alertmanager: selectedAlertmanager ?? '',
fetchPolicies: false, fetchPolicies: false,
fetchStatuses: canSeeContactPointsStatus, fetchStatuses: true,
skip: !canSeePoliciesTab, skip: !shouldFetchContactPoints,
}); });
const rootRoute = useMemo(() => { const rootRoute = useMemo(() => {
@ -244,43 +192,10 @@ const AmRoutes = () => {
return null; return null;
} }
const numberOfMuteTimings = muteTimings.length;
const hasPoliciesData = rootRoute && !resultError && !isLoading; const hasPoliciesData = rootRoute && !resultError && !isLoading;
const hasPoliciesError = !!resultError && !isLoading; const hasPoliciesError = !!resultError && !isLoading;
const muteTimingsTabActive = activeTab === ActiveTab.MuteTimings;
const policyTreeTabActive = activeTab === ActiveTab.NotificationPolicies;
return ( return (
<>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager} />
<TabsBar>
{policiesSupported && canSeePoliciesTab && (
<Tab
label={'Notification Policies'}
active={policyTreeTabActive}
onChangeTab={() => {
setActiveTab(ActiveTab.NotificationPolicies);
setQueryParams({ tab: ActiveTab.NotificationPolicies });
}}
/>
)}
{timingsSupported && canSeeTimingsTab && (
<Tab
label={'Mute Timings'}
active={muteTimingsTabActive}
counter={numberOfMuteTimings}
onChangeTab={() => {
setActiveTab(ActiveTab.MuteTimings);
setQueryParams({ tab: ActiveTab.MuteTimings });
}}
/>
)}
</TabsBar>
<TabContent className={styles.tabContent}>
{isLoading && <LoadingPlaceholder text="Loading notification policies..." />}
{policyTreeTabActive && (
<> <>
{hasPoliciesError && ( {hasPoliciesError && (
<Alert severity="error" title="Error loading Alertmanager config"> <Alert severity="error" title="Error loading Alertmanager config">
@ -337,12 +252,6 @@ const AmRoutes = () => {
{deleteModal} {deleteModal}
{alertInstancesModal} {alertInstancesModal}
</> </>
)}
{muteTimingsTabActive && (
<MuteTimingsTable alertManagerSourceName={selectedAlertmanager} hideActions={!hasConfigurationAPI} />
)}
</TabContent>
</>
); );
}; };
@ -429,37 +338,3 @@ function findMapIntersection(...matchingRoutes: FilterResult[]): FilterResult {
return result; return result;
} }
const getStyles = (theme: GrafanaTheme2) => ({
tabContent: css({
marginTop: theme.spacing(2),
}),
});
interface QueryParamValues {
tab: ActiveTab;
}
function getActiveTabFromUrl(queryParams: UrlQueryMap, defaultTab: ActiveTab): QueryParamValues {
let tab = defaultTab;
if (queryParams.tab === ActiveTab.NotificationPolicies) {
tab = ActiveTab.NotificationPolicies;
}
if (queryParams.tab === ActiveTab.MuteTimings) {
tab = ActiveTab.MuteTimings;
}
return {
tab,
};
}
const NotificationPoliciesPage = () => (
<AlertmanagerPageWrapper navId="am-routes" accessType="notification">
<AmRoutes />
</AlertmanagerPageWrapper>
);
export default withErrorBoundary(NotificationPoliciesPage, { style: 'page' });

View File

@ -35,7 +35,6 @@ import {
} from 'app/plugins/datasource/alertmanager/types'; } from 'app/plugins/datasource/alertmanager/types';
import { ReceiversState } from 'app/types'; import { ReceiversState } from 'app/types';
import { RoutesMatchingFilters } from '../../NotificationPolicies';
import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities'; import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities';
import { getAmMatcherFormatter } from '../../utils/alertmanager'; import { getAmMatcherFormatter } from '../../utils/alertmanager';
import { MatcherFormatter, normalizeMatchers } from '../../utils/matchers'; import { MatcherFormatter, normalizeMatchers } from '../../utils/matchers';
@ -51,6 +50,7 @@ import { Spacer } from '../Spacer';
import { GrafanaPoliciesExporter } from '../export/GrafanaPoliciesExporter'; import { GrafanaPoliciesExporter } from '../export/GrafanaPoliciesExporter';
import { Matchers } from './Matchers'; import { Matchers } from './Matchers';
import { RoutesMatchingFilters } from './NotificationPoliciesList';
import { TIMING_OPTIONS_DEFAULTS, TimingOptions } from './timingOptions'; import { TIMING_OPTIONS_DEFAULTS, TimingOptions } from './timingOptions';
interface PolicyComponentProps { interface PolicyComponentProps {
@ -742,7 +742,7 @@ const TimeIntervals: FC<{ timings: string[]; alertManagerSourceName: string }> =
href={createMuteTimingLink(timing, alertManagerSourceName)} href={createMuteTimingLink(timing, alertManagerSourceName)}
color={canSeeMuteTimings ? 'primary' : 'secondary'} color={canSeeMuteTimings ? 'primary' : 'secondary'}
variant="bodySmall" variant="bodySmall"
inline={false} inline={canSeeMuteTimings ? false : undefined}
> >
{timing} {timing}
</Wrapper> </Wrapper>

View File

@ -5,6 +5,7 @@ import { FormEvent, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { Button, Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { Trans } from 'app/core/internationalization';
import { parsePromQLStyleMatcherLoose } from '../../utils/matchers'; import { parsePromQLStyleMatcherLoose } from '../../utils/matchers';
import { getSilenceFiltersFromUrlParams } from '../../utils/misc'; import { getSilenceFiltersFromUrlParams } from '../../utils/misc';
@ -48,7 +49,7 @@ export const SilencesFilter = () => {
label={ label={
<Label> <Label>
<Stack gap={0.5}> <Stack gap={0.5}>
<span>Search by matchers</span> <Trans i18nKey="alerting.common.search-by-matchers">Search by matchers</Trans>
<Tooltip <Tooltip
content={ content={
<div> <div>
@ -79,7 +80,7 @@ export const SilencesFilter = () => {
{queryString && ( {queryString && (
<div className={styles.rowChild}> <div className={styles.rowChild}>
<Button variant="secondary" icon="times" onClick={clearFilters}> <Button variant="secondary" icon="times" onClick={clearFilters}>
Clear filters <Trans i18nKey="alerting.common.clear-filters">Clear filters</Trans>
</Button> </Button>
</div> </div>
)} )}

View File

@ -241,11 +241,13 @@
}, },
"common": { "common": {
"cancel": "Cancel", "cancel": "Cancel",
"clear-filters": "Clear filters",
"delete": "Delete", "delete": "Delete",
"edit": "Edit", "edit": "Edit",
"export": "Export", "export": "Export",
"export-all": "Export all", "export-all": "Export all",
"loading": "Loading...", "loading": "Loading...",
"search-by-matchers": "Search by matchers",
"view": "View" "view": "View"
}, },
"contact-points": { "contact-points": {
@ -327,6 +329,8 @@
} }
}, },
"mute-timings": { "mute-timings": {
"add-mute-timing": "Add mute timing",
"description": "Enter specific time intervals when not to send notifications or freeze notifications for recurring periods of time.",
"save": "Save mute timing", "save": "Save mute timing",
"saving": "Saving mute timing" "saving": "Saving mute timing"
}, },
@ -349,6 +353,7 @@
"warning-1": "Deleting this notification policy will permanently remove it.", "warning-1": "Deleting this notification policy will permanently remove it.",
"warning-2": "Are you sure you want to delete this policy?" "warning-2": "Are you sure you want to delete this policy?"
}, },
"filter-description": "Filter notification policies by using a comma separated list of matchers, e.g.:<1>severity=critical, region=EMEA</1>",
"generated-policies": "Auto-generated policies", "generated-policies": "Auto-generated policies",
"matchers": "Matchers", "matchers": "Matchers",
"metadata": { "metadata": {

View File

@ -241,11 +241,13 @@
}, },
"common": { "common": {
"cancel": "Cäʼnčęľ", "cancel": "Cäʼnčęľ",
"clear-filters": "Cľęäř ƒįľŧęřş",
"delete": "Đęľęŧę", "delete": "Đęľęŧę",
"edit": "Ēđįŧ", "edit": "Ēđįŧ",
"export": "Ēχpőřŧ", "export": "Ēχpőřŧ",
"export-all": "Ēχpőřŧ äľľ", "export-all": "Ēχpőřŧ äľľ",
"loading": "Ŀőäđįʼnģ...", "loading": "Ŀőäđįʼnģ...",
"search-by-matchers": "Ŝęäřčĥ þy mäŧčĥęřş",
"view": "Vįęŵ" "view": "Vįęŵ"
}, },
"contact-points": { "contact-points": {
@ -327,6 +329,8 @@
} }
}, },
"mute-timings": { "mute-timings": {
"add-mute-timing": "Åđđ mūŧę ŧįmįʼnģ",
"description": "Ēʼnŧęř şpęčįƒįč ŧįmę įʼnŧęřväľş ŵĥęʼn ʼnőŧ ŧő şęʼnđ ʼnőŧįƒįčäŧįőʼnş őř ƒřęęžę ʼnőŧįƒįčäŧįőʼnş ƒőř řęčūřřįʼnģ pęřįőđş őƒ ŧįmę.",
"save": "Ŝävę mūŧę ŧįmįʼnģ", "save": "Ŝävę mūŧę ŧįmįʼnģ",
"saving": "Ŝävįʼnģ mūŧę ŧįmįʼnģ" "saving": "Ŝävįʼnģ mūŧę ŧįmįʼnģ"
}, },
@ -349,6 +353,7 @@
"warning-1": "Đęľęŧįʼnģ ŧĥįş ʼnőŧįƒįčäŧįőʼn pőľįčy ŵįľľ pęřmäʼnęʼnŧľy řęmővę įŧ.", "warning-1": "Đęľęŧįʼnģ ŧĥįş ʼnőŧįƒįčäŧįőʼn pőľįčy ŵįľľ pęřmäʼnęʼnŧľy řęmővę įŧ.",
"warning-2": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő đęľęŧę ŧĥįş pőľįčy?" "warning-2": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő đęľęŧę ŧĥįş pőľįčy?"
}, },
"filter-description": "Fįľŧęř ʼnőŧįƒįčäŧįőʼn pőľįčįęş þy ūşįʼnģ ä čőmmä şępäřäŧęđ ľįşŧ őƒ mäŧčĥęřş, ę.ģ.:<1>şęvęřįŧy=čřįŧįčäľ, řęģįőʼn=ĒMĒÅ</1>",
"generated-policies": "Åūŧő-ģęʼnęřäŧęđ pőľįčįęş", "generated-policies": "Åūŧő-ģęʼnęřäŧęđ pőľįčįęş",
"matchers": "Mäŧčĥęřş", "matchers": "Mäŧčĥęřş",
"metadata": { "metadata": {