ydb-embedded-ui 4.20.4 → 4.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ };