redux.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { AsyncThunk, createSlice, Draft, isAsyncThunkAction, PayloadAction, SerializedError } from '@reduxjs/toolkit';
  2. import { AppEvents } from '@grafana/data';
  3. import { FetchError } from '@grafana/runtime';
  4. import { appEvents } from 'app/core/core';
  5. import { isFetchError } from './alertmanager';
  6. export interface AsyncRequestState<T> {
  7. result?: T;
  8. loading: boolean;
  9. error?: SerializedError;
  10. dispatched: boolean;
  11. requestId?: string;
  12. }
  13. export const initialAsyncRequestState: Pick<
  14. AsyncRequestState<undefined>,
  15. 'loading' | 'dispatched' | 'result' | 'error'
  16. > = Object.freeze({
  17. loading: false,
  18. result: undefined,
  19. error: undefined,
  20. dispatched: false,
  21. });
  22. export type AsyncRequestMapSlice<T> = Record<string, AsyncRequestState<T>>;
  23. export type AsyncRequestAction<T> = PayloadAction<Draft<T>, string, any, any>;
  24. function requestStateReducer<T, ThunkArg = void, ThunkApiConfig = {}>(
  25. asyncThunk: AsyncThunk<T, ThunkArg, ThunkApiConfig>,
  26. state: Draft<AsyncRequestState<T>> = initialAsyncRequestState,
  27. action: AsyncRequestAction<T>
  28. ): Draft<AsyncRequestState<T>> {
  29. if (asyncThunk.pending.match(action)) {
  30. return {
  31. result: state.result,
  32. loading: true,
  33. error: state.error,
  34. dispatched: true,
  35. requestId: action.meta.requestId,
  36. };
  37. } else if (asyncThunk.fulfilled.match(action)) {
  38. if (state.requestId === undefined || state.requestId === action.meta.requestId) {
  39. return {
  40. ...state,
  41. result: action.payload,
  42. loading: false,
  43. error: undefined,
  44. };
  45. }
  46. } else if (asyncThunk.rejected.match(action)) {
  47. if (state.requestId === action.meta.requestId) {
  48. return {
  49. ...state,
  50. loading: false,
  51. error: action.error,
  52. };
  53. }
  54. }
  55. return state;
  56. }
  57. /*
  58. * createAsyncSlice creates a slice based on a given async action, exposing it's state.
  59. * takes care to only use state of the latest invocation of the action if there are several in flight.
  60. */
  61. export function createAsyncSlice<T, ThunkArg = void, ThunkApiConfig = {}>(
  62. name: string,
  63. asyncThunk: AsyncThunk<T, ThunkArg, ThunkApiConfig>
  64. ) {
  65. return createSlice({
  66. name,
  67. initialState: initialAsyncRequestState as AsyncRequestState<T>,
  68. reducers: {},
  69. extraReducers: (builder) =>
  70. builder.addDefaultCase((state, action) =>
  71. requestStateReducer(asyncThunk, state, action as unknown as AsyncRequestAction<T>)
  72. ),
  73. });
  74. }
  75. /*
  76. * createAsyncMapSlice creates a slice based on a given async action exposing a map of request states.
  77. * separate requests are uniquely indentified by result of provided getEntityId function
  78. * takes care to only use state of the latest invocation of the action if there are several in flight.
  79. */
  80. export function createAsyncMapSlice<T, ThunkArg = void, ThunkApiConfig = {}>(
  81. name: string,
  82. asyncThunk: AsyncThunk<T, ThunkArg, ThunkApiConfig>,
  83. getEntityId: (arg: ThunkArg) => string
  84. ) {
  85. return createSlice({
  86. name,
  87. initialState: {} as AsyncRequestMapSlice<T>,
  88. reducers: {},
  89. extraReducers: (builder) =>
  90. builder.addDefaultCase((state, action) => {
  91. if (isAsyncThunkAction(asyncThunk)(action)) {
  92. const asyncAction = action as unknown as AsyncRequestAction<T>;
  93. const entityId = getEntityId(asyncAction.meta.arg);
  94. return {
  95. ...state,
  96. [entityId]: requestStateReducer(asyncThunk, state[entityId], asyncAction),
  97. };
  98. }
  99. return state;
  100. }),
  101. });
  102. }
  103. // rethrow promise error in redux serialized format
  104. export function withSerializedError<T>(p: Promise<T>): Promise<T> {
  105. return p.catch((e) => {
  106. const err: SerializedError = {
  107. message: messageFromError(e),
  108. code: e.statusCode,
  109. };
  110. throw err;
  111. });
  112. }
  113. export function withAppEvents<T>(
  114. p: Promise<T>,
  115. options: { successMessage?: string; errorMessage?: string }
  116. ): Promise<T> {
  117. return p
  118. .then((v) => {
  119. if (options.successMessage) {
  120. appEvents.emit(AppEvents.alertSuccess, [options.successMessage]);
  121. }
  122. return v;
  123. })
  124. .catch((e) => {
  125. const msg = messageFromError(e);
  126. appEvents.emit(AppEvents.alertError, [`${options.errorMessage ?? 'Error'}: ${msg}`]);
  127. throw e;
  128. });
  129. }
  130. export function messageFromError(e: Error | FetchError | SerializedError): string {
  131. if (isFetchError(e)) {
  132. if (e.data?.message) {
  133. let msg = e.data?.message;
  134. if (typeof e.data?.error === 'string') {
  135. msg += `; ${e.data.error}`;
  136. }
  137. return msg;
  138. } else if (Array.isArray(e.data) && e.data.length && e.data[0]?.message) {
  139. return e.data
  140. .map((d) => d?.message)
  141. .filter((m) => !!m)
  142. .join(' ');
  143. } else if (e.statusText) {
  144. return e.statusText;
  145. }
  146. }
  147. return (e as Error)?.message || String(e);
  148. }
  149. export function isAsyncRequestMapSliceFulfilled<T>(slice: AsyncRequestMapSlice<T>): boolean {
  150. return Object.values(slice).every(isAsyncRequestStateFulfilled);
  151. }
  152. export function isAsyncRequestStateFulfilled<T>(state: AsyncRequestState<T>): boolean {
  153. return state.dispatched && !state.loading && !state.error;
  154. }
  155. export function isAsyncRequestMapSlicePending<T>(slice: AsyncRequestMapSlice<T>): boolean {
  156. return Object.values(slice).some(isAsyncRequestStatePending);
  157. }
  158. export function isAsyncRequestStatePending<T>(state: AsyncRequestState<T>): boolean {
  159. return state.dispatched && state.loading;
  160. }