AmRoutesTable.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import { intersectionWith, isEqual } from 'lodash';
  2. import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
  3. import { Button, ConfirmModal, HorizontalGroup, IconButton } from '@grafana/ui';
  4. import { contextSrv } from 'app/core/services/context_srv';
  5. import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
  6. import { getNotificationsPermissions } from '../../utils/access-control';
  7. import { matcherFieldToMatcher, parseMatchers } from '../../utils/alertmanager';
  8. import { prepareItems } from '../../utils/dynamicTable';
  9. import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
  10. import { EmptyArea } from '../EmptyArea';
  11. import { Matchers } from '../silences/Matchers';
  12. import { AmRoutesExpandedForm } from './AmRoutesExpandedForm';
  13. import { AmRoutesExpandedRead } from './AmRoutesExpandedRead';
  14. export interface AmRoutesTableProps {
  15. isAddMode: boolean;
  16. onChange: (routes: FormAmRoute[]) => void;
  17. onCancelAdd: () => void;
  18. receivers: AmRouteReceiver[];
  19. routes: FormAmRoute[];
  20. filters?: { queryString?: string; contactPoint?: string };
  21. readOnly?: boolean;
  22. alertManagerSourceName: string;
  23. }
  24. type RouteTableColumnProps = DynamicTableColumnProps<FormAmRoute>;
  25. type RouteTableItemProps = DynamicTableItemProps<FormAmRoute>;
  26. export const getFilteredRoutes = (routes: FormAmRoute[], labelMatcherQuery?: string, contactPointQuery?: string) => {
  27. const matchers = parseMatchers(labelMatcherQuery ?? '');
  28. let filteredRoutes = routes;
  29. if (matchers.length) {
  30. filteredRoutes = routes.filter((route) => {
  31. const routeMatchers = route.object_matchers.map(matcherFieldToMatcher);
  32. return intersectionWith(routeMatchers, matchers, isEqual).length > 0;
  33. });
  34. }
  35. if (contactPointQuery && contactPointQuery.length > 0) {
  36. filteredRoutes = filteredRoutes.filter((route) =>
  37. route.receiver.toLowerCase().includes(contactPointQuery.toLowerCase())
  38. );
  39. }
  40. return filteredRoutes;
  41. };
  42. export const updatedRoute = (routes: FormAmRoute[], updatedRoute: FormAmRoute): FormAmRoute[] => {
  43. const newRoutes = [...routes];
  44. const editIndex = newRoutes.findIndex((route) => route.id === updatedRoute.id);
  45. if (editIndex >= 0) {
  46. newRoutes[editIndex] = {
  47. ...newRoutes[editIndex],
  48. ...updatedRoute,
  49. };
  50. }
  51. return newRoutes;
  52. };
  53. export const deleteRoute = (routes: FormAmRoute[], routeId: string): FormAmRoute[] => {
  54. return routes.filter((route) => route.id !== routeId);
  55. };
  56. export const AmRoutesTable: FC<AmRoutesTableProps> = ({
  57. isAddMode,
  58. onCancelAdd,
  59. onChange,
  60. receivers,
  61. routes,
  62. filters,
  63. readOnly = false,
  64. alertManagerSourceName,
  65. }) => {
  66. const [editMode, setEditMode] = useState(false);
  67. const [deletingRouteId, setDeletingRouteId] = useState<string | undefined>(undefined);
  68. const [expandedId, setExpandedId] = useState<string | number>();
  69. const permissions = getNotificationsPermissions(alertManagerSourceName);
  70. const canEditRoutes = contextSrv.hasPermission(permissions.update);
  71. const canDeleteRoutes = contextSrv.hasPermission(permissions.delete);
  72. const showActions = !readOnly && (canEditRoutes || canDeleteRoutes);
  73. const expandItem = useCallback((item: RouteTableItemProps) => setExpandedId(item.id), []);
  74. const collapseItem = useCallback(() => setExpandedId(undefined), []);
  75. const cols: RouteTableColumnProps[] = [
  76. {
  77. id: 'matchingCriteria',
  78. label: 'Matching labels',
  79. // eslint-disable-next-line react/display-name
  80. renderCell: (item) => {
  81. return item.data.object_matchers.length ? (
  82. <Matchers matchers={item.data.object_matchers.map(matcherFieldToMatcher)} />
  83. ) : (
  84. <span>Matches all alert instances</span>
  85. );
  86. },
  87. size: 10,
  88. },
  89. {
  90. id: 'groupBy',
  91. label: 'Group by',
  92. renderCell: (item) => (item.data.overrideGrouping && item.data.groupBy.join(', ')) || '-',
  93. size: 5,
  94. },
  95. {
  96. id: 'receiverChannel',
  97. label: 'Contact point',
  98. renderCell: (item) => item.data.receiver || '-',
  99. size: 5,
  100. },
  101. {
  102. id: 'muteTimings',
  103. label: 'Mute timings',
  104. renderCell: (item) => item.data.muteTimeIntervals.join(', ') || '-',
  105. size: 5,
  106. },
  107. ...(!showActions
  108. ? []
  109. : [
  110. {
  111. id: 'actions',
  112. label: 'Actions',
  113. // eslint-disable-next-line react/display-name
  114. renderCell: (item) => {
  115. if (item.renderExpandedContent) {
  116. return null;
  117. }
  118. const expandWithCustomContent = () => {
  119. expandItem(item);
  120. setEditMode(true);
  121. };
  122. return (
  123. <>
  124. <HorizontalGroup>
  125. <Button
  126. aria-label="Edit route"
  127. icon="pen"
  128. onClick={expandWithCustomContent}
  129. size="sm"
  130. type="button"
  131. variant="secondary"
  132. >
  133. Edit
  134. </Button>
  135. <IconButton
  136. aria-label="Delete route"
  137. name="trash-alt"
  138. onClick={() => {
  139. setDeletingRouteId(item.data.id);
  140. }}
  141. type="button"
  142. />
  143. </HorizontalGroup>
  144. </>
  145. );
  146. },
  147. size: '100px',
  148. } as RouteTableColumnProps,
  149. ]),
  150. ];
  151. const filteredRoutes = useMemo(
  152. () => getFilteredRoutes(routes, filters?.queryString, filters?.contactPoint),
  153. [routes, filters]
  154. );
  155. const dynamicTableRoutes = useMemo(
  156. () => prepareItems(isAddMode ? routes : filteredRoutes),
  157. [isAddMode, routes, filteredRoutes]
  158. );
  159. // expand the last item when adding or reset when the length changed
  160. useEffect(() => {
  161. if (isAddMode && dynamicTableRoutes.length) {
  162. setExpandedId(dynamicTableRoutes[dynamicTableRoutes.length - 1].id);
  163. }
  164. if (!isAddMode && dynamicTableRoutes.length) {
  165. setExpandedId(undefined);
  166. }
  167. }, [isAddMode, dynamicTableRoutes]);
  168. if (routes.length > 0 && filteredRoutes.length === 0) {
  169. return (
  170. <EmptyArea>
  171. <p>No policies found</p>
  172. </EmptyArea>
  173. );
  174. }
  175. return (
  176. <>
  177. <DynamicTable
  178. cols={cols}
  179. isExpandable={true}
  180. items={dynamicTableRoutes}
  181. testIdGenerator={() => 'am-routes-row'}
  182. onCollapse={collapseItem}
  183. onExpand={expandItem}
  184. isExpanded={(item) => expandedId === item.id}
  185. renderExpandedContent={(item: RouteTableItemProps) =>
  186. isAddMode || editMode ? (
  187. <AmRoutesExpandedForm
  188. onCancel={() => {
  189. if (isAddMode) {
  190. onCancelAdd();
  191. }
  192. setEditMode(false);
  193. }}
  194. onSave={(data) => {
  195. const newRoutes = updatedRoute(routes, data);
  196. setEditMode(false);
  197. onChange(newRoutes);
  198. }}
  199. receivers={receivers}
  200. routes={item.data}
  201. />
  202. ) : (
  203. <AmRoutesExpandedRead
  204. onChange={(data) => {
  205. const newRoutes = updatedRoute(routes, data);
  206. onChange(newRoutes);
  207. }}
  208. receivers={receivers}
  209. routes={item.data}
  210. readOnly={readOnly}
  211. alertManagerSourceName={alertManagerSourceName}
  212. />
  213. )
  214. }
  215. />
  216. <ConfirmModal
  217. isOpen={!!deletingRouteId}
  218. title="Delete notification policy"
  219. body="Deleting this notification policy will permanently remove it. Are you sure you want to delete this policy?"
  220. confirmText="Yes, delete"
  221. icon="exclamation-triangle"
  222. onConfirm={() => {
  223. if (deletingRouteId) {
  224. const newRoutes = deleteRoute(routes, deletingRouteId);
  225. onChange(newRoutes);
  226. setDeletingRouteId(undefined);
  227. }
  228. }}
  229. onDismiss={() => setDeletingRouteId(undefined)}
  230. />
  231. </>
  232. );
  233. };