mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Visualize autogenerated policy tree for simplified routing. (#79509)
* WIP * WIP: disable some actions when is autogenerated policy * WIP * Wip: add checks for group by in auto-generated policy * Make autogenerated policy readOnly and enable Readonly modal for it * Use real check for autogenerated root * Fix test * Refactor: rename consts * Add test for policy form being read only * Add tests * Update some code comments * Fix Switch component not being styled as disabled * Rename isAutogeneratedChunkOpen property to isBranchOpen and fix test * Revert fix for Switch as it has moved to another separate PR * Split Policy component in smaller sub components * use useAlertmanagerAbility form for checking autogenerated tree visibility and fix container for autogenerated policy being rendered when it's not supported * Update useAbilities test and dont use toAbility for ViewAutogeneratedPolicyTree * Fix Policy being unmounted every 10 secs and move the collapsed/expanded state to each Policy component * remove permissions from createDropdownMenuActions method parameters and convert the method to a hook * Revert using PolicyItem * Add test for createDropdownMenuActions * Revert having a read only view form for the policy * Remove readonly from default policy form * Only show collapsible when node has children * Split DefaultPolicyIndicator * use hidehideCurrentPolicy instead of showCurrentPolicy * Address some review suggestions
This commit is contained in:
parent
9f0bb9cb07
commit
10f0d094ad
@ -1659,15 +1659,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/notification-policies/Policy.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "4"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "5"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "6"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "7"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "8"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/notification-policies/PromDurationDocs.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
|
@ -4,7 +4,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2, UrlQueryMap } from '@grafana/data';
|
||||
import { Alert, LoadingPlaceholder, Tab, TabContent, TabsBar, useStyles2, withErrorBoundary, Stack } from '@grafana/ui';
|
||||
import { Alert, LoadingPlaceholder, Stack, Tab, TabContent, TabsBar, useStyles2, withErrorBoundary } from '@grafana/ui';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
import { ObjectMatcher, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
@ -16,12 +16,12 @@ import { useGetContactPointsState } from './api/receiversApi';
|
||||
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
|
||||
import { MuteTimingsTable } from './components/mute-timings/MuteTimingsTable';
|
||||
import { findRoutesMatchingPredicate, NotificationPoliciesFilter } from './components/notification-policies/Filters';
|
||||
import { NotificationPoliciesFilter, findRoutesMatchingPredicate } from './components/notification-policies/Filters';
|
||||
import {
|
||||
useAddPolicyModal,
|
||||
useEditPolicyModal,
|
||||
useDeletePolicyModal,
|
||||
useAlertGroupsModal,
|
||||
useDeletePolicyModal,
|
||||
useEditPolicyModal,
|
||||
} from './components/notification-policies/Modals';
|
||||
import { Policy } from './components/notification-policies/Policy';
|
||||
import { useAlertmanagerConfig } from './hooks/useAlertmanagerConfig';
|
||||
@ -249,6 +249,7 @@ const AmRoutes = () => {
|
||||
onShowAlertInstances={showAlertGroupsModal}
|
||||
routesMatchingFilters={routesMatchingFilters}
|
||||
matchingInstancesPreview={{ groupsMap: routeAlertGroupsMap, enabled: !instancesPreviewError }}
|
||||
isAutoGenerated={false}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
@ -322,6 +322,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
`,
|
||||
noMatchersWarning: css`
|
||||
padding: ${theme.spacing(1)} ${theme.spacing(2)};
|
||||
margin-bottom: ${theme.spacing(1)};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import { render, renderHook, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { noop } from 'lodash';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import {
|
||||
AlertmanagerGroup,
|
||||
@ -19,7 +19,12 @@ import { mockAlertGroup, mockAlertmanagerAlert, mockReceiversState } from '../..
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import { Policy } from './Policy';
|
||||
import {
|
||||
AUTOGENERATED_ROOT_LABEL_NAME,
|
||||
Policy,
|
||||
isAutoGeneratedRootAndSimplifiedEnabled,
|
||||
useCreateDropdownMenuActions,
|
||||
} from './Policy';
|
||||
|
||||
jest.mock('../../hooks/useAbilities', () => ({
|
||||
...jest.requireActual('../../hooks/useAbilities'),
|
||||
@ -163,6 +168,7 @@ describe('Policy', () => {
|
||||
onAddPolicy={onAddPolicy}
|
||||
onDeletePolicy={onDeletePolicy}
|
||||
onShowAlertInstances={onShowAlertInstances}
|
||||
isAutoGenerated={false}
|
||||
/>
|
||||
);
|
||||
// should have default policy
|
||||
@ -379,3 +385,104 @@ const mockRoutes: RouteWithID = {
|
||||
group_interval: undefined,
|
||||
repeat_interval: undefined,
|
||||
};
|
||||
|
||||
describe('isAutoGeneratedRootAndSimplifiedEnabled', () => {
|
||||
it('returns false when simplified routing is not enabled', () => {
|
||||
const route: RouteWithID = {
|
||||
id: '1',
|
||||
object_matchers: [['label', MatcherOperator.equal, 'true']],
|
||||
};
|
||||
config.featureToggles.alertingSimplifiedRouting = false;
|
||||
expect(isAutoGeneratedRootAndSimplifiedEnabled(route)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when object_matchers is not defined', () => {
|
||||
const route: RouteWithID = {
|
||||
id: '1',
|
||||
};
|
||||
config.featureToggles.alertingSimplifiedRouting = true;
|
||||
expect(isAutoGeneratedRootAndSimplifiedEnabled(route)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when object_matchers contains AUTOGENERATED_ROOT_LABEL_NAME, and simplified routing is enabled', () => {
|
||||
const route: RouteWithID = {
|
||||
id: '1',
|
||||
object_matchers: [[AUTOGENERATED_ROOT_LABEL_NAME, MatcherOperator.equal, 'true']],
|
||||
};
|
||||
config.featureToggles.alertingSimplifiedRouting = true;
|
||||
expect(isAutoGeneratedRootAndSimplifiedEnabled(route)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when object_matchers does not contain AUTOGENERATED_ROOT_LABEL_NAME, and simplified routing is enabled', () => {
|
||||
const route: RouteWithID = {
|
||||
id: '1',
|
||||
object_matchers: [['label', MatcherOperator.equal, 'true']],
|
||||
};
|
||||
config.featureToggles.alertingSimplifiedRouting = true;
|
||||
expect(isAutoGeneratedRootAndSimplifiedEnabled(route)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateDropdownMenuActions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const openDetailModal = jest.fn();
|
||||
const currentRoute: RouteWithID = { id: '0', routes: [{ id: '1' }] };
|
||||
const toggleShowExportDrawer = jest.fn();
|
||||
const onDeletePolicy = jest.fn();
|
||||
const testCases = [
|
||||
{
|
||||
isAutoGenerated: false,
|
||||
isDefaultPolicy: true,
|
||||
provisioned: false,
|
||||
expectedMenu: ['edit-policy', 'export-policy'],
|
||||
},
|
||||
{
|
||||
isAutoGenerated: false,
|
||||
isDefaultPolicy: true,
|
||||
provisioned: true,
|
||||
expectedMenu: ['edit-policy', 'export-policy'],
|
||||
},
|
||||
{
|
||||
isAutoGenerated: false,
|
||||
isDefaultPolicy: false,
|
||||
provisioned: false,
|
||||
expectedMenu: ['edit-policy', 'delete-policy'],
|
||||
},
|
||||
{
|
||||
isAutoGenerated: false,
|
||||
isDefaultPolicy: false,
|
||||
provisioned: true,
|
||||
expectedMenu: ['edit-policy', 'delete-policy'],
|
||||
},
|
||||
{ isAutoGenerated: true, isDefaultPolicy: true, provisioned: true, expectedMenu: ['edit-policy'] },
|
||||
{ isAutoGenerated: true, isDefaultPolicy: false, provisioned: false, expectedMenu: ['edit-policy'] },
|
||||
{ isAutoGenerated: true, isDefaultPolicy: true, provisioned: false, expectedMenu: ['edit-policy'] },
|
||||
{ isAutoGenerated: true, isDefaultPolicy: false, provisioned: true, expectedMenu: ['edit-policy'] },
|
||||
];
|
||||
|
||||
testCases.forEach(({ isAutoGenerated, isDefaultPolicy, provisioned, expectedMenu }) => {
|
||||
it(`Having all the permissions returns ${expectedMenu.length} menu items for isAutoGenerated=${isAutoGenerated}, isDefaultPolicy=${isDefaultPolicy}, provisioned=${provisioned}`, () => {
|
||||
useAlertmanagerAbilitiesMock.mockReturnValue([
|
||||
[true, true],
|
||||
[true, true],
|
||||
[true, true],
|
||||
]);
|
||||
const { result } = renderHook(() =>
|
||||
useCreateDropdownMenuActions(
|
||||
isAutoGenerated,
|
||||
isDefaultPolicy,
|
||||
provisioned,
|
||||
openDetailModal,
|
||||
currentRoute,
|
||||
toggleShowExportDrawer,
|
||||
onDeletePolicy
|
||||
)
|
||||
);
|
||||
|
||||
const menuItemsKeys = result.current.map((item) => item.key ?? '');
|
||||
expect(menuItemsKeys).toEqual(expectedMenu);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,11 +6,13 @@ import { Link } from 'react-router-dom';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Dropdown,
|
||||
Icon,
|
||||
IconButton,
|
||||
Menu,
|
||||
Stack,
|
||||
Text,
|
||||
@ -19,14 +21,20 @@ import {
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap';
|
||||
import { AlertmanagerGroup, ObjectMatcher, Receiver, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||
import {
|
||||
AlertmanagerGroup,
|
||||
MatcherOperator,
|
||||
ObjectMatcher,
|
||||
Receiver,
|
||||
RouteWithID,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { ReceiversState } from 'app/types';
|
||||
|
||||
import { AlertmanagerAction, useAlertmanagerAbilities } from '../../hooks/useAbilities';
|
||||
import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||
import { INTEGRATION_ICONS } from '../../types/contact-points';
|
||||
import { normalizeMatchers } from '../../utils/matchers';
|
||||
import { createContactPointLink, createMuteTimingLink } from '../../utils/misc';
|
||||
import { getInheritedProperties, InhertitableProperties } from '../../utils/notification-policies';
|
||||
import { InhertitableProperties, getInheritedProperties } from '../../utils/notification-policies';
|
||||
import { Authorize } from '../Authorize';
|
||||
import { HoverCard } from '../HoverCard';
|
||||
import { Label } from '../Label';
|
||||
@ -37,7 +45,7 @@ import { Strong } from '../Strong';
|
||||
import { GrafanaPoliciesExporter } from '../export/GrafanaPoliciesExporter';
|
||||
|
||||
import { Matchers } from './Matchers';
|
||||
import { TimingOptions, TIMING_OPTIONS_DEFAULTS } from './timingOptions';
|
||||
import { TIMING_OPTIONS_DEFAULTS, TimingOptions } from './timingOptions';
|
||||
|
||||
interface PolicyComponentProps {
|
||||
receivers?: Receiver[];
|
||||
@ -54,55 +62,42 @@ interface PolicyComponentProps {
|
||||
routeTree: RouteWithID;
|
||||
currentRoute: RouteWithID;
|
||||
alertManagerSourceName: string;
|
||||
onEditPolicy: (route: RouteWithID, isDefault?: boolean) => void;
|
||||
onEditPolicy: (route: RouteWithID, isDefault?: boolean, isAutogenerated?: boolean) => void;
|
||||
onAddPolicy: (route: RouteWithID) => void;
|
||||
onDeletePolicy: (route: RouteWithID) => void;
|
||||
onShowAlertInstances: (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[]) => void;
|
||||
isAutoGenerated?: boolean;
|
||||
}
|
||||
|
||||
const Policy: FC<PolicyComponentProps> = ({
|
||||
receivers = [],
|
||||
contactPointsState,
|
||||
readOnly = false,
|
||||
provisioned = false,
|
||||
alertGroups = [],
|
||||
alertManagerSourceName,
|
||||
currentRoute,
|
||||
routeTree,
|
||||
inheritedProperties,
|
||||
routesMatchingFilters = [],
|
||||
matchingInstancesPreview = { enabled: false },
|
||||
onEditPolicy,
|
||||
onAddPolicy,
|
||||
onDeletePolicy,
|
||||
onShowAlertInstances,
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const isDefaultPolicy = currentRoute === routeTree;
|
||||
const Policy = (props: PolicyComponentProps) => {
|
||||
const {
|
||||
receivers = [],
|
||||
contactPointsState,
|
||||
readOnly = false,
|
||||
provisioned = false,
|
||||
alertGroups = [],
|
||||
alertManagerSourceName,
|
||||
currentRoute,
|
||||
routeTree,
|
||||
inheritedProperties,
|
||||
routesMatchingFilters = [],
|
||||
matchingInstancesPreview = { enabled: false },
|
||||
onEditPolicy,
|
||||
onAddPolicy,
|
||||
onDeletePolicy,
|
||||
onShowAlertInstances,
|
||||
isAutoGenerated = false,
|
||||
} = props;
|
||||
|
||||
const [
|
||||
[updatePoliciesSupported, updatePoliciesAllowed],
|
||||
[deletePolicySupported, deletePolicyAllowed],
|
||||
[exportPoliciesSupported, exportPoliciesAllowed],
|
||||
] = useAlertmanagerAbilities([
|
||||
AlertmanagerAction.UpdateNotificationPolicyTree,
|
||||
AlertmanagerAction.DeleteNotificationPolicy,
|
||||
AlertmanagerAction.ExportNotificationPolicies,
|
||||
]);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const isDefaultPolicy = currentRoute === routeTree;
|
||||
|
||||
const contactPoint = currentRoute.receiver;
|
||||
const continueMatching = currentRoute.continue ?? false;
|
||||
const groupBy = currentRoute.group_by;
|
||||
const muteTimings = currentRoute.mute_time_intervals ?? [];
|
||||
const timingOptions: TimingOptions = {
|
||||
group_wait: currentRoute.group_wait,
|
||||
group_interval: currentRoute.group_interval,
|
||||
repeat_interval: currentRoute.repeat_interval,
|
||||
};
|
||||
|
||||
const matchers = normalizeMatchers(currentRoute);
|
||||
const hasMatchers = Boolean(matchers && matchers.length);
|
||||
const hasMuteTimings = Boolean(muteTimings.length);
|
||||
const hasFocus = routesMatchingFilters.some((route) => route.id === currentRoute.id);
|
||||
|
||||
// gather errors here
|
||||
@ -116,33 +111,340 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
const actualContactPoint = contactPoint ?? inheritedProperties?.receiver ?? '';
|
||||
const contactPointErrors = contactPointsState ? getContactPointErrors(actualContactPoint, contactPointsState) : [];
|
||||
|
||||
contactPointErrors.forEach((error) => {
|
||||
errors.push(error);
|
||||
});
|
||||
|
||||
const hasInheritedProperties = inheritedProperties && Object.keys(inheritedProperties).length > 0;
|
||||
|
||||
const childPolicies = currentRoute.routes ?? [];
|
||||
|
||||
const inheritedGrouping = hasInheritedProperties && inheritedProperties.group_by;
|
||||
const noGrouping = isArray(groupBy) && groupBy[0] === '...';
|
||||
const customGrouping = !noGrouping && isArray(groupBy) && groupBy.length > 0;
|
||||
const singleGroup = isDefaultPolicy && isArray(groupBy) && groupBy.length === 0;
|
||||
|
||||
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
|
||||
const matchingAlertGroups = matchingInstancesPreview?.groupsMap?.get(currentRoute.id);
|
||||
|
||||
// sum all alert instances for all groups we're handling
|
||||
const numberOfAlertInstances = matchingAlertGroups
|
||||
? sumBy(matchingAlertGroups, (group) => group.alerts.length)
|
||||
: undefined;
|
||||
|
||||
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
|
||||
const showExportAction = exportPoliciesAllowed && exportPoliciesSupported && isDefaultPolicy;
|
||||
const showEditAction = updatePoliciesSupported && updatePoliciesAllowed;
|
||||
const showDeleteAction = deletePolicySupported && deletePolicyAllowed && !isDefaultPolicy;
|
||||
// simplified routing permissions
|
||||
const [isSupportedToSeeAutogeneratedChunk, isAllowedToSeeAutogeneratedChunk] = useAlertmanagerAbility(
|
||||
AlertmanagerAction.ViewAutogeneratedPolicyTree
|
||||
);
|
||||
// collapsible policies variables
|
||||
const isThisPolicyCollapsible = useShouldPolicyBeCollapsible(currentRoute);
|
||||
const [isBranchOpen, toggleBranchOpen] = useToggle(false);
|
||||
const renderChildPolicies = (isThisPolicyCollapsible && isBranchOpen) || !isThisPolicyCollapsible;
|
||||
|
||||
const groupBy = currentRoute.group_by;
|
||||
const muteTimings = currentRoute.mute_time_intervals ?? [];
|
||||
|
||||
const timingOptions: TimingOptions = {
|
||||
group_wait: currentRoute.group_wait,
|
||||
group_interval: currentRoute.group_interval,
|
||||
repeat_interval: currentRoute.repeat_interval,
|
||||
};
|
||||
|
||||
contactPointErrors.forEach((error) => {
|
||||
errors.push(error);
|
||||
});
|
||||
|
||||
const isAutogeneratedPolicyRoot = isAutoGeneratedRootAndSimplifiedEnabled(currentRoute);
|
||||
|
||||
// build the menu actions for our policy
|
||||
const dropdownMenuActions: JSX.Element[] = [];
|
||||
const dropdownMenuActions: JSX.Element[] = useCreateDropdownMenuActions(
|
||||
isAutoGenerated,
|
||||
isDefaultPolicy,
|
||||
provisioned,
|
||||
onEditPolicy,
|
||||
currentRoute,
|
||||
toggleShowExportDrawer,
|
||||
onDeletePolicy
|
||||
);
|
||||
|
||||
// check if this policy should be visible. If it's autogenerated and the user is not allowed to see autogenerated
|
||||
// policies then we should not show it. Same if the user is not supported to see autogenerated policies.
|
||||
const hideCurrentPolicy =
|
||||
isAutoGenerated && (!isAllowedToSeeAutogeneratedChunk || !isSupportedToSeeAutogeneratedChunk);
|
||||
|
||||
if (hideCurrentPolicy) {
|
||||
return null;
|
||||
}
|
||||
const isImmutablePolicy = isDefaultPolicy || isAutogeneratedPolicyRoot;
|
||||
// TODO dead branch detection, warnings for all sort of configs that won't work or will never be activated
|
||||
return (
|
||||
<>
|
||||
<Stack direction="column" gap={1.5}>
|
||||
<div
|
||||
className={styles.policyWrapper(hasFocus)}
|
||||
data-testid={isDefaultPolicy ? 'am-root-route-container' : 'am-route-container'}
|
||||
>
|
||||
{/* continueMatching and showMatchesAllLabelsWarning are mutually exclusive so the icons can't overlap */}
|
||||
{continueMatching && <ContinueMatchingIndicator />}
|
||||
{showMatchesAllLabelsWarning && <AllMatchesIndicator />}
|
||||
|
||||
<div className={styles.policyItemWrapper}>
|
||||
<Stack direction="column" gap={1}>
|
||||
{/* Matchers and actions */}
|
||||
<div>
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
{isThisPolicyCollapsible && (
|
||||
<IconButton
|
||||
name={isBranchOpen ? 'angle-down' : 'angle-right'}
|
||||
onClick={toggleBranchOpen}
|
||||
aria-label={isBranchOpen ? 'Collapse' : 'Expand'}
|
||||
/>
|
||||
)}
|
||||
{isImmutablePolicy ? (
|
||||
isAutogeneratedPolicyRoot ? (
|
||||
<AutogeneratedRootIndicator />
|
||||
) : (
|
||||
<DefaultPolicyIndicator />
|
||||
)
|
||||
) : hasMatchers ? (
|
||||
<Matchers matchers={matchers ?? []} />
|
||||
) : (
|
||||
<span className={styles.metadata}>No matchers</span>
|
||||
)}
|
||||
<Spacer />
|
||||
{/* TODO maybe we should move errors to the gutter instead? */}
|
||||
{errors.length > 0 && <Errors errors={errors} />}
|
||||
{provisioned && <ProvisioningBadge />}
|
||||
<Stack direction="row" gap={0.5}>
|
||||
{!isAutoGenerated && !readOnly && (
|
||||
<Authorize actions={[AlertmanagerAction.CreateNotificationPolicy]}>
|
||||
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
size="sm"
|
||||
onClick={() => onAddPolicy(currentRoute)}
|
||||
disabled={provisioned}
|
||||
type="button"
|
||||
>
|
||||
New nested policy
|
||||
</Button>
|
||||
</ConditionalWrap>
|
||||
</Authorize>
|
||||
)}
|
||||
{dropdownMenuActions.length > 0 && (
|
||||
<Dropdown overlay={<Menu>{dropdownMenuActions}</Menu>}>
|
||||
<Button
|
||||
icon="ellipsis-h"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
aria-label="more-actions"
|
||||
data-testid="more-actions"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
{/* Metadata row */}
|
||||
<MetadataRow
|
||||
matchingInstancesPreview={matchingInstancesPreview}
|
||||
numberOfAlertInstances={numberOfAlertInstances}
|
||||
contactPoint={contactPoint}
|
||||
groupBy={groupBy}
|
||||
muteTimings={muteTimings}
|
||||
timingOptions={timingOptions}
|
||||
inheritedProperties={inheritedProperties}
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
receivers={receivers}
|
||||
matchingAlertGroups={matchingAlertGroups}
|
||||
matchers={matchers}
|
||||
isDefaultPolicy={isDefaultPolicy}
|
||||
onShowAlertInstances={onShowAlertInstances}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.childPolicies}>
|
||||
{renderChildPolicies && (
|
||||
<>
|
||||
{childPolicies.map((child) => {
|
||||
const childInheritedProperties = getInheritedProperties(currentRoute, child, inheritedProperties);
|
||||
// This child is autogenerated if it's the autogenerated root or if it's a child of an autogenerated policy.
|
||||
const isThisChildAutoGenerated = isAutoGeneratedRootAndSimplifiedEnabled(child) || isAutoGenerated;
|
||||
/* pass the "readOnly" prop from the parent, because for any child policy , if its parent it's not editable,
|
||||
then the child policy should not be editable either */
|
||||
const isThisChildReadOnly = readOnly || provisioned || isAutoGenerated;
|
||||
|
||||
return (
|
||||
<Policy
|
||||
key={child.id}
|
||||
routeTree={routeTree}
|
||||
currentRoute={child}
|
||||
receivers={receivers}
|
||||
contactPointsState={contactPointsState}
|
||||
readOnly={isThisChildReadOnly}
|
||||
inheritedProperties={childInheritedProperties}
|
||||
onAddPolicy={onAddPolicy}
|
||||
onEditPolicy={onEditPolicy}
|
||||
onDeletePolicy={onDeletePolicy}
|
||||
onShowAlertInstances={onShowAlertInstances}
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
alertGroups={alertGroups}
|
||||
routesMatchingFilters={routesMatchingFilters}
|
||||
matchingInstancesPreview={matchingInstancesPreview}
|
||||
isAutoGenerated={isThisChildAutoGenerated}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{showExportDrawer && <GrafanaPoliciesExporter onClose={toggleShowExportDrawer} />}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
/**
|
||||
* This function returns if the policy should be collapsible or not.
|
||||
* Add here conditions for policies that should be collapsible.
|
||||
*/
|
||||
function useShouldPolicyBeCollapsible(route: RouteWithID): boolean {
|
||||
const childrenCount = route.routes?.length ?? 0;
|
||||
const [isSupportedToSeeAutogeneratedChunk, isAllowedToSeeAutogeneratedChunk] = useAlertmanagerAbility(
|
||||
AlertmanagerAction.ViewAutogeneratedPolicyTree
|
||||
);
|
||||
return (
|
||||
childrenCount > 0 &&
|
||||
isSupportedToSeeAutogeneratedChunk &&
|
||||
isAllowedToSeeAutogeneratedChunk &&
|
||||
isAutoGeneratedRootAndSimplifiedEnabled(route)
|
||||
);
|
||||
}
|
||||
|
||||
interface MetadataRowProps {
|
||||
matchingInstancesPreview: { groupsMap?: Map<string, AlertmanagerGroup[]>; enabled: boolean };
|
||||
numberOfAlertInstances?: number;
|
||||
contactPoint?: string;
|
||||
groupBy?: string[];
|
||||
muteTimings?: string[];
|
||||
timingOptions?: TimingOptions;
|
||||
inheritedProperties?: Partial<InhertitableProperties>;
|
||||
alertManagerSourceName: string;
|
||||
receivers: Receiver[];
|
||||
matchingAlertGroups?: AlertmanagerGroup[];
|
||||
matchers?: ObjectMatcher[];
|
||||
isDefaultPolicy: boolean;
|
||||
onShowAlertInstances: (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[]) => void;
|
||||
}
|
||||
|
||||
function MetadataRow({
|
||||
numberOfAlertInstances,
|
||||
isDefaultPolicy,
|
||||
timingOptions,
|
||||
groupBy,
|
||||
muteTimings = [],
|
||||
matchingInstancesPreview,
|
||||
inheritedProperties,
|
||||
matchingAlertGroups,
|
||||
onShowAlertInstances,
|
||||
matchers,
|
||||
contactPoint,
|
||||
alertManagerSourceName,
|
||||
receivers,
|
||||
}: MetadataRowProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const inheritedGrouping = inheritedProperties && inheritedProperties.group_by;
|
||||
const hasInheritedProperties = inheritedProperties && Object.keys(inheritedProperties).length > 0;
|
||||
|
||||
const noGrouping = isArray(groupBy) && groupBy[0] === '...';
|
||||
const customGrouping = !noGrouping && isArray(groupBy) && groupBy.length > 0;
|
||||
const singleGroup = isDefaultPolicy && isArray(groupBy) && groupBy.length === 0;
|
||||
|
||||
const hasMuteTimings = Boolean(muteTimings.length);
|
||||
|
||||
return (
|
||||
<div className={styles.metadataRow}>
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
{matchingInstancesPreview.enabled && (
|
||||
<MetaText
|
||||
icon="layers-alt"
|
||||
onClick={() => {
|
||||
matchingAlertGroups && onShowAlertInstances(matchingAlertGroups, matchers);
|
||||
}}
|
||||
data-testid="matching-instances"
|
||||
>
|
||||
<Strong>{numberOfAlertInstances ?? '-'}</Strong>
|
||||
<span>{pluralize('instance', numberOfAlertInstances)}</span>
|
||||
</MetaText>
|
||||
)}
|
||||
{contactPoint && (
|
||||
<MetaText icon="at" data-testid="contact-point">
|
||||
<span>Delivered to</span>
|
||||
<ContactPointsHoverDetails
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
receivers={receivers}
|
||||
contactPoint={contactPoint}
|
||||
/>
|
||||
</MetaText>
|
||||
)}
|
||||
{!inheritedGrouping && (
|
||||
<>
|
||||
{customGrouping && (
|
||||
<MetaText icon="layer-group" data-testid="grouping">
|
||||
<span>Grouped by</span>
|
||||
<Strong>{groupBy.join(', ')}</Strong>
|
||||
</MetaText>
|
||||
)}
|
||||
{singleGroup && (
|
||||
<MetaText icon="layer-group">
|
||||
<span>Single group</span>
|
||||
</MetaText>
|
||||
)}
|
||||
{noGrouping && (
|
||||
<MetaText icon="layer-group">
|
||||
<span>Not grouping</span>
|
||||
</MetaText>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{hasMuteTimings && (
|
||||
<MetaText icon="calendar-slash" data-testid="mute-timings">
|
||||
<span>Muted when</span>
|
||||
<MuteTimings timings={muteTimings} alertManagerSourceName={alertManagerSourceName} />
|
||||
</MetaText>
|
||||
)}
|
||||
{timingOptions && (
|
||||
// for the default policy we will also merge the default timings, that way a user can observe what the timing options would be
|
||||
<TimingOptionsMeta
|
||||
timingOptions={isDefaultPolicy ? defaults(timingOptions, TIMING_OPTIONS_DEFAULTS) : timingOptions}
|
||||
/>
|
||||
)}
|
||||
{hasInheritedProperties && (
|
||||
<>
|
||||
<MetaText icon="corner-down-right-alt" data-testid="inherited-properties">
|
||||
<span>Inherited</span>
|
||||
<InheritedProperties properties={inheritedProperties} />
|
||||
</MetaText>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const useCreateDropdownMenuActions = (
|
||||
isAutoGenerated: boolean,
|
||||
isDefaultPolicy: boolean,
|
||||
provisioned: boolean,
|
||||
onEditPolicy: (route: RouteWithID, isDefault?: boolean, readOnly?: boolean) => void,
|
||||
currentRoute: RouteWithID,
|
||||
toggleShowExportDrawer: (nextValue?: any) => void,
|
||||
onDeletePolicy: (route: RouteWithID) => void
|
||||
) => {
|
||||
const [
|
||||
[updatePoliciesSupported, updatePoliciesAllowed],
|
||||
[deletePolicySupported, deletePolicyAllowed],
|
||||
[exportPoliciesSupported, exportPoliciesAllowed],
|
||||
] = useAlertmanagerAbilities([
|
||||
AlertmanagerAction.UpdateNotificationPolicyTree,
|
||||
AlertmanagerAction.DeleteNotificationPolicy,
|
||||
AlertmanagerAction.ExportNotificationPolicies,
|
||||
]);
|
||||
const dropdownMenuActions = [];
|
||||
const showExportAction = exportPoliciesAllowed && exportPoliciesSupported && isDefaultPolicy && !isAutoGenerated;
|
||||
const showEditAction = updatePoliciesSupported && updatePoliciesAllowed;
|
||||
const showDeleteAction = deletePolicySupported && deletePolicyAllowed && !isDefaultPolicy && !isAutoGenerated;
|
||||
|
||||
if (showEditAction) {
|
||||
dropdownMenuActions.push(
|
||||
@ -150,7 +452,7 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
|
||||
<Menu.Item
|
||||
icon="edit"
|
||||
disabled={provisioned}
|
||||
disabled={provisioned || isAutoGenerated}
|
||||
label="Edit"
|
||||
onClick={() => onEditPolicy(currentRoute, isDefaultPolicy)}
|
||||
/>
|
||||
@ -173,7 +475,7 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
<Menu.Item
|
||||
destructive
|
||||
icon="trash-alt"
|
||||
disabled={provisioned}
|
||||
disabled={provisioned || isAutoGenerated}
|
||||
label="Delete"
|
||||
onClick={() => onDeletePolicy(currentRoute)}
|
||||
/>
|
||||
@ -181,167 +483,31 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO dead branch detection, warnings for all sort of configs that won't work or will never be activated
|
||||
return (
|
||||
<Stack direction="column" gap={1.5}>
|
||||
<div
|
||||
className={styles.policyWrapper(hasFocus)}
|
||||
data-testid={isDefaultPolicy ? 'am-root-route-container' : 'am-route-container'}
|
||||
>
|
||||
{/* continueMatching and showMatchesAllLabelsWarning are mutually exclusive so the icons can't overlap */}
|
||||
{continueMatching && <ContinueMatchingIndicator />}
|
||||
{showMatchesAllLabelsWarning && <AllMatchesIndicator />}
|
||||
<div className={styles.policyItemWrapper}>
|
||||
<Stack direction="column" gap={1}>
|
||||
{/* Matchers and actions */}
|
||||
<div>
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
{isDefaultPolicy ? (
|
||||
<DefaultPolicyIndicator />
|
||||
) : hasMatchers ? (
|
||||
<Matchers matchers={matchers ?? []} />
|
||||
) : (
|
||||
<span className={styles.metadata}>No matchers</span>
|
||||
)}
|
||||
<Spacer />
|
||||
{/* TODO maybe we should move errors to the gutter instead? */}
|
||||
{errors.length > 0 && <Errors errors={errors} />}
|
||||
{provisioned && <ProvisioningBadge />}
|
||||
{!readOnly && (
|
||||
<Stack direction="row" gap={0.5}>
|
||||
<Authorize actions={[AlertmanagerAction.CreateNotificationPolicy]}>
|
||||
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
size="sm"
|
||||
onClick={() => onAddPolicy(currentRoute)}
|
||||
disabled={provisioned}
|
||||
type="button"
|
||||
>
|
||||
New nested policy
|
||||
</Button>
|
||||
</ConditionalWrap>
|
||||
</Authorize>
|
||||
{dropdownMenuActions.length > 0 && (
|
||||
<Dropdown overlay={<Menu>{dropdownMenuActions}</Menu>}>
|
||||
<Button
|
||||
icon="ellipsis-h"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
aria-label="more-actions"
|
||||
data-testid="more-actions"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
{/* Metadata row */}
|
||||
<div className={styles.metadataRow}>
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
{matchingInstancesPreview.enabled && (
|
||||
<MetaText
|
||||
icon="layers-alt"
|
||||
onClick={() => {
|
||||
matchingAlertGroups && onShowAlertInstances(matchingAlertGroups, matchers);
|
||||
}}
|
||||
data-testid="matching-instances"
|
||||
>
|
||||
<Strong>{numberOfAlertInstances ?? '-'}</Strong>
|
||||
<span>{pluralize('instance', numberOfAlertInstances)}</span>
|
||||
</MetaText>
|
||||
)}
|
||||
{contactPoint && (
|
||||
<MetaText icon="at" data-testid="contact-point">
|
||||
<span>Delivered to</span>
|
||||
<ContactPointsHoverDetails
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
receivers={receivers}
|
||||
contactPoint={contactPoint}
|
||||
/>
|
||||
</MetaText>
|
||||
)}
|
||||
{!inheritedGrouping && (
|
||||
<>
|
||||
{customGrouping && (
|
||||
<MetaText icon="layer-group" data-testid="grouping">
|
||||
<span>Grouped by</span>
|
||||
<Strong>{groupBy.join(', ')}</Strong>
|
||||
</MetaText>
|
||||
)}
|
||||
{singleGroup && (
|
||||
<MetaText icon="layer-group">
|
||||
<span>Single group</span>
|
||||
</MetaText>
|
||||
)}
|
||||
{noGrouping && (
|
||||
<MetaText icon="layer-group">
|
||||
<span>Not grouping</span>
|
||||
</MetaText>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{hasMuteTimings && (
|
||||
<MetaText icon="calendar-slash" data-testid="mute-timings">
|
||||
<span>Muted when</span>
|
||||
<MuteTimings timings={muteTimings} alertManagerSourceName={alertManagerSourceName} />
|
||||
</MetaText>
|
||||
)}
|
||||
{timingOptions && (
|
||||
// for the default policy we will also merge the default timings, that way a user can observe what the timing options would be
|
||||
<TimingOptionsMeta
|
||||
timingOptions={isDefaultPolicy ? defaults(timingOptions, TIMING_OPTIONS_DEFAULTS) : timingOptions}
|
||||
/>
|
||||
)}
|
||||
{hasInheritedProperties && (
|
||||
<>
|
||||
<MetaText icon="corner-down-right-alt" data-testid="inherited-properties">
|
||||
<span>Inherited</span>
|
||||
<InheritedProperties properties={inheritedProperties} />
|
||||
</MetaText>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.childPolicies}>
|
||||
{/* pass the "readOnly" prop from the parent, because if you can't edit the parent you can't edit children */}
|
||||
{childPolicies.map((child) => {
|
||||
const childInheritedProperties = getInheritedProperties(currentRoute, child, inheritedProperties);
|
||||
|
||||
return (
|
||||
<Policy
|
||||
key={uniqueId()}
|
||||
routeTree={routeTree}
|
||||
currentRoute={child}
|
||||
receivers={receivers}
|
||||
contactPointsState={contactPointsState}
|
||||
readOnly={readOnly || provisioned}
|
||||
inheritedProperties={childInheritedProperties}
|
||||
onAddPolicy={onAddPolicy}
|
||||
onEditPolicy={onEditPolicy}
|
||||
onDeletePolicy={onDeletePolicy}
|
||||
onShowAlertInstances={onShowAlertInstances}
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
alertGroups={alertGroups}
|
||||
routesMatchingFilters={routesMatchingFilters}
|
||||
matchingInstancesPreview={matchingInstancesPreview}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{showExportDrawer && <GrafanaPoliciesExporter onClose={toggleShowExportDrawer} />}
|
||||
</Stack>
|
||||
);
|
||||
return dropdownMenuActions;
|
||||
};
|
||||
|
||||
export const AUTOGENERATED_ROOT_LABEL_NAME = '__grafana_autogenerated__';
|
||||
|
||||
export function isAutoGeneratedRootAndSimplifiedEnabled(route: RouteWithID) {
|
||||
const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false;
|
||||
if (!simplifiedRoutingToggleEnabled) {
|
||||
return false;
|
||||
}
|
||||
if (!route.object_matchers) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
route.object_matchers.some((objectMatcher) => {
|
||||
return (
|
||||
objectMatcher[0] === AUTOGENERATED_ROOT_LABEL_NAME &&
|
||||
objectMatcher[1] === MatcherOperator.equal &&
|
||||
objectMatcher[2] === 'true'
|
||||
);
|
||||
}) ?? false
|
||||
);
|
||||
// return simplifiedRoutingToggleEnabled && route.receiver === 'contact_point_5';
|
||||
}
|
||||
|
||||
const ProvisionedTooltip = (children: ReactNode) => (
|
||||
<Tooltip content="Provisioned items cannot be edited in the UI" placement="top">
|
||||
<span>{children}</span>
|
||||
@ -388,7 +554,7 @@ const AllMatchesIndicator: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultPolicyIndicator: FC = () => {
|
||||
function DefaultPolicyIndicator() {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<>
|
||||
@ -398,7 +564,11 @@ const DefaultPolicyIndicator: FC = () => {
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function AutogeneratedRootIndicator() {
|
||||
return <strong> Auto-generated policies</strong>;
|
||||
}
|
||||
|
||||
const InheritedProperties: FC<{ properties: InhertitableProperties }> = ({ properties }) => (
|
||||
<HoverCard
|
||||
@ -627,84 +797,69 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
const { color, borderColor } = getTagColorsFromName(label);
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
color: #fff;
|
||||
background: ${color};
|
||||
padding: ${theme.spacing(0.33)} ${theme.spacing(0.66)};
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
|
||||
border: solid 1px ${borderColor};
|
||||
border-radius: ${theme.shape.radius.default};
|
||||
`,
|
||||
wrapper: css({
|
||||
color: '#fff',
|
||||
background: color,
|
||||
padding: `${theme.spacing(0.33)} ${theme.spacing(0.66)}`,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
border: `solid 1px ${borderColor}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
}),
|
||||
};
|
||||
},
|
||||
childPolicies: css`
|
||||
margin-left: ${theme.spacing(4)};
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: calc(100% - 10px);
|
||||
|
||||
border-left: solid 1px ${theme.colors.border.weak};
|
||||
|
||||
margin-top: 0;
|
||||
margin-left: -20px;
|
||||
}
|
||||
`,
|
||||
policyItemWrapper: css`
|
||||
padding: ${theme.spacing(1.5)};
|
||||
`,
|
||||
metadataRow: css`
|
||||
background: ${theme.colors.background.secondary};
|
||||
|
||||
border-bottom-left-radius: ${theme.shape.borderRadius(2)};
|
||||
border-bottom-right-radius: ${theme.shape.borderRadius(2)};
|
||||
`,
|
||||
policyWrapper: (hasFocus = false) => css`
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: ${theme.colors.background.secondary};
|
||||
|
||||
border-radius: ${theme.shape.radius.default};
|
||||
border: solid 1px ${theme.colors.border.weak};
|
||||
|
||||
${hasFocus &&
|
||||
css`
|
||||
border-color: ${theme.colors.primary.border};
|
||||
`}
|
||||
`,
|
||||
metadata: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
font-weight: ${theme.typography.bodySmall.fontWeight};
|
||||
`,
|
||||
break: css`
|
||||
width: 100%;
|
||||
height: 0;
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
gutterIcon: css`
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
transform: translateY(50%);
|
||||
left: -${theme.spacing(4)};
|
||||
|
||||
color: ${theme.colors.text.secondary};
|
||||
background: ${theme.colors.background.primary};
|
||||
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
text-align: center;
|
||||
|
||||
border: solid 1px ${theme.colors.border.weak};
|
||||
border-radius: ${theme.shape.radius.default};
|
||||
|
||||
padding: 0;
|
||||
`,
|
||||
childPolicies: css({
|
||||
marginLeft: theme.spacing(4),
|
||||
position: 'relative',
|
||||
'&:before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
height: 'calc(100% - 10px)',
|
||||
borderLeft: `solid 1px ${theme.colors.border.weak}`,
|
||||
marginTop: 0,
|
||||
marginLeft: '-20px',
|
||||
},
|
||||
}),
|
||||
policyItemWrapper: css({
|
||||
padding: theme.spacing(1.5),
|
||||
}),
|
||||
metadataRow: css({
|
||||
background: theme.colors.background.secondary,
|
||||
borderBottomLeftRadius: theme.shape.borderRadius(2),
|
||||
borderBottomRightRadius: theme.shape.borderRadius(2),
|
||||
}),
|
||||
policyWrapper: (hasFocus = false) =>
|
||||
css({
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
background: theme.colors.background.secondary,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
border: `solid 1px ${theme.colors.border.weak}`,
|
||||
...(hasFocus && { borderColor: theme.colors.primary.border }),
|
||||
}),
|
||||
metadata: css({
|
||||
color: theme.colors.text.secondary,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
fontWeight: theme.typography.bodySmall.fontWeight,
|
||||
}),
|
||||
break: css({
|
||||
width: '100%',
|
||||
height: 0,
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
gutterIcon: css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
transform: 'translateY(50%)',
|
||||
left: `-${theme.spacing(4)}`,
|
||||
color: theme.colors.text.secondary,
|
||||
background: theme.colors.background.primary,
|
||||
width: '25px',
|
||||
height: '25px',
|
||||
textAlign: 'center',
|
||||
border: `solid 1px ${theme.colors.border.weak}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
padding: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
export { Policy };
|
||||
|
@ -78,6 +78,10 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"view-autogenerated-policy-tree": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"view-contact-point": [
|
||||
true,
|
||||
false,
|
||||
@ -183,6 +187,10 @@ exports[`alertmanager abilities should report everything except exporting for Mi
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"view-autogenerated-policy-tree": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"view-contact-point": [
|
||||
true,
|
||||
true,
|
||||
@ -288,6 +296,10 @@ exports[`alertmanager abilities should report everything is supported for builti
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"view-autogenerated-policy-tree": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"view-contact-point": [
|
||||
true,
|
||||
true,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { contextSrv as ctx } from 'app/core/services/context_srv';
|
||||
import { contextSrv, contextSrv as ctx } from 'app/core/services/context_srv';
|
||||
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
|
||||
@ -45,6 +45,7 @@ export enum AlertmanagerAction {
|
||||
UpdateNotificationPolicyTree = 'update-notification-policy-tree',
|
||||
DeleteNotificationPolicy = 'delete-notification-policy',
|
||||
ExportNotificationPolicies = 'export-notification-policies',
|
||||
ViewAutogeneratedPolicyTree = 'view-autogenerated-policy-tree',
|
||||
|
||||
// silences – these cannot be deleted only "expired" (updated)
|
||||
CreateSilence = 'create-silence',
|
||||
@ -190,6 +191,9 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
|
||||
const notificationsPermissions = getNotificationsPermissions(selectedAlertmanager!);
|
||||
const instancePermissions = getInstancesPermissions(selectedAlertmanager!);
|
||||
|
||||
//we need to know user role to determine if they can view autogenerated policy tree
|
||||
const isAdmin = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin;
|
||||
|
||||
// list out all of the abilities, and if the user has permissions to perform them
|
||||
const abilities: Abilities<AlertmanagerAction> = {
|
||||
// -- configuration --
|
||||
@ -226,6 +230,7 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
|
||||
isGrafanaFlavoredAlertmanager,
|
||||
notificationsPermissions.provisioning.readSecrets
|
||||
),
|
||||
[AlertmanagerAction.ViewAutogeneratedPolicyTree]: [isGrafanaFlavoredAlertmanager, isAdmin],
|
||||
// -- silences --
|
||||
// for now, all supported Alertmanager flavors have API endpoints for managing silences
|
||||
[AlertmanagerAction.CreateSilence]: toAbility(AlwaysSupported, instancePermissions.create),
|
||||
|
Loading…
Reference in New Issue
Block a user