ydb-embedded-ui 1.13.1 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/assets/icons/flask.svg +3 -0
  3. package/dist/components/InfoViewer/formatters/common.ts +15 -0
  4. package/dist/components/InfoViewer/formatters/index.ts +2 -0
  5. package/dist/components/InfoViewer/formatters/schema.ts +43 -0
  6. package/dist/components/InfoViewer/schemaInfo/CDCStreamInfo.tsx +44 -0
  7. package/dist/components/InfoViewer/schemaInfo/PersQueueGroupInfo.tsx +34 -0
  8. package/dist/components/{IndexInfoViewer/IndexInfoViewer.tsx → InfoViewer/schemaInfo/TableIndexInfo.tsx} +7 -18
  9. package/dist/components/InfoViewer/schemaInfo/index.ts +3 -0
  10. package/dist/components/InfoViewer/schemaOverview/CDCStreamOverview.tsx +44 -0
  11. package/dist/components/InfoViewer/schemaOverview/PersQueueGroupOverview.tsx +35 -0
  12. package/dist/components/InfoViewer/schemaOverview/index.ts +2 -0
  13. package/dist/components/QueryResultTable/Cell/Cell.tsx +33 -0
  14. package/dist/components/QueryResultTable/Cell/index.ts +1 -0
  15. package/dist/components/QueryResultTable/QueryResultTable.scss +11 -0
  16. package/dist/components/QueryResultTable/QueryResultTable.tsx +115 -0
  17. package/dist/components/QueryResultTable/i18n/en.json +3 -0
  18. package/dist/components/QueryResultTable/i18n/index.ts +11 -0
  19. package/dist/components/QueryResultTable/i18n/ru.json +3 -0
  20. package/dist/components/QueryResultTable/index.ts +1 -0
  21. package/dist/containers/App/App.scss +1 -0
  22. package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.scss +39 -14
  23. package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.tsx +18 -7
  24. package/dist/containers/Storage/Pdisk/__tests__/colors.tsx +4 -3
  25. package/dist/containers/Storage/Vdisk/__tests__/colors.tsx +7 -7
  26. package/dist/containers/Tenant/Acl/Acl.js +7 -1
  27. package/dist/containers/Tenant/Diagnostics/DiagnosticsPages.ts +6 -2
  28. package/dist/containers/Tenant/Diagnostics/HotKeys/HotKeys.js +1 -1
  29. package/dist/containers/Tenant/Diagnostics/Overview/Overview.tsx +8 -3
  30. package/dist/containers/Tenant/Diagnostics/TopQueries/TopQueries.js +1 -1
  31. package/dist/containers/Tenant/Diagnostics/TopShards/TopShards.js +1 -1
  32. package/dist/containers/Tenant/ObjectSummary/ObjectSummary.tsx +36 -10
  33. package/dist/containers/Tenant/Preview/Preview.js +15 -57
  34. package/dist/containers/Tenant/Preview/Preview.scss +4 -8
  35. package/dist/containers/Tenant/QueryEditor/QueryEditor.js +12 -41
  36. package/dist/containers/Tenant/QueryEditor/QueryEditor.scss +1 -5
  37. package/dist/containers/Tenant/QueryEditor/QueryExplain/QueryExplain.scss +1 -2
  38. package/dist/containers/Tenant/QueryEditor/QueryResult/QueryResult.scss +2 -2
  39. package/dist/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx +9 -1
  40. package/dist/containers/Tenant/Tenant.scss +2 -50
  41. package/dist/containers/Tenant/Tenant.tsx +24 -22
  42. package/dist/containers/Tenant/utils/schema.ts +3 -0
  43. package/dist/containers/Tenant/utils/schemaActions.ts +1 -2
  44. package/dist/containers/Tenants/Tenants.js +12 -2
  45. package/dist/containers/UserSettings/UserSettings.tsx +26 -3
  46. package/dist/services/api.d.ts +19 -2
  47. package/dist/services/api.js +2 -2
  48. package/dist/setupTests.js +4 -0
  49. package/dist/store/reducers/executeQuery.js +4 -9
  50. package/dist/store/reducers/{preview.js → preview.ts} +22 -18
  51. package/dist/store/reducers/settings.js +3 -1
  52. package/dist/store/utils.ts +88 -0
  53. package/dist/types/api/query.ts +147 -0
  54. package/dist/types/api/schema.ts +235 -2
  55. package/dist/types/index.ts +33 -0
  56. package/dist/types/store/query.ts +9 -0
  57. package/dist/utils/{constants.js → constants.ts} +11 -6
  58. package/dist/utils/index.js +0 -24
  59. package/dist/utils/query.test.ts +189 -0
  60. package/dist/utils/query.ts +156 -0
  61. package/dist/utils/tests/providers.tsx +29 -0
  62. package/package.json +2 -2
  63. package/dist/store/utils.js +0 -51
@@ -1,3 +1,4 @@
1
+ import {useEffect} from 'react';
1
2
  import {useDispatch} from 'react-redux';
2
3
 
3
4
  import {NavigationTree} from 'ydb-ui-components';
@@ -13,7 +14,7 @@ import {getActions} from '../../utils/schemaActions';
13
14
  interface SchemaTreeProps {
14
15
  rootPath: string;
15
16
  rootName: string;
16
- rootType: EPathType;
17
+ rootType?: EPathType;
17
18
  currentPath: string;
18
19
  }
19
20
 
@@ -48,6 +49,13 @@ export function SchemaTree(props: SchemaTreeProps) {
48
49
  dispatch(getSchemaAcl({path: activePath}));
49
50
  };
50
51
 
52
+ useEffect(() => {
53
+ // if the cached path is not in the current tree, show root
54
+ if (!currentPath.startsWith(rootPath)) {
55
+ handleActivePathUpdate(rootPath);
56
+ }
57
+ }, []);
58
+
51
59
  return (
52
60
  <NavigationTree
53
61
  rootState={{
@@ -11,55 +11,7 @@
11
11
  overflow: initial;
12
12
  }
13
13
 
14
- &__loader {
15
- display: flex;
16
- flex: 1 1 auto;
17
- justify-content: center;
18
- align-items: center;
19
- }
20
-
21
- &__content {
22
- overflow: auto;
23
- @include flex-container();
24
- }
25
-
26
- &__storage {
27
- overflow: auto;
28
-
29
- height: 100%;
30
- padding-left: 20px;
31
- }
32
-
33
- &__row {
34
- display: flex;
35
- align-items: flex-start;
36
-
37
- &_schema {
38
- height: 100%;
39
- }
40
-
41
- &_schema-tags {
42
- align-items: center;
43
- }
44
- }
45
-
46
- &__schema-wrapper {
47
- overflow: auto;
48
-
49
- min-width: 300px;
50
- height: 100%;
51
- padding-top: 15px;
52
- padding-left: 8px;
53
- }
54
-
55
- &__schema-tabs-wrapper {
56
- overflow: auto;
57
-
58
- width: 100%;
59
- height: 100%;
60
- padding: 15px 0 0 15px;
61
-
62
- border-left: 1px solid rgba(0, 0, 0, 0.07);
63
- @include flex-container();
14
+ &__tab-content {
15
+ height: calc(100% - 56px); // general tabs height
64
16
  }
65
17
  }
@@ -134,28 +134,30 @@ function Tenant(props: TenantProps) {
134
134
  ) : (
135
135
  <>
136
136
  <ObjectGeneralTabs />
137
- <SplitPane
138
- defaultSizePaneKey={DEFAULT_SIZE_TENANT_KEY}
139
- defaultSizes={[25, 75]}
140
- triggerCollapse={summaryVisibilityState.triggerCollapse}
141
- triggerExpand={summaryVisibilityState.triggerExpand}
142
- minSize={[36, 200]}
143
- onSplitStartDragAdditional={onSplitStartDragAdditional}
144
- >
145
- <ObjectSummary
146
- type={currentPathType}
147
- subType={currentPathSubType}
148
- onCollapseSummary={onCollapseSummaryHandler}
149
- onExpandSummary={onExpandSummaryHandler}
150
- isCollapsed={summaryVisibilityState.collapsed}
151
- additionalTenantInfo={props.additionalTenantInfo}
152
- />
153
- <ObjectGeneral
154
- type={currentPathType}
155
- additionalTenantInfo={props.additionalTenantInfo}
156
- additionalNodesInfo={props.additionalNodesInfo}
157
- />
158
- </SplitPane>
137
+ <div className={b('tab-content')}>
138
+ <SplitPane
139
+ defaultSizePaneKey={DEFAULT_SIZE_TENANT_KEY}
140
+ defaultSizes={[25, 75]}
141
+ triggerCollapse={summaryVisibilityState.triggerCollapse}
142
+ triggerExpand={summaryVisibilityState.triggerExpand}
143
+ minSize={[36, 200]}
144
+ onSplitStartDragAdditional={onSplitStartDragAdditional}
145
+ >
146
+ <ObjectSummary
147
+ type={currentPathType}
148
+ subType={currentPathSubType}
149
+ onCollapseSummary={onCollapseSummaryHandler}
150
+ onExpandSummary={onExpandSummaryHandler}
151
+ isCollapsed={summaryVisibilityState.collapsed}
152
+ additionalTenantInfo={props.additionalTenantInfo}
153
+ />
154
+ <ObjectGeneral
155
+ type={currentPathType}
156
+ additionalTenantInfo={props.additionalTenantInfo}
157
+ additionalNodesInfo={props.additionalNodesInfo}
158
+ />
159
+ </SplitPane>
160
+ </div>
159
161
  </>
160
162
  )}
161
163
  </div>
@@ -29,6 +29,7 @@ const pathTypeToNodeType: Record<EPathType, NavigationTreeNodeType | undefined>
29
29
  [EPathType.EPathTypeColumnTable]: 'column_table',
30
30
 
31
31
  [EPathType.EPathTypeCdcStream]: 'topic',
32
+ [EPathType.EPathTypePersQueueGroup]: 'topic',
32
33
  };
33
34
 
34
35
  export const mapPathTypeToNavigationTreeType = (
@@ -51,6 +52,7 @@ const pathTypeToIsTable: Record<EPathType, boolean> = {
51
52
  [EPathType.EPathTypeExtSubDomain]: false,
52
53
  [EPathType.EPathTypeColumnStore]: false,
53
54
  [EPathType.EPathTypeCdcStream]: false,
55
+ [EPathType.EPathTypePersQueueGroup]: false,
54
56
  };
55
57
 
56
58
  export const isTableType = (pathType?: EPathType) =>
@@ -82,6 +84,7 @@ const pathTypeToIsColumn: Record<EPathType, boolean> = {
82
84
  [EPathType.EPathTypeTableIndex]: false,
83
85
  [EPathType.EPathTypeExtSubDomain]: false,
84
86
  [EPathType.EPathTypeCdcStream]: false,
87
+ [EPathType.EPathTypePersQueueGroup]: false,
85
88
  };
86
89
 
87
90
  export const isColumnEntityType = (type?: EPathType) =>
@@ -20,9 +20,8 @@ const alterTableTemplate = (path: string) => {
20
20
  ADD COLUMN is_deleted Bool;`;
21
21
  };
22
22
  const selectQueryTemplate = (path: string) => {
23
- return `SELECT \`id\`, \`name\`
23
+ return `SELECT *
24
24
  FROM \`${path}\`
25
- ORDER BY \`id\`
26
25
  LIMIT 10;`;
27
26
  };
28
27
  const upsertQueryTemplate = (path: string) => {
@@ -14,7 +14,7 @@ import ProblemFilter, {problemFilterType} from '../../components/ProblemFilter/P
14
14
  import {AutoFetcher} from '../../utils/autofetcher';
15
15
 
16
16
  import routes, {CLUSTER_PAGES, createHref} from '../../routes';
17
- import {formatCPU, formatBytesToGigabyte} from '../../utils';
17
+ import {formatCPU, formatBytesToGigabyte, formatNumber} from '../../utils';
18
18
  import {hideTooltip, showTooltip} from '../../store/reducers/tooltip';
19
19
  import {withSearch} from '../../HOCS';
20
20
  import {ALL, DEFAULT_TABLE_SETTINGS, TENANT_INITIAL_TAB_KEY} from '../../utils/constants';
@@ -257,13 +257,23 @@ class Tenants extends React.Component {
257
257
  align: DataTable.RIGHT,
258
258
  defaultOrder: DataTable.DESCENDING,
259
259
  },
260
+ {
261
+ name: 'NodeIds',
262
+ header: 'Nodes',
263
+ width: 100,
264
+ accessor: ({NodeIds}) => NodeIds.length,
265
+ sortAccessor: ({NodeIds}) => NodeIds.length,
266
+ render: ({value}) => formatNumber(value) || '—',
267
+ align: DataTable.RIGHT,
268
+ defaultOrder: DataTable.DESCENDING,
269
+ },
260
270
  {
261
271
  name: 'StorageGroups',
262
272
  header: 'Groups',
263
273
  width: 100,
264
274
  sortAccessor: ({StorageGroups}) =>
265
275
  isNaN(Number(StorageGroups)) ? 0 : Number(StorageGroups),
266
- render: ({value}) => value ?? '—',
276
+ render: ({value}) => formatNumber(value) || '—',
267
277
  align: DataTable.RIGHT,
268
278
  defaultOrder: DataTable.DESCENDING,
269
279
  },
@@ -1,10 +1,11 @@
1
1
  import {connect} from 'react-redux';
2
2
 
3
- import {RadioButton} from '@yandex-cloud/uikit';
3
+ import {RadioButton, Switch} from '@yandex-cloud/uikit';
4
4
  import {Settings} from '../../components/AsideNavigation/Settings';
5
5
  import favoriteFilledIcon from '../../assets/icons/star.svg';
6
+ import flaskIcon from '../../assets/icons/flask.svg';
6
7
  //@ts-ignore
7
- import {THEME_KEY} from '../../utils/constants';
8
+ import {INVERTED_DISKS_KEY, THEME_KEY} from '../../utils/constants';
8
9
  //@ts-ignore
9
10
  import {setSettingValue} from '../../store/reducers/settings';
10
11
 
@@ -19,6 +20,10 @@ function UserSettings(props: any) {
19
20
  props.setSettingValue(THEME_KEY, value);
20
21
  };
21
22
 
23
+ const _onInvertedDisksChangeHandler = (value: boolean) => {
24
+ props.setSettingValue(INVERTED_DISKS_KEY, value);
25
+ };
26
+
22
27
  return (
23
28
  <Settings>
24
29
  <Settings.Page
@@ -36,15 +41,33 @@ function UserSettings(props: any) {
36
41
  </Settings.Item>
37
42
  </Settings.Section>
38
43
  </Settings.Page>
44
+ <Settings.Page
45
+ id="experiments"
46
+ title="Experiments"
47
+ icon={{data: flaskIcon}}
48
+ >
49
+ <Settings.Section title="Experiments">
50
+ <Settings.Item title="Inverted disks space indicators">
51
+ <Switch
52
+ checked={props.invertedDisks}
53
+ onUpdate={_onInvertedDisksChangeHandler}
54
+ />
55
+ </Settings.Item>
56
+ </Settings.Section>
57
+ </Settings.Page>
39
58
  </Settings>
40
59
  );
41
60
  }
42
61
 
43
62
  const mapStateToProps = (state: any) => {
44
- const {theme} = state.settings.userSettings;
63
+ const {
64
+ theme,
65
+ invertedDisks,
66
+ } = state.settings.userSettings;
45
67
 
46
68
  return {
47
69
  theme,
70
+ invertedDisks,
48
71
  };
49
72
  };
50
73
 
@@ -1,8 +1,12 @@
1
+ type AxiosOptions = {
2
+ concurrentId?: string;
3
+ };
4
+
1
5
  interface Window {
2
6
  api: {
3
7
  getSchema: (
4
8
  params: {path: string},
5
- axiosOptions?: {concurrentId?: string},
9
+ axiosOptions?: AxiosOptions,
6
10
  ) => Promise<import('../types/api/schema').TEvDescribeSchemeResult>;
7
11
  getStorageInfo: (
8
12
  params: {
@@ -11,8 +15,21 @@ interface Window {
11
15
  nodeId: string,
12
16
  type: 'Groups' | 'Nodes',
13
17
  },
14
- axiosOptions?: {concurrentId?: string},
18
+ axiosOptions?: AxiosOptions,
15
19
  ) => Promise<import('../types/api/storage').TStorageInfo>;
20
+ sendQuery: <
21
+ Action extends import('../types/api/query').Actions,
22
+ Schema extends import('../types/api/query').Schemas = undefined
23
+ >(
24
+ params: {
25
+ query?: string,
26
+ database?: string,
27
+ action?: Action,
28
+ stats?: string,
29
+ schema?: Schema,
30
+ },
31
+ axiosOptions?: AxiosOptions,
32
+ ) => Promise<import('../types/api/query').QueryAPIResponse<Action, Schema>>;
16
33
  [method: string]: Function;
17
34
  };
18
35
  }
@@ -146,9 +146,9 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
146
146
  state: 0,
147
147
  });
148
148
  }
149
- sendQuery({query, database, action, stats}, {concurrentId} = {}) {
149
+ sendQuery({query, database, action, stats, schema}, {concurrentId} = {}) {
150
150
  return this.post(
151
- this.getPath('/viewer/json/query'),
151
+ this.getPath(`/viewer/json/query${schema ? `?schema=${schema}` : ''}`),
152
152
  {
153
153
  query,
154
154
  database,
@@ -11,3 +11,7 @@ import {i18n, Lang} from '../src/utils/i18n';
11
11
  i18n.setLang(Lang.En);
12
12
  configureYdbUiComponents({lang: Lang.En});
13
13
  configureUiKit({lang: Lang.En});
14
+
15
+ // only to prevent warnings from history lib
16
+ // all api calls in tests should be mocked
17
+ window.custom_backend = '/';
@@ -2,6 +2,7 @@ import {createRequestActionTypes, createApiRequest} from '../utils';
2
2
  import '../../services/api';
3
3
  import {getValueFromLS, parseJson} from '../../utils/utils';
4
4
  import {QUERIES_HISTORY_KEY, QUERY_INITIAL_RUN_ACTION_KEY} from '../../utils/constants';
5
+ import {parseQueryAPIExecuteResponse} from '../../utils/query';
5
6
  import {readSavedSettingsValue} from './settings';
6
7
 
7
8
  const MAXIMUM_QUERIES_IN_HISTORY = 20;
@@ -57,7 +58,7 @@ const executeQuery = (state = initialState, action) => {
57
58
  case SEND_QUERY.SUCCESS: {
58
59
  return {
59
60
  ...state,
60
- data: action.data.result ?? action.data,
61
+ data: action.data,
61
62
  stats: action.data.stats,
62
63
  loading: false,
63
64
  error: undefined,
@@ -142,15 +143,9 @@ const executeQuery = (state = initialState, action) => {
142
143
 
143
144
  export const sendQuery = ({query, database, action}) => {
144
145
  return createApiRequest({
145
- request: window.api.sendQuery({query, database, action, stats: 'profile'}),
146
+ request: window.api.sendQuery({schema: 'modern', query, database, action, stats: 'profile'}),
146
147
  actions: SEND_QUERY,
147
- dataHandler: (result) => {
148
- const resultData = result.result ?? result;
149
- if (resultData && typeof resultData === 'string') {
150
- throw 'Unexpected token in JSON.';
151
- }
152
- return result;
153
- },
148
+ dataHandler: parseQueryAPIExecuteResponse,
154
149
  });
155
150
  };
156
151
 
@@ -1,15 +1,23 @@
1
- import {createRequestActionTypes, createApiRequest} from '../utils';
2
1
  import '../../services/api';
3
2
 
3
+ import type {ErrorRepsonse, ExecuteActions} from '../../types/api/query';
4
+ import type {IQueryResult} from '../../types/store/query';
5
+ import {parseQueryAPIExecuteResponse} from '../../utils/query';
6
+
7
+ import {createRequestActionTypes, createApiRequest, ApiRequestAction} from '../utils';
8
+
4
9
  const SEND_QUERY = createRequestActionTypes('preview', 'SEND_QUERY');
5
- const SET_QUERY_OPTIONS = createRequestActionTypes('preview', 'SET_QUERY_OPTIONS');
10
+ const SET_QUERY_OPTIONS = 'preview/SET_QUERY_OPTIONS';
6
11
 
7
12
  const initialState = {
8
13
  loading: false,
9
14
  wasLoaded: false,
10
15
  };
11
16
 
12
- const preview = (state = initialState, action) => {
17
+ const preview = (
18
+ state = initialState,
19
+ action: ApiRequestAction<typeof SEND_QUERY, IQueryResult, ErrorRepsonse> | ReturnType<typeof setQueryOptions>,
20
+ ) => {
13
21
  switch (action.type) {
14
22
  case SEND_QUERY.REQUEST: {
15
23
  return {
@@ -45,29 +53,25 @@ const preview = (state = initialState, action) => {
45
53
  }
46
54
  };
47
55
 
48
- export const sendQuery = ({query, database, action}) => {
56
+ interface SendQueryParams {
57
+ query?: string;
58
+ database?: string;
59
+ action?: ExecuteActions;
60
+ };
61
+
62
+ export const sendQuery = ({query, database, action}: SendQueryParams) => {
49
63
  return createApiRequest({
50
- request: window.api.sendQuery({query, database, action}),
64
+ request: window.api.sendQuery({schema: 'modern', query, database, action}),
51
65
  actions: SEND_QUERY,
52
- dataHandler: (data) => {
53
- if (!Array.isArray(data)) {
54
- try {
55
- return JSON.parse(data);
56
- } catch (e) {
57
- return [];
58
- }
59
- }
60
-
61
- return data;
62
- },
66
+ dataHandler: parseQueryAPIExecuteResponse,
63
67
  });
64
68
  };
65
69
 
66
- export function setQueryOptions(options) {
70
+ export function setQueryOptions(options: any) {
67
71
  return {
68
72
  type: SET_QUERY_OPTIONS,
69
73
  data: options,
70
- };
74
+ } as const;
71
75
  }
72
76
 
73
77
  export default preview;
@@ -5,6 +5,7 @@ import {
5
5
  THEME_KEY,
6
6
  TENANT_INITIAL_TAB_KEY,
7
7
  QUERY_INITIAL_RUN_ACTION_KEY,
8
+ INVERTED_DISKS_KEY,
8
9
  } from '../../utils/constants';
9
10
  import '../../services/api';
10
11
  import {getValueFromLS} from '../../utils/utils';
@@ -28,7 +29,8 @@ export const initialState = {
28
29
  userSettings: {
29
30
  ...defaultUserSettings,
30
31
  ...userSettings,
31
- theme: readSavedSettingsValue(THEME_KEY, 'light'),
32
+ [THEME_KEY]: readSavedSettingsValue(THEME_KEY, 'light'),
33
+ [INVERTED_DISKS_KEY]: readSavedSettingsValue(INVERTED_DISKS_KEY) === 'true',
32
34
  [SAVED_QUERIES_KEY]: readSavedSettingsValue(SAVED_QUERIES_KEY, '[]'),
33
35
  [TENANT_INITIAL_TAB_KEY]: readSavedSettingsValue(TENANT_INITIAL_TAB_KEY),
34
36
  [QUERY_INITIAL_RUN_ACTION_KEY]: readSavedSettingsValue(QUERY_INITIAL_RUN_ACTION_KEY),
@@ -0,0 +1,88 @@
1
+ import type {Dispatch} from 'redux';
2
+ import {AxiosResponse} from 'axios';
3
+
4
+ import createToast from '../utils/createToast';
5
+
6
+ import {SET_UNAUTHENTICATED} from './reducers/authentication';
7
+
8
+ export const nop = (result: any) => result;
9
+
10
+ export function createRequestActionTypes<Prefix extends string, Type extends string>(prefix: Prefix, type: Type) {
11
+ return {
12
+ REQUEST: `${prefix}/${type}_REQUEST`,
13
+ SUCCESS: `${prefix}/${type}_SUCCESS`,
14
+ FAILURE: `${prefix}/${type}_FAILURE`,
15
+ } as const;
16
+ }
17
+
18
+ const isAxiosResponse = (response: any): response is AxiosResponse => response && 'status' in response;
19
+
20
+ type CreateApiRequestParams<Actions, Response, HandledResponse> = {
21
+ actions: Actions;
22
+ request: Promise<Response>;
23
+ dataHandler: (data: Response, getState?: () => any) => HandledResponse;
24
+ };
25
+
26
+ export function createApiRequest<
27
+ Actions extends ReturnType<typeof createRequestActionTypes>,
28
+ Response,
29
+ HandledResponse,
30
+ >({actions, request, dataHandler = nop}: CreateApiRequestParams<Actions, Response, HandledResponse>) {
31
+ const doRequest = async function (dispatch: Dispatch, getState: () => any) {
32
+ dispatch({
33
+ type: actions.REQUEST,
34
+ });
35
+
36
+ try {
37
+ const result = await request;
38
+ const data = dataHandler(result, getState);
39
+
40
+ dispatch({
41
+ type: actions.SUCCESS,
42
+ data,
43
+ });
44
+
45
+ return data;
46
+ } catch (error) {
47
+ if (isAxiosResponse(error) && error.status === 401) {
48
+ dispatch({
49
+ type: SET_UNAUTHENTICATED.SUCCESS,
50
+ });
51
+ } else if (isAxiosResponse(error) && error.status >= 500 && error.statusText) {
52
+ createToast({
53
+ name: 'Request failure',
54
+ title: 'Request failure',
55
+ type: 'error',
56
+ content: `${error.status} ${error.statusText}`,
57
+ });
58
+ }
59
+
60
+ dispatch({
61
+ type: actions.FAILURE,
62
+ error,
63
+ });
64
+
65
+ // TODO should probably throw the received error here, but this change requires a thorough revision of all api calls
66
+ return undefined;
67
+ }
68
+ };
69
+
70
+ return doRequest;
71
+ }
72
+
73
+ export type ApiRequestAction<
74
+ Actions extends ReturnType<typeof createRequestActionTypes>,
75
+ SuccessResponse = unknown,
76
+ ErrorResponse = unknown
77
+ > =
78
+ | {
79
+ type: Actions['REQUEST'],
80
+ }
81
+ | {
82
+ type: Actions['SUCCESS'],
83
+ data: SuccessResponse,
84
+ }
85
+ | {
86
+ type: Actions['FAILURE'],
87
+ error: ErrorResponse,
88
+ };