Alerting: Add hide plugins rules filter (#87445)

* Add hide plugins rules filter

* Change the filter label and available values

* Setup plugins hook in tests

* Fix lint errors

* remove unused code
pull/87908/head
Konrad Lalik 1 year ago committed by GitHub
parent 4fef6ff30d
commit 1cf1c5f03a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      public/app/features/alerting/unified/RuleList.test.tsx
  2. 3
      public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx
  3. 28
      public/app/features/alerting/unified/components/rules/RulesFilter.tsx
  4. 9
      public/app/features/alerting/unified/home/PluginIntegrations.tsx
  5. 18
      public/app/features/alerting/unified/hooks/useFilteredRules.ts
  6. 9
      public/app/features/alerting/unified/plugins/useAlertingHomePageExtensions.ts
  7. 6
      public/app/features/alerting/unified/search/rulesSearchParser.ts
  8. 7
      public/app/features/alerting/unified/search/search.grammar
  9. 33
      public/app/features/alerting/unified/search/search.js
  10. 6
      public/app/features/alerting/unified/search/search.terms.js
  11. 2
      public/app/features/alerting/unified/search/searchParser.ts
  12. 16
      public/app/features/alerting/unified/testSetup/plugins.ts

@ -54,6 +54,7 @@ import {
somePromRules,
someRulerRules,
} from './mocks';
import { setupPluginsExtensionsHook } from './testSetup/plugins';
import * as config from './utils/config';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
@ -83,6 +84,8 @@ jest.spyOn(config, 'getAllDataSources');
jest.spyOn(actions, 'rulesInSameGroupHaveInvalidFor').mockReturnValue([]);
jest.spyOn(apiRuler, 'rulerUrlBuilder');
setupPluginsExtensionsHook();
const mocks = {
getAllDataSourcesMock: jest.mocked(config.getAllDataSources),
getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions),

@ -9,6 +9,7 @@ import { mockSearchApi, setupMswServer } from 'app/features/alerting/unified/moc
import * as analytics from '../../Analytics';
import { MockDataSourceSrv } from '../../mocks';
import { setupPluginsExtensionsHook } from '../../testSetup/plugins';
import RulesFilter from './RulesFilter';
@ -25,6 +26,8 @@ jest.mock('./MultipleDataSourcePicker', () => {
setDataSourceSrv(new MockDataSourceSrv({}));
setupPluginsExtensionsHook();
const ui = {
stateFilter: {
firing: byRole('radio', { name: 'Firing' }),

@ -5,7 +5,6 @@ import { useForm } from 'react-hook-form';
import { DataSourceInstanceSettings, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Button, Field, Icon, Input, Label, RadioButtonGroup, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
import {
@ -16,6 +15,8 @@ import {
trackRulesSearchInputInteraction,
} from '../../Analytics';
import { useRulesFilter } from '../../hooks/useFilteredRules';
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
import { useAlertingHomePageExtensions } from '../../plugins/useAlertingHomePageExtensions';
import { RuleHealth } from '../../search/rulesSearchParser';
import { alertStateToReadable } from '../../utils/rules';
import { HoverCard } from '../HoverCard';
@ -68,7 +69,8 @@ const RuleStateOptions = Object.entries(PromAlertingRuleState).map(([key, value]
const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) => {
const styles = useStyles2(getStyles);
const [queryParams, setQueryParams] = useQueryParams();
const [queryParams, updateQueryParams] = useURLSearchParams();
const { pluginsFilterEnabled } = usePluginsFilterStatus();
const { filterState, hasActiveFilters, searchQuery, setSearchQuery, updateFilters } = useRulesFilter();
// This key is used to force a rerender on the inputs when the filters are cleared
@ -133,7 +135,7 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) =>
};
const handleViewChange = (view: string) => {
setQueryParams({ view });
updateQueryParams({ view });
trackRulesListViewChange({ view });
};
@ -220,6 +222,19 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) =>
onChange={handleRuleHealthChange}
/>
</div>
{pluginsFilterEnabled && (
<div>
<Label>Plugin rules</Label>
<RadioButtonGroup<'hide'>
options={[
{ label: 'Show', value: undefined },
{ label: 'Hide', value: 'hide' },
]}
value={filterState.plugins}
onChange={(value) => updateFilters({ ...filterState, plugins: value })}
/>
</div>
)}
</Stack>
<Stack direction="column" gap={1}>
<Stack direction="row" gap={1}>
@ -262,7 +277,7 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) =>
<Label>View as</Label>
<RadioButtonGroup
options={ViewOptions}
value={String(queryParams['view'] ?? ViewOptions[0].value)}
value={queryParams.get('view') ?? ViewOptions[0].value}
onChange={handleViewChange}
/>
</div>
@ -348,4 +363,9 @@ const helpStyles = (theme: GrafanaTheme2) => ({
}),
});
function usePluginsFilterStatus() {
const { extensions } = useAlertingHomePageExtensions();
return { pluginsFilterEnabled: extensions.length > 0 };
}
export default RulesFilter;

@ -1,19 +1,16 @@
import { css } from '@emotion/css';
import React from 'react';
import { PluginExtensionPoints } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data/';
import { usePluginComponentExtensions } from '@grafana/runtime';
import { Stack, Text } from '@grafana/ui';
import { useStyles2 } from '@grafana/ui/';
import { useAlertingHomePageExtensions } from '../plugins/useAlertingHomePageExtensions';
export function PluginIntegrations() {
const styles = useStyles2(getStyles);
const { extensions } = usePluginComponentExtensions({
extensionPointId: PluginExtensionPoints.AlertingHomePage,
limitPerPlugin: 1,
});
const { extensions } = useAlertingHomePageExtensions();
if (extensions.length === 0) {
return null;

@ -13,7 +13,13 @@ import { labelsMatchMatchers, matcherToMatcherField, parseMatchers } from '../ut
import { Annotation } from '../utils/constants';
import { isCloudRulesSource } from '../utils/datasource';
import { parseMatcher } from '../utils/matchers';
import { getRuleHealth, isAlertingRule, isGrafanaRulerRule, isPromRuleType } from '../utils/rules';
import {
getRuleHealth,
isAlertingRule,
isGrafanaRulerRule,
isPluginProvidedRule,
isPromRuleType,
} from '../utils/rules';
import { calculateGroupTotals, calculateRuleFilteredTotals, calculateRuleTotals } from './useCombinedRuleNamespaces';
import { useURLSearchParams } from './useURLSearchParams';
@ -26,7 +32,9 @@ export function useRulesFilter() {
const [queryParams, updateQueryParams] = useURLSearchParams();
const searchQuery = queryParams.get('search') ?? '';
const filterState = useMemo(() => getSearchFilterFromQuery(searchQuery), [searchQuery]);
const filterState = useMemo<RulesFilter>(() => {
return getSearchFilterFromQuery(searchQuery);
}, [searchQuery]);
const hasActiveFilters = useMemo(() => Object.values(filterState).some((filter) => !isEmpty(filter)), [filterState]);
const updateFilters = useCallback(
@ -208,7 +216,7 @@ const reduceGroups = (filterState: RulesFilter) => {
const matchesFilterFor = chain(filterState)
// ⚠ keep this list of properties we filter for here up-to-date ⚠
// We are ignoring predicates we've matched before we get here (like "freeFormWords")
.pick(['ruleType', 'dataSourceNames', 'ruleHealth', 'labels', 'ruleState', 'dashboardUid'])
.pick(['ruleType', 'dataSourceNames', 'ruleHealth', 'labels', 'ruleState', 'dashboardUid', 'plugins'])
.omitBy(isEmpty)
.mapValues(() => false)
.value();
@ -217,6 +225,10 @@ const reduceGroups = (filterState: RulesFilter) => {
matchesFilterFor.ruleType = true;
}
if ('plugins' in matchesFilterFor && filterState.plugins === 'hide') {
matchesFilterFor.plugins = !isPluginProvidedRule(rule);
}
if ('dataSourceNames' in matchesFilterFor) {
if (isGrafanaRulerRule(rule.rulerRule)) {
const doesNotQueryDs = isQueryingDataSource(rule.rulerRule, filterState);

@ -0,0 +1,9 @@
import { PluginExtensionPoints } from '@grafana/data';
import { usePluginComponentExtensions } from '@grafana/runtime';
export function useAlertingHomePageExtensions() {
return usePluginComponentExtensions({
extensionPointId: PluginExtensionPoints.AlertingHomePage,
limitPerPlugin: 1,
});
}

@ -21,6 +21,7 @@ export interface RulesFilter {
labels: string[];
ruleHealth?: RuleHealth;
dashboardUid?: string;
plugins?: 'hide';
}
const filterSupportedTerms: FilterSupportedTerm[] = [
@ -33,6 +34,7 @@ const filterSupportedTerms: FilterSupportedTerm[] = [
FilterSupportedTerm.type,
FilterSupportedTerm.health,
FilterSupportedTerm.dashboard,
FilterSupportedTerm.plugins,
];
export enum RuleHealth {
@ -56,6 +58,7 @@ export function getSearchFilterFromQuery(query: string): RulesFilter {
[terms.TypeToken]: (value) => (isPromRuleType(value) ? (filter.ruleType = value) : undefined),
[terms.HealthToken]: (value) => (filter.ruleHealth = getRuleHealth(value)),
[terms.DashboardToken]: (value) => (filter.dashboardUid = value),
[terms.PluginsToken]: (value) => (filter.plugins = value === 'hide' ? value : undefined),
[terms.FreeFormExpression]: (value) => filter.freeFormWords.push(value),
};
@ -98,6 +101,9 @@ export function applySearchFilterToQuery(query: string, filter: RulesFilter): st
if (filter.dashboardUid) {
filterStateArray.push({ type: terms.DashboardToken, value: filter.dashboardUid });
}
if (filter.plugins) {
filterStateArray.push({ type: terms.PluginsToken, value: filter.plugins });
}
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 }
@dialects { dataSourceFilter, nameSpaceFilter, labelFilter, groupFilter, ruleFilter, stateFilter, typeFilter, healthFilter, dashboardFilter, pluginsFilter }
expression { (FilterExpression | FreeFormExpression) expression }
@ -15,7 +15,8 @@ FilterExpression {
filter<StateToken> |
filter<TypeToken> |
filter<HealthToken> |
filter<DashboardToken>
filter<DashboardToken> |
filter<PluginsToken>
}
filter<token> { token FilterValue }
@ -43,6 +44,7 @@ filter<token> { token FilterValue }
TypeToken[@dialect=typeFilter] { filterToken<"type"> }
HealthToken[@dialect=healthFilter] { filterToken<"health"> }
DashboardToken[@dialect=dashboardFilter] { filterToken<"dashboard"> }
PluginsToken[@dialect=pluginsFilter] { filterToken<"plugins"> }
@precedence { DataSourceToken, word }
@precedence { NameSpaceToken, word }
@ -53,5 +55,6 @@ filter<token> { token FilterValue }
@precedence { TypeToken, word }
@precedence { HealthToken, word }
@precedence { DashboardToken, word }
@precedence { PluginsToken, word }
}

File diff suppressed because one or more lines are too long

@ -11,7 +11,8 @@ export const AlertRuleSearch = 1,
TypeToken = 10,
HealthToken = 11,
DashboardToken = 12,
FreeFormExpression = 13,
PluginsToken = 13,
FreeFormExpression = 14,
Dialect_dataSourceFilter = 0,
Dialect_nameSpaceFilter = 1,
Dialect_labelFilter = 2,
@ -20,4 +21,5 @@ export const AlertRuleSearch = 1,
Dialect_stateFilter = 5,
Dialect_typeFilter = 6,
Dialect_healthFilter = 7,
Dialect_dashboardFilter = 8;
Dialect_dashboardFilter = 8,
Dialect_pluginsFilter = 9;

@ -14,6 +14,7 @@ const filterTokenToTypeMap: Record<number, string> = {
[terms.TypeToken]: 'type',
[terms.HealthToken]: 'health',
[terms.DashboardToken]: 'dashboard',
[terms.PluginsToken]: 'plugins',
};
// This enum allows to configure parser behavior
@ -29,6 +30,7 @@ export enum FilterSupportedTerm {
type = 'typeFilter',
health = 'healthFilter',
dashboard = 'dashboardFilter',
plugins = 'pluginsFilter',
}
export type QueryFilterMapper = Record<number, (filter: string) => void>;

@ -1,8 +1,9 @@
import { RequestHandler } from 'msw';
import { PluginMeta, PluginType } from '@grafana/data';
import { config } from '@grafana/runtime';
import { config, setPluginExtensionsHook } from '@grafana/runtime';
import { mockPluginLinkExtension } from '../mocks';
import { pluginsHandler } from '../mocks/plugins';
export function setupPlugins(plugins: PluginMeta[]): { apiHandlers: RequestHandler[] } {
@ -21,6 +22,19 @@ export function setupPlugins(plugins: PluginMeta[]): { apiHandlers: RequestHandl
};
}
export function setupPluginsExtensionsHook() {
setPluginExtensionsHook(() => ({
extensions: plugins.map((plugin) =>
mockPluginLinkExtension({
pluginId: plugin.id,
title: plugin.name,
path: `/a/${plugin.id}`,
})
),
isLoading: false,
}));
}
export const plugins: PluginMeta[] = [
{
id: 'grafana-slo-app',

Loading…
Cancel
Save