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
This commit is contained in:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user