mirror of https://github.com/grafana/grafana
Alerting: Filter rules list (#32818)
parent
ffe90b7abf
commit
345d9f93fe
@ -0,0 +1,122 @@ |
||||
import React, { FormEvent, useState } from 'react'; |
||||
import { Button, Icon, Input, Label, RadioButtonGroup, useStyles } from '@grafana/ui'; |
||||
import { DataSourceInstanceSettings, GrafanaTheme } from '@grafana/data'; |
||||
import { css, cx } from '@emotion/css'; |
||||
import { debounce } from 'lodash'; |
||||
|
||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; |
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams'; |
||||
import { getFiltersFromUrlParams } from '../../utils/misc'; |
||||
import { DataSourcePicker } from '@grafana/runtime'; |
||||
|
||||
const RulesFilter = () => { |
||||
const [queryParams, setQueryParams] = useQueryParams(); |
||||
// This key is used to force a rerender on the inputs when the filters are cleared
|
||||
const [filterKey, setFilterKey] = useState<number>(Math.floor(Math.random() * 100)); |
||||
const dataSourceKey = `dataSource-${filterKey}`; |
||||
const queryStringKey = `queryString-${filterKey}`; |
||||
|
||||
const { dataSource, alertState, queryString } = getFiltersFromUrlParams(queryParams); |
||||
|
||||
const styles = useStyles(getStyles); |
||||
const stateOptions = Object.entries(PromAlertingRuleState).map(([key, value]) => ({ label: key, value })); |
||||
|
||||
const handleDataSourceChange = (dataSourceValue: DataSourceInstanceSettings) => { |
||||
setQueryParams({ dataSource: dataSourceValue.name }); |
||||
}; |
||||
|
||||
const handleQueryStringChange = debounce((e: FormEvent<HTMLInputElement>) => { |
||||
const target = e.target as HTMLInputElement; |
||||
setQueryParams({ queryString: target.value || null }); |
||||
}, 600); |
||||
|
||||
const handleAlertStateChange = (value: string) => { |
||||
setQueryParams({ alertState: value }); |
||||
}; |
||||
|
||||
const handleClearFiltersClick = () => { |
||||
setQueryParams({ |
||||
alertState: null, |
||||
queryString: null, |
||||
dataSource: null, |
||||
}); |
||||
setFilterKey(filterKey + 1); |
||||
}; |
||||
|
||||
const searchIcon = <Icon name={'search'} />; |
||||
return ( |
||||
<div className={styles.container}> |
||||
<div className={styles.inputWidth}> |
||||
<Label>Select data source</Label> |
||||
<DataSourcePicker |
||||
key={dataSourceKey} |
||||
alerting |
||||
noDefault |
||||
current={dataSource} |
||||
onChange={handleDataSourceChange} |
||||
/> |
||||
</div> |
||||
<div className={cx(styles.flexRow, styles.spaceBetween)}> |
||||
<div className={styles.flexRow}> |
||||
<div className={styles.rowChild}> |
||||
<Label>Search by name or label</Label> |
||||
<Input |
||||
key={queryStringKey} |
||||
className={styles.inputWidth} |
||||
prefix={searchIcon} |
||||
onChange={handleQueryStringChange} |
||||
defaultValue={queryString} |
||||
/> |
||||
</div> |
||||
<div className={styles.rowChild}> |
||||
<RadioButtonGroup options={stateOptions} value={alertState} onChange={handleAlertStateChange} /> |
||||
</div> |
||||
</div> |
||||
{(dataSource || alertState || queryString) && ( |
||||
<div className={styles.flexRow}> |
||||
<Button fullWidth={false} icon="times" variant="secondary" onClick={handleClearFiltersClick}> |
||||
Clear filters |
||||
</Button> |
||||
</div> |
||||
)} |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
const getStyles = (theme: GrafanaTheme) => { |
||||
return { |
||||
container: css` |
||||
display: flex; |
||||
flex-direction: column; |
||||
border-bottom: 1px solid ${theme.colors.border1}; |
||||
padding-bottom: ${theme.spacing.sm}; |
||||
|
||||
& > div { |
||||
margin-bottom: ${theme.spacing.sm}; |
||||
} |
||||
`,
|
||||
inputWidth: css` |
||||
width: 340px; |
||||
flex-grow: 0; |
||||
`,
|
||||
flexRow: css` |
||||
display: flex; |
||||
flex-direction: row; |
||||
align-items: flex-end; |
||||
`,
|
||||
spaceBetween: css` |
||||
justify-content: space-between; |
||||
`,
|
||||
rowChild: css` |
||||
& + & { |
||||
margin-left: ${theme.spacing.sm}; |
||||
} |
||||
`,
|
||||
clearButton: css` |
||||
align-self: flex-end; |
||||
`,
|
||||
}; |
||||
}; |
||||
|
||||
export default RulesFilter; |
||||
@ -0,0 +1,79 @@ |
||||
import { useMemo } from 'react'; |
||||
|
||||
import { CombinedRuleGroup, CombinedRuleNamespace, RuleFilterState } from 'app/types/unified-alerting'; |
||||
import { isCloudRulesSource } from '../utils/datasource'; |
||||
import { isAlertingRule } from '../utils/rules'; |
||||
import { getFiltersFromUrlParams } from '../utils/misc'; |
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams'; |
||||
|
||||
export const useFilteredRules = (namespaces: CombinedRuleNamespace[]) => { |
||||
const [queryParams] = useQueryParams(); |
||||
const filters = getFiltersFromUrlParams(queryParams); |
||||
|
||||
return useMemo(() => { |
||||
if (!filters.queryString && !filters.dataSource && !filters.alertState) { |
||||
return namespaces; |
||||
} |
||||
const filteredNamespaces = namespaces |
||||
// Filter by data source
|
||||
// TODO: filter by multiple data sources for grafana-managed alerts
|
||||
.filter(({ rulesSource }) => |
||||
filters.dataSource && isCloudRulesSource(rulesSource) ? rulesSource.name === filters.dataSource : true |
||||
) |
||||
// If a namespace and group have rules that match the rules filters then keep them.
|
||||
.reduce(reduceNamespaces(filters), [] as CombinedRuleNamespace[]); |
||||
return filteredNamespaces; |
||||
}, [namespaces, filters]); |
||||
}; |
||||
|
||||
const reduceNamespaces = (filters: RuleFilterState) => { |
||||
return (namespaceAcc: CombinedRuleNamespace[], namespace: CombinedRuleNamespace) => { |
||||
const groups = namespace.groups.reduce(reduceGroups(filters), [] as CombinedRuleGroup[]); |
||||
|
||||
if (groups.length) { |
||||
namespaceAcc.push({ |
||||
...namespace, |
||||
groups, |
||||
}); |
||||
} |
||||
|
||||
return namespaceAcc; |
||||
}; |
||||
}; |
||||
|
||||
// Reduces groups to only groups that have rules matching the filters
|
||||
const reduceGroups = (filters: RuleFilterState) => { |
||||
return (groupAcc: CombinedRuleGroup[], group: CombinedRuleGroup) => { |
||||
const rules = group.rules.filter((rule) => { |
||||
let shouldKeep = true; |
||||
// Query strings can match alert name, label keys, and label values
|
||||
if (filters.queryString) { |
||||
const normalizedQueryString = filters.queryString.toLocaleLowerCase(); |
||||
const doesNameContainsQueryString = rule.name?.toLocaleLowerCase().includes(normalizedQueryString); |
||||
|
||||
const doLabelsContainQueryString = Object.entries(rule.labels || {}).some( |
||||
([key, value]) => |
||||
key.toLocaleLowerCase().includes(normalizedQueryString) || |
||||
value.toLocaleLowerCase().includes(normalizedQueryString) |
||||
); |
||||
shouldKeep = doesNameContainsQueryString || doLabelsContainQueryString; |
||||
} |
||||
if (filters.alertState) { |
||||
const matchesAlertState = Boolean( |
||||
rule.promRule && isAlertingRule(rule.promRule) && rule.promRule.state === filters.alertState |
||||
); |
||||
|
||||
shouldKeep = shouldKeep && matchesAlertState; |
||||
} |
||||
return shouldKeep; |
||||
}); |
||||
// Add rules to the group that match the rule list filters
|
||||
if (rules.length) { |
||||
groupAcc.push({ |
||||
...group, |
||||
rules, |
||||
}); |
||||
} |
||||
return groupAcc; |
||||
}; |
||||
}; |
||||
Loading…
Reference in new issue