123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- import { AsyncThunk, createSlice, Draft, isAsyncThunkAction, PayloadAction, SerializedError } from '@reduxjs/toolkit';
- import { AppEvents } from '@grafana/data';
- import { FetchError } from '@grafana/runtime';
- import { appEvents } from 'app/core/core';
- import { isFetchError } from './alertmanager';
- 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 = {}>(
- 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 it's 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 = {}>(
- 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 = {}>(
- 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 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;
- }
- }
- return (e as Error)?.message || String(e);
- }
- 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 isAsyncRequestStatePending<T>(state: AsyncRequestState<T>): boolean {
- return state.dispatched && state.loading;
- }
|