diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx
index 5fa4aa00a1..4db155687c 100644
--- a/src/containers/Node/Node.tsx
+++ b/src/containers/Node/Node.tsx
@@ -15,7 +15,7 @@ import NodeStructure from './NodeStructure/NodeStructure';
import {Loader} from '../../components/Loader';
import {BasicNodeViewer} from '../../components/BasicNodeViewer';
-import {getNodeInfo, resetNode} from '../../store/reducers/node';
+import {getNodeInfo, resetNode} from '../../store/reducers/node/node';
import routes, {CLUSTER_PAGES, createHref} from '../../routes';
import {setHeader} from '../../store/reducers/header';
import {AutoFetcher} from '../../utils/autofetcher';
diff --git a/src/containers/Node/NodeStructure/NodeStructure.tsx b/src/containers/Node/NodeStructure/NodeStructure.tsx
index cd51db4505..03ea6c7c90 100644
--- a/src/containers/Node/NodeStructure/NodeStructure.tsx
+++ b/src/containers/Node/NodeStructure/NodeStructure.tsx
@@ -1,16 +1,19 @@
import {useEffect, useRef, useMemo} from 'react';
-import {useDispatch, useSelector} from 'react-redux';
+import {useDispatch} from 'react-redux';
import url from 'url';
import _ from 'lodash';
import cn from 'bem-cn-lite';
-import {PDisk} from './Pdisk';
import {Loader} from '../.././../components/Loader';
-import {getNodeStructure, selectNodeStructure} from '../../../store/reducers/node';
+import {getNodeStructure} from '../../../store/reducers/node/node';
+import {selectNodeStructure} from '../../../store/reducers/node/selectors';
import {AutoFetcher} from '../../../utils/autofetcher';
+import {useTypedSelector} from '../../../utils/hooks';
+
+import {PDisk} from './Pdisk';
import './NodeStructure.scss';
@@ -32,20 +35,19 @@ interface NodeStructureProps {
const autofetcher = new AutoFetcher();
-function NodeStructure(props: NodeStructureProps) {
+function NodeStructure({nodeId, className, additionalNodesInfo}: NodeStructureProps) {
const dispatch = useDispatch();
- const nodeStructure: any = useSelector(selectNodeStructure);
+ const nodeStructure = useTypedSelector(selectNodeStructure);
- const loadingStructure = useSelector((state: any) => state.node.loadingStructure);
- const wasLoadedStructure = useSelector((state: any) => state.node.wasLoadedStructure);
- const nodeData = useSelector((state: any) => state.node?.data?.SystemStateInfo?.[0]);
+ const {loadingStructure, wasLoadedStructure} = useTypedSelector((state) => state.node);
+ const nodeData = useTypedSelector((state) => state.node?.data?.SystemStateInfo?.[0]);
const nodeHref = useMemo(() => {
- return props.additionalNodesInfo?.getNodeRef
- ? props.additionalNodesInfo.getNodeRef(nodeData)
+ return additionalNodesInfo?.getNodeRef
+ ? additionalNodesInfo.getNodeRef(nodeData)
: undefined;
- }, [nodeData, props.additionalNodesInfo]);
+ }, [nodeData, additionalNodesInfo]);
const {pdiskId: pdiskIdFromUrl, vdiskId: vdiskIdFromUrl} = url.parse(
window.location.href,
@@ -71,16 +73,16 @@ function NodeStructure(props: NodeStructureProps) {
}, []);
useEffect(() => {
- dispatch(getNodeStructure(props.nodeId));
+ dispatch(getNodeStructure(nodeId));
autofetcher.start();
- autofetcher.fetch(() => dispatch(getNodeStructure(props.nodeId)));
+ autofetcher.fetch(() => dispatch(getNodeStructure(nodeId)));
return () => {
scrolled.current = false;
isReady.current = false;
autofetcher.stop();
};
- }, [props.nodeId, dispatch]);
+ }, [nodeId, dispatch]);
useEffect(() => {
if (!_.isEmpty(nodeStructure) && scrollContainer) {
@@ -98,9 +100,9 @@ function NodeStructure(props: NodeStructureProps) {
if (vdiskIdFromUrl) {
const vDisks = nodeStructure[pdiskIdFromUrl as string]?.vDisks;
- const vDisk = vDisks?.find((el: any) => el.id === vdiskIdFromUrl);
+ const vDisk = vDisks?.find((el) => el.id === vdiskIdFromUrl);
const dataTable = vDisk ? document.querySelector('.data-table') : undefined;
- const order = vDisk?.order;
+ const order = vDisk?.order || 0;
if (dataTable) {
scrollToVdisk += (dataTable as HTMLElement).offsetTop + 40 * order;
@@ -147,7 +149,7 @@ function NodeStructure(props: NodeStructureProps) {
return (
-
{renderContent()}
+
{renderContent()}
);
}
diff --git a/src/services/api.ts b/src/services/api.ts
index 5688bb0e88..de3ee45dcb 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -99,8 +99,8 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
filter,
nodeId,
}: {
- tenant: string;
- filter: string;
+ tenant?: string;
+ filter?: string;
nodeId: string;
},
{concurrentId}: AxiosOptions = {},
diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts
index 554925aff0..76ad4f4480 100644
--- a/src/store/reducers/index.ts
+++ b/src/store/reducers/index.ts
@@ -5,7 +5,7 @@ import cluster from './cluster/cluster';
import clusterNodes from './clusterNodes/clusterNodes';
import tenant from './tenant/tenant';
import storage from './storage';
-import node from './node';
+import node from './node/node';
import tooltip from './tooltip';
import tablets from './tablets';
import heatmap from './heatmap';
diff --git a/src/store/reducers/node.js b/src/store/reducers/node.js
deleted file mode 100644
index d490af2c30..0000000000
--- a/src/store/reducers/node.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import {createRequestActionTypes, createApiRequest} from '../utils';
-import '../../services/api';
-import {stringifyVdiskId} from '../../utils';
-import {createSelector} from 'reselect';
-
-const FETCH_NODE = createRequestActionTypes('node', 'FETCH_NODE');
-const FETCH_NODE_STRUCTURE = createRequestActionTypes('node', 'FETCH_NODE_STRUCTURE');
-const RESET_NODE = 'node/RESET_NODE';
-
-const node = (
- state = {
- data: {},
- loading: true,
- wasLoaded: false,
- nodeStructure: {},
- loadingStructure: true,
- wasLoadedStructure: false,
- },
- action,
-) => {
- switch (action.type) {
- case FETCH_NODE.REQUEST: {
- return {
- ...state,
- loading: true,
- };
- }
- case FETCH_NODE.SUCCESS: {
- return {
- ...state,
- data: action.data,
- loading: false,
- wasLoaded: true,
- error: undefined,
- };
- }
- case FETCH_NODE.FAILURE: {
- return {
- ...state,
- error: action.error,
- loading: false,
- };
- }
- case FETCH_NODE_STRUCTURE.REQUEST: {
- return {
- ...state,
- loadingStructure: true,
- };
- }
- case FETCH_NODE_STRUCTURE.SUCCESS: {
- return {
- ...state,
- nodeStructure: action.data,
- loadingStructure: false,
- wasLoadedStructure: true,
- errorStructure: undefined,
- };
- }
- case FETCH_NODE_STRUCTURE.FAILURE: {
- return {
- ...state,
- errorStructure: action.error,
- loadingStructure: false,
- };
- }
- case RESET_NODE: {
- return {
- ...state,
- data: {},
- wasLoaded: false,
- nodeStructure: {},
- wasLoadedStructure: false,
- };
- }
- default:
- return state;
- }
-};
-
-export const getNodeInfo = (id) => {
- return createApiRequest({
- request: window.api.getNodeInfo(id),
- actions: FETCH_NODE,
- });
-};
-
-export const getNodeStructure = (nodeId) => {
- return createApiRequest({
- request: window.api.getStorageInfo({nodeId}, {concurrentId: 'getNodeStructure'}),
- actions: FETCH_NODE_STRUCTURE,
- });
-};
-
-export function resetNode() {
- return {
- type: RESET_NODE,
- };
-}
-
-const getNodeId = (state) => state.node?.data?.SystemStateInfo?.[0].NodeId;
-
-const getRawNodeStructure = (state) => state.node?.nodeStructure;
-
-export const selectNodeStructure = createSelector(
- [getNodeId, getRawNodeStructure],
- (nodeId, rawNodeStructure) => {
- const pools = rawNodeStructure?.StoragePools;
- const structure = {};
- pools?.forEach((pool) => {
- const groups = pool.Groups;
- groups?.forEach((group) => {
- const vDisks = group.VDisks?.filter((el) => el.NodeId === nodeId);
- vDisks?.forEach((vd) => {
- const vDiskId = stringifyVdiskId(vd.VDiskId);
- const pDiskId = vd.PDisk?.PDiskId;
- if (!structure[String(pDiskId)]) {
- structure[String(pDiskId)] = {vDisks: {}, ...vd.PDisk};
- }
- structure[String(pDiskId)].vDisks[vDiskId] = {
- ...vd,
- // VDisk doesn't have its own StoragePoolName when located inside StoragePool data
- StoragePoolName: pool.Name,
- };
- });
- });
- });
-
- const structureWithVdisksArray = Object.keys(structure).reduce((acc, el) => {
- const vDisks = structure[el].vDisks;
- const vDisksArray = Object.keys(vDisks).reduce((acc, key, index) => {
- acc.push({...vDisks[key], id: key, order: index});
- return acc;
- }, []);
- acc[el] = {...structure[el], vDisks: vDisksArray};
- return acc;
- }, {});
- return structureWithVdisksArray;
- },
-);
-
-export default node;
diff --git a/src/store/reducers/node/node.ts b/src/store/reducers/node/node.ts
new file mode 100644
index 0000000000..5ddcc90d5b
--- /dev/null
+++ b/src/store/reducers/node/node.ts
@@ -0,0 +1,102 @@
+import {Reducer} from 'redux';
+
+import '../../../services/api';
+import {createRequestActionTypes, createApiRequest} from '../../utils';
+
+import type {NodeAction, NodeState} from './types';
+
+export const FETCH_NODE = createRequestActionTypes('node', 'FETCH_NODE');
+export const FETCH_NODE_STRUCTURE = createRequestActionTypes('node', 'FETCH_NODE_STRUCTURE');
+
+const RESET_NODE = 'node/RESET_NODE';
+
+const initialState = {
+ data: {},
+ loading: true,
+ wasLoaded: false,
+ nodeStructure: {},
+ loadingStructure: true,
+ wasLoadedStructure: false,
+};
+
+const node: Reducer = (state = initialState, action) => {
+ switch (action.type) {
+ case FETCH_NODE.REQUEST: {
+ return {
+ ...state,
+ loading: true,
+ };
+ }
+ case FETCH_NODE.SUCCESS: {
+ return {
+ ...state,
+ data: action.data,
+ loading: false,
+ wasLoaded: true,
+ error: undefined,
+ };
+ }
+ case FETCH_NODE.FAILURE: {
+ return {
+ ...state,
+ error: action.error,
+ loading: false,
+ };
+ }
+ case FETCH_NODE_STRUCTURE.REQUEST: {
+ return {
+ ...state,
+ loadingStructure: true,
+ };
+ }
+ case FETCH_NODE_STRUCTURE.SUCCESS: {
+ return {
+ ...state,
+ nodeStructure: action.data,
+ loadingStructure: false,
+ wasLoadedStructure: true,
+ errorStructure: undefined,
+ };
+ }
+ case FETCH_NODE_STRUCTURE.FAILURE: {
+ return {
+ ...state,
+ errorStructure: action.error,
+ loadingStructure: false,
+ };
+ }
+ case RESET_NODE: {
+ return {
+ ...state,
+ data: {},
+ wasLoaded: false,
+ nodeStructure: {},
+ wasLoadedStructure: false,
+ };
+ }
+ default:
+ return state;
+ }
+};
+
+export const getNodeInfo = (id: string) => {
+ return createApiRequest({
+ request: window.api.getNodeInfo(id),
+ actions: FETCH_NODE,
+ });
+};
+
+export const getNodeStructure = (nodeId: string) => {
+ return createApiRequest({
+ request: window.api.getStorageInfo({nodeId}, {concurrentId: 'getNodeStructure'}),
+ actions: FETCH_NODE_STRUCTURE,
+ });
+};
+
+export function resetNode() {
+ return {
+ type: RESET_NODE,
+ } as const;
+}
+
+export default node;
diff --git a/src/store/reducers/node/selectors.ts b/src/store/reducers/node/selectors.ts
new file mode 100644
index 0000000000..efc4e486d5
--- /dev/null
+++ b/src/store/reducers/node/selectors.ts
@@ -0,0 +1,59 @@
+import type {Selector} from 'reselect';
+import {createSelector} from 'reselect';
+
+import {stringifyVdiskId} from '../../../utils';
+
+import type {
+ NodeStateSlice,
+ PreparedNodeStructure,
+ PreparedStructureVDisk,
+ RawNodeStructure,
+} from './types';
+
+const selectNodeId = (state: NodeStateSlice) => state.node?.data?.SystemStateInfo?.[0].NodeId;
+
+const selectRawNodeStructure = (state: NodeStateSlice) => state.node?.nodeStructure;
+
+export const selectNodeStructure: Selector = createSelector(
+ [selectNodeId, selectRawNodeStructure],
+ (nodeId, storageInfo) => {
+ const pools = storageInfo?.StoragePools;
+ const structure: RawNodeStructure = {};
+
+ pools?.forEach((pool) => {
+ const groups = pool.Groups;
+ groups?.forEach((group) => {
+ const vDisks = group.VDisks?.filter((el) => el.NodeId === nodeId);
+ vDisks?.forEach((vd) => {
+ const vDiskId = stringifyVdiskId(vd.VDiskId);
+ const pDiskId = vd.PDisk?.PDiskId;
+ if (!structure[String(pDiskId)]) {
+ structure[String(pDiskId)] = {vDisks: {}, ...vd.PDisk};
+ }
+ structure[String(pDiskId)].vDisks[vDiskId] = {
+ ...vd,
+ // VDisk doesn't have its own StoragePoolName when located inside StoragePool data
+ StoragePoolName: pool.Name,
+ };
+ });
+ });
+ });
+
+ const structureWithVdisksArray = Object.keys(structure).reduce(
+ (preparedStructure, el) => {
+ const vDisks = structure[el].vDisks;
+ const vDisksArray = Object.keys(vDisks).reduce(
+ (acc, key, index) => {
+ acc.push({...vDisks[key], id: key, order: index});
+ return acc;
+ },
+ [],
+ );
+ preparedStructure[el] = {...structure[el], vDisks: vDisksArray};
+ return preparedStructure;
+ },
+ {},
+ );
+ return structureWithVdisksArray;
+ },
+);
diff --git a/src/store/reducers/node/types.ts b/src/store/reducers/node/types.ts
new file mode 100644
index 0000000000..dd13612eda
--- /dev/null
+++ b/src/store/reducers/node/types.ts
@@ -0,0 +1,44 @@
+import type {IResponseError} from '../../../types/api/error';
+import type {TPDiskStateInfo} from '../../../types/api/pdisk';
+import type {TStorageInfo} from '../../../types/api/storage';
+import type {TEvSystemStateResponse} from '../../../types/api/systemState';
+import type {TVDiskStateInfo} from '../../../types/api/vdisk';
+import type {ApiRequestAction} from '../../utils';
+
+import {FETCH_NODE, FETCH_NODE_STRUCTURE, resetNode} from './node';
+
+interface RawStructurePDisk extends TPDiskStateInfo {
+ vDisks: Record;
+}
+
+export type RawNodeStructure = Record;
+
+export interface PreparedStructureVDisk extends TVDiskStateInfo {
+ id: string;
+ order: number;
+}
+
+export interface PreparedStructurePDisk extends TPDiskStateInfo {
+ vDisks: PreparedStructureVDisk[];
+}
+
+export type PreparedNodeStructure = Record;
+
+export interface NodeState {
+ data: TEvSystemStateResponse;
+ loading: boolean;
+ wasLoaded: boolean;
+
+ nodeStructure: TStorageInfo;
+ loadingStructure: boolean;
+ wasLoadedStructure: boolean;
+}
+
+export type NodeAction =
+ | ApiRequestAction
+ | ApiRequestAction
+ | ReturnType;
+
+export interface NodeStateSlice {
+ node: NodeState;
+}