AmSpecificRouting.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import { css } from '@emotion/css';
  2. import React, { FC, useState } from 'react';
  3. import { useDebounce } from 'react-use';
  4. import { GrafanaTheme2 } from '@grafana/data';
  5. import { Button, Icon, Input, Label, useStyles2 } from '@grafana/ui';
  6. import { contextSrv } from 'app/core/services/context_srv';
  7. import { Authorize } from '../../components/Authorize';
  8. import { useURLSearchParams } from '../../hooks/useURLSearchParams';
  9. import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
  10. import { getNotificationsPermissions } from '../../utils/access-control';
  11. import { emptyArrayFieldMatcher, emptyRoute } from '../../utils/amroutes';
  12. import { getNotificationPoliciesFilters } from '../../utils/misc';
  13. import { EmptyArea } from '../EmptyArea';
  14. import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
  15. import { MatcherFilter } from '../alert-groups/MatcherFilter';
  16. import { AmRoutesTable } from './AmRoutesTable';
  17. export interface AmSpecificRoutingProps {
  18. alertManagerSourceName: string;
  19. onChange: (routes: FormAmRoute) => void;
  20. onRootRouteEdit: () => void;
  21. receivers: AmRouteReceiver[];
  22. routes: FormAmRoute;
  23. readOnly?: boolean;
  24. }
  25. interface Filters {
  26. queryString?: string;
  27. contactPoint?: string;
  28. }
  29. export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
  30. alertManagerSourceName,
  31. onChange,
  32. onRootRouteEdit,
  33. receivers,
  34. routes,
  35. readOnly = false,
  36. }) => {
  37. const [actualRoutes, setActualRoutes] = useState([...routes.routes]);
  38. const [isAddMode, setIsAddMode] = useState(false);
  39. const permissions = getNotificationsPermissions(alertManagerSourceName);
  40. const canCreateNotifications = contextSrv.hasPermission(permissions.create);
  41. const [searchParams, setSearchParams] = useURLSearchParams();
  42. const { queryString, contactPoint } = getNotificationPoliciesFilters(searchParams);
  43. const [filters, setFilters] = useState<Filters>({ queryString, contactPoint });
  44. useDebounce(
  45. () => {
  46. setSearchParams({ queryString: filters.queryString, contactPoint: filters.contactPoint });
  47. },
  48. 400,
  49. [filters]
  50. );
  51. const styles = useStyles2(getStyles);
  52. const clearFilters = () => {
  53. setFilters({ queryString: undefined, contactPoint: undefined });
  54. setSearchParams({ queryString: undefined, contactPoint: undefined });
  55. };
  56. const addNewRoute = () => {
  57. clearFilters();
  58. setIsAddMode(true);
  59. setActualRoutes(() => [
  60. ...routes.routes,
  61. {
  62. ...emptyRoute,
  63. matchers: [emptyArrayFieldMatcher],
  64. },
  65. ]);
  66. };
  67. const onCancelAdd = () => {
  68. setIsAddMode(false);
  69. setActualRoutes([...routes.routes]);
  70. };
  71. const onTableRouteChange = (newRoutes: FormAmRoute[]): void => {
  72. onChange({
  73. ...routes,
  74. routes: newRoutes,
  75. });
  76. if (isAddMode) {
  77. setIsAddMode(false);
  78. }
  79. };
  80. return (
  81. <div className={styles.container}>
  82. <h5>Specific routing</h5>
  83. <p>Send specific alerts to chosen contact points, based on matching criteria</p>
  84. {!routes.receiver ? (
  85. readOnly ? (
  86. <EmptyArea>
  87. <p>There is no default contact point configured for the root route.</p>
  88. </EmptyArea>
  89. ) : (
  90. <EmptyAreaWithCTA
  91. buttonIcon="rocket"
  92. buttonLabel="Set a default contact point"
  93. onButtonClick={onRootRouteEdit}
  94. text="You haven't set a default contact point for the root route yet."
  95. showButton={canCreateNotifications}
  96. />
  97. )
  98. ) : actualRoutes.length > 0 ? (
  99. <>
  100. <div>
  101. {!isAddMode && (
  102. <div className={styles.searchContainer}>
  103. <MatcherFilter
  104. onFilterChange={(filter) =>
  105. setFilters((currentFilters) => ({ ...currentFilters, queryString: filter }))
  106. }
  107. queryString={filters.queryString ?? ''}
  108. className={styles.filterInput}
  109. />
  110. <div className={styles.filterInput}>
  111. <Label>Search by contact point</Label>
  112. <Input
  113. onChange={({ currentTarget }) =>
  114. setFilters((currentFilters) => ({ ...currentFilters, contactPoint: currentTarget.value }))
  115. }
  116. value={filters.contactPoint ?? ''}
  117. placeholder="Search by contact point"
  118. data-testid="search-query-input"
  119. prefix={<Icon name={'search'} />}
  120. />
  121. </div>
  122. {(queryString || contactPoint) && (
  123. <Button variant="secondary" icon="times" onClick={clearFilters} className={styles.clearFilterBtn}>
  124. Clear filters
  125. </Button>
  126. )}
  127. </div>
  128. )}
  129. {!isAddMode && !readOnly && (
  130. <Authorize actions={[permissions.create]}>
  131. <div className={styles.addMatcherBtnRow}>
  132. <Button className={styles.addMatcherBtn} icon="plus" onClick={addNewRoute} type="button">
  133. New policy
  134. </Button>
  135. </div>
  136. </Authorize>
  137. )}
  138. </div>
  139. <AmRoutesTable
  140. isAddMode={isAddMode}
  141. readOnly={readOnly}
  142. onCancelAdd={onCancelAdd}
  143. onChange={onTableRouteChange}
  144. receivers={receivers}
  145. routes={actualRoutes}
  146. filters={{ queryString, contactPoint }}
  147. alertManagerSourceName={alertManagerSourceName}
  148. />
  149. </>
  150. ) : readOnly ? (
  151. <EmptyArea>
  152. <p>There are no specific policies configured.</p>
  153. </EmptyArea>
  154. ) : (
  155. <EmptyAreaWithCTA
  156. buttonIcon="plus"
  157. buttonLabel="New specific policy"
  158. onButtonClick={addNewRoute}
  159. text="You haven't created any specific policies yet."
  160. showButton={canCreateNotifications}
  161. />
  162. )}
  163. </div>
  164. );
  165. };
  166. const getStyles = (theme: GrafanaTheme2) => {
  167. return {
  168. container: css`
  169. display: flex;
  170. flex-flow: column wrap;
  171. `,
  172. searchContainer: css`
  173. display: flex;
  174. flex-flow: row nowrap;
  175. padding-bottom: ${theme.spacing(2)};
  176. border-bottom: 1px solid ${theme.colors.border.strong};
  177. `,
  178. clearFilterBtn: css`
  179. align-self: flex-end;
  180. margin-left: ${theme.spacing(1)};
  181. `,
  182. filterInput: css`
  183. width: 340px;
  184. & + & {
  185. margin-left: ${theme.spacing(1)};
  186. }
  187. `,
  188. addMatcherBtnRow: css`
  189. display: flex;
  190. flex-flow: column nowrap;
  191. padding: ${theme.spacing(2)} 0;
  192. `,
  193. addMatcherBtn: css`
  194. align-self: flex-end;
  195. `,
  196. };
  197. };