mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 01:15:46 +08:00

* Lint import member orders within alerting * Consistently order member imports in alerting code
207 lines
6.5 KiB
TypeScript
207 lines
6.5 KiB
TypeScript
import { AsyncThunk, Draft, PayloadAction, SerializedError, createSlice, isAsyncThunkAction } from '@reduxjs/toolkit';
|
|
|
|
import { AppEvents } from '@grafana/data';
|
|
import { FetchError, isFetchError } from '@grafana/runtime';
|
|
import { appEvents } from 'app/core/core';
|
|
|
|
import { LogMessages, logInfo } from '../Analytics';
|
|
|
|
import { isErrorLike } from './misc';
|
|
|
|
export interface AsyncRequestState<T> {
|
|
result?: T;
|
|
loading: boolean;
|
|
error?: SerializedError;
|
|
dispatched: boolean;
|
|
requestId?: string;
|
|
}
|
|
|
|
export const initialAsyncRequestState: Pick<
|
|
AsyncRequestState<undefined>,
|
|
'loading' | 'dispatched' | 'result' | 'error'
|
|
> = Object.freeze({
|
|
loading: false,
|
|
result: undefined,
|
|
error: undefined,
|
|
dispatched: false,
|
|
});
|
|
|
|
export type AsyncRequestMapSlice<T> = Record<string, AsyncRequestState<T>>;
|
|
|
|
export type AsyncRequestAction<T> = PayloadAction<Draft<T>, string, any, any>;
|
|
|
|
function requestStateReducer<T, ThunkArg = void, ThunkApiConfig extends {} = {}>(
|
|
asyncThunk: AsyncThunk<T, ThunkArg, ThunkApiConfig>,
|
|
state: Draft<AsyncRequestState<T>> = initialAsyncRequestState,
|
|
action: AsyncRequestAction<T>
|
|
): Draft<AsyncRequestState<T>> {
|
|
if (asyncThunk.pending.match(action)) {
|
|
return {
|
|
result: state.result,
|
|
loading: true,
|
|
error: state.error,
|
|
dispatched: true,
|
|
requestId: action.meta.requestId,
|
|
};
|
|
} else if (asyncThunk.fulfilled.match(action)) {
|
|
if (state.requestId === undefined || state.requestId === action.meta.requestId) {
|
|
return {
|
|
...state,
|
|
result: action.payload,
|
|
loading: false,
|
|
error: undefined,
|
|
};
|
|
}
|
|
} else if (asyncThunk.rejected.match(action)) {
|
|
if (state.requestId === action.meta.requestId) {
|
|
return {
|
|
...state,
|
|
loading: false,
|
|
error: action.error,
|
|
};
|
|
}
|
|
}
|
|
return state;
|
|
}
|
|
|
|
/*
|
|
* createAsyncSlice creates a slice based on a given async action, exposing its state.
|
|
* takes care to only use state of the latest invocation of the action if there are several in flight.
|
|
*/
|
|
export function createAsyncSlice<T, ThunkArg = void, ThunkApiConfig extends {} = {}>(
|
|
name: string,
|
|
asyncThunk: AsyncThunk<T, ThunkArg, ThunkApiConfig>
|
|
) {
|
|
return createSlice({
|
|
name,
|
|
initialState: initialAsyncRequestState as AsyncRequestState<T>,
|
|
reducers: {},
|
|
extraReducers: (builder) =>
|
|
builder.addDefaultCase((state, action) =>
|
|
requestStateReducer(asyncThunk, state, action as unknown as AsyncRequestAction<T>)
|
|
),
|
|
});
|
|
}
|
|
|
|
/*
|
|
* createAsyncMapSlice creates a slice based on a given async action exposing a map of request states.
|
|
* separate requests are uniquely indentified by result of provided getEntityId function
|
|
* takes care to only use state of the latest invocation of the action if there are several in flight.
|
|
*/
|
|
export function createAsyncMapSlice<T, ThunkArg = void, ThunkApiConfig extends {} = {}>(
|
|
name: string,
|
|
asyncThunk: AsyncThunk<T, ThunkArg, ThunkApiConfig>,
|
|
getEntityId: (arg: ThunkArg) => string
|
|
) {
|
|
return createSlice({
|
|
name,
|
|
initialState: {} as AsyncRequestMapSlice<T>,
|
|
reducers: {},
|
|
extraReducers: (builder) =>
|
|
builder.addDefaultCase((state, action) => {
|
|
if (isAsyncThunkAction(asyncThunk)(action)) {
|
|
const asyncAction = action as unknown as AsyncRequestAction<T>;
|
|
const entityId = getEntityId(asyncAction.meta.arg);
|
|
return {
|
|
...state,
|
|
[entityId]: requestStateReducer(asyncThunk, state[entityId], asyncAction),
|
|
};
|
|
}
|
|
return state;
|
|
}),
|
|
});
|
|
}
|
|
|
|
// rethrow promise error in redux serialized format
|
|
export function withSerializedError<T>(p: Promise<T>): Promise<T> {
|
|
return p.catch((e) => {
|
|
const err: SerializedError = {
|
|
message: messageFromError(e),
|
|
code: e.statusCode,
|
|
};
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
export function withAppEvents<T>(
|
|
p: Promise<T>,
|
|
options: { successMessage?: string; errorMessage?: string }
|
|
): Promise<T> {
|
|
return p
|
|
.then((v) => {
|
|
if (options.successMessage) {
|
|
appEvents.emit(AppEvents.alertSuccess, [options.successMessage]);
|
|
}
|
|
return v;
|
|
})
|
|
.catch((e) => {
|
|
const msg = messageFromError(e);
|
|
appEvents.emit(AppEvents.alertError, [`${options.errorMessage ?? 'Error'}: ${msg}`]);
|
|
throw e;
|
|
});
|
|
}
|
|
|
|
export const UNKNOW_ERROR = 'Unknown Error';
|
|
export function messageFromError(e: Error | FetchError | SerializedError): string {
|
|
if (isFetchError(e)) {
|
|
if (e.data?.message) {
|
|
let msg = e.data?.message;
|
|
if (typeof e.data?.error === 'string') {
|
|
msg += `; ${e.data.error}`;
|
|
}
|
|
return msg;
|
|
} else if (Array.isArray(e.data) && e.data.length && e.data[0]?.message) {
|
|
return e.data
|
|
.map((d) => d?.message)
|
|
.filter((m) => !!m)
|
|
.join(' ');
|
|
} else if (e.statusText) {
|
|
return e.statusText;
|
|
}
|
|
}
|
|
// message in e object, return message
|
|
if (isErrorLike(e)) {
|
|
return e.message;
|
|
}
|
|
// for some reason (upstream this code), sometimes we get an object without the message field neither in the e.data and nor in e.message
|
|
// in this case we want to avoid String(e) printing [object][object]
|
|
logInfo(LogMessages.unknownMessageFromError, { error: JSON.stringify(e) });
|
|
return UNKNOW_ERROR;
|
|
}
|
|
|
|
export function isAsyncRequestMapSliceSettled<T>(slice: AsyncRequestMapSlice<T>): boolean {
|
|
return Object.values(slice).every(isAsyncRequestStateSettled);
|
|
}
|
|
|
|
export function isAsyncRequestStateSettled<T>(state: AsyncRequestState<T>): boolean {
|
|
return state.dispatched && !state.loading;
|
|
}
|
|
|
|
export function isAsyncRequestMapSliceFulfilled<T>(slice: AsyncRequestMapSlice<T>): boolean {
|
|
return Object.values(slice).every(isAsyncRequestStateFulfilled);
|
|
}
|
|
|
|
export function isAsyncRequestStateFulfilled<T>(state: AsyncRequestState<T>): boolean {
|
|
return state.dispatched && !state.loading && !state.error;
|
|
}
|
|
|
|
export function isAsyncRequestMapSlicePending<T>(slice: AsyncRequestMapSlice<T>): boolean {
|
|
return Object.values(slice).some(isAsyncRequestStatePending);
|
|
}
|
|
|
|
export function isAsyncRequestMapSlicePartiallyDispatched<T>(slice: AsyncRequestMapSlice<T>): boolean {
|
|
return Object.values(slice).some((state) => state.dispatched);
|
|
}
|
|
|
|
export function isAsyncRequestMapSlicePartiallyFulfilled<T>(slice: AsyncRequestMapSlice<T>): boolean {
|
|
return Object.values(slice).some(isAsyncRequestStateFulfilled);
|
|
}
|
|
|
|
export function isAsyncRequestStatePending<T>(state?: AsyncRequestState<T>): boolean {
|
|
if (!state) {
|
|
return false;
|
|
}
|
|
|
|
return state.dispatched && state.loading;
|
|
}
|