ydb-embedded-ui 4.20.4 → 4.21.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/components/EmptyState/EmptyState.scss +0 -1
  3. package/dist/components/NodeHostWrapper/NodeHostWrapper.scss +2 -1
  4. package/dist/components/ProgressViewer/ProgressViewer.scss +1 -0
  5. package/dist/components/QueryResultTable/QueryResultTable.tsx +7 -3
  6. package/dist/components/TableWithControlsLayout/TableWithControlsLayout.scss +4 -0
  7. package/dist/components/VirtualTable/TableChunk.tsx +84 -0
  8. package/dist/components/VirtualTable/TableHead.tsx +139 -0
  9. package/dist/components/VirtualTable/TableRow.tsx +91 -0
  10. package/dist/components/VirtualTable/VirtualTable.scss +146 -0
  11. package/dist/components/VirtualTable/VirtualTable.tsx +277 -0
  12. package/dist/components/VirtualTable/constants.ts +17 -0
  13. package/dist/components/VirtualTable/i18n/en.json +3 -0
  14. package/dist/components/VirtualTable/i18n/index.ts +11 -0
  15. package/dist/components/VirtualTable/i18n/ru.json +3 -0
  16. package/dist/components/VirtualTable/index.ts +3 -0
  17. package/dist/components/VirtualTable/reducer.ts +143 -0
  18. package/dist/components/VirtualTable/shared.ts +3 -0
  19. package/dist/components/VirtualTable/types.ts +60 -0
  20. package/dist/components/VirtualTable/useIntersectionObserver.ts +42 -0
  21. package/dist/components/VirtualTable/utils.ts +3 -0
  22. package/dist/containers/App/App.scss +2 -1
  23. package/dist/containers/Cluster/Cluster.tsx +17 -4
  24. package/dist/containers/Node/Node.tsx +9 -17
  25. package/dist/containers/Node/NodeStructure/NodeStructure.tsx +8 -30
  26. package/dist/containers/Node/NodeStructure/Pdisk.tsx +29 -18
  27. package/dist/containers/Node/i18n/en.json +4 -0
  28. package/dist/containers/Node/i18n/index.ts +11 -0
  29. package/dist/containers/Node/i18n/ru.json +4 -0
  30. package/dist/containers/Nodes/Nodes.tsx +7 -28
  31. package/dist/containers/Nodes/VirtualNodes.tsx +146 -0
  32. package/dist/containers/Nodes/getNodes.ts +26 -0
  33. package/dist/containers/Nodes/getNodesColumns.tsx +49 -39
  34. package/dist/containers/Tablets/Tablets.tsx +3 -8
  35. package/dist/containers/Tenant/Diagnostics/Diagnostics.tsx +0 -1
  36. package/dist/containers/UserSettings/i18n/en.json +4 -4
  37. package/dist/containers/UserSettings/i18n/ru.json +4 -4
  38. package/dist/containers/UserSettings/settings.ts +5 -6
  39. package/dist/store/reducers/nodes/nodes.ts +2 -2
  40. package/dist/store/reducers/nodes/types.ts +1 -0
  41. package/dist/store/reducers/settings/settings.ts +3 -3
  42. package/dist/utils/developerUI.ts +32 -0
  43. package/dist/utils/hooks/useNodesRequestParams.ts +4 -8
  44. package/dist/utils/nodes.ts +12 -0
  45. package/dist/utils/query.ts +8 -1
  46. package/package.json +1 -1
@@ -2,7 +2,6 @@ import * as React from 'react';
2
2
  import {useLocation, useRouteMatch} from 'react-router';
3
3
  import cn from 'bem-cn-lite';
4
4
  import {useDispatch} from 'react-redux';
5
- import _ from 'lodash';
6
5
 
7
6
  import {Tabs} from '@gravity-ui/uikit';
8
7
  import {Link} from 'react-router-dom';
@@ -52,21 +51,22 @@ function Node(props: NodeProps) {
52
51
  const {tenantName: tenantNameFromQuery} = parseQuery(location);
53
52
 
54
53
  const {activeTabVerified, nodeTabs} = React.useMemo(() => {
55
- const hasStorage = _.find(node?.Roles as any[], (el) => el === STORAGE_ROLE);
56
- let activeTabVerified = activeTab;
54
+ const hasStorage = node?.Roles?.find((el) => el === STORAGE_ROLE);
55
+
56
+ let actualActiveTab = activeTab;
57
57
  if (!hasStorage && activeTab === STORAGE) {
58
- activeTabVerified = OVERVIEW;
58
+ actualActiveTab = OVERVIEW;
59
59
  }
60
60
  const nodePages = hasStorage ? NODE_PAGES : NODE_PAGES.filter((el) => el.id !== STORAGE);
61
61
 
62
- const nodeTabs = nodePages.map((page) => {
62
+ const actualNodeTabs = nodePages.map((page) => {
63
63
  return {
64
64
  ...page,
65
65
  title: page.name,
66
66
  };
67
67
  });
68
68
 
69
- return {activeTabVerified, nodeTabs};
69
+ return {activeTabVerified: actualActiveTab, nodeTabs: actualNodeTabs};
70
70
  }, [activeTab, node]);
71
71
 
72
72
  React.useEffect(() => {
@@ -100,13 +100,13 @@ function Node(props: NodeProps) {
100
100
  size="l"
101
101
  items={nodeTabs}
102
102
  activeTab={activeTabVerified}
103
- wrapTo={({id}, node) => (
103
+ wrapTo={({id}, tabNode) => (
104
104
  <Link
105
105
  to={createHref(routes.node, {id: nodeId, activeTab: id})}
106
106
  key={id}
107
107
  className={b('tab')}
108
108
  >
109
- {node}
109
+ {tabNode}
110
110
  </Link>
111
111
  )}
112
112
  allowNotSelected={true}
@@ -115,8 +115,6 @@ function Node(props: NodeProps) {
115
115
  );
116
116
  };
117
117
  const renderTabContent = () => {
118
- const {additionalNodesProps} = props;
119
-
120
118
  switch (activeTab) {
121
119
  case STORAGE: {
122
120
  return (
@@ -134,13 +132,7 @@ function Node(props: NodeProps) {
134
132
  }
135
133
 
136
134
  case STRUCTURE: {
137
- return (
138
- <NodeStructure
139
- className={b('node-page-wrapper')}
140
- nodeId={nodeId}
141
- additionalNodesProps={additionalNodesProps}
142
- />
143
- );
135
+ return <NodeStructure className={b('node-page-wrapper')} nodeId={nodeId} />;
144
136
  }
145
137
  default:
146
138
  return false;
@@ -1,7 +1,7 @@
1
- import {useEffect, useRef, useMemo} from 'react';
1
+ import {useEffect, useRef} from 'react';
2
2
  import {useDispatch} from 'react-redux';
3
3
  import url from 'url';
4
- import _ from 'lodash';
4
+ import {isEmpty} from 'lodash/fp';
5
5
 
6
6
  import cn from 'bem-cn-lite';
7
7
 
@@ -13,15 +13,13 @@ import {selectNodeStructure} from '../../../store/reducers/node/selectors';
13
13
  import {AutoFetcher} from '../../../utils/autofetcher';
14
14
  import {useTypedSelector} from '../../../utils/hooks';
15
15
 
16
- import type {AdditionalNodesProps} from '../../../types/additionalProps';
17
-
18
16
  import {PDisk} from './Pdisk';
19
17
 
20
18
  import './NodeStructure.scss';
21
19
 
22
20
  const b = cn('kv-node-structure');
23
21
 
24
- export function valueIsDefined(value: any) {
22
+ export function valueIsDefined<T>(value: T | null | undefined): value is T {
25
23
  return value !== null && value !== undefined;
26
24
  }
27
25
 
@@ -32,24 +30,16 @@ function generateId({type, id}: {type: 'pdisk' | 'vdisk'; id: string}) {
32
30
  interface NodeStructureProps {
33
31
  nodeId: string;
34
32
  className?: string;
35
- additionalNodesProps?: AdditionalNodesProps;
36
33
  }
37
34
 
38
35
  const autofetcher = new AutoFetcher();
39
36
 
40
- function NodeStructure({nodeId, className, additionalNodesProps}: NodeStructureProps) {
37
+ function NodeStructure({nodeId, className}: NodeStructureProps) {
41
38
  const dispatch = useDispatch();
42
39
 
43
40
  const nodeStructure = useTypedSelector(selectNodeStructure);
44
41
 
45
42
  const {loadingStructure, wasLoadedStructure} = useTypedSelector((state) => state.node);
46
- const nodeData = useTypedSelector((state) => state.node?.data?.SystemStateInfo?.[0]);
47
-
48
- const nodeHref = useMemo(() => {
49
- return additionalNodesProps?.getNodeRef
50
- ? additionalNodesProps.getNodeRef(nodeData)
51
- : undefined;
52
- }, [nodeData, additionalNodesProps]);
53
43
 
54
44
  const {pdiskId: pdiskIdFromUrl, vdiskId: vdiskIdFromUrl} = url.parse(
55
45
  window.location.href,
@@ -57,23 +47,11 @@ function NodeStructure({nodeId, className, additionalNodesProps}: NodeStructureP
57
47
  ).query;
58
48
 
59
49
  const scrollContainerRef = useRef<HTMLDivElement>(null);
60
- const scrollContainer = scrollContainerRef.current;
61
50
 
62
51
  const isReady = useRef(false);
63
52
 
64
53
  const scrolled = useRef(false);
65
54
 
66
- useEffect(() => {
67
- return () => {
68
- if (scrollContainer) {
69
- scrollContainer.scrollTo({
70
- behavior: 'smooth',
71
- top: 0,
72
- });
73
- }
74
- };
75
- }, []);
76
-
77
55
  useEffect(() => {
78
56
  dispatch(getNodeStructure(nodeId));
79
57
  autofetcher.start();
@@ -87,13 +65,13 @@ function NodeStructure({nodeId, className, additionalNodesProps}: NodeStructureP
87
65
  }, [nodeId, dispatch]);
88
66
 
89
67
  useEffect(() => {
90
- if (!_.isEmpty(nodeStructure) && scrollContainer) {
68
+ if (!isEmpty(nodeStructure) && scrollContainerRef.current) {
91
69
  isReady.current = true;
92
70
  }
93
71
  }, [nodeStructure]);
94
72
 
95
73
  useEffect(() => {
96
- if (isReady.current && !scrolled.current && scrollContainer) {
74
+ if (isReady.current && !scrolled.current && scrollContainerRef.current) {
97
75
  const element = document.getElementById(
98
76
  generateId({type: 'pdisk', id: pdiskIdFromUrl as string}),
99
77
  );
@@ -112,7 +90,7 @@ function NodeStructure({nodeId, className, additionalNodesProps}: NodeStructureP
112
90
  }
113
91
 
114
92
  if (element) {
115
- scrollContainer.scrollTo({
93
+ scrollContainerRef.current.scrollTo({
116
94
  behavior: 'smooth',
117
95
  // should subtract 20 to avoid sticking the element to tabs
118
96
  top: scrollToVdisk ? scrollToVdisk : element.offsetTop,
@@ -136,7 +114,7 @@ function NodeStructure({nodeId, className, additionalNodesProps}: NodeStructureP
136
114
  id={generateId({type: 'pdisk', id: pDiskId})}
137
115
  unfolded={pdiskIdFromUrl === pDiskId}
138
116
  selectedVdiskId={vdiskIdFromUrl as string}
139
- nodeHref={nodeHref}
117
+ nodeId={nodeId}
140
118
  />
141
119
  ))
142
120
  : renderStub();
@@ -12,15 +12,17 @@ import type {
12
12
  PreparedStructureVDisk,
13
13
  } from '../../../store/reducers/node/types';
14
14
  import {EVDiskState} from '../../../types/api/vdisk';
15
- import {bytesToGB, pad9} from '../../../utils/utils';
15
+ import {bytesToGB} from '../../../utils/utils';
16
16
  import {formatStorageValuesToGb} from '../../../utils/dataFormatters/dataFormatters';
17
17
  import {getPDiskType} from '../../../utils/pdisk';
18
18
  import {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants';
19
+ import {createPDiskDeveloperUILink, createVDiskDeveloperUILink} from '../../../utils/developerUI';
19
20
  import EntityStatus from '../../../components/EntityStatus/EntityStatus';
20
21
  import InfoViewer, {type InfoViewerItem} from '../../../components/InfoViewer/InfoViewer';
21
22
  import {ProgressViewer} from '../../../components/ProgressViewer/ProgressViewer';
22
23
  import {Icon} from '../../../components/Icon';
23
24
 
25
+ import i18n from '../i18n';
24
26
  import {Vdisk} from './Vdisk';
25
27
  import {valueIsDefined} from './NodeStructure';
26
28
  import {PDiskTitleBadge} from './PDiskTitleBadge';
@@ -32,7 +34,7 @@ interface PDiskProps {
32
34
  unfolded?: boolean;
33
35
  id: string;
34
36
  selectedVdiskId?: string;
35
- nodeHref?: string | null;
37
+ nodeId: string | number;
36
38
  }
37
39
 
38
40
  enum VDiskTableColumnsIds {
@@ -54,11 +56,11 @@ const vDiskTableColumnsNames: Record<VDiskTableColumnsIdsValues, string> = {
54
56
  function getColumns({
55
57
  pDiskId,
56
58
  selectedVdiskId,
57
- nodeHref,
59
+ nodeId,
58
60
  }: {
59
61
  pDiskId: number | undefined;
60
62
  selectedVdiskId?: string;
61
- nodeHref?: string | null;
63
+ nodeId?: string | number;
62
64
  }) {
63
65
  const columns: Column<PreparedStructureVDisk>[] = [
64
66
  {
@@ -66,26 +68,31 @@ function getColumns({
66
68
  header: vDiskTableColumnsNames[VDiskTableColumnsIds.slotId],
67
69
  width: 100,
68
70
  render: ({row}) => {
69
- let vdiskInternalViewerLink = '';
71
+ const vDiskSlotId = row.VDiskSlotId;
72
+ let vdiskInternalViewerLink = null;
70
73
 
71
- if (nodeHref && pDiskId !== undefined && row.VDiskSlotId !== undefined) {
72
- vdiskInternalViewerLink +=
73
- nodeHref +
74
- 'actors/vdisks/vdisk' +
75
- pad9(pDiskId) +
76
- '_' +
77
- pad9(row.VDiskSlotId);
74
+ if (
75
+ valueIsDefined(nodeId) &&
76
+ valueIsDefined(pDiskId) &&
77
+ valueIsDefined(vDiskSlotId)
78
+ ) {
79
+ vdiskInternalViewerLink = createVDiskDeveloperUILink({
80
+ nodeId,
81
+ pDiskId,
82
+ vDiskSlotId,
83
+ });
78
84
  }
79
85
 
80
86
  return (
81
87
  <div className={b('vdisk-id', {selected: row.id === selectedVdiskId})}>
82
- <span>{row.VDiskSlotId}</span>
88
+ <span>{vDiskSlotId}</span>
83
89
  {vdiskInternalViewerLink && (
84
90
  <Button
85
91
  size="s"
86
92
  className={b('external-button', {hidden: true})}
87
93
  href={vdiskInternalViewerLink}
88
94
  target="_blank"
95
+ title={i18n('vdisk.developer-ui-button-title')}
89
96
  >
90
97
  <Icon name="external" />
91
98
  </Button>
@@ -156,7 +163,7 @@ export function PDisk({
156
163
  id,
157
164
  data,
158
165
  selectedVdiskId,
159
- nodeHref,
166
+ nodeId,
160
167
  unfolded: unfoldedFromProps,
161
168
  }: PDiskProps) {
162
169
  const [unfolded, setUnfolded] = useState(unfoldedFromProps ?? false);
@@ -190,7 +197,7 @@ export function PDisk({
190
197
  <DataTable
191
198
  theme="yandex-cloud"
192
199
  data={vDisks}
193
- columns={getColumns({nodeHref, pDiskId: PDiskId, selectedVdiskId})}
200
+ columns={getColumns({nodeId, pDiskId: PDiskId, selectedVdiskId})}
194
201
  settings={{...DEFAULT_TABLE_SETTINGS, dynamicRender: false}}
195
202
  rowClassName={(row) => {
196
203
  return row.id === selectedVdiskId ? b('selected-vdisk') : '';
@@ -203,10 +210,13 @@ export function PDisk({
203
210
  if (isEmpty(data)) {
204
211
  return <div>No information about PDisk</div>;
205
212
  }
206
- let pDiskInternalViewerLink = '';
213
+ let pDiskInternalViewerLink = null;
207
214
 
208
- if (nodeHref) {
209
- pDiskInternalViewerLink += nodeHref + 'actors/pdisks/pdisk' + pad9(PDiskId);
215
+ if (valueIsDefined(PDiskId) && valueIsDefined(nodeId)) {
216
+ pDiskInternalViewerLink = createPDiskDeveloperUILink({
217
+ nodeId,
218
+ pDiskId: PDiskId,
219
+ });
210
220
  }
211
221
 
212
222
  const pdiskInfo: InfoViewerItem[] = [
@@ -222,6 +232,7 @@ export function PDisk({
222
232
  href={pDiskInternalViewerLink}
223
233
  target="_blank"
224
234
  view="flat-secondary"
235
+ title={i18n('pdisk.developer-ui-button-title')}
225
236
  >
226
237
  <Icon name="external" />
227
238
  </Button>
@@ -0,0 +1,4 @@
1
+ {
2
+ "pdisk.developer-ui-button-title": "PDisk Developer UI page",
3
+ "vdisk.developer-ui-button-title": "VDisk Developer UI page"
4
+ }
@@ -0,0 +1,11 @@
1
+ import {i18n, Lang} from '../../../utils/i18n';
2
+
3
+ import en from './en.json';
4
+ import ru from './ru.json';
5
+
6
+ const COMPONENT = 'ydb-node-page';
7
+
8
+ i18n.registerKeyset(Lang.En, COMPONENT, en);
9
+ i18n.registerKeyset(Lang.Ru, COMPONENT, ru);
10
+
11
+ export default i18n.keyset(COMPONENT);
@@ -0,0 +1,4 @@
1
+ {
2
+ "pdisk.developer-ui-button-title": "Страница PDisk в Developer UI",
3
+ "vdisk.developer-ui-button-title": "Страница VDisk в Developer UI"
4
+ }
@@ -5,7 +5,6 @@ import {useDispatch} from 'react-redux';
5
5
  import DataTable from '@gravity-ui/react-data-table';
6
6
  import {ASCENDING} from '@gravity-ui/react-data-table/build/esm/lib/constants';
7
7
 
8
- import type {EPathType} from '../../types/api/schema';
9
8
  import type {ProblemFilterValue} from '../../store/reducers/settings/types';
10
9
  import type {NodesSortParams} from '../../store/reducers/nodes/types';
11
10
 
@@ -19,13 +18,7 @@ import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/
19
18
  import {ResponseError} from '../../components/Errors/ResponseError';
20
19
 
21
20
  import {DEFAULT_TABLE_SETTINGS, USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY} from '../../utils/constants';
22
- import {
23
- useAutofetcher,
24
- useSetting,
25
- useTypedSelector,
26
- useNodesRequestParams,
27
- useTableSort,
28
- } from '../../utils/hooks';
21
+ import {useAutofetcher, useSetting, useTypedSelector, useTableSort} from '../../utils/hooks';
29
22
  import {
30
23
  isSortableNodesProperty,
31
24
  isUnavailableNode,
@@ -45,8 +38,6 @@ import {selectFilteredNodes} from '../../store/reducers/nodes/selectors';
45
38
  import {changeFilter, ProblemFilterValues} from '../../store/reducers/settings/settings';
46
39
  import type {AdditionalNodesProps} from '../../types/additionalProps';
47
40
 
48
- import {isDatabaseEntityType} from '../Tenant/utils/schema';
49
-
50
41
  import {getNodesColumns} from './getNodesColumns';
51
42
 
52
43
  import './Nodes.scss';
@@ -57,11 +48,10 @@ const b = cn('ydb-nodes');
57
48
 
58
49
  interface NodesProps {
59
50
  path?: string;
60
- type?: EPathType;
61
51
  additionalNodesProps?: AdditionalNodesProps;
62
52
  }
63
53
 
64
- export const Nodes = ({path, type, additionalNodesProps = {}}: NodesProps) => {
54
+ export const Nodes = ({path, additionalNodesProps = {}}: NodesProps) => {
65
55
  const dispatch = useDispatch();
66
56
 
67
57
  const isClusterNodes = !path;
@@ -90,31 +80,20 @@ export const Nodes = ({path, type, additionalNodesProps = {}}: NodesProps) => {
90
80
 
91
81
  const [useNodesEndpoint] = useSetting(USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY);
92
82
 
93
- const requestParams = useNodesRequestParams({
94
- filter: searchValue,
95
- problemFilter,
96
- nodesUptimeFilter,
97
- sortOrder,
98
- sortValue,
99
- });
100
-
101
83
  const fetchNodes = useCallback(
102
84
  (isBackground) => {
103
85
  if (!isBackground) {
104
86
  dispatch(setDataWasNotLoaded());
105
87
  }
106
88
 
107
- const params = requestParams || {};
108
-
109
- // For not DB entities we always use /compute endpoint instead of /nodes
110
- // since /nodes can return data only for tenants
111
- if (path && (!useNodesEndpoint || !isDatabaseEntityType(type))) {
112
- dispatch(getComputeNodes({path, ...params}));
89
+ // If there is no path, it's cluster Nodes tab
90
+ if (path && !useNodesEndpoint) {
91
+ dispatch(getComputeNodes({path}));
113
92
  } else {
114
- dispatch(getNodes({tenant: path, ...params}));
93
+ dispatch(getNodes({path}));
115
94
  }
116
95
  },
117
- [dispatch, path, type, useNodesEndpoint, requestParams],
96
+ [dispatch, path, useNodesEndpoint],
118
97
  );
119
98
 
120
99
  useAutofetcher(fetchNodes, [fetchNodes], isClusterNodes ? true : autorefresh);
@@ -0,0 +1,146 @@
1
+ import {useCallback, useMemo, useState} from 'react';
2
+ import cn from 'bem-cn-lite';
3
+
4
+ import type {AdditionalNodesProps} from '../../types/additionalProps';
5
+ import type {ProblemFilterValue} from '../../store/reducers/settings/types';
6
+ import type {NodesPreparedEntity} from '../../store/reducers/nodes/types';
7
+ import {ProblemFilterValues} from '../../store/reducers/settings/settings';
8
+ import {
9
+ NodesSortValue,
10
+ NodesUptimeFilterValues,
11
+ getProblemParamValue,
12
+ getUptimeParamValue,
13
+ isSortableNodesProperty,
14
+ isUnavailableNode,
15
+ } from '../../utils/nodes';
16
+
17
+ import {Search} from '../../components/Search';
18
+ import {ProblemFilter} from '../../components/ProblemFilter';
19
+ import {UptimeFilter} from '../../components/UptimeFIlter';
20
+ import {EntitiesCount} from '../../components/EntitiesCount';
21
+ import {AccessDenied} from '../../components/Errors/403';
22
+ import {ResponseError} from '../../components/Errors/ResponseError';
23
+ import {Illustration} from '../../components/Illustration';
24
+ import {
25
+ type FetchData,
26
+ type RenderControls,
27
+ type RenderErrorMessage,
28
+ VirtualTable,
29
+ GetRowClassName,
30
+ } from '../../components/VirtualTable';
31
+
32
+ import {getNodesColumns} from './getNodesColumns';
33
+ import {getNodes} from './getNodes';
34
+ import i18n from './i18n';
35
+
36
+ import './Nodes.scss';
37
+
38
+ const b = cn('ydb-nodes');
39
+
40
+ interface NodesProps {
41
+ parentContainer?: Element | null;
42
+ additionalNodesProps?: AdditionalNodesProps;
43
+ }
44
+
45
+ export const VirtualNodes = ({parentContainer, additionalNodesProps}: NodesProps) => {
46
+ const [searchValue, setSearchValue] = useState('');
47
+ const [problemFilter, setProblemFilter] = useState<ProblemFilterValue>(ProblemFilterValues.ALL);
48
+ const [uptimeFilter, setUptimeFilter] = useState<NodesUptimeFilterValues>(
49
+ NodesUptimeFilterValues.All,
50
+ );
51
+
52
+ const filters = useMemo(() => {
53
+ return [searchValue, problemFilter, uptimeFilter];
54
+ }, [searchValue, problemFilter, uptimeFilter]);
55
+
56
+ const fetchData = useCallback<FetchData<NodesPreparedEntity>>(
57
+ async (limit, offset, {sortOrder, columnId} = {}) => {
58
+ return await getNodes({
59
+ limit,
60
+ offset,
61
+ filter: searchValue,
62
+ problems_only: getProblemParamValue(problemFilter),
63
+ uptime: getUptimeParamValue(uptimeFilter),
64
+ sortOrder,
65
+ sortValue: columnId as NodesSortValue,
66
+ });
67
+ },
68
+ [problemFilter, searchValue, uptimeFilter],
69
+ );
70
+
71
+ const getRowClassName: GetRowClassName<NodesPreparedEntity> = (row) => {
72
+ return b('node', {unavailable: isUnavailableNode(row)});
73
+ };
74
+
75
+ const handleSearchQueryChange = (value: string) => {
76
+ setSearchValue(value);
77
+ };
78
+ const handleProblemFilterChange = (value: string) => {
79
+ setProblemFilter(value as ProblemFilterValue);
80
+ };
81
+ const handleUptimeFilterChange = (value: string) => {
82
+ setUptimeFilter(value as NodesUptimeFilterValues);
83
+ };
84
+
85
+ const renderControls: RenderControls = ({totalEntities, foundEntities, inited}) => {
86
+ return (
87
+ <>
88
+ <Search
89
+ onChange={handleSearchQueryChange}
90
+ placeholder="Host name"
91
+ className={b('search')}
92
+ value={searchValue}
93
+ />
94
+ <ProblemFilter value={problemFilter} onChange={handleProblemFilterChange} />
95
+ <UptimeFilter value={uptimeFilter} onChange={handleUptimeFilterChange} />
96
+ <EntitiesCount
97
+ total={totalEntities}
98
+ current={foundEntities}
99
+ label={'Nodes'}
100
+ loading={!inited}
101
+ />
102
+ </>
103
+ );
104
+ };
105
+
106
+ const renderEmptyDataMessage = () => {
107
+ if (
108
+ problemFilter !== ProblemFilterValues.ALL ||
109
+ uptimeFilter !== NodesUptimeFilterValues.All
110
+ ) {
111
+ return <Illustration name="thumbsUp" width="200" />;
112
+ }
113
+
114
+ return i18n('empty.default');
115
+ };
116
+
117
+ const renderErrorMessage: RenderErrorMessage = (error) => {
118
+ if (error && error.status === 403) {
119
+ return <AccessDenied />;
120
+ }
121
+
122
+ return <ResponseError error={error} />;
123
+ };
124
+
125
+ const rawColumns = getNodesColumns({
126
+ getNodeRef: additionalNodesProps?.getNodeRef,
127
+ });
128
+
129
+ const columns = rawColumns.map((column) => {
130
+ return {...column, sortable: isSortableNodesProperty(column.name)};
131
+ });
132
+
133
+ return (
134
+ <VirtualTable
135
+ parentContainer={parentContainer}
136
+ columns={columns}
137
+ fetchData={fetchData}
138
+ limit={50}
139
+ renderControls={renderControls}
140
+ renderErrorMessage={renderErrorMessage}
141
+ renderEmptyDataMessage={renderEmptyDataMessage}
142
+ dependencyArray={filters}
143
+ getRowClassName={getRowClassName}
144
+ />
145
+ );
146
+ };
@@ -0,0 +1,26 @@
1
+ import type {NodesApiRequestParams} from '../../store/reducers/nodes/types';
2
+ import {prepareNodesData} from '../../store/reducers/nodes/utils';
3
+
4
+ const getConcurrentId = (limit?: number, offset?: number) => {
5
+ return `getNodes|offset${offset}|limit${limit}`;
6
+ };
7
+
8
+ export const getNodes = async ({
9
+ type = 'any',
10
+ storage = false,
11
+ limit,
12
+ offset,
13
+ ...params
14
+ }: NodesApiRequestParams) => {
15
+ const response = await window.api.getNodes(
16
+ {type, storage, limit, offset, ...params},
17
+ {concurrentId: getConcurrentId(limit, offset)},
18
+ );
19
+ const preparedResponse = prepareNodesData(response);
20
+
21
+ return {
22
+ data: preparedResponse.Nodes || [],
23
+ found: preparedResponse.FoundNodes || 0,
24
+ total: preparedResponse.TotalNodes || 0,
25
+ };
26
+ };