RTK APIs: Extract base query logic (#99800)

* RTK APIs: Extract base query function

* Add error handling

* Add return type

* Use createBaseQuery in browseDashboards

* Support custom manageError

* Export getConfigError

* Remove redundant type

* data -> body
This commit is contained in:
Alex Khomenko 2025-01-31 14:25:16 +02:00 committed by GitHub
parent 111f973242
commit f51eacef9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 79 additions and 167 deletions

View File

@ -0,0 +1,43 @@
import { BaseQueryFn } from '@reduxjs/toolkit/query';
import { lastValueFrom } from 'rxjs';
import { BackendSrvRequest, getBackendSrv, isFetchError } from '@grafana/runtime';
interface RequestOptions extends BackendSrvRequest {
manageError?: (err: unknown) => { error: unknown };
body?: BackendSrvRequest['data'];
}
export function createBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn<RequestOptions> {
async function backendSrvBaseQuery(requestOptions: RequestOptions) {
try {
const { data: responseData, ...meta } = await lastValueFrom(
getBackendSrv().fetch({
...requestOptions,
url: baseURL + requestOptions.url,
showErrorAlert: requestOptions.showErrorAlert ?? false,
data: requestOptions.body,
})
);
return { data: responseData, meta };
} catch (error) {
if (requestOptions.manageError) {
return requestOptions.manageError(error);
} else {
return handleRequestError(error);
}
}
}
return backendSrvBaseQuery;
}
export function handleRequestError(error: unknown) {
if (isFetchError(error)) {
return { error: new Error(error.data.message) };
} else if (error instanceof Error) {
return { error };
} else {
return { error: new Error('Unknown error') };
}
}

View File

@ -1,10 +1,10 @@
import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react';
import { lastValueFrom } from 'rxjs';
import { createApi } from '@reduxjs/toolkit/query/react';
import { AppEvents, isTruthy, locationUtil } from '@grafana/data';
import { BackendSrvRequest, config, getBackendSrv, locationService } from '@grafana/runtime';
import { config, getBackendSrv, locationService } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { createBaseQuery, handleRequestError } from 'app/api/createBaseQuery';
import appEvents from 'app/core/app_events';
import { contextSrv } from 'app/core/core';
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
@ -28,11 +28,6 @@ import { DashboardTreeSelection } from '../types';
import { PAGE_SIZE } from './services';
interface RequestOptions extends BackendSrvRequest {
manageError?: (err: unknown) => { error: unknown };
showErrorAlert?: boolean;
}
interface DeleteItemsArgs {
selectedItems: Omit<DashboardTreeSelection, 'panel' | '$all'>;
}
@ -64,28 +59,6 @@ interface HardDeleteDashboardArgs {
dashboardUID: string;
}
function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn<RequestOptions> {
async function backendSrvBaseQuery(requestOptions: RequestOptions) {
// Suppress error pop-up for root (aka 'general') folder
const isGeneralFolder = requestOptions.url === `/folders/general`;
requestOptions = isGeneralFolder ? { ...requestOptions, showErrorAlert: false } : requestOptions;
try {
const { data: responseData, ...meta } = await lastValueFrom(
getBackendSrv().fetch({
...requestOptions,
url: baseURL + requestOptions.url,
})
);
return { data: responseData, meta };
} catch (error) {
return requestOptions.manageError ? requestOptions.manageError(error) : { error };
}
}
return backendSrvBaseQuery;
}
export interface ListFolderQueryArgs {
page: number;
parentUid: string | undefined;
@ -96,7 +69,7 @@ export interface ListFolderQueryArgs {
export const browseDashboardsAPI = createApi({
tagTypes: ['getFolder'],
reducerPath: 'browseDashboardsAPI',
baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
baseQuery: createBaseQuery({ baseURL: '/api' }),
endpoints: (builder) => ({
listFolders: builder.query<FolderListItemDTO[], ListFolderQueryArgs>({
providesTags: (result) => result?.map((folder) => ({ type: 'getFolder', id: folder.uid })) ?? [],
@ -117,7 +90,7 @@ export const browseDashboardsAPI = createApi({
query: ({ title, parentUid }) => ({
method: 'POST',
url: '/folders',
data: {
body: {
title,
parentUid,
},
@ -144,7 +117,7 @@ export const browseDashboardsAPI = createApi({
query: ({ uid, title, version }) => ({
method: 'PUT',
url: `/folders/${uid}`,
data: {
body: {
title,
version,
},
@ -167,7 +140,7 @@ export const browseDashboardsAPI = createApi({
query: ({ folder, destinationUID }) => ({
url: `/folders/${folder.uid}/move`,
method: 'POST',
data: { parentUID: destinationUID },
body: { parentUID: destinationUID },
}),
onQueryStarted: ({ folder, destinationUID }, { queryFulfilled, dispatch }) => {
const { parentUid } = folder;
@ -256,7 +229,7 @@ export const browseDashboardsAPI = createApi({
await baseQuery({
url: `/folders/${folderUID}/move`,
method: 'POST',
data: { parentUID: destinationUID },
body: { parentUID: destinationUID },
});
}
@ -363,7 +336,7 @@ export const browseDashboardsAPI = createApi({
}
throw new Error('Invalid dashboard version');
} catch (error) {
return { error };
return handleRequestError(error);
}
},
@ -385,7 +358,7 @@ export const browseDashboardsAPI = createApi({
query: ({ dashboard, overwrite, inputs, folderUid }) => ({
method: 'POST',
url: '/dashboards/import',
data: {
body: {
dashboard,
overwrite,
inputs,
@ -410,7 +383,7 @@ export const browseDashboardsAPI = createApi({
restoreDashboard: builder.mutation<void, RestoreDashboardArgs>({
query: ({ dashboardUID, targetFolderUID }) => ({
url: `/dashboards/uid/${dashboardUID}/trash`,
data: {
body: {
folderUid: targetFolderUID,
},
method: 'PATCH',

View File

@ -1,7 +1,7 @@
import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react';
import { lastValueFrom } from 'rxjs';
import { createApi } from '@reduxjs/toolkit/query/react';
import { BackendSrvRequest, config, FetchError, getBackendSrv, isFetchError } from '@grafana/runtime/src';
import { config, FetchError, isFetchError } from '@grafana/runtime/src';
import { createBaseQuery } from 'app/api/createBaseQuery';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
import { t } from 'app/core/internationalization';
@ -19,39 +19,17 @@ import {
PublicDashboardListWithPaginationResponse,
} from 'app/features/manage-dashboards/types';
type ReqOptions = {
manageError?: (err: unknown) => { error: unknown };
showErrorAlert?: boolean;
};
function isFetchBaseQueryError(error: unknown): error is { error: FetchError } {
return typeof error === 'object' && error != null && 'error' in error;
}
const backendSrvBaseQuery =
({ baseUrl }: { baseUrl: string }): BaseQueryFn<BackendSrvRequest & ReqOptions> =>
async (requestOptions) => {
try {
const { data: responseData, ...meta } = await lastValueFrom(
getBackendSrv().fetch({
...requestOptions,
url: baseUrl + requestOptions.url,
showErrorAlert: requestOptions.showErrorAlert,
})
);
return { data: responseData, meta };
} catch (error) {
return requestOptions.manageError ? requestOptions.manageError(error) : { error };
}
};
const getConfigError = (err: unknown) => ({
export const getConfigError = (err: unknown) => ({
error: isFetchError(err) && err.data.messageId !== 'publicdashboards.notFound' ? err : null,
});
export const publicDashboardApi = createApi({
reducerPath: 'publicDashboardApi',
baseQuery: backendSrvBaseQuery({ baseUrl: '/api' }),
baseQuery: createBaseQuery({ baseURL: '/api' }),
tagTypes: ['PublicDashboard', 'AuditTablePublicDashboard', 'UsersWithActiveSessions', 'ActiveUserDashboards'],
refetchOnMountOrArgChange: true,
endpoints: (builder) => ({
@ -81,7 +59,7 @@ export const publicDashboardApi = createApi({
return {
url: `/dashboards/uid/${dashUid}/public-dashboards`,
method: 'POST',
data: params.payload,
body: params.payload,
};
},
async onQueryStarted({ dashboard, payload: { share } }, { dispatch, queryFulfilled }) {
@ -121,7 +99,7 @@ export const publicDashboardApi = createApi({
return {
url: `/dashboards/uid/${dashUid}/public-dashboards/${payload.uid}`,
method: 'PATCH',
data: payload,
body: payload,
};
},
async onQueryStarted({ dashboard }, { dispatch, queryFulfilled }) {
@ -153,7 +131,7 @@ export const publicDashboardApi = createApi({
return {
url: `/dashboards/uid/${dashUid}/public-dashboards/${payload.uid}`,
method: 'PATCH',
data: payload,
body: payload,
};
},
async onQueryStarted({ dashboard, payload: { isEnabled } }, { dispatch, queryFulfilled }) {
@ -193,7 +171,7 @@ export const publicDashboardApi = createApi({
return {
url: `/dashboards/uid/${dashUid}/public-dashboards/${payload.uid}`,
method: 'PATCH',
data: payload,
body: payload,
};
},
async onQueryStarted({ dashboard, payload: { share } }, { dispatch, queryFulfilled }) {

View File

@ -53,7 +53,7 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) {
return uniqBy(loadQueryMetadataResult.value, 'datasourceName').map((row) => row.datasourceName);
}, [loadQueryMetadataResult.value]);
if (error) {
if (error instanceof Error) {
return (
<EmptyState variant="not-found" message={`Something went wrong`}>
{error.message}

View File

@ -1,38 +1,9 @@
import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react';
import { lastValueFrom } from 'rxjs';
import { createApi } from '@reduxjs/toolkit/query/react';
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
interface RequestOptions extends BackendSrvRequest {
manageError?: (err: unknown) => { error: unknown };
showErrorAlert?: boolean;
// rtk codegen sets this
body?: BackendSrvRequest['data'];
}
function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn<RequestOptions> {
async function backendSrvBaseQuery(requestOptions: RequestOptions) {
try {
const { data: responseData, ...meta } = await lastValueFrom(
getBackendSrv().fetch({
...requestOptions,
url: baseURL + requestOptions.url,
showErrorAlert: false,
data: requestOptions.body,
})
);
return { data: responseData, meta };
} catch (error) {
return requestOptions.manageError ? requestOptions.manageError(error) : { error };
}
}
return backendSrvBaseQuery;
}
import { createBaseQuery } from 'app/api/createBaseQuery';
export const baseAPI = createApi({
reducerPath: 'migrateToCloudGeneratedAPI',
baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
baseQuery: createBaseQuery({ baseURL: '/api' }),
endpoints: () => ({}),
});

View File

@ -1,11 +1,14 @@
export * from './endpoints.gen';
import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/query';
import { getLocalPlugins } from 'app/features/plugins/admin/api';
import { LocalPlugin } from 'app/features/plugins/admin/types';
import { handleRequestError } from '../../../api/createBaseQuery';
import { generatedAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const cloudMigrationAPI = generatedAPI
.injectEndpoints({
endpoints: (build) => ({
@ -16,7 +19,7 @@ export const cloudMigrationAPI = generatedAPI
const list = await getLocalPlugins();
return { data: list };
} catch (error) {
return { error: error };
return handleRequestError(error);
}
},
}),

View File

@ -1,36 +1,9 @@
import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react';
import { lastValueFrom } from 'rxjs';
import { createApi } from '@reduxjs/toolkit/query/react';
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
interface RequestOptions extends BackendSrvRequest {
manageError?: (err: unknown) => { error: unknown };
showErrorAlert?: boolean;
body?: BackendSrvRequest['data'];
}
function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn<RequestOptions> {
async function backendSrvBaseQuery(requestOptions: RequestOptions) {
try {
const { data: responseData, ...meta } = await lastValueFrom(
getBackendSrv().fetch({
...requestOptions,
url: baseURL + requestOptions.url,
showErrorAlert: requestOptions.showErrorAlert,
data: requestOptions.body,
})
);
return { data: responseData, meta };
} catch (error) {
return requestOptions.manageError ? requestOptions.manageError(error) : { error };
}
}
return backendSrvBaseQuery;
}
import { createBaseQuery } from 'app/api/createBaseQuery';
export const baseAPI = createApi({
reducerPath: 'userPreferencesAPI',
baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
baseQuery: createBaseQuery({ baseURL: '/api' }),
endpoints: () => ({}),
});

View File

@ -1,12 +1,14 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { baseQuery } from './query';
import { createBaseQuery } from '../../../api/createBaseQuery';
import { BASE_URL } from './query';
// Currently, we are loading all query templates
// Organizations can have maximum of 1000 query templates
export const QUERY_LIBRARY_GET_LIMIT = 1000;
export const queryLibraryApi = createApi({
baseQuery,
baseQuery: createBaseQuery({ baseURL: BASE_URL }),
endpoints: () => ({}),
});

View File

@ -1,8 +1,3 @@
import { BaseQueryFn } from '@reduxjs/toolkit/query/react';
import { lastValueFrom } from 'rxjs';
import { BackendSrvRequest, getBackendSrv, isFetchError } from '@grafana/runtime/src/services/backendSrv';
import { getAPINamespace } from '../../../api/utils';
/**
@ -23,29 +18,3 @@ export enum QueryTemplateKinds {
* @alpha
*/
export const BASE_URL = `/apis/${API_VERSION}/namespaces/${getAPINamespace()}`;
interface QueryLibraryBackendRequest extends BackendSrvRequest {
body?: BackendSrvRequest['data'];
}
export const baseQuery: BaseQueryFn<QueryLibraryBackendRequest, unknown, Error> = async (requestOptions) => {
try {
const responseObservable = getBackendSrv().fetch({
url: `${BASE_URL}/${requestOptions.url ?? ''}`,
showErrorAlert: true,
method: requestOptions.method || 'GET',
data: requestOptions.body,
params: requestOptions.params,
headers: { ...requestOptions.headers },
});
return await lastValueFrom(responseObservable);
} catch (error) {
if (isFetchError(error)) {
return { error: new Error(error.data.message) };
} else if (error instanceof Error) {
return { error };
} else {
return { error: new Error('Unknown error') };
}
}
};