mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Filtering for notification policies (#44363)
* Add filtering by matching label * Add label and contact based filters to Notification policies * Improve filters UI, add clear filters option * Add clearing of filters before switching to adding mode * Move filtering code to the AmRoutesTable component * Fix the clearing of silences filter * Remove key-based input resetting * Use uniqueId for input key generation * Add tests for notification policies filtering
This commit is contained in:
parent
844b194f5b
commit
c6e6e92a80
@ -44,7 +44,7 @@ export const AlertGroupFilter = ({ groups }: Props) => {
|
|||||||
<MatcherFilter
|
<MatcherFilter
|
||||||
className={styles.filterInput}
|
className={styles.filterInput}
|
||||||
key={matcherFilterKey}
|
key={matcherFilterKey}
|
||||||
queryString={queryString}
|
defaultQueryString={queryString}
|
||||||
onFilterChange={(value) => setQueryParams({ queryString: value ? value : null })}
|
onFilterChange={(value) => setQueryParams({ queryString: value ? value : null })}
|
||||||
/>
|
/>
|
||||||
<GroupBy
|
<GroupBy
|
||||||
|
@ -6,10 +6,11 @@ import { css } from '@emotion/css';
|
|||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
queryString?: string;
|
queryString?: string;
|
||||||
|
defaultQueryString?: string;
|
||||||
onFilterChange: (filterString: string) => void;
|
onFilterChange: (filterString: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MatcherFilter = ({ className, onFilterChange, queryString }: Props) => {
|
export const MatcherFilter = ({ className, onFilterChange, defaultQueryString, queryString }: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const handleSearchChange = (e: FormEvent<HTMLInputElement>) => {
|
const handleSearchChange = (e: FormEvent<HTMLInputElement>) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
@ -33,7 +34,8 @@ export const MatcherFilter = ({ className, onFilterChange, queryString }: Props)
|
|||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
defaultValue={queryString}
|
defaultValue={defaultQueryString}
|
||||||
|
value={queryString}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
data-testid="search-query-input"
|
data-testid="search-query-input"
|
||||||
prefix={searchIcon}
|
prefix={searchIcon}
|
||||||
|
@ -0,0 +1,112 @@
|
|||||||
|
import { MatcherOperator } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
import { FormAmRoute } from '../../types/amroutes';
|
||||||
|
import { MatcherFieldValue } from '../../types/silence-form';
|
||||||
|
import { getFilteredRoutes } from './AmRoutesTable';
|
||||||
|
|
||||||
|
const defaultAmRoute: FormAmRoute = {
|
||||||
|
id: '',
|
||||||
|
object_matchers: [],
|
||||||
|
continue: false,
|
||||||
|
receiver: '',
|
||||||
|
groupBy: [],
|
||||||
|
groupWaitValue: '',
|
||||||
|
groupWaitValueType: '',
|
||||||
|
groupIntervalValue: '',
|
||||||
|
groupIntervalValueType: '',
|
||||||
|
repeatIntervalValue: '',
|
||||||
|
repeatIntervalValueType: '',
|
||||||
|
muteTimeIntervals: [],
|
||||||
|
routes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildAmRoute = (override: Partial<FormAmRoute> = {}): FormAmRoute => {
|
||||||
|
return { ...defaultAmRoute, ...override };
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildMatcher = (name: string, value: string, operator: MatcherOperator): MatcherFieldValue => {
|
||||||
|
return { name, value, operator };
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('getFilteredRoutes', () => {
|
||||||
|
it('Shoult return all entries when filters are empty', () => {
|
||||||
|
// Arrange
|
||||||
|
const routes: FormAmRoute[] = [buildAmRoute({ id: '1' }), buildAmRoute({ id: '2' }), buildAmRoute({ id: '3' })];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredRoutes = getFilteredRoutes(routes, undefined, undefined);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filteredRoutes).toHaveLength(3);
|
||||||
|
expect(filteredRoutes).toContain(routes[0]);
|
||||||
|
expect(filteredRoutes).toContain(routes[1]);
|
||||||
|
expect(filteredRoutes).toContain(routes[2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should only return entries matching provided label query', () => {
|
||||||
|
// Arrange
|
||||||
|
const routes: FormAmRoute[] = [
|
||||||
|
buildAmRoute({ id: '1' }),
|
||||||
|
buildAmRoute({ id: '2', object_matchers: [buildMatcher('severity', 'critical', MatcherOperator.equal)] }),
|
||||||
|
buildAmRoute({ id: '3' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredRoutes = getFilteredRoutes(routes, 'severity=critical', undefined);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filteredRoutes).toHaveLength(1);
|
||||||
|
expect(filteredRoutes).toContain(routes[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should only return entries matching provided contact query', () => {
|
||||||
|
// Arrange
|
||||||
|
const routes: FormAmRoute[] = [
|
||||||
|
buildAmRoute({ id: '1' }),
|
||||||
|
buildAmRoute({ id: '2', receiver: 'TestContactPoint' }),
|
||||||
|
buildAmRoute({ id: '3' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredRoutes = getFilteredRoutes(routes, undefined, 'contact');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filteredRoutes).toHaveLength(1);
|
||||||
|
expect(filteredRoutes).toContain(routes[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should only return entries matching provided label and contact query', () => {
|
||||||
|
// Arrange
|
||||||
|
const routes: FormAmRoute[] = [
|
||||||
|
buildAmRoute({ id: '1' }),
|
||||||
|
buildAmRoute({
|
||||||
|
id: '2',
|
||||||
|
receiver: 'TestContactPoint',
|
||||||
|
object_matchers: [buildMatcher('severity', 'critical', MatcherOperator.equal)],
|
||||||
|
}),
|
||||||
|
buildAmRoute({ id: '3' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredRoutes = getFilteredRoutes(routes, 'severity=critical', 'contact');
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filteredRoutes).toHaveLength(1);
|
||||||
|
expect(filteredRoutes).toContain(routes[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return entries matching regex matcher configuration without regex evaluation', () => {
|
||||||
|
// Arrange
|
||||||
|
const routes: FormAmRoute[] = [
|
||||||
|
buildAmRoute({ id: '1' }),
|
||||||
|
buildAmRoute({ id: '2', object_matchers: [buildMatcher('severity', 'critical', MatcherOperator.equal)] }),
|
||||||
|
buildAmRoute({ id: '3', object_matchers: [buildMatcher('severity', 'crit', MatcherOperator.regex)] }),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const filteredRoutes = getFilteredRoutes(routes, 'severity=~crit', undefined);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(filteredRoutes).toHaveLength(1);
|
||||||
|
expect(filteredRoutes).toContain(routes[2]);
|
||||||
|
});
|
||||||
|
});
|
@ -6,7 +6,9 @@ import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '..
|
|||||||
import { AmRoutesExpandedForm } from './AmRoutesExpandedForm';
|
import { AmRoutesExpandedForm } from './AmRoutesExpandedForm';
|
||||||
import { AmRoutesExpandedRead } from './AmRoutesExpandedRead';
|
import { AmRoutesExpandedRead } from './AmRoutesExpandedRead';
|
||||||
import { Matchers } from '../silences/Matchers';
|
import { Matchers } from '../silences/Matchers';
|
||||||
import { matcherFieldToMatcher } from '../../utils/alertmanager';
|
import { matcherFieldToMatcher, parseMatchers } from '../../utils/alertmanager';
|
||||||
|
import { intersectionWith, isEqual } from 'lodash';
|
||||||
|
import { EmptyArea } from '../EmptyArea';
|
||||||
|
|
||||||
export interface AmRoutesTableProps {
|
export interface AmRoutesTableProps {
|
||||||
isAddMode: boolean;
|
isAddMode: boolean;
|
||||||
@ -14,26 +16,47 @@ export interface AmRoutesTableProps {
|
|||||||
onCancelAdd: () => void;
|
onCancelAdd: () => void;
|
||||||
receivers: AmRouteReceiver[];
|
receivers: AmRouteReceiver[];
|
||||||
routes: FormAmRoute[];
|
routes: FormAmRoute[];
|
||||||
|
filters?: { queryString?: string; contactPoint?: string };
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouteTableColumnProps = DynamicTableColumnProps<FormAmRoute>;
|
type RouteTableColumnProps = DynamicTableColumnProps<FormAmRoute>;
|
||||||
type RouteTableItemProps = DynamicTableItemProps<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 AmRoutesTable: FC<AmRoutesTableProps> = ({
|
export const AmRoutesTable: FC<AmRoutesTableProps> = ({
|
||||||
isAddMode,
|
isAddMode,
|
||||||
onCancelAdd,
|
onCancelAdd,
|
||||||
onChange,
|
onChange,
|
||||||
receivers,
|
receivers,
|
||||||
routes,
|
routes,
|
||||||
|
filters,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
|
||||||
const [expandedId, setExpandedId] = useState<string | number>();
|
const [expandedId, setExpandedId] = useState<string | number>();
|
||||||
|
|
||||||
const expandItem = useCallback((item: RouteTableItemProps) => setExpandedId(item.id), []);
|
const expandItem = useCallback((item: RouteTableItemProps) => setExpandedId(item.id), []);
|
||||||
|
|
||||||
const collapseItem = useCallback(() => setExpandedId(undefined), []);
|
const collapseItem = useCallback(() => setExpandedId(undefined), []);
|
||||||
|
|
||||||
const cols: RouteTableColumnProps[] = [
|
const cols: RouteTableColumnProps[] = [
|
||||||
@ -111,20 +134,37 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
|
|||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
const items = useMemo(() => prepareItems(routes), [routes]);
|
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
|
// expand the last item when adding
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAddMode && items.length) {
|
if (isAddMode && dynamicTableRoutes.length) {
|
||||||
setExpandedId(items[items.length - 1].id);
|
setExpandedId(dynamicTableRoutes[dynamicTableRoutes.length - 1].id);
|
||||||
}
|
}
|
||||||
}, [isAddMode, items]);
|
}, [isAddMode, dynamicTableRoutes]);
|
||||||
|
|
||||||
|
if (routes.length > 0 && filteredRoutes.length === 0) {
|
||||||
|
return (
|
||||||
|
<EmptyArea>
|
||||||
|
<p>No policies found</p>
|
||||||
|
</EmptyArea>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DynamicTable
|
<DynamicTable
|
||||||
cols={cols}
|
cols={cols}
|
||||||
isExpandable={true}
|
isExpandable={true}
|
||||||
items={items}
|
items={dynamicTableRoutes}
|
||||||
testIdGenerator={() => 'am-routes-row'}
|
testIdGenerator={() => 'am-routes-row'}
|
||||||
onCollapse={collapseItem}
|
onCollapse={collapseItem}
|
||||||
onExpand={expandItem}
|
onExpand={expandItem}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import React, { FC, useState } from 'react';
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Button, useStyles2 } from '@grafana/ui';
|
import { Button, Icon, Input, Label, useStyles2 } from '@grafana/ui';
|
||||||
|
import React, { FC, useState } from 'react';
|
||||||
|
import { useDebounce } from 'react-use';
|
||||||
|
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
|
||||||
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
|
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
|
||||||
import { emptyArrayFieldMatcher, emptyRoute } from '../../utils/amroutes';
|
import { emptyArrayFieldMatcher, emptyRoute } from '../../utils/amroutes';
|
||||||
|
import { getNotificationPoliciesFilters } from '../../utils/misc';
|
||||||
|
import { MatcherFilter } from '../alert-groups/MatcherFilter';
|
||||||
import { EmptyArea } from '../EmptyArea';
|
import { EmptyArea } from '../EmptyArea';
|
||||||
import { AmRoutesTable } from './AmRoutesTable';
|
|
||||||
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
|
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
|
||||||
|
import { AmRoutesTable } from './AmRoutesTable';
|
||||||
|
|
||||||
export interface AmSpecificRoutingProps {
|
export interface AmSpecificRoutingProps {
|
||||||
onChange: (routes: FormAmRoute) => void;
|
onChange: (routes: FormAmRoute) => void;
|
||||||
@ -16,6 +20,11 @@ export interface AmSpecificRoutingProps {
|
|||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Filters {
|
||||||
|
queryString?: string;
|
||||||
|
contactPoint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
||||||
onChange,
|
onChange,
|
||||||
onRootRouteEdit,
|
onRootRouteEdit,
|
||||||
@ -23,15 +32,34 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
|||||||
routes,
|
routes,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [actualRoutes, setActualRoutes] = useState(routes.routes);
|
const [actualRoutes, setActualRoutes] = useState([...routes.routes]);
|
||||||
const [isAddMode, setIsAddMode] = useState(false);
|
const [isAddMode, setIsAddMode] = useState(false);
|
||||||
|
|
||||||
|
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 styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setFilters({ queryString: undefined, contactPoint: undefined });
|
||||||
|
setSearchParams({ queryString: undefined, contactPoint: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
const addNewRoute = () => {
|
const addNewRoute = () => {
|
||||||
|
clearFilters();
|
||||||
setIsAddMode(true);
|
setIsAddMode(true);
|
||||||
setActualRoutes((actualRoutes) => [
|
setActualRoutes(() => [
|
||||||
...actualRoutes,
|
...routes.routes,
|
||||||
{
|
{
|
||||||
...emptyRoute,
|
...emptyRoute,
|
||||||
matchers: [emptyArrayFieldMatcher],
|
matchers: [emptyArrayFieldMatcher],
|
||||||
@ -39,6 +67,21 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCancelAdd = () => {
|
||||||
|
setIsAddMode(false);
|
||||||
|
setActualRoutes([...routes.routes]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTableRouteChange = (newRoutes: FormAmRoute[]): void => {
|
||||||
|
onChange({
|
||||||
|
...routes,
|
||||||
|
routes: newRoutes,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isAddMode) {
|
||||||
|
setIsAddMode(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<h5>Specific routing</h5>
|
<h5>Specific routing</h5>
|
||||||
@ -58,35 +101,52 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
|||||||
)
|
)
|
||||||
) : actualRoutes.length > 0 ? (
|
) : actualRoutes.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{!isAddMode && !readOnly && (
|
<div>
|
||||||
<Button className={styles.addMatcherBtn} icon="plus" onClick={addNewRoute} type="button">
|
{!isAddMode && (
|
||||||
New policy
|
<div className={styles.searchContainer}>
|
||||||
</Button>
|
<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 && (
|
||||||
|
<div className={styles.addMatcherBtnRow}>
|
||||||
|
<Button className={styles.addMatcherBtn} icon="plus" onClick={addNewRoute} type="button">
|
||||||
|
New policy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<AmRoutesTable
|
<AmRoutesTable
|
||||||
isAddMode={isAddMode}
|
isAddMode={isAddMode}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onCancelAdd={() => {
|
onCancelAdd={onCancelAdd}
|
||||||
setIsAddMode(false);
|
onChange={onTableRouteChange}
|
||||||
setActualRoutes((actualRoutes) => {
|
|
||||||
const newRoutes = [...actualRoutes];
|
|
||||||
newRoutes.pop();
|
|
||||||
|
|
||||||
return newRoutes;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onChange={(newRoutes) => {
|
|
||||||
onChange({
|
|
||||||
...routes,
|
|
||||||
routes: newRoutes,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isAddMode) {
|
|
||||||
setIsAddMode(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
receivers={receivers}
|
receivers={receivers}
|
||||||
routes={actualRoutes}
|
routes={actualRoutes}
|
||||||
|
filters={{ queryString, contactPoint }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : readOnly ? (
|
) : readOnly ? (
|
||||||
@ -108,12 +168,32 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
|||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
container: css`
|
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;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
|
padding: ${theme.spacing(2)} 0;
|
||||||
`,
|
`,
|
||||||
addMatcherBtn: css`
|
addMatcherBtn: css`
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
margin-bottom: ${theme.spacing(3.5)};
|
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -48,7 +48,7 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
|||||||
<MatcherFilter
|
<MatcherFilter
|
||||||
className={styles.rowChild}
|
className={styles.rowChild}
|
||||||
key={queryStringKey}
|
key={queryStringKey}
|
||||||
queryString={queryString}
|
defaultQueryString={queryString}
|
||||||
onFilterChange={(value) => setQueryString(value)}
|
onFilterChange={(value) => setQueryString(value)}
|
||||||
/>
|
/>
|
||||||
<AlertInstanceStateFilter
|
<AlertInstanceStateFilter
|
||||||
|
@ -6,15 +6,17 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
|||||||
import { getSilenceFiltersFromUrlParams } from '../../utils/misc';
|
import { getSilenceFiltersFromUrlParams } from '../../utils/misc';
|
||||||
import { SilenceState } from 'app/plugins/datasource/alertmanager/types';
|
import { SilenceState } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { parseMatchers } from '../../utils/alertmanager';
|
import { parseMatchers } from '../../utils/alertmanager';
|
||||||
import { debounce } from 'lodash';
|
import { debounce, uniqueId } from 'lodash';
|
||||||
|
|
||||||
const stateOptions: SelectableValue[] = Object.entries(SilenceState).map(([key, value]) => ({
|
const stateOptions: SelectableValue[] = Object.entries(SilenceState).map(([key, value]) => ({
|
||||||
label: key,
|
label: key,
|
||||||
value,
|
value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const getQueryStringKey = () => uniqueId('query-string-');
|
||||||
|
|
||||||
export const SilencesFilter = () => {
|
export const SilencesFilter = () => {
|
||||||
const [queryStringKey, setQueryStringKey] = useState(`queryString-${Math.random() * 100}`);
|
const [queryStringKey, setQueryStringKey] = useState(getQueryStringKey());
|
||||||
const [queryParams, setQueryParams] = useQueryParams();
|
const [queryParams, setQueryParams] = useQueryParams();
|
||||||
const { queryString, silenceState } = getSilenceFiltersFromUrlParams(queryParams);
|
const { queryString, silenceState } = getSilenceFiltersFromUrlParams(queryParams);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@ -33,7 +35,7 @@ export const SilencesFilter = () => {
|
|||||||
queryString: null,
|
queryString: null,
|
||||||
silenceState: null,
|
silenceState: null,
|
||||||
});
|
});
|
||||||
setTimeout(() => setQueryStringKey(''));
|
setTimeout(() => setQueryStringKey(getQueryStringKey()));
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputInvalid = queryString && queryString.length > 3 ? parseMatchers(queryString).length === 0 : false;
|
const inputInvalid = queryString && queryString.length > 3 ? parseMatchers(queryString).length === 0 : false;
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
|
|
||||||
export function useURLSearchParams(): [URLSearchParams] {
|
export function useURLSearchParams(): [
|
||||||
|
URLSearchParams,
|
||||||
|
(searchValues: Record<string, string | string[] | undefined>, replace?: boolean) => void
|
||||||
|
] {
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
|
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
|
||||||
return [queryParams];
|
|
||||||
|
const update = useCallback((searchValues: Record<string, string | string[] | undefined>, replace?: boolean) => {
|
||||||
|
locationService.partial(searchValues, replace);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [queryParams, update];
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,13 @@ export const getFiltersFromUrlParams = (queryParams: UrlQueryMap): FilterState =
|
|||||||
return { queryString, alertState, dataSource, groupBy, ruleType };
|
return { queryString, alertState, dataSource, groupBy, ruleType };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getNotificationPoliciesFilters = (searchParams: URLSearchParams) => {
|
||||||
|
return {
|
||||||
|
queryString: searchParams.get('queryString') ?? undefined,
|
||||||
|
contactPoint: searchParams.get('contactPoint') ?? undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const getSilenceFiltersFromUrlParams = (queryParams: UrlQueryMap): SilenceFilterState => {
|
export const getSilenceFiltersFromUrlParams = (queryParams: UrlQueryMap): SilenceFilterState => {
|
||||||
const queryString = queryParams['queryString'] === undefined ? undefined : String(queryParams['queryString']);
|
const queryString = queryParams['queryString'] === undefined ? undefined : String(queryParams['queryString']);
|
||||||
const silenceState = queryParams['silenceState'] === undefined ? undefined : String(queryParams['silenceState']);
|
const silenceState = queryParams['silenceState'] === undefined ? undefined : String(queryParams['silenceState']);
|
||||||
|
Loading…
Reference in New Issue
Block a user