123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- import { css } from '@emotion/css';
- import React, { FC, useState } from 'react';
- import { useDebounce } from 'react-use';
- import { GrafanaTheme2 } from '@grafana/data';
- import { Button, Icon, Input, Label, useStyles2 } from '@grafana/ui';
- import { contextSrv } from 'app/core/services/context_srv';
- import { Authorize } from '../../components/Authorize';
- import { useURLSearchParams } from '../../hooks/useURLSearchParams';
- import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
- import { getNotificationsPermissions } from '../../utils/access-control';
- import { emptyArrayFieldMatcher, emptyRoute } from '../../utils/amroutes';
- import { getNotificationPoliciesFilters } from '../../utils/misc';
- import { EmptyArea } from '../EmptyArea';
- import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
- import { MatcherFilter } from '../alert-groups/MatcherFilter';
- import { AmRoutesTable } from './AmRoutesTable';
- export interface AmSpecificRoutingProps {
- alertManagerSourceName: string;
- onChange: (routes: FormAmRoute) => void;
- onRootRouteEdit: () => void;
- receivers: AmRouteReceiver[];
- routes: FormAmRoute;
- readOnly?: boolean;
- }
- interface Filters {
- queryString?: string;
- contactPoint?: string;
- }
- export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
- alertManagerSourceName,
- onChange,
- onRootRouteEdit,
- receivers,
- routes,
- readOnly = false,
- }) => {
- const [actualRoutes, setActualRoutes] = useState([...routes.routes]);
- const [isAddMode, setIsAddMode] = useState(false);
- const permissions = getNotificationsPermissions(alertManagerSourceName);
- const canCreateNotifications = contextSrv.hasPermission(permissions.create);
- const [searchParams, setSearchParams] = useURLSearchParams();
- const { queryString, contactPoint } = getNotificationPoliciesFilters(searchParams);
- const [filters, setFilters] = useState<Filters>({ queryString, contactPoint });
- useDebounce(
- () => {
- setSearchParams({ queryString: filters.queryString, contactPoint: filters.contactPoint });
- },
- 400,
- [filters]
- );
- const styles = useStyles2(getStyles);
- const clearFilters = () => {
- setFilters({ queryString: undefined, contactPoint: undefined });
- setSearchParams({ queryString: undefined, contactPoint: undefined });
- };
- const addNewRoute = () => {
- clearFilters();
- setIsAddMode(true);
- setActualRoutes(() => [
- ...routes.routes,
- {
- ...emptyRoute,
- matchers: [emptyArrayFieldMatcher],
- },
- ]);
- };
- const onCancelAdd = () => {
- setIsAddMode(false);
- setActualRoutes([...routes.routes]);
- };
- const onTableRouteChange = (newRoutes: FormAmRoute[]): void => {
- onChange({
- ...routes,
- routes: newRoutes,
- });
- if (isAddMode) {
- setIsAddMode(false);
- }
- };
- return (
- <div className={styles.container}>
- <h5>Specific routing</h5>
- <p>Send specific alerts to chosen contact points, based on matching criteria</p>
- {!routes.receiver ? (
- readOnly ? (
- <EmptyArea>
- <p>There is no default contact point configured for the root route.</p>
- </EmptyArea>
- ) : (
- <EmptyAreaWithCTA
- buttonIcon="rocket"
- buttonLabel="Set a default contact point"
- onButtonClick={onRootRouteEdit}
- text="You haven't set a default contact point for the root route yet."
- showButton={canCreateNotifications}
- />
- )
- ) : actualRoutes.length > 0 ? (
- <>
- <div>
- {!isAddMode && (
- <div className={styles.searchContainer}>
- <MatcherFilter
- onFilterChange={(filter) =>
- setFilters((currentFilters) => ({ ...currentFilters, queryString: filter }))
- }
- queryString={filters.queryString ?? ''}
- className={styles.filterInput}
- />
- <div className={styles.filterInput}>
- <Label>Search by contact point</Label>
- <Input
- onChange={({ currentTarget }) =>
- setFilters((currentFilters) => ({ ...currentFilters, contactPoint: currentTarget.value }))
- }
- value={filters.contactPoint ?? ''}
- placeholder="Search by contact point"
- data-testid="search-query-input"
- prefix={<Icon name={'search'} />}
- />
- </div>
- {(queryString || contactPoint) && (
- <Button variant="secondary" icon="times" onClick={clearFilters} className={styles.clearFilterBtn}>
- Clear filters
- </Button>
- )}
- </div>
- )}
- {!isAddMode && !readOnly && (
- <Authorize actions={[permissions.create]}>
- <div className={styles.addMatcherBtnRow}>
- <Button className={styles.addMatcherBtn} icon="plus" onClick={addNewRoute} type="button">
- New policy
- </Button>
- </div>
- </Authorize>
- )}
- </div>
- <AmRoutesTable
- isAddMode={isAddMode}
- readOnly={readOnly}
- onCancelAdd={onCancelAdd}
- onChange={onTableRouteChange}
- receivers={receivers}
- routes={actualRoutes}
- filters={{ queryString, contactPoint }}
- alertManagerSourceName={alertManagerSourceName}
- />
- </>
- ) : readOnly ? (
- <EmptyArea>
- <p>There are no specific policies configured.</p>
- </EmptyArea>
- ) : (
- <EmptyAreaWithCTA
- buttonIcon="plus"
- buttonLabel="New specific policy"
- onButtonClick={addNewRoute}
- text="You haven't created any specific policies yet."
- showButton={canCreateNotifications}
- />
- )}
- </div>
- );
- };
- const getStyles = (theme: GrafanaTheme2) => {
- return {
- container: css`
- display: flex;
- flex-flow: column wrap;
- `,
- searchContainer: css`
- display: flex;
- flex-flow: row nowrap;
- padding-bottom: ${theme.spacing(2)};
- border-bottom: 1px solid ${theme.colors.border.strong};
- `,
- clearFilterBtn: css`
- align-self: flex-end;
- margin-left: ${theme.spacing(1)};
- `,
- filterInput: css`
- width: 340px;
- & + & {
- margin-left: ${theme.spacing(1)};
- }
- `,
- addMatcherBtnRow: css`
- display: flex;
- flex-flow: column nowrap;
- padding: ${theme.spacing(2)} 0;
- `,
- addMatcherBtn: css`
- align-self: flex-end;
- `,
- };
- };
|