Library Panels: Cancel in flight previous search requests (#112606)
This commit is contained in:
+4
@@ -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),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+27
-4
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user