diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx
index 26e1538146b..325c718be5f 100644
--- a/public/app/features/alerting/unified/RuleList.test.tsx
+++ b/public/app/features/alerting/unified/RuleList.test.tsx
@@ -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),
diff --git a/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx b/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx
index 9d610940269..058e0a61f25 100644
--- a/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx
+++ b/public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx
@@ -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' }),
diff --git a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx
index a8ce422fd02..713dcb3d56e 100644
--- a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx
+++ b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx
@@ -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}
/>
+ {pluginsFilterEnabled && (
+
+
+
+ options={[
+ { label: 'Show', value: undefined },
+ { label: 'Hide', value: 'hide' },
+ ]}
+ value={filterState.plugins}
+ onChange={(value) => updateFilters({ ...filterState, plugins: value })}
+ />
+
+ )}
@@ -262,7 +277,7 @@ const RulesFilter = ({ onFilterCleared = () => undefined }: RulesFilerProps) =>
@@ -348,4 +363,9 @@ const helpStyles = (theme: GrafanaTheme2) => ({
}),
});
+function usePluginsFilterStatus() {
+ const { extensions } = useAlertingHomePageExtensions();
+ return { pluginsFilterEnabled: extensions.length > 0 };
+}
+
export default RulesFilter;
diff --git a/public/app/features/alerting/unified/home/PluginIntegrations.tsx b/public/app/features/alerting/unified/home/PluginIntegrations.tsx
index 1e9baf298b6..dda9dd0f3b2 100644
--- a/public/app/features/alerting/unified/home/PluginIntegrations.tsx
+++ b/public/app/features/alerting/unified/home/PluginIntegrations.tsx
@@ -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;
diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts
index a71c8790f3a..3ceacb11b07 100644
--- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts
+++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts
@@ -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(() => {
+ 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);
diff --git a/public/app/features/alerting/unified/plugins/useAlertingHomePageExtensions.ts b/public/app/features/alerting/unified/plugins/useAlertingHomePageExtensions.ts
new file mode 100644
index 00000000000..5afd7cef697
--- /dev/null
+++ b/public/app/features/alerting/unified/plugins/useAlertingHomePageExtensions.ts
@@ -0,0 +1,9 @@
+import { PluginExtensionPoints } from '@grafana/data';
+import { usePluginComponentExtensions } from '@grafana/runtime';
+
+export function useAlertingHomePageExtensions() {
+ return usePluginComponentExtensions({
+ extensionPointId: PluginExtensionPoints.AlertingHomePage,
+ limitPerPlugin: 1,
+ });
+}
diff --git a/public/app/features/alerting/unified/search/rulesSearchParser.ts b/public/app/features/alerting/unified/search/rulesSearchParser.ts
index d1d47f707e6..6088f518e72 100644
--- a/public/app/features/alerting/unified/search/rulesSearchParser.ts
+++ b/public/app/features/alerting/unified/search/rulesSearchParser.ts
@@ -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 })));
}
diff --git a/public/app/features/alerting/unified/search/search.grammar b/public/app/features/alerting/unified/search/search.grammar
index f1be5445220..cbbab17dc18 100644
--- a/public/app/features/alerting/unified/search/search.grammar
+++ b/public/app/features/alerting/unified/search/search.grammar
@@ -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 |
filter |
filter |
- filter
+ filter |
+ filter
}
filter { token FilterValue }
@@ -43,6 +44,7 @@ filter { 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 FilterValue }
@precedence { TypeToken, word }
@precedence { HealthToken, word }
@precedence { DashboardToken, word }
+ @precedence { PluginsToken, word }
}
diff --git a/public/app/features/alerting/unified/search/search.js b/public/app/features/alerting/unified/search/search.js
index 8dd0e3c309e..97dbb48acf6 100644
--- a/public/app/features/alerting/unified/search/search.js
+++ b/public/app/features/alerting/unified/search/search.js
@@ -3,29 +3,30 @@ import { LRParser } from '@lezer/lr';
export const parser = LRParser.deserialize({
version: 14,
states:
- "!vOQOPOOOuOPO'#CiOOOO'#Ci'#CiOQOPO'#CmOOOO'#Cj'#CjQQOPOOO!mOQO'#C^O!rOPO'#CkO!wOPO,59TOOOO,59X,59XOOOO-E6h-E6hOOOO,58x,58xOOOO,59V,59VOOOO-E6i-E6i",
+ "!vOQOPOOOxOPO'#CjOOOO'#Cj'#CjOQOPO'#CnOOOO'#Ck'#CkQQOPOOO!sOQO'#C^O!xOPO'#ClO!}OPO,59UOOOO,59Y,59YOOOO-E6i-E6iOOOO,58x,58xOOOO,59W,59WOOOO-E6j-E6j",
stateData:
- '$[~ORUOTUOUUOVUOWUOXUOYUOZUO[UObPOdQO~OcVOR]XT]XU]XV]XW]XX]XY]XZ]X[]Xb]Xd]X~OSZO~Ob[O~OcVOR]aT]aU]aV]aW]aX]aY]aZ]a[]ab]ad]a~OR~T~U~V~W~Y~Z~[~R[ZYXWVUTb~',
- goto: '{bPPcPPPPPPPPPPchnPtVRORTQTORYTQWPR]WSSOTRXR',
+ '$h~ORUOTUOUUOVUOWUOXUOYUOZUO[UO]UOcPOeQO~OdVOR^XT^XU^XV^XW^XX^XY^XZ^X[^X]^Xc^Xe^X~OSZO~Oc[O~OdVOR^aT^aU^aV^aW^aX^aY^aZ^a[^a]^ac^ae^a~OR~T~U~V~W~Y~Z~[~]~R][ZYXWVUTc~',
+ goto: '|cPPdPPPPPPPPPPPdioPuVRORTQTORYTQWPR]WSSOTRXR',
nodeNames:
- '⚠ AlertRuleSearch FilterExpression DataSourceToken FilterValue NameSpaceToken LabelToken GroupToken RuleToken StateToken TypeToken HealthToken DashboardToken FreeFormExpression',
- maxTerm: 20,
+ '⚠ AlertRuleSearch FilterExpression DataSourceToken FilterValue NameSpaceToken LabelToken GroupToken RuleToken StateToken TypeToken HealthToken DashboardToken PluginsToken FreeFormExpression',
+ maxTerm: 21,
skippedNodes: [0],
repeatNodeCount: 2,
tokenData:
- "#0PRRqqr#Yrs&fst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]+h!]#W#Y#W#X,z#X#Z#Y#Z#[Ia#[#]!$Z#]#`#Y#`#a!.m#a#b#Y#b#c!7g#c#f#Y#f#g!Fe#g#h!Mu#h#i#(o#i$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR#acSQbPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YQ$qcSQqr$lst$ltu$luv$lvw$lwx$lxy$lyz$lz{$l{|$l|!P$l!P!Q$l!Q![$l![!]$l!]$Ch$l$JU;'S$l;'S;(d%|;(d;(e&S<%lO$lQ&PP;=`<%l$lQ&VP;=`;NQ$lR&]P;=`<%l#YR&cP;=`;NQ#YR&irX^(spq(sqr(sst(stu(suv(svw(swx(sxy(syz(sz{(s{|(s|!P(s!P!Q(s!Q: { 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',