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 { result?: T; loading: boolean; error?: SerializedError; dispatched: boolean; requestId?: string; } export const initialAsyncRequestState: Pick< AsyncRequestState, 'loading' | 'dispatched' | 'result' | 'error' > = Object.freeze({ loading: false, result: undefined, error: undefined, dispatched: false, }); export type AsyncRequestMapSlice = Record>; export type AsyncRequestAction = PayloadAction, string, any, any>; function requestStateReducer( asyncThunk: AsyncThunk, state: Draft> = initialAsyncRequestState, action: AsyncRequestAction ): Draft> { 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( name: string, asyncThunk: AsyncThunk ) { return createSlice({ name, initialState: initialAsyncRequestState as AsyncRequestState, reducers: {}, extraReducers: (builder) => builder.addDefaultCase((state, action) => requestStateReducer(asyncThunk, state, action as unknown as AsyncRequestAction) ), }); } /* * 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( name: string, asyncThunk: AsyncThunk, getEntityId: (arg: ThunkArg) => string ) { return createSlice({ name, initialState: {} as AsyncRequestMapSlice, reducers: {}, extraReducers: (builder) => builder.addDefaultCase((state, action) => { if (isAsyncThunkAction(asyncThunk)(action)) { const asyncAction = action as unknown as AsyncRequestAction; 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(p: Promise): Promise { return p.catch((e) => { const err: SerializedError = { message: messageFromError(e), code: e.statusCode, }; throw err; }); } export function withAppEvents( p: Promise, options: { successMessage?: string; errorMessage?: string } ): Promise { 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(slice: AsyncRequestMapSlice): boolean { return Object.values(slice).every(isAsyncRequestStateFulfilled); } export function isAsyncRequestStateFulfilled(state: AsyncRequestState): boolean { return state.dispatched && !state.loading && !state.error; } export function isAsyncRequestMapSlicePending(slice: AsyncRequestMapSlice): boolean { return Object.values(slice).some(isAsyncRequestStatePending); } export function isAsyncRequestStatePending(state: AsyncRequestState): boolean { return state.dispatched && state.loading; }