From 2430e21ffaab4ee1d39e97bf3397dda2572ced04 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Mon, 3 Mar 2025 16:14:34 +0100 Subject: [PATCH] Alerting: Fix no search results loop for new list UI (#101513) --- .../alerting/unified/rule-list/FilterView.tsx | 10 ++++++++-- .../rule-list/hooks/useFilteredRulesIterator.ts | 13 +++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/public/app/features/alerting/unified/rule-list/FilterView.tsx b/public/app/features/alerting/unified/rule-list/FilterView.tsx index 25358a57390..ea6b3ceec49 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.tsx @@ -1,4 +1,5 @@ -import { take, tap, withAbort } from 'ix/asynciterable/operators'; +import { empty } from 'ix/asynciterable'; +import { catchError, take, tap, withAbort } from 'ix/asynciterable/operators'; import { useEffect, useRef, useState, useTransition } from 'react'; import { Card, EmptyState, Stack, Text } from '@grafana/ui'; @@ -75,7 +76,12 @@ function FilterViewResults({ filterState }: FilterViewProps) { /* This function will fetch a page of results from the iterable */ const [{ execute: loadResultPage }, state] = useAsync(async () => { - for await (const rule of rulesIterator.current.pipe(take(FRONTENT_PAGE_SIZE))) { + for await (const rule of rulesIterator.current.pipe( + // grab from the rules iterable + take(FRONTENT_PAGE_SIZE), + // if an error occurs trying to fetch a page, return an empty iterable so the front-end isn't caught in an infinite loop + catchError(() => empty()) + )) { startTransition(() => { // Rule key could be computed on the fly, but we do it here to avoid recalculating it with each render // It's a not trivial computation because it involves hashing the rule diff --git a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts index ddbfea8e216..6512ddcea18 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts @@ -1,4 +1,4 @@ -import { AsyncIterableX, from } from 'ix/asynciterable/index'; +import { AsyncIterableX, empty, from } from 'ix/asynciterable'; import { merge } from 'ix/asynciterable/merge'; import { filter, flatMap, map } from 'ix/asynciterable/operators'; import { compact } from 'lodash'; @@ -68,11 +68,16 @@ export function useFilteredRulesIteratorProvider() { map(([group, rule]) => mapGrafanaRuleToRuleWithOrigin(group, rule)) ); - const [source, ...iterables] = ruleSourcesToFetchFrom.map((ds) => { - return from(prometheusGroupsGenerator(ds, groupLimit)).pipe(map((group) => [ds, group] as const)); + const sourceIterables = ruleSourcesToFetchFrom.map((ds) => { + const generator = prometheusGroupsGenerator(ds, groupLimit); + return from(generator).pipe(map((group) => [ds, group] as const)); }); - const dataSourcesIterator = merge(source, ...iterables).pipe( + // if we have no prometheus data sources, use an empty async iterable + const source = sourceIterables.at(0) ?? empty(); + const otherIterables = sourceIterables.slice(1); + + const dataSourcesIterator = merge(source, ...otherIterables).pipe( filter(([_, group]) => groupFilter(group, normalizedFilterState)), flatMap(([rulesSource, group]) => group.rules.map((rule) => [rulesSource, group, rule] as const)), filter(([_, __, rule]) => ruleFilter(rule, filterState)),