123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- import { intersectionWith, isEqual } from 'lodash';
- import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
- import { Button, ConfirmModal, HorizontalGroup, IconButton } from '@grafana/ui';
- import { contextSrv } from 'app/core/services/context_srv';
- import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
- import { getNotificationsPermissions } from '../../utils/access-control';
- import { matcherFieldToMatcher, parseMatchers } from '../../utils/alertmanager';
- import { prepareItems } from '../../utils/dynamicTable';
- import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
- import { EmptyArea } from '../EmptyArea';
- import { Matchers } from '../silences/Matchers';
- import { AmRoutesExpandedForm } from './AmRoutesExpandedForm';
- import { AmRoutesExpandedRead } from './AmRoutesExpandedRead';
- export interface AmRoutesTableProps {
- isAddMode: boolean;
- onChange: (routes: FormAmRoute[]) => void;
- onCancelAdd: () => void;
- receivers: AmRouteReceiver[];
- routes: FormAmRoute[];
- filters?: { queryString?: string; contactPoint?: string };
- readOnly?: boolean;
- alertManagerSourceName: string;
- }
- type RouteTableColumnProps = DynamicTableColumnProps<FormAmRoute>;
- type RouteTableItemProps = DynamicTableItemProps<FormAmRoute>;
- export const getFilteredRoutes = (routes: FormAmRoute[], labelMatcherQuery?: string, contactPointQuery?: string) => {
- const matchers = parseMatchers(labelMatcherQuery ?? '');
- let filteredRoutes = routes;
- if (matchers.length) {
- filteredRoutes = routes.filter((route) => {
- const routeMatchers = route.object_matchers.map(matcherFieldToMatcher);
- return intersectionWith(routeMatchers, matchers, isEqual).length > 0;
- });
- }
- if (contactPointQuery && contactPointQuery.length > 0) {
- filteredRoutes = filteredRoutes.filter((route) =>
- route.receiver.toLowerCase().includes(contactPointQuery.toLowerCase())
- );
- }
- return filteredRoutes;
- };
- export const updatedRoute = (routes: FormAmRoute[], updatedRoute: FormAmRoute): FormAmRoute[] => {
- const newRoutes = [...routes];
- const editIndex = newRoutes.findIndex((route) => route.id === updatedRoute.id);
- if (editIndex >= 0) {
- newRoutes[editIndex] = {
- ...newRoutes[editIndex],
- ...updatedRoute,
- };
- }
- return newRoutes;
- };
- export const deleteRoute = (routes: FormAmRoute[], routeId: string): FormAmRoute[] => {
- return routes.filter((route) => route.id !== routeId);
- };
- export const AmRoutesTable: FC<AmRoutesTableProps> = ({
- isAddMode,
- onCancelAdd,
- onChange,
- receivers,
- routes,
- filters,
- readOnly = false,
- alertManagerSourceName,
- }) => {
- const [editMode, setEditMode] = useState(false);
- const [deletingRouteId, setDeletingRouteId] = useState<string | undefined>(undefined);
- const [expandedId, setExpandedId] = useState<string | number>();
- const permissions = getNotificationsPermissions(alertManagerSourceName);
- const canEditRoutes = contextSrv.hasPermission(permissions.update);
- const canDeleteRoutes = contextSrv.hasPermission(permissions.delete);
- const showActions = !readOnly && (canEditRoutes || canDeleteRoutes);
- const expandItem = useCallback((item: RouteTableItemProps) => setExpandedId(item.id), []);
- const collapseItem = useCallback(() => setExpandedId(undefined), []);
- const cols: RouteTableColumnProps[] = [
- {
- id: 'matchingCriteria',
- label: 'Matching labels',
- // eslint-disable-next-line react/display-name
- renderCell: (item) => {
- return item.data.object_matchers.length ? (
- <Matchers matchers={item.data.object_matchers.map(matcherFieldToMatcher)} />
- ) : (
- <span>Matches all alert instances</span>
- );
- },
- size: 10,
- },
- {
- id: 'groupBy',
- label: 'Group by',
- renderCell: (item) => (item.data.overrideGrouping && item.data.groupBy.join(', ')) || '-',
- size: 5,
- },
- {
- id: 'receiverChannel',
- label: 'Contact point',
- renderCell: (item) => item.data.receiver || '-',
- size: 5,
- },
- {
- id: 'muteTimings',
- label: 'Mute timings',
- renderCell: (item) => item.data.muteTimeIntervals.join(', ') || '-',
- size: 5,
- },
- ...(!showActions
- ? []
- : [
- {
- id: 'actions',
- label: 'Actions',
- // eslint-disable-next-line react/display-name
- renderCell: (item) => {
- if (item.renderExpandedContent) {
- return null;
- }
- const expandWithCustomContent = () => {
- expandItem(item);
- setEditMode(true);
- };
- return (
- <>
- <HorizontalGroup>
- <Button
- aria-label="Edit route"
- icon="pen"
- onClick={expandWithCustomContent}
- size="sm"
- type="button"
- variant="secondary"
- >
- Edit
- </Button>
- <IconButton
- aria-label="Delete route"
- name="trash-alt"
- onClick={() => {
- setDeletingRouteId(item.data.id);
- }}
- type="button"
- />
- </HorizontalGroup>
- </>
- );
- },
- size: '100px',
- } as RouteTableColumnProps,
- ]),
- ];
- const filteredRoutes = useMemo(
- () => getFilteredRoutes(routes, filters?.queryString, filters?.contactPoint),
- [routes, filters]
- );
- const dynamicTableRoutes = useMemo(
- () => prepareItems(isAddMode ? routes : filteredRoutes),
- [isAddMode, routes, filteredRoutes]
- );
- // expand the last item when adding or reset when the length changed
- useEffect(() => {
- if (isAddMode && dynamicTableRoutes.length) {
- setExpandedId(dynamicTableRoutes[dynamicTableRoutes.length - 1].id);
- }
- if (!isAddMode && dynamicTableRoutes.length) {
- setExpandedId(undefined);
- }
- }, [isAddMode, dynamicTableRoutes]);
- if (routes.length > 0 && filteredRoutes.length === 0) {
- return (
- <EmptyArea>
- <p>No policies found</p>
- </EmptyArea>
- );
- }
- return (
- <>
- <DynamicTable
- cols={cols}
- isExpandable={true}
- items={dynamicTableRoutes}
- testIdGenerator={() => 'am-routes-row'}
- onCollapse={collapseItem}
- onExpand={expandItem}
- isExpanded={(item) => expandedId === item.id}
- renderExpandedContent={(item: RouteTableItemProps) =>
- isAddMode || editMode ? (
- <AmRoutesExpandedForm
- onCancel={() => {
- if (isAddMode) {
- onCancelAdd();
- }
- setEditMode(false);
- }}
- onSave={(data) => {
- const newRoutes = updatedRoute(routes, data);
- setEditMode(false);
- onChange(newRoutes);
- }}
- receivers={receivers}
- routes={item.data}
- />
- ) : (
- <AmRoutesExpandedRead
- onChange={(data) => {
- const newRoutes = updatedRoute(routes, data);
- onChange(newRoutes);
- }}
- receivers={receivers}
- routes={item.data}
- readOnly={readOnly}
- alertManagerSourceName={alertManagerSourceName}
- />
- )
- }
- />
- <ConfirmModal
- isOpen={!!deletingRouteId}
- title="Delete notification policy"
- body="Deleting this notification policy will permanently remove it. Are you sure you want to delete this policy?"
- confirmText="Yes, delete"
- icon="exclamation-triangle"
- onConfirm={() => {
- if (deletingRouteId) {
- const newRoutes = deleteRoute(routes, deletingRouteId);
- onChange(newRoutes);
- setDeletingRouteId(undefined);
- }
- }}
- onDismiss={() => setDeletingRouteId(undefined)}
- />
- </>
- );
- };
|