Alerting: Allow filter by rule source in Filter V2 (#110336)
* add UI for rule source section of filter * add logic to filter grafana vs external datasources * run yarn i18n-extract * resolve PR comments * resolve design comments * add rule source to search parser * rename external to datasource * import empty from ix * fix tests * fix typing * resolve comments- treat undefined as * * resolve PR comments
This commit is contained in:
@@ -8,7 +8,12 @@ import { CombinedRuleGroup, CombinedRuleNamespace, Rule } from 'app/types/unifie
|
||||
import { PromRuleType, RulerGrafanaRuleDTO, isPromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { logError } from '../Analytics';
|
||||
import { RulesFilter, applySearchFilterToQuery, getSearchFilterFromQuery } from '../search/rulesSearchParser';
|
||||
import {
|
||||
RuleSource,
|
||||
RulesFilter,
|
||||
applySearchFilterToQuery,
|
||||
getSearchFilterFromQuery,
|
||||
} from '../search/rulesSearchParser';
|
||||
import { labelsMatchMatchers, matcherToMatcherField } from '../utils/alertmanager';
|
||||
import { Annotation } from '../utils/constants';
|
||||
import { isCloudRulesSource } from '../utils/datasource';
|
||||
@@ -183,6 +188,15 @@ const reduceGroups = (filterState: RulesFilter) => {
|
||||
filteredRules = fuzzyFilter(filteredRules, (r) => r.name, ruleNameQuery);
|
||||
}
|
||||
|
||||
// Filter by rule source at rule-level (Grafana-managed vs datasource-managed)
|
||||
if (filterState.ruleSource) {
|
||||
const grafanaSelected = filterState.ruleSource === RuleSource.Grafana;
|
||||
filteredRules = filteredRules.filter((rule) => {
|
||||
const isGrafana = !!(rule.rulerRule && rulerRuleType.grafana.rule(rule.rulerRule));
|
||||
return grafanaSelected && isGrafana;
|
||||
});
|
||||
}
|
||||
|
||||
filteredRules = filteredRules.filter((rule) => {
|
||||
const promRuleDefition = rule.promRule;
|
||||
|
||||
@@ -201,6 +215,7 @@ const reduceGroups = (filterState: RulesFilter) => {
|
||||
'dashboardUid',
|
||||
'plugins',
|
||||
'contactPoint',
|
||||
'ruleSource',
|
||||
])
|
||||
.omitBy(isEmpty)
|
||||
.mapValues(() => false)
|
||||
@@ -332,6 +347,7 @@ const RULES_FILTER_KEYS: Set<keyof RulesFilter> = new Set([
|
||||
'dashboardUid',
|
||||
'plugins',
|
||||
'contactPoint',
|
||||
'ruleSource',
|
||||
]);
|
||||
|
||||
const isRuleFilterKey = (key: string): key is keyof RulesFilter => RULES_FILTER_KEYS.has(key as keyof RulesFilter);
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
useNamespaceAndGroupOptions,
|
||||
} from '../../components/rules/Filter/useRuleFilterAutocomplete';
|
||||
import { useRulesFilter } from '../../hooks/useFilteredRules';
|
||||
import { RuleHealth, getSearchFilterFromQuery } from '../../search/rulesSearchParser';
|
||||
import { RuleHealth, RuleSource, getSearchFilterFromQuery } from '../../search/rulesSearchParser';
|
||||
|
||||
import { RulesFilterProps } from './RulesFilter';
|
||||
import {
|
||||
@@ -53,6 +53,8 @@ import {
|
||||
|
||||
const canRenderContactPointSelector = contextSrv.hasPermission(AccessControlAction.AlertingReceiversRead);
|
||||
|
||||
const radioGroupCompactClass = css({ width: 'max-content' });
|
||||
|
||||
type SearchQueryForm = {
|
||||
query: string;
|
||||
};
|
||||
@@ -298,6 +300,7 @@ const FilterOptions = ({ onSubmit, onClear, pluginsFilterEnabled }: FilterOption
|
||||
/>
|
||||
<DataSourceNamesField dataSourceOptions={dataSourceOptions} portalContainer={portalContainer} />
|
||||
{canRenderContactPointSelector && <ContactPointField portalContainer={portalContainer} />}
|
||||
<RuleSourceField />
|
||||
<RuleStateField />
|
||||
<RuleTypeField />
|
||||
<RuleHealthField />
|
||||
@@ -570,6 +573,8 @@ function RuleStateField() {
|
||||
]}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
fullWidth={false}
|
||||
className={radioGroupCompactClass}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -596,6 +601,39 @@ function RuleTypeField() {
|
||||
]}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
fullWidth={false}
|
||||
className={radioGroupCompactClass}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RuleSourceField() {
|
||||
const { control } = useFormContext<AdvancedFilters>();
|
||||
return (
|
||||
<>
|
||||
<Label>
|
||||
<Trans i18nKey="alerting.search.property.rule-source">Rule source</Trans>
|
||||
</Label>
|
||||
<Controller
|
||||
name="ruleSource"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioButtonGroup<AdvancedFilters['ruleSource']>
|
||||
options={[
|
||||
{ label: t('common.all', 'All'), value: null },
|
||||
{ label: t('alerting.rules-filter.rule-source.grafana', 'Grafana managed'), value: RuleSource.Grafana },
|
||||
{
|
||||
label: t('alerting.rules-filter.rule-source.datasource', 'Data source managed'),
|
||||
value: RuleSource.DataSource,
|
||||
},
|
||||
]}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
fullWidth={false}
|
||||
className={radioGroupCompactClass}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -623,6 +661,8 @@ function RuleHealthField() {
|
||||
]}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
fullWidth={false}
|
||||
className={radioGroupCompactClass}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -648,6 +688,8 @@ function PluginsField() {
|
||||
]}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
fullWidth={false}
|
||||
className={radioGroupCompactClass}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import type { RuleHealth } from '../../search/rulesSearchParser';
|
||||
import type { RuleHealth, RuleSource } from '../../search/rulesSearchParser';
|
||||
|
||||
export type AdvancedFilters = {
|
||||
namespace?: string | null;
|
||||
@@ -14,4 +14,5 @@ export type AdvancedFilters = {
|
||||
dashboardUid?: string;
|
||||
plugins?: 'show' | 'hide';
|
||||
contactPoint?: string | null;
|
||||
ruleSource?: RuleSource | null;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ export function formAdvancedFiltersToRuleFilter(values: AdvancedFilters): RulesF
|
||||
ruleState: values.ruleState === '*' ? undefined : values.ruleState,
|
||||
ruleType: values.ruleType === '*' ? undefined : values.ruleType,
|
||||
plugins: values.plugins === 'show' ? undefined : 'hide',
|
||||
ruleSource: values.ruleSource ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +32,7 @@ export const emptyAdvancedFilters: AdvancedFilters = {
|
||||
dashboardUid: undefined,
|
||||
plugins: 'show',
|
||||
contactPoint: null,
|
||||
ruleSource: null,
|
||||
};
|
||||
|
||||
export function searchQueryToDefaultValues(filterState: RulesFilter): AdvancedFilters {
|
||||
@@ -46,6 +48,7 @@ export function searchQueryToDefaultValues(filterState: RulesFilter): AdvancedFi
|
||||
dashboardUid: filterState.dashboardUid,
|
||||
plugins: filterState.plugins ?? 'show',
|
||||
contactPoint: filterState.contactPoint ?? null,
|
||||
ruleSource: filterState.ruleSource ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AsyncIterableX, empty, from } from 'ix/asynciterable';
|
||||
import { AsyncIterableX, from } from 'ix/asynciterable';
|
||||
import { empty } from 'ix/asynciterable/empty';
|
||||
import { merge } from 'ix/asynciterable/merge';
|
||||
import { catchError, concatMap, withAbort } from 'ix/asynciterable/operators';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import {
|
||||
DataSourceRuleGroupIdentifier,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
PromRuleGroupDTO,
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { RulesFilter } from '../../search/rulesSearchParser';
|
||||
import { RuleSource, RulesFilter } from '../../search/rulesSearchParser';
|
||||
import {
|
||||
getDataSourceByUid,
|
||||
getDatasourceAPIUid,
|
||||
@@ -63,7 +63,7 @@ export function useFilteredRulesIteratorProvider() {
|
||||
const normalizedFilterState = normalizeFilterState(filterState);
|
||||
const hasDataSourceFilterActive = Boolean(filterState.dataSourceNames.length);
|
||||
|
||||
const grafanaRulesGenerator = from(
|
||||
const grafanaRulesGenerator: AsyncIterableX<RuleWithOrigin> = from(
|
||||
grafanaGroupsGenerator(groupLimit, {
|
||||
contactPoint: filterState.contactPoint ?? undefined,
|
||||
health: filterState.ruleHealth ? [filterState.ruleHealth] : [],
|
||||
@@ -74,9 +74,9 @@ export function useFilteredRulesIteratorProvider() {
|
||||
concatMap((groups) =>
|
||||
groups
|
||||
.filter((group) => groupFilter(group, normalizedFilterState))
|
||||
.flatMap((group) => group.rules.map((rule) => [group, rule] as const))
|
||||
.filter(([, rule]) => ruleFilter(rule, normalizedFilterState))
|
||||
.map(([group, rule]) => mapGrafanaRuleToRuleWithOrigin(group, rule))
|
||||
.flatMap((group) => group.rules.map((rule) => ({ group, rule })))
|
||||
.filter(({ rule }) => ruleFilter(rule, normalizedFilterState))
|
||||
.map(({ group, rule }) => mapGrafanaRuleToRuleWithOrigin(group, rule))
|
||||
),
|
||||
catchError(() => empty())
|
||||
);
|
||||
@@ -86,38 +86,57 @@ export function useFilteredRulesIteratorProvider() {
|
||||
? getRulesSourcesFromFilter(filterState)
|
||||
: allExternalRulesSources;
|
||||
|
||||
// If no data sources, just return Grafana rules
|
||||
if (isEmpty(externalRulesSourcesToFetchFrom)) {
|
||||
if (filterState.ruleSource === RuleSource.Grafana) {
|
||||
return { iterable: grafanaRulesGenerator, abortController };
|
||||
}
|
||||
|
||||
// Create a generator for each data source
|
||||
const dataSourceGenerators = externalRulesSourcesToFetchFrom.map((dataSourceIdentifier) => {
|
||||
const promGroupsGenerator = from(prometheusGroupsGenerator(dataSourceIdentifier, groupLimit)).pipe(
|
||||
withAbort(abortController.signal),
|
||||
concatMap((groups) =>
|
||||
groups
|
||||
.filter((group) => groupFilter(group, normalizedFilterState))
|
||||
.flatMap((group) => group.rules.map((rule) => [group, rule] as const))
|
||||
.filter(([, rule]) => ruleFilter(rule, normalizedFilterState))
|
||||
.map(([group, rule]) => mapRuleToRuleWithOrigin(dataSourceIdentifier, group, rule))
|
||||
),
|
||||
catchError(() => empty())
|
||||
);
|
||||
const dataSourceGenerators: Array<AsyncIterableX<RuleWithOrigin>> = externalRulesSourcesToFetchFrom.map(
|
||||
(dataSourceIdentifier) => {
|
||||
const promGroupsGenerator: AsyncIterableX<RuleWithOrigin> = from(
|
||||
prometheusGroupsGenerator(dataSourceIdentifier, groupLimit)
|
||||
).pipe(
|
||||
withAbort(abortController.signal),
|
||||
concatMap((groups) =>
|
||||
groups
|
||||
.filter((group) => groupFilter(group, normalizedFilterState))
|
||||
.flatMap((group) => group.rules.map((rule) => ({ group, rule })))
|
||||
.filter(({ rule }) => ruleFilter(rule, normalizedFilterState))
|
||||
.map(({ group, rule }) => mapRuleToRuleWithOrigin(dataSourceIdentifier, group, rule))
|
||||
),
|
||||
catchError(() => empty())
|
||||
);
|
||||
|
||||
return promGroupsGenerator;
|
||||
});
|
||||
return promGroupsGenerator;
|
||||
}
|
||||
);
|
||||
|
||||
// Merge all generators
|
||||
return {
|
||||
iterable: merge<RuleWithOrigin>(grafanaRulesGenerator, ...dataSourceGenerators),
|
||||
abortController,
|
||||
};
|
||||
const iterablesToMerge: Array<AsyncIterableX<RuleWithOrigin>> = [];
|
||||
const includeGrafana = filterState.ruleSource !== 'datasource';
|
||||
const includeExternal = true;
|
||||
|
||||
if (includeGrafana) {
|
||||
iterablesToMerge.push(grafanaRulesGenerator);
|
||||
}
|
||||
if (includeExternal) {
|
||||
iterablesToMerge.push(...dataSourceGenerators);
|
||||
}
|
||||
|
||||
const iterable = mergeIterables(iterablesToMerge);
|
||||
|
||||
return { iterable, abortController };
|
||||
};
|
||||
|
||||
return getFilteredRulesIterable;
|
||||
}
|
||||
|
||||
function mergeIterables(iterables: Array<AsyncIterableX<RuleWithOrigin>>): AsyncIterableX<RuleWithOrigin> {
|
||||
if (iterables.length === 0) {
|
||||
return empty();
|
||||
}
|
||||
const [firstIterable, ...rest] = iterables;
|
||||
return merge(firstIterable, ...rest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all data sources that the user might want to filter by.
|
||||
* Only allows Prometheus and Loki data source types.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PromAlertingRuleState, PromRuleType, isPromAlertingRuleState } from '../../../../types/unified-alerting-dto';
|
||||
import { getRuleHealth, isPromRuleType } from '../utils/rules';
|
||||
import { getRuleHealth, getRuleSource, isPromRuleType } from '../utils/rules';
|
||||
|
||||
import * as terms from './search.terms';
|
||||
import {
|
||||
@@ -23,6 +23,7 @@ export interface RulesFilter {
|
||||
dashboardUid?: string;
|
||||
plugins?: 'hide';
|
||||
contactPoint?: string | null;
|
||||
ruleSource?: RuleSource;
|
||||
}
|
||||
|
||||
const filterSupportedTerms: FilterSupportedTerm[] = [
|
||||
@@ -37,6 +38,7 @@ const filterSupportedTerms: FilterSupportedTerm[] = [
|
||||
FilterSupportedTerm.dashboard,
|
||||
FilterSupportedTerm.plugins,
|
||||
FilterSupportedTerm.contactPoint,
|
||||
FilterSupportedTerm.source,
|
||||
];
|
||||
|
||||
export enum RuleHealth {
|
||||
@@ -46,6 +48,11 @@ export enum RuleHealth {
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
export enum RuleSource {
|
||||
Grafana = 'grafana',
|
||||
DataSource = 'datasource',
|
||||
}
|
||||
|
||||
// Define how to map parsed tokens into the filter object
|
||||
export function getSearchFilterFromQuery(query: string): RulesFilter {
|
||||
const filter: RulesFilter = { labels: [], freeFormWords: [], dataSourceNames: [] };
|
||||
@@ -62,6 +69,7 @@ export function getSearchFilterFromQuery(query: string): RulesFilter {
|
||||
[terms.DashboardToken]: (value) => (filter.dashboardUid = value),
|
||||
[terms.PluginsToken]: (value) => (filter.plugins = value === 'hide' ? value : undefined),
|
||||
[terms.ContactPointToken]: (value) => (filter.contactPoint = value),
|
||||
[terms.RuleSourceToken]: (value) => (filter.ruleSource = getRuleSource(value)),
|
||||
[terms.FreeFormExpression]: (value) => filter.freeFormWords.push(value),
|
||||
};
|
||||
|
||||
@@ -107,6 +115,9 @@ export function applySearchFilterToQuery(query: string, filter: RulesFilter): st
|
||||
if (filter.plugins) {
|
||||
filterStateArray.push({ type: terms.PluginsToken, value: filter.plugins });
|
||||
}
|
||||
if (filter.ruleSource) {
|
||||
filterStateArray.push({ type: terms.RuleSourceToken, value: filter.ruleSource });
|
||||
}
|
||||
if (filter.freeFormWords) {
|
||||
filterStateArray.push(...filter.freeFormWords.map((word) => ({ type: terms.FreeFormExpression, value: word })));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@top AlertRuleSearch { expression+ }
|
||||
|
||||
@dialects { dataSourceFilter, nameSpaceFilter, labelFilter, groupFilter, ruleFilter, stateFilter, typeFilter, healthFilter, dashboardFilter, pluginsFilter, contactPointFilter }
|
||||
@dialects { dataSourceFilter, nameSpaceFilter, labelFilter, groupFilter, ruleFilter, stateFilter, typeFilter, healthFilter, dashboardFilter, pluginsFilter, contactPointFilter, sourceFilter }
|
||||
|
||||
expression { (FilterExpression | FreeFormExpression) expression }
|
||||
|
||||
@@ -17,7 +17,8 @@ FilterExpression {
|
||||
filter<HealthToken> |
|
||||
filter<DashboardToken> |
|
||||
filter<PluginsToken> |
|
||||
filter<ContactPointToken>
|
||||
filter<ContactPointToken> |
|
||||
filter<RuleSourceToken>
|
||||
}
|
||||
|
||||
filter<token> { token FilterValue }
|
||||
@@ -47,6 +48,7 @@ filter<token> { token FilterValue }
|
||||
DashboardToken[@dialect=dashboardFilter] { filterToken<"dashboard"> }
|
||||
PluginsToken[@dialect=pluginsFilter] { filterToken<"plugins"> }
|
||||
ContactPointToken[@dialect=contactPointFilter] { filterToken<"contactPoint"> }
|
||||
RuleSourceToken[@dialect=sourceFilter] { filterToken<"source"> }
|
||||
|
||||
@precedence { DataSourceToken, word }
|
||||
@precedence { NameSpaceToken, word }
|
||||
@@ -59,5 +61,6 @@ filter<token> { token FilterValue }
|
||||
@precedence { DashboardToken, word }
|
||||
@precedence { PluginsToken, word }
|
||||
@precedence { ContactPointToken, word }
|
||||
@precedence { RuleSourceToken, word }
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -13,7 +13,8 @@ export const AlertRuleSearch = 1,
|
||||
DashboardToken = 12,
|
||||
PluginsToken = 13,
|
||||
ContactPointToken = 14,
|
||||
FreeFormExpression = 15,
|
||||
RuleSourceToken = 15,
|
||||
FreeFormExpression = 16,
|
||||
Dialect_dataSourceFilter = 0,
|
||||
Dialect_nameSpaceFilter = 1,
|
||||
Dialect_labelFilter = 2,
|
||||
@@ -24,4 +25,5 @@ export const AlertRuleSearch = 1,
|
||||
Dialect_healthFilter = 7,
|
||||
Dialect_dashboardFilter = 8,
|
||||
Dialect_pluginsFilter = 9,
|
||||
Dialect_contactPointFilter = 10;
|
||||
Dialect_contactPointFilter = 10,
|
||||
Dialect_sourceFilter = 11;
|
||||
|
||||
@@ -16,6 +16,7 @@ const filterTokenToTypeMap: Record<number, string> = {
|
||||
[terms.DashboardToken]: 'dashboard',
|
||||
[terms.PluginsToken]: 'plugins',
|
||||
[terms.ContactPointToken]: 'contactPoint',
|
||||
[terms.RuleSourceToken]: 'source',
|
||||
};
|
||||
|
||||
// This enum allows to configure parser behavior
|
||||
@@ -33,6 +34,7 @@ export enum FilterSupportedTerm {
|
||||
dashboard = 'dashboardFilter',
|
||||
plugins = 'pluginsFilter',
|
||||
contactPoint = 'contactPointFilter',
|
||||
source = 'sourceFilter',
|
||||
}
|
||||
|
||||
export type QueryFilterMapper = Record<number, (filter: string) => void>;
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
|
||||
import { CombinedRuleNamespace } from '../../../../types/unified-alerting';
|
||||
import { State } from '../components/StateTag';
|
||||
import { RuleHealth } from '../search/rulesSearchParser';
|
||||
import { RuleHealth, RuleSource } from '../search/rulesSearchParser';
|
||||
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
||||
|
||||
import { RULER_NOT_SUPPORTED_MSG } from './constants';
|
||||
@@ -192,6 +192,16 @@ export function getRuleHealth(health: string): RuleHealth | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
export function getRuleSource(source: string): RuleSource | undefined {
|
||||
if (source === 'grafana') {
|
||||
return RuleSource.Grafana;
|
||||
}
|
||||
if (source === 'datasource') {
|
||||
return RuleSource.DataSource;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getPendingPeriod(rule: CombinedRule): string | undefined {
|
||||
if (rulerRuleType.any.recordingRule(rule.rulerRule)) {
|
||||
return undefined;
|
||||
|
||||
@@ -2643,6 +2643,10 @@
|
||||
"placeholder-data-sources": "Select data sources",
|
||||
"placeholder-labels": "Select labels",
|
||||
"plugin-rules": "Plugin rules",
|
||||
"rule-source": {
|
||||
"datasource": "Data source managed",
|
||||
"grafana": "Grafana managed"
|
||||
},
|
||||
"rule-type": "Rule type",
|
||||
"rulesSearchInput-placeholder-search": "Search",
|
||||
"search": "Search",
|
||||
@@ -2664,6 +2668,7 @@
|
||||
"namespace": "Folder / Namespace",
|
||||
"rule-health": "Health",
|
||||
"rule-name": "Rule name",
|
||||
"rule-source": "Rule source",
|
||||
"rule-type": "Type",
|
||||
"state": "State"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user