Library Panels: Cancel in flight previous search requests (#112606)

This commit is contained in:
Ezequiel Victorero
2025-10-20 12:26:29 -03:00
committed by GitHub
parent caac463427
commit 8c9624ba5f
4 changed files with 64 additions and 17 deletions
@@ -120,6 +120,7 @@ describe('LibraryPanelsSearch', () => {
page: 0,
typeFilter: [],
perPage: 40,
signal: expect.any(AbortSignal),
})
);
});
@@ -148,6 +149,7 @@ describe('LibraryPanelsSearch', () => {
page: 0,
typeFilter: [],
perPage: 40,
signal: expect.any(AbortSignal),
})
);
});
@@ -176,6 +178,7 @@ describe('LibraryPanelsSearch', () => {
page: 0,
typeFilter: ['graph', 'timeseries'],
perPage: 40,
signal: expect.any(AbortSignal),
})
);
});
@@ -234,6 +237,7 @@ describe('LibraryPanelsSearch', () => {
page: 0,
typeFilter: [],
perPage: 40,
signal: expect.any(AbortSignal),
});
});
});
@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useMemo, useReducer } from 'react';
import { useEffect, useMemo, useReducer, useRef } from 'react';
import { useDebounce } from 'react-use';
import { GrafanaTheme2, LoadingState } from '@grafana/data';
@@ -43,8 +43,20 @@ export const LibraryPanelsView = ({
}
);
const asyncDispatch = useMemo(() => asyncDispatcher(dispatch), [dispatch]);
const abortControllerRef = useRef<AbortController>();
useDebounce(
() =>
() => {
// Abort previous request if it exists
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// Create new AbortController for this request
const abortController = new AbortController();
abortControllerRef.current = abortController;
// Start search with abort controller
asyncDispatch(
searchForLibraryPanels({
searchString,
@@ -54,12 +66,23 @@ export const LibraryPanelsView = ({
page,
perPage,
currentPanelId,
})
),
}),
abortController
);
},
300,
[searchString, sortDirection, panelFilter, folderFilter, page, asyncDispatch]
);
// Cleanup: abort any pending request on unmount
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
const onDelete = ({ uid }: LibraryElementDTO) =>
asyncDispatch(
deleteLibraryPanel(uid, {
@@ -1,4 +1,4 @@
import { AnyAction } from '@reduxjs/toolkit';
import { Action } from '@reduxjs/toolkit';
import { Dispatch } from 'react';
import { from, merge, of, Subscription, timer } from 'rxjs';
import { catchError, finalize, mapTo, mergeMap, share, takeUntil } from 'rxjs/operators';
@@ -7,7 +7,8 @@ import { deleteLibraryPanel as apiDeleteLibraryPanel, getLibraryPanels } from '.
import { initialLibraryPanelsViewState, initSearch, searchCompleted } from './reducer';
type DispatchResult = (dispatch: Dispatch<AnyAction>) => void;
type SearchDispatchResult = (dispatch: Dispatch<Action>, abortController?: AbortController) => void;
interface SearchArgs {
perPage: number;
page: number;
@@ -18,10 +19,10 @@ interface SearchArgs {
currentPanelId?: string;
}
export function searchForLibraryPanels(args: SearchArgs): DispatchResult {
export function searchForLibraryPanels(args: SearchArgs): SearchDispatchResult {
// Functions to support filtering out library panels per plugin type that have skipDataQuery set to true
return function (dispatch) {
return function (dispatch, abortController) {
const subscription = new Subscription();
const dataObservable = from(
getLibraryPanels({
@@ -32,6 +33,7 @@ export function searchForLibraryPanels(args: SearchArgs): DispatchResult {
sortDirection: args.sortDirection,
typeFilter: args.panelFilter,
folderFilterUIDs: args.folderFilterUIDs,
signal: abortController?.signal,
})
).pipe(
//filter out library panels per plugin type that have skipDataQuery set to true
@@ -43,7 +45,18 @@ export function searchForLibraryPanels(args: SearchArgs): DispatchResult {
of(searchCompleted({ libraryPanels, page, perPage, totalCount }))
),
catchError((err) => {
console.error(err);
// Check if this is an aborted request - if so, silently ignore it
const isAbortError =
err.name === 'AbortError' || err.cancelled === true || err.statusText === 'Request was aborted';
if (isAbortError) {
return of(); // Silently ignore aborted requests
}
// For real errors, log and show error to user
console.error('Error fetching library panels:', err);
// Update state to show empty results
return of(searchCompleted({ ...initialLibraryPanelsViewState, page: args.page, perPage: args.perPage }));
}),
finalize(() => subscription.unsubscribe()), // make sure we unsubscribe
@@ -59,8 +72,8 @@ export function searchForLibraryPanels(args: SearchArgs): DispatchResult {
};
}
export function deleteLibraryPanel(uid: string, args: SearchArgs): DispatchResult {
return async function (dispatch) {
export function deleteLibraryPanel(uid: string, args: SearchArgs) {
return async function (dispatch: Dispatch<Action>) {
try {
await apiDeleteLibraryPanel(uid);
searchForLibraryPanels(args)(dispatch);
@@ -70,10 +83,10 @@ export function deleteLibraryPanel(uid: string, args: SearchArgs): DispatchResul
};
}
export function asyncDispatcher(dispatch: Dispatch<AnyAction>) {
return function (action: AnyAction | DispatchResult) {
export function asyncDispatcher(dispatch: Dispatch<Action>) {
return function (action: Action | SearchDispatchResult | Function, abortController?: AbortController) {
if (action instanceof Function) {
return action(dispatch);
return action(dispatch, abortController);
}
return dispatch(action);
};
@@ -25,6 +25,7 @@ export interface GetLibraryPanelsOptions {
sortDirection?: string;
typeFilter?: string[];
folderFilterUIDs?: string[];
signal?: AbortSignal;
}
export async function getLibraryPanels({
@@ -35,6 +36,7 @@ export async function getLibraryPanels({
sortDirection = '',
typeFilter = [],
folderFilterUIDs = [],
signal,
}: GetLibraryPanelsOptions = {}): Promise<LibraryElementsSearchResult> {
const params = new URLSearchParams();
params.append('searchString', searchString);
@@ -46,10 +48,15 @@ export async function getLibraryPanels({
params.append('page', page.toString(10));
params.append('kind', LibraryElementKind.Panel.toString(10));
const { result } = await getBackendSrv().get<{ result: LibraryElementsSearchResult }>(
`/api/library-elements?${params.toString()}`
const response = await lastValueFrom(
getBackendSrv().fetch<{ result: LibraryElementsSearchResult }>({
method: 'GET',
url: `/api/library-elements?${params.toString()}`,
abortSignal: signal,
showErrorAlert: false,
})
);
return result;
return response.data.result;
}
export async function getLibraryPanel(uid: string, isHandled = false): Promise<LibraryElementDTO> {