Alerting: Fix for fetching evaluation group in new filter (#113694)

* Alerting: Fix for fetching evaluation group in new filter

* WIP: Add async evaluation groups dropdown with 500 group limit

* Add text to query param if threshold limit reached

* update translations, remove group info tooltip

* resolve PR comment
This commit is contained in:
Lauren
2025-11-19 09:11:07 +00:00
committed by GitHub
parent 56c2c1cfe2
commit 6dcb921333
3 changed files with 142 additions and 61 deletions
@@ -15,10 +15,19 @@ function getExternalRuleDataSources() {
return getRulesDataSources().filter((ds: DataSourceInstanceSettings) => !!ds?.url);
}
const THRESHOLD_LIMIT = 500;
function createInfoOption(message: string): ComboboxOption<string> {
return {
label: message,
value: '__GRAFANA_INFO_OPTION__',
infoOption: true,
};
}
export function useNamespaceAndGroupOptions(): {
namespaceOptions: (inputValue: string) => Promise<Array<ComboboxOption<string>>>;
allGroupNames: string[];
isLoadingNamespaces: boolean;
groupOptions: (inputValue: string) => Promise<Array<ComboboxOption<string>>>;
namespacePlaceholder: string;
groupPlaceholder: string;
} {
@@ -50,17 +59,11 @@ export function useNamespaceAndGroupOptions(): {
const namespaceOptions = useCallback(
async (inputValue: string) => {
// Grafana namespaces
const grafanaResponse = await fetchGrafanaGroups({ limitAlerts: 0, groupLimit: 1000 }).unwrap();
const grafanaFolders: Array<ComboboxOption<string>> = Array.from(
// Grafana namespaces - fetch with limit to check threshold
const grafanaResponse = await fetchGrafanaGroups({ limitAlerts: 0, groupLimit: THRESHOLD_LIMIT + 1 }).unwrap();
const grafanaFolderNames = Array.from(
new Set(grafanaResponse.data.groups.map((g: GrafanaPromRuleGroupDTO) => g.file || 'default'))
)
.map((name) => ({
label: name,
value: name,
description: t('alerting.rules-filter.grafana-folder', 'Grafana folder'),
}))
.sort((a, b) => collator.compare(a.label ?? '', b.label ?? ''));
);
// External namespaces
const namespaceNameSet = new Set<string>();
@@ -68,7 +71,7 @@ export function useNamespaceAndGroupOptions(): {
fetchExternalGroups({
ruleSource: { uid: ds.uid },
excludeAlerts: true,
groupLimit: 500,
groupLimit: THRESHOLD_LIMIT + 1,
notificationOptions: { showErrorAlert: false },
}).unwrap()
);
@@ -78,6 +81,29 @@ export function useNamespaceAndGroupOptions(): {
res.value.data.groups.forEach((group: { file?: string }) => namespaceNameSet.add(group.file || 'default'));
}
}
const totalNamespaces = grafanaFolderNames.length + namespaceNameSet.size;
// If we have more than THRESHOLD_LIMIT unique namespaces, show info message
if (totalNamespaces > THRESHOLD_LIMIT) {
return [
createInfoOption(
t(
'alerting.rules-filter.namespace-autocomplete-unavailable',
'Due to large number of folders, autocomplete is not available'
)
),
];
}
const grafanaFolders: Array<ComboboxOption<string>> = grafanaFolderNames
.map((name) => ({
label: name,
value: name,
description: t('alerting.rules-filter.grafana-folder', 'Grafana folder'),
}))
.sort((a, b) => collator.compare(a.label ?? '', b.label ?? ''));
const externalNamespaces = Array.from(namespaceNameSet)
.map(formatNamespaceOption)
.sort((a, b) => collator.compare(a.label ?? '', b.label ?? ''));
@@ -89,12 +115,37 @@ export function useNamespaceAndGroupOptions(): {
[fetchGrafanaGroups, fetchExternalGroups, formatNamespaceOption]
);
const allGroupNames: string[] = [];
const isLoadingNamespaces = false;
const groupOptions = useCallback(
async (inputValue: string) => {
const grafanaResponse = await fetchGrafanaGroups({ limitAlerts: 0, groupLimit: THRESHOLD_LIMIT + 1 }).unwrap();
const groupNames = Array.from(new Set(grafanaResponse.data.groups.map((g: GrafanaPromRuleGroupDTO) => g.name)));
// If we have more than THRESHOLD_LIMIT unique groups, show info message
if (groupNames.length > THRESHOLD_LIMIT) {
return [
createInfoOption(
t(
'alerting.rules-filter.group-autocomplete-unavailable',
'Due to large number of groups, autocomplete is not available'
)
),
];
}
const options: Array<ComboboxOption<string>> = groupNames
.map((name) => ({ label: name, value: name }))
.sort((a, b) => collator.compare(a.label ?? '', b.label ?? ''));
const filtered = filterBySearch(options, inputValue);
return filtered;
},
[fetchGrafanaGroups]
);
const namespacePlaceholder = t('alerting.rules-filter.filter-options.placeholder-namespace', 'Select namespace');
const groupPlaceholder = t('grafana.select-group', 'Select group');
return { namespaceOptions, allGroupNames, isLoadingNamespaces, namespacePlaceholder, groupPlaceholder };
return { namespaceOptions, groupOptions, namespacePlaceholder, groupPlaceholder };
}
export function useLabelOptions(): {
@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Controller, FormProvider, SubmitHandler, useForm, useFormContext } from 'react-hook-form';
import { ContactPointSelector } from '@grafana/alerting/unstable';
@@ -55,6 +55,25 @@ const canRenderContactPointSelector = contextSrv.hasPermission(AccessControlActi
const radioGroupCompactClass = css({ width: 'max-content' });
// Helper to create a wrapped options function that captures typed input when threshold is exceeded
function createThresholdAwareOptions<T extends { infoOption?: boolean }>(
optionsFunc: (inputValue: string) => Promise<T[]>,
setValue: (value: string) => void,
fieldName: string
) {
return async (inputValue: string): Promise<T[]> => {
const options = await optionsFunc(inputValue);
const exceeded = options.length === 1 && options[0].infoOption === true;
// If threshold exceeded and user is typing, capture the typed value
if (exceeded && inputValue) {
setValue(inputValue);
}
return options;
};
}
type SearchQueryForm = {
query: string;
};
@@ -85,11 +104,9 @@ export default function RulesFilter({ viewMode, onViewModeChange }: RulesFilterP
};
const handleAdvancedFilters: SubmitHandler<AdvancedFilters> = (values) => {
const newFilter = formAdvancedFiltersToRuleFilter(values);
updateFilters(newFilter);
updateFilters(formAdvancedFiltersToRuleFilter(values));
trackFilterButtonApplyClick(values, pluginsFilterEnabled);
setIsPopupOpen(false); // Should close popup after applying filters?
setIsPopupOpen(false);
};
const handleClearFilters = () => {
@@ -248,8 +265,7 @@ const FilterOptions = ({ onSubmit, onClear, pluginsFilterEnabled }: FilterOption
const defaultValues = searchQueryToDefaultValues(filterState);
// Fetch namespace and group data from all sources (optimized for filter UI)
const { namespaceOptions, allGroupNames, isLoadingNamespaces, namespacePlaceholder, groupPlaceholder } =
useNamespaceAndGroupOptions();
const { namespaceOptions, groupOptions, namespacePlaceholder, groupPlaceholder } = useNamespaceAndGroupOptions();
const { labelOptions } = useLabelOptions();
@@ -294,13 +310,11 @@ const FilterOptions = ({ onSubmit, onClear, pluginsFilterEnabled }: FilterOption
<NamespaceField
namespaceOptions={namespaceOptions}
namespacePlaceholder={namespacePlaceholder}
isLoadingNamespaces={isLoadingNamespaces}
portalContainer={portalContainer}
/>
<GroupField
allGroupNames={allGroupNames}
groupOptions={groupOptions}
groupPlaceholder={groupPlaceholder}
isLoadingNamespaces={isLoadingNamespaces}
portalContainer={portalContainer}
/>
<DataSourceNamesField dataSourceOptions={dataSourceOptions} portalContainer={portalContainer} />
@@ -373,15 +387,22 @@ function LabelsField({
function NamespaceField({
namespaceOptions,
namespacePlaceholder,
isLoadingNamespaces,
portalContainer,
}: {
namespaceOptions: (inputValue: string) => Promise<Array<{ label?: string; value: string; description?: string }>>;
namespaceOptions: (
inputValue: string
) => Promise<Array<{ label?: string; value: string; description?: string; infoOption?: boolean }>>;
namespacePlaceholder: string;
isLoadingNamespaces: boolean;
portalContainer?: HTMLElement;
}) {
const { control } = useFormContext<AdvancedFilters>();
const { control, setValue } = useFormContext<AdvancedFilters>();
const wrappedOptions = useCallback(
(inputValue: string) =>
createThresholdAwareOptions(namespaceOptions, (value) => setValue('namespace', value), 'namespace')(inputValue),
[namespaceOptions, setValue]
);
return (
<>
<Label>
@@ -390,36 +411,42 @@ function NamespaceField({
<Controller
name="namespace"
control={control}
render={({ field }) => {
return (
<Combobox<string>
placeholder={namespacePlaceholder}
options={namespaceOptions}
onChange={(option) => field.onChange(option?.value || null)}
value={field.value}
loading={isLoadingNamespaces}
isClearable
portalContainer={portalContainer}
/>
);
}}
render={({ field }) => (
<Combobox<string>
placeholder={namespacePlaceholder}
options={wrappedOptions}
onChange={(option) => {
if (!option?.infoOption) {
field.onChange(option?.value || null);
}
}}
value={field.value}
isClearable
portalContainer={portalContainer}
/>
)}
/>
</>
);
}
function GroupField({
allGroupNames,
groupOptions,
groupPlaceholder,
isLoadingNamespaces,
portalContainer,
}: {
allGroupNames: string[];
groupOptions: (inputValue: string) => Promise<Array<{ label?: string; value: string; infoOption?: boolean }>>;
groupPlaceholder: string;
isLoadingNamespaces: boolean;
portalContainer?: HTMLElement;
}) {
const { control } = useFormContext<AdvancedFilters>();
const { control, setValue } = useFormContext<AdvancedFilters>();
const wrappedOptions = useCallback(
(inputValue: string) =>
createThresholdAwareOptions(groupOptions, (value) => setValue('groupName', value), 'groupName')(inputValue),
[groupOptions, setValue]
);
return (
<>
<Label>
@@ -428,19 +455,20 @@ function GroupField({
<Controller
name="groupName"
control={control}
render={({ field }) => {
return (
<Combobox<string>
placeholder={groupPlaceholder}
options={allGroupNames.map((name) => ({ label: name, value: name }))}
onChange={(option) => field.onChange(option?.value || null)}
value={field.value}
loading={isLoadingNamespaces}
isClearable
portalContainer={portalContainer}
/>
);
}}
render={({ field }) => (
<Combobox<string>
placeholder={groupPlaceholder}
options={wrappedOptions}
onChange={(option) => {
if (!option?.infoOption) {
field.onChange(option?.value || null);
}
}}
value={field.value}
isClearable
portalContainer={portalContainer}
/>
)}
/>
</>
);
+2
View File
@@ -2623,12 +2623,14 @@
"placeholder-search-input": "Search by name or enter filter query..."
},
"grafana-folder": "Grafana folder",
"group-autocomplete-unavailable": "Due to large number of groups, autocomplete is not available",
"health": "Health",
"label": {
"hide": "Hide",
"show": "Show"
},
"manage-alerts": "In these data sources, you can select Manage alerts via Alerting UI to be able to manage these alert rules in the Grafana UI as well as in the data source where they were configured.",
"namespace-autocomplete-unavailable": "Due to large number of folders, autocomplete is not available",
"placeholder-all-data-sources": "All data sources",
"placeholder-contact-point": "Select contact point",
"placeholder-data-sources": "Select data sources",