Skip to content

Commit 7388ff5

Browse files
committed
feat(NavigationTree): virtualized render
1 parent 2e5d42a commit 7388ff5

File tree

11 files changed

+258
-135
lines changed

11 files changed

+258
-135
lines changed

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
},
2525
"dependencies": {
2626
"bem-cn-lite": "^4.1.0",
27+
"react-list": "^0.8.17",
2728
"react-treeview": "^0.4.7"
2829
},
2930
"devDependencies": {
@@ -36,6 +37,7 @@
3637
"@storybook/addon-essentials": "^6.4.19",
3738
"@storybook/preset-scss": "^1.0.3",
3839
"@storybook/react": "^6.4.19",
40+
"@types/react-list": "^0.8.7",
3941
"@types/react-treeview": "^0.4.3",
4042
"@yandex-cloud/browserslist-config": "^1.0.1",
4143
"@yandex-cloud/eslint-config": "^1.0.0",

src/components/NavigationTree/EmptyView/EmptyView.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ import {TreeView} from '../../TreeView/TreeView';
66

77
import './EmptyView.scss';
88

9+
interface EmptyViewProps {
10+
level?: number;
11+
}
12+
913
const b = block('ydb-navigation-tree-view-empty');
1014

11-
export function EmptyView() {
12-
return <TreeView name={<span className={b()}>{i18n('label_empty')}</span>} />;
15+
export function EmptyView({level}: EmptyViewProps) {
16+
return <TreeView name={<span className={b()}>{i18n('label_empty')}</span>} level={level} />;
1317
}

src/components/NavigationTree/ErrorView/ErrorView.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ import {TreeView} from '../../TreeView/TreeView';
66

77
import './ErrorView.scss';
88

9+
interface ErrorViewProps {
10+
level?: number;
11+
}
12+
913
const b = block('ydb-navigation-tree-view-error');
1014

11-
export function ErrorView() {
12-
return <TreeView name={<span className={b()}>{i18n('label_error')}</span>} />;
15+
export function ErrorView({level}: ErrorViewProps) {
16+
return <TreeView name={<span className={b()}>{i18n('label_error')}</span>} level={level} />;
1317
}

src/components/NavigationTree/LoaderView/LoaderView.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,21 @@ import {TreeView} from '../../TreeView/TreeView';
66

77
import './LoaderView.scss';
88

9+
interface LoaderViewProps {
10+
level?: number;
11+
}
12+
913
const b = block('ydb-navigation-tree-view-loader');
1014

11-
export function LoaderView() {
15+
export function LoaderView({level}: LoaderViewProps) {
1216
return (
1317
<TreeView
1418
name={
1519
<div className={b()}>
1620
<Spin size="xs" />
1721
</div>
1822
}
23+
level={level}
1924
/>
2025
);
2126
}
Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,85 @@
11
import React from 'react';
2+
import ReactList from 'react-list';
3+
4+
import type {
5+
NavigationTreeNodeState,
6+
NavigationTreeProps,
7+
NavigationTreeServiceNode,
8+
} from './types';
9+
import {reducer, getNodeState, selectTreeAsList} from './state';
10+
import {isServiceNode} from './utils';
211

3-
import {NavigationTreeProps} from './types';
4-
import {reducer, getNodeState} from './state';
512
import {NavigationTreeNode} from './NavigationTreeNode';
6-
import {NavigationTreeDirectory} from './NavigationTreeDirectory';
13+
import {LoaderView} from './LoaderView/LoaderView';
14+
import {ErrorView} from './ErrorView/ErrorView';
15+
import {EmptyView} from './EmptyView/EmptyView';
716

817
export type {NavigationTreeProps};
918

19+
const renderServiceNode = (node: NavigationTreeServiceNode) => {
20+
const key = `${node.path}|${node.status}`;
21+
22+
if (node.status === 'loading') {
23+
return <LoaderView key={key} level={node.level} />;
24+
}
25+
26+
if (node.status === 'error') {
27+
return <ErrorView key={key} level={node.level} />;
28+
}
29+
30+
return <EmptyView key={key} level={node.level} />;
31+
};
32+
1033
export function NavigationTree({
1134
rootState: partialRootState,
1235
fetchPath,
1336
getActions,
1437
activePath,
1538
onActivePathUpdate,
1639
cache = true,
40+
virtualize = false,
1741
}: NavigationTreeProps) {
1842
const [state, dispatch] = React.useReducer(reducer, {
1943
[partialRootState.path]: getNodeState(partialRootState),
2044
});
21-
const rootState = state[partialRootState.path];
22-
23-
return (
24-
<NavigationTreeNode
25-
path={rootState.path}
26-
state={state}
27-
dispatch={dispatch}
28-
active={rootState.path === activePath}
29-
onActivate={onActivePathUpdate}
30-
getActions={getActions}
31-
>
32-
<NavigationTreeDirectory
45+
const nodesList = React.useMemo(() => selectTreeAsList(state, partialRootState.path), [state]);
46+
47+
const renderNode = (node: NavigationTreeNodeState) => {
48+
return (
49+
<NavigationTreeNode
50+
key={node.path}
3351
state={state}
34-
dispatch={dispatch}
35-
path={rootState.path}
36-
fetchPath={fetchPath}
52+
path={node.path}
3753
activePath={activePath}
38-
onItemActivate={onActivePathUpdate}
54+
fetchPath={fetchPath}
55+
dispatch={dispatch}
56+
onActivate={onActivePathUpdate}
3957
getActions={getActions}
4058
cache={cache}
59+
level={node.level}
4160
/>
42-
</NavigationTreeNode>
61+
);
62+
};
63+
64+
const renderVirtualizedTree = () => (
65+
<ReactList
66+
type="uniform"
67+
length={nodesList.length}
68+
useStaticSize
69+
itemRenderer={(index) => {
70+
const node = nodesList[index];
71+
return isServiceNode(node) ? renderServiceNode(node) : renderNode(node);
72+
}}
73+
/>
4374
);
75+
76+
const renderSimpleTree = () => (
77+
<>
78+
{nodesList.map((node) =>
79+
isServiceNode(node) ? renderServiceNode(node) : renderNode(node),
80+
)}
81+
</>
82+
);
83+
84+
return virtualize ? renderVirtualizedTree() : renderSimpleTree();
4485
}

src/components/NavigationTree/NavigationTreeDirectory.tsx

Lines changed: 0 additions & 106 deletions
This file was deleted.

src/components/NavigationTree/NavigationTreeNode.tsx

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ import {TreeView} from '../TreeView/TreeView';
1111

1212
export interface NavigationTreeNodeProps {
1313
path: string;
14+
fetchPath: NavigationTreeProps['fetchPath'];
15+
activePath?: string;
1416
state: NavigationTreeState;
17+
level?: number;
1518
dispatch: React.Dispatch<NavigationTreeAction>;
1619
children?: React.ReactNode;
17-
active?: boolean;
1820
onActivate?: (path: string) => void;
1921
getActions?: NavigationTreeProps['getActions'];
22+
cache?: boolean;
2023
}
2124

2225
function renderIcon(type: NavigationTreeNodeType | string, collapsed: boolean) {
@@ -36,15 +39,54 @@ function renderIcon(type: NavigationTreeNodeType | string, collapsed: boolean) {
3639

3740
export function NavigationTreeNode({
3841
path,
42+
fetchPath,
43+
activePath,
3944
state,
45+
level,
4046
dispatch,
4147
children,
42-
active,
4348
onActivate,
4449
getActions,
50+
cache,
4551
}: NavigationTreeNodeProps) {
4652
const nodeState = state[path];
4753

54+
React.useEffect(() => {
55+
if (nodeState.collapsed) {
56+
if (!cache) {
57+
dispatch({
58+
type: NavigationTreeActionType.ResetNode,
59+
payload: {path},
60+
});
61+
}
62+
63+
return;
64+
}
65+
66+
if (nodeState.loaded || nodeState.loading) {
67+
return;
68+
}
69+
70+
dispatch({
71+
type: NavigationTreeActionType.StartLoading,
72+
payload: {path},
73+
});
74+
75+
fetchPath(path)
76+
.then((data) => {
77+
dispatch({
78+
type: NavigationTreeActionType.FinishLoading,
79+
payload: {path, activePath, data},
80+
});
81+
})
82+
.catch((error) => {
83+
dispatch({
84+
type: NavigationTreeActionType.FinishLoading,
85+
payload: {path, error},
86+
});
87+
});
88+
}, [nodeState.collapsed]);
89+
4890
const handleClick = React.useCallback(() => {
4991
if (onActivate) {
5092
onActivate(path);
@@ -64,11 +106,12 @@ export function NavigationTreeNode({
64106
name={nodeState.name}
65107
icon={renderIcon(nodeState.type, nodeState.collapsed)}
66108
collapsed={nodeState.collapsed}
67-
active={active}
109+
active={nodeState.path === activePath}
68110
actions={actions}
69111
hasArrow={nodeState.expandable}
70112
onClick={handleClick}
71113
onArrowClick={handleArrowClick}
114+
level={level}
72115
>
73116
{children}
74117
</TreeView>

0 commit comments

Comments
 (0)