ydb-ui-components 5.4.1 → 6.1.0
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.
- package/build/cjs/components/NavigationTree/NavigationTree.d.ts +15 -2
- package/build/cjs/components/NavigationTree/NavigationTree.js +66 -4
- package/build/cjs/components/NavigationTree/NavigationTreeNode.d.ts +6 -2
- package/build/cjs/components/NavigationTree/NavigationTreeNode.js +9 -40
- package/build/cjs/components/NavigationTree/state.d.ts +7 -0
- package/build/cjs/components/NavigationTree/state.js +32 -4
- package/build/cjs/components/NavigationTree/types.d.ts +11 -2
- package/build/cjs/components/TreeView/TreeView.d.ts +7 -1
- package/build/cjs/components/TreeView/TreeView.js +2 -2
- package/build/cjs/components/icons/Secret.d.ts +2 -0
- package/build/cjs/components/icons/Secret.js +8 -0
- package/build/cjs/components/icons/index.d.ts +1 -0
- package/build/cjs/components/icons/index.js +3 -1
- package/build/esm/components/NavigationTree/NavigationTree.d.ts +15 -2
- package/build/esm/components/NavigationTree/NavigationTree.js +67 -4
- package/build/esm/components/NavigationTree/NavigationTreeNode.d.ts +6 -2
- package/build/esm/components/NavigationTree/NavigationTreeNode.js +10 -41
- package/build/esm/components/NavigationTree/state.d.ts +7 -0
- package/build/esm/components/NavigationTree/state.js +32 -4
- package/build/esm/components/NavigationTree/types.d.ts +11 -2
- package/build/esm/components/TreeView/TreeView.d.ts +7 -1
- package/build/esm/components/TreeView/TreeView.js +2 -2
- package/build/esm/components/icons/Secret.d.ts +2 -0
- package/build/esm/components/icons/Secret.js +5 -0
- package/build/esm/components/icons/index.d.ts +1 -0
- package/build/esm/components/icons/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import type { NavigationTreeProps } from './types';
|
|
3
3
|
export type { NavigationTreeProps };
|
|
4
|
-
export
|
|
4
|
+
export interface NavigationTreeHandle {
|
|
5
|
+
/**
|
|
6
|
+
* Scrolls the currently active node into view (if any).
|
|
7
|
+
* Has no effect if there is no active node or it is not present in the tree.
|
|
8
|
+
*/
|
|
9
|
+
scrollToActive: () => void;
|
|
10
|
+
}
|
|
11
|
+
declare function NavigationTreeInner<D = any, M = any>({ rootState: partialRootState, fetchPath, getActions, renderAdditionalNodeElements, activePath, onActionsOpenToggle, onActivePathUpdate, cache, virtualize, }: NavigationTreeProps<D, M>, ref: React.ForwardedRef<NavigationTreeHandle>): JSX.Element;
|
|
12
|
+
/**
|
|
13
|
+
* `React.forwardRef` loses generics, so we cast to preserve the `<D, M>` API.
|
|
14
|
+
*/
|
|
15
|
+
export declare const NavigationTree: <D = any, M = any>(props: NavigationTreeProps<D, M> & {
|
|
16
|
+
ref?: React.ForwardedRef<NavigationTreeHandle> | undefined;
|
|
17
|
+
}) => ReturnType<typeof NavigationTreeInner>;
|
|
@@ -23,19 +23,81 @@ const renderServiceNode = (node) => {
|
|
|
23
23
|
}
|
|
24
24
|
return (0, jsx_runtime_1.jsx)(EmptyView_1.EmptyView, { level: node.level }, key);
|
|
25
25
|
};
|
|
26
|
-
function
|
|
26
|
+
function NavigationTreeInner({ rootState: partialRootState, fetchPath, getActions, renderAdditionalNodeElements, activePath, onActionsOpenToggle, onActivePathUpdate, cache = true, virtualize = false, }, ref) {
|
|
27
27
|
const [state, dispatch] = react_1.default.useReducer(state_1.reducer, {
|
|
28
28
|
[partialRootState.path]: (0, state_1.getNodeState)(partialRootState),
|
|
29
29
|
});
|
|
30
30
|
const nodesList = react_1.default.useMemo(() => (0, state_1.selectTreeAsList)(state, partialRootState.path), [partialRootState.path, state]);
|
|
31
|
+
const nodesToLoad = react_1.default.useMemo(() => nodesList.filter((node) => !(0, utils_1.isServiceNode)(node) &&
|
|
32
|
+
!node.collapsed &&
|
|
33
|
+
!node.loaded &&
|
|
34
|
+
!node.loading &&
|
|
35
|
+
!node.error), [nodesList]);
|
|
36
|
+
const reactListRef = react_1.default.useRef(null);
|
|
37
|
+
/**
|
|
38
|
+
* Monotonic counter of in-flight load requests. The current value is captured per
|
|
39
|
+
* dispatched `StartLoading` and forwarded to the resolving `FinishLoading` /
|
|
40
|
+
* `ErrorLoading`, so the reducer can drop responses that no longer match the latest
|
|
41
|
+
* request for a given path (see the stale-result guard in `reducer`).
|
|
42
|
+
*/
|
|
43
|
+
const requestIdRef = react_1.default.useRef(0);
|
|
44
|
+
const activeItemRef = react_1.default.useRef(null);
|
|
45
|
+
const activeNodeIndex = react_1.default.useMemo(() => nodesList.findIndex((node) => !(0, utils_1.isServiceNode)(node) && node.path === activePath), [activePath, nodesList]);
|
|
46
|
+
const scrolledActivePathRef = react_1.default.useRef(undefined);
|
|
47
|
+
const scrollToActive = react_1.default.useCallback(() => {
|
|
48
|
+
var _a, _b;
|
|
49
|
+
if (activeNodeIndex < 0)
|
|
50
|
+
return;
|
|
51
|
+
if (virtualize) {
|
|
52
|
+
(_a = reactListRef.current) === null || _a === void 0 ? void 0 : _a.scrollAround(activeNodeIndex);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
(_b = activeItemRef.current) === null || _b === void 0 ? void 0 : _b.scrollIntoView({ block: 'nearest' });
|
|
56
|
+
}
|
|
57
|
+
}, [activeNodeIndex, virtualize]);
|
|
58
|
+
react_1.default.useImperativeHandle(ref, () => ({ scrollToActive }), [scrollToActive]);
|
|
59
|
+
react_1.default.useEffect(() => {
|
|
60
|
+
nodesToLoad.forEach((node) => {
|
|
61
|
+
const requestId = ++requestIdRef.current;
|
|
62
|
+
dispatch({
|
|
63
|
+
type: state_1.NavigationTreeActionType.StartLoading,
|
|
64
|
+
payload: { path: node.path, requestId },
|
|
65
|
+
});
|
|
66
|
+
fetchPath(node.path)
|
|
67
|
+
.then((data) => {
|
|
68
|
+
dispatch({
|
|
69
|
+
type: state_1.NavigationTreeActionType.FinishLoading,
|
|
70
|
+
payload: { path: node.path, activePath, data, requestId },
|
|
71
|
+
});
|
|
72
|
+
})
|
|
73
|
+
.catch((error) => {
|
|
74
|
+
dispatch({
|
|
75
|
+
type: state_1.NavigationTreeActionType.ErrorLoading,
|
|
76
|
+
payload: { path: node.path, error, requestId },
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}, [activePath, dispatch, fetchPath, nodesToLoad]);
|
|
81
|
+
react_1.default.useEffect(() => {
|
|
82
|
+
if (scrolledActivePathRef.current === activePath)
|
|
83
|
+
return;
|
|
84
|
+
if (activeNodeIndex < 0)
|
|
85
|
+
return;
|
|
86
|
+
scrollToActive();
|
|
87
|
+
scrolledActivePathRef.current = activePath;
|
|
88
|
+
}, [activeNodeIndex, activePath, scrollToActive]);
|
|
31
89
|
const renderNode = (node) => {
|
|
32
|
-
|
|
90
|
+
const isActive = node.path === activePath;
|
|
91
|
+
return ((0, jsx_runtime_1.jsx)(NavigationTreeNode_1.NavigationTreeNode, { state: state, path: node.path, activePath: activePath, dispatch: dispatch, onActivate: onActivePathUpdate, getActions: getActions, onActionsOpenToggle: onActionsOpenToggle, renderAdditionalNodeElements: renderAdditionalNodeElements, cache: cache, level: node.level, activeItemRef: isActive ? activeItemRef : undefined }, node.path));
|
|
33
92
|
};
|
|
34
|
-
const renderVirtualizedTree = () => ((0, jsx_runtime_1.jsx)(react_list_1.default, { type: "uniform", length: nodesList.length, useStaticSize: true, itemRenderer: (index) => {
|
|
93
|
+
const renderVirtualizedTree = () => ((0, jsx_runtime_1.jsx)(react_list_1.default, { ref: reactListRef, type: "uniform", length: nodesList.length, useStaticSize: true, itemRenderer: (index) => {
|
|
35
94
|
const node = nodesList[index];
|
|
36
95
|
return (0, utils_1.isServiceNode)(node) ? renderServiceNode(node) : renderNode(node);
|
|
37
96
|
} }));
|
|
38
97
|
const renderSimpleTree = () => ((0, jsx_runtime_1.jsx)(react_1.default.Fragment, { children: nodesList.map((node) => (0, utils_1.isServiceNode)(node) ? renderServiceNode(node) : renderNode(node)) }));
|
|
39
98
|
return virtualize ? renderVirtualizedTree() : renderSimpleTree();
|
|
40
99
|
}
|
|
41
|
-
|
|
100
|
+
/**
|
|
101
|
+
* `React.forwardRef` loses generics, so we cast to preserve the `<D, M>` API.
|
|
102
|
+
*/
|
|
103
|
+
exports.NavigationTree = react_1.default.forwardRef(NavigationTreeInner);
|
|
@@ -3,7 +3,6 @@ import type { NavigationTreeAction } from './state';
|
|
|
3
3
|
import type { NavigationTreeProps, NavigationTreeState } from './types';
|
|
4
4
|
export interface NavigationTreeNodeProps {
|
|
5
5
|
path: string;
|
|
6
|
-
fetchPath: NavigationTreeProps['fetchPath'];
|
|
7
6
|
activePath?: string;
|
|
8
7
|
state: NavigationTreeState;
|
|
9
8
|
level?: number;
|
|
@@ -14,5 +13,10 @@ export interface NavigationTreeNodeProps {
|
|
|
14
13
|
onActionsOpenToggle?: NavigationTreeProps['onActionsOpenToggle'];
|
|
15
14
|
renderAdditionalNodeElements?: NavigationTreeProps['renderAdditionalNodeElements'];
|
|
16
15
|
cache?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Ref object attached to the item element when this node is the active one.
|
|
18
|
+
* Set only for the active node so the parent can scroll it into view.
|
|
19
|
+
*/
|
|
20
|
+
activeItemRef?: React.RefObject<HTMLDivElement>;
|
|
17
21
|
}
|
|
18
|
-
export declare function NavigationTreeNode({ path,
|
|
22
|
+
export declare function NavigationTreeNode({ path, activePath, state, level, dispatch, children, onActivate, getActions, onActionsOpenToggle, renderAdditionalNodeElements, cache, activeItemRef, }: NavigationTreeNodeProps): JSX.Element;
|
|
@@ -40,53 +40,22 @@ function renderIcon(type, collapsed) {
|
|
|
40
40
|
return (0, jsx_runtime_1.jsx)(icons_1.ViewIcon, {});
|
|
41
41
|
case 'resource_pool':
|
|
42
42
|
return (0, jsx_runtime_1.jsx)(icons_1.ResourcePoolIcon, {});
|
|
43
|
+
case 'secret':
|
|
44
|
+
return (0, jsx_runtime_1.jsx)(icons_1.SecretIcon, {});
|
|
43
45
|
default:
|
|
44
46
|
return null;
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
|
-
function NavigationTreeNode({ path,
|
|
49
|
+
function NavigationTreeNode({ path, activePath, state, level, dispatch, children, onActivate, getActions, onActionsOpenToggle, renderAdditionalNodeElements, cache, activeItemRef, }) {
|
|
48
50
|
const nodeState = state[path];
|
|
49
51
|
react_1.default.useEffect(() => {
|
|
50
|
-
if (nodeState.collapsed) {
|
|
51
|
-
if (!cache) {
|
|
52
|
-
dispatch({
|
|
53
|
-
type: state_1.NavigationTreeActionType.ResetNode,
|
|
54
|
-
payload: { path },
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
if (nodeState.loaded || nodeState.loading || nodeState.error) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
dispatch({
|
|
63
|
-
type: state_1.NavigationTreeActionType.StartLoading,
|
|
64
|
-
payload: { path },
|
|
65
|
-
});
|
|
66
|
-
fetchPath(path)
|
|
67
|
-
.then((data) => {
|
|
52
|
+
if (nodeState.collapsed && !cache) {
|
|
68
53
|
dispatch({
|
|
69
|
-
type: state_1.NavigationTreeActionType.
|
|
70
|
-
payload: { path
|
|
54
|
+
type: state_1.NavigationTreeActionType.ResetNode,
|
|
55
|
+
payload: { path },
|
|
71
56
|
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
dispatch({
|
|
75
|
-
type: state_1.NavigationTreeActionType.ErrorLoading,
|
|
76
|
-
payload: { path, error },
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
}, [
|
|
80
|
-
activePath,
|
|
81
|
-
cache,
|
|
82
|
-
dispatch,
|
|
83
|
-
fetchPath,
|
|
84
|
-
nodeState.collapsed,
|
|
85
|
-
nodeState.loaded,
|
|
86
|
-
nodeState.loading,
|
|
87
|
-
nodeState.error,
|
|
88
|
-
path,
|
|
89
|
-
]);
|
|
57
|
+
}
|
|
58
|
+
}, [cache, dispatch, nodeState.collapsed, path]);
|
|
90
59
|
const handleClick = react_1.default.useCallback(() => {
|
|
91
60
|
if (onActivate) {
|
|
92
61
|
onActivate(path);
|
|
@@ -108,6 +77,6 @@ function NavigationTreeNode({ path, fetchPath, activePath, state, level, dispatc
|
|
|
108
77
|
isOpen,
|
|
109
78
|
});
|
|
110
79
|
}, [nodeState.path, nodeState.type, onActionsOpenToggle]);
|
|
111
|
-
return ((0, jsx_runtime_1.jsx)(TreeView_1.TreeView, { name: nodeState.name, icon: renderIcon(nodeState.type, nodeState.collapsed), collapsed: nodeState.collapsed, active: nodeState.path === activePath, actions: actions, additionalNodeElements: additionalNodeElements, hasArrow: nodeState.expandable, onClick: handleClick, onArrowClick: handleArrowClick, onActionsOpenToggle: handleActionsOpenToggle, level: level, children: children }));
|
|
80
|
+
return ((0, jsx_runtime_1.jsx)(TreeView_1.TreeView, { name: nodeState.name, icon: renderIcon(nodeState.type, nodeState.collapsed), collapsed: nodeState.collapsed, active: nodeState.path === activePath, actions: actions, additionalNodeElements: additionalNodeElements, hasArrow: nodeState.expandable, onClick: handleClick, onArrowClick: handleArrowClick, onActionsOpenToggle: handleActionsOpenToggle, level: level, itemRef: activeItemRef, children: children }));
|
|
112
81
|
}
|
|
113
82
|
exports.NavigationTreeNode = NavigationTreeNode;
|
|
@@ -15,6 +15,8 @@ export type NavigationTreeAction = {
|
|
|
15
15
|
type: NavigationTreeActionType.StartLoading;
|
|
16
16
|
payload: {
|
|
17
17
|
path: string;
|
|
18
|
+
/** Monotonic id captured at dispatch time; written into node state. */
|
|
19
|
+
requestId: number;
|
|
18
20
|
};
|
|
19
21
|
} | {
|
|
20
22
|
type: NavigationTreeActionType.FinishLoading;
|
|
@@ -22,12 +24,16 @@ export type NavigationTreeAction = {
|
|
|
22
24
|
path: string;
|
|
23
25
|
activePath?: string;
|
|
24
26
|
data: NavigationTreeDataItem[];
|
|
27
|
+
/** Id of the `StartLoading` this result belongs to; stale ones are dropped. */
|
|
28
|
+
requestId: number;
|
|
25
29
|
};
|
|
26
30
|
} | {
|
|
27
31
|
type: NavigationTreeActionType.ErrorLoading;
|
|
28
32
|
payload: {
|
|
29
33
|
path: string;
|
|
30
34
|
error: unknown;
|
|
35
|
+
/** Id of the `StartLoading` this error belongs to; stale ones are dropped. */
|
|
36
|
+
requestId: number;
|
|
31
37
|
};
|
|
32
38
|
} | {
|
|
33
39
|
type: NavigationTreeActionType.ResetNode;
|
|
@@ -41,6 +47,7 @@ export declare function getDefaultNodeState(): {
|
|
|
41
47
|
loaded: boolean;
|
|
42
48
|
error: boolean;
|
|
43
49
|
children: never[];
|
|
50
|
+
requestId: number;
|
|
44
51
|
};
|
|
45
52
|
export declare function getNodeState(partialState: NavigationTreeNodePartialState): NavigationTreeNodeState;
|
|
46
53
|
export declare function reducer(state: NavigationTreeState | undefined, action: NavigationTreeAction): NavigationTreeState;
|
|
@@ -17,6 +17,7 @@ function getDefaultNodeState() {
|
|
|
17
17
|
loaded: false,
|
|
18
18
|
error: false,
|
|
19
19
|
children: [],
|
|
20
|
+
requestId: 0,
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
23
|
exports.getDefaultNodeState = getDefaultNodeState;
|
|
@@ -30,9 +31,27 @@ function reducer(state = {}, action) {
|
|
|
30
31
|
case NavigationTreeActionType.ToggleCollapsed:
|
|
31
32
|
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { collapsed: !state[action.payload.path].collapsed }) });
|
|
32
33
|
case NavigationTreeActionType.StartLoading:
|
|
33
|
-
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { loading: true, loaded: false, error: false, children: [] }) });
|
|
34
|
+
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { loading: true, loaded: false, error: false, children: [], requestId: action.payload.requestId }) });
|
|
34
35
|
case NavigationTreeActionType.FinishLoading: {
|
|
35
|
-
const
|
|
36
|
+
const currentNode = state[action.payload.path];
|
|
37
|
+
// Ignore stale results. A result is stale when:
|
|
38
|
+
// * the node was removed entirely, OR
|
|
39
|
+
// * the node's `requestId` no longer matches the one captured at dispatch time
|
|
40
|
+
// (i.e. `ResetNode` cleared it, or a newer `StartLoading` superseded it).
|
|
41
|
+
// The previous `loading`-only guard couldn't distinguish "still loading the original
|
|
42
|
+
// request" from "loading a second, newer request", which allowed an older response
|
|
43
|
+
// to overwrite a newer one after a collapse-expand cycle with `cache: false`.
|
|
44
|
+
// Note: a plain collapse with `cache: true` keeps `requestId` intact, so the fetch
|
|
45
|
+
// is still allowed to complete and populate the cache — that's intentional.
|
|
46
|
+
if (!currentNode || currentNode.requestId !== action.payload.requestId) {
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
const newState = Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, currentNode), { loading: false, loaded: Boolean(action.payload.data), error: false,
|
|
50
|
+
// Clear the in-flight marker now that this request has completed.
|
|
51
|
+
// The counter in `NavigationTree` only allocates ids >= 1, so any
|
|
52
|
+
// late stale `FinishLoading` / `ErrorLoading` for this path will
|
|
53
|
+
// still fail the guard above (its id > 0 won't match this 0).
|
|
54
|
+
requestId: 0 }) });
|
|
36
55
|
if (action.payload.data) {
|
|
37
56
|
newState[action.payload.path].children = action.payload.data.map(({ name }) => name);
|
|
38
57
|
for (const item of action.payload.data) {
|
|
@@ -50,8 +69,17 @@ function reducer(state = {}, action) {
|
|
|
50
69
|
}
|
|
51
70
|
return newState;
|
|
52
71
|
}
|
|
53
|
-
case NavigationTreeActionType.ErrorLoading:
|
|
54
|
-
|
|
72
|
+
case NavigationTreeActionType.ErrorLoading: {
|
|
73
|
+
const currentNode = state[action.payload.path];
|
|
74
|
+
// Ignore stale errors — see the matching note on `FinishLoading` above.
|
|
75
|
+
if (!currentNode || currentNode.requestId !== action.payload.requestId) {
|
|
76
|
+
return state;
|
|
77
|
+
}
|
|
78
|
+
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, currentNode), { loading: false, loaded: false, error: true,
|
|
79
|
+
// See note in `FinishLoading`: clear the in-flight id on completion
|
|
80
|
+
// to keep `requestId === 0` ⇔ "no active request".
|
|
81
|
+
requestId: 0 }) });
|
|
82
|
+
}
|
|
55
83
|
case NavigationTreeActionType.ResetNode:
|
|
56
84
|
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), getDefaultNodeState()) });
|
|
57
85
|
default:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import type { DropdownMenuItemMixed } from '@gravity-ui/uikit';
|
|
3
|
-
export type NavigationTreeNodeType = 'async_replication' | 'column_table' | 'resource_pool' | 'database' | 'directory' | 'external_data_source' | 'external_table' | 'index_table' | 'index' | 'stream' | 'system_table' | 'streaming_query' | 'table' | 'topic' | 'transfer' | 'view';
|
|
3
|
+
export type NavigationTreeNodeType = 'async_replication' | 'column_table' | 'resource_pool' | 'database' | 'directory' | 'external_data_source' | 'external_table' | 'index_table' | 'index' | 'secret' | 'stream' | 'system_table' | 'streaming_query' | 'table' | 'topic' | 'transfer' | 'view';
|
|
4
4
|
export interface NavigationTreeDataItem<M = unknown> {
|
|
5
5
|
name: string;
|
|
6
6
|
type: NavigationTreeNodeType;
|
|
@@ -24,13 +24,22 @@ export interface NavigationTreeNodeState {
|
|
|
24
24
|
children: string[];
|
|
25
25
|
level?: number;
|
|
26
26
|
meta?: unknown;
|
|
27
|
+
/**
|
|
28
|
+
* Id of the in-flight load request for this node. Assigned by `StartLoading`,
|
|
29
|
+
* cleared back to `0` on `FinishLoading` / `ErrorLoading` / `ResetNode`, so
|
|
30
|
+
* `requestId === 0` always means "no active request". Used to discard stale
|
|
31
|
+
* results: a `FinishLoading` / `ErrorLoading` whose payload id does not match
|
|
32
|
+
* the current value is dropped (so a late response from a request superseded
|
|
33
|
+
* by `ResetNode` + a new `StartLoading` cannot overwrite the newer one).
|
|
34
|
+
*/
|
|
35
|
+
requestId: number;
|
|
27
36
|
}
|
|
28
37
|
export interface NavigationTreeServiceNode {
|
|
29
38
|
path: string;
|
|
30
39
|
status: 'loading' | 'error' | 'empty';
|
|
31
40
|
level?: number;
|
|
32
41
|
}
|
|
33
|
-
export type NavigationTreeNodePartialState = Omit<NavigationTreeNodeState, 'loading' | 'loaded' | 'error' | 'children'>;
|
|
42
|
+
export type NavigationTreeNodePartialState = Omit<NavigationTreeNodeState, 'loading' | 'loaded' | 'error' | 'children' | 'requestId'>;
|
|
34
43
|
export interface NavigationTreeProps<D = any, M = any> {
|
|
35
44
|
rootState: NavigationTreeNodePartialState;
|
|
36
45
|
fetchPath: (path: string) => Promise<NavigationTreeDataItem<M>[]>;
|
|
@@ -15,5 +15,11 @@ export interface TreeViewProps {
|
|
|
15
15
|
actions?: DropdownMenuItemMixed<any>[];
|
|
16
16
|
additionalNodeElements?: JSX.Element;
|
|
17
17
|
level?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Ref object passed to the inner item element.
|
|
20
|
+
* Use this if you need to control scroll-into-view from a parent component
|
|
21
|
+
* (e.g. `itemRef.current?.scrollIntoView({block: 'nearest'})`).
|
|
22
|
+
*/
|
|
23
|
+
itemRef?: React.Ref<HTMLDivElement>;
|
|
18
24
|
}
|
|
19
|
-
export declare function TreeView({ children, name, title, icon, collapsed, active, onClick, onArrowClick, onActionsOpenToggle, hasArrow, actions, additionalNodeElements, level, }: TreeViewProps): JSX.Element;
|
|
25
|
+
export declare function TreeView({ children, name, title, icon, collapsed, active, onClick, onArrowClick, onActionsOpenToggle, hasArrow, actions, additionalNodeElements, level, itemRef, }: TreeViewProps): JSX.Element;
|
|
@@ -11,7 +11,7 @@ const cn_1 = require("../../utils/cn");
|
|
|
11
11
|
require("./TreeView.css");
|
|
12
12
|
const TREE_LEVEL_CSS_VAR = '--ydb-tree-view-level';
|
|
13
13
|
const b = (0, cn_1.block)('ydb-tree-view');
|
|
14
|
-
function TreeView({ children, name, title, icon, collapsed = true, active = false, onClick, onArrowClick, onActionsOpenToggle, hasArrow = false, actions, additionalNodeElements, level, }) {
|
|
14
|
+
function TreeView({ children, name, title, icon, collapsed = true, active = false, onClick, onArrowClick, onActionsOpenToggle, hasArrow = false, actions, additionalNodeElements, level, itemRef, }) {
|
|
15
15
|
const handleClick = react_1.default.useCallback((event) => {
|
|
16
16
|
if (!onClick)
|
|
17
17
|
return;
|
|
@@ -37,7 +37,7 @@ function TreeView({ children, name, title, icon, collapsed = true, active = fals
|
|
|
37
37
|
}
|
|
38
38
|
const tooltipContent = title !== null && title !== void 0 ? title : (typeof name === 'string' ? name : '');
|
|
39
39
|
const isTooltipDisabled = tooltipContent.trim().length === 0;
|
|
40
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: b(), style: { [TREE_LEVEL_CSS_VAR]: level }, children: (0, jsx_runtime_1.jsxs)("div", { className: "tree-view", children: [(0, jsx_runtime_1.jsxs)("div", { className: `${itemClassName} ${b('item', { active })}`, onClick: handleClick, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: `${arrowClassName} ${b('arrow', {
|
|
40
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: b(), style: { [TREE_LEVEL_CSS_VAR]: level }, children: (0, jsx_runtime_1.jsxs)("div", { className: "tree-view", children: [(0, jsx_runtime_1.jsxs)("div", { ref: itemRef, className: `${itemClassName} ${b('item', { active })}`, onClick: handleClick, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: `${arrowClassName} ${b('arrow', {
|
|
41
41
|
collapsed,
|
|
42
42
|
hidden: !hasArrow,
|
|
43
43
|
})}`, disabled: !handleArrowClick, onClick: handleArrowClick }), (0, jsx_runtime_1.jsxs)("div", { className: b('content'), children: [(0, jsx_runtime_1.jsx)(uikit_1.ActionTooltip, { title: tooltipContent, disabled: isTooltipDisabled, children: (0, jsx_runtime_1.jsxs)("div", { className: b('label'), children: [icon && (0, jsx_runtime_1.jsx)("div", { className: b('icon'), children: icon }), (0, jsx_runtime_1.jsx)("div", { className: b('text'), children: name })] }) }), actions && actions.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: b('actions'), children: [additionalNodeElements, (0, jsx_runtime_1.jsx)(uikit_1.DropdownMenu, { onOpenToggle: onActionsOpenToggle, defaultSwitcherProps: {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SecretIcon = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
function SecretIcon(props) {
|
|
6
|
+
return ((0, jsx_runtime_1.jsx)("svg", Object.assign({ xmlns: "http://www.w3.org/2000/svg", height: 16, width: 16, viewBox: "0 0 16 16", fill: "currentColor" }, props, { children: (0, jsx_runtime_1.jsx)("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M8 1a4 4 0 0 1 4 4v1l.154.004A3 3 0 0 1 15 9v3a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V9a3 3 0 0 1 2.846-2.996L4 6V5a4 4 0 0 1 4-4m0 7.75a.75.75 0 0 0-.75.75v2a.75.75 0 0 0 1.5 0v-2A.75.75 0 0 0 8 8.75M8 2.5A2.5 2.5 0 0 0 5.5 5v1h5V5A2.5 2.5 0 0 0 8 2.5" }) })));
|
|
7
|
+
}
|
|
8
|
+
exports.SecretIcon = SecretIcon;
|
|
@@ -6,6 +6,7 @@ export { ExternalTableIcon } from './ExternalTable';
|
|
|
6
6
|
export { FolderIcon } from './Folder';
|
|
7
7
|
export { FolderOpenIcon } from './FolderOpen';
|
|
8
8
|
export { ResourcePoolIcon } from './ResourcePool';
|
|
9
|
+
export { SecretIcon } from './Secret';
|
|
9
10
|
export { TableIcon } from './Table';
|
|
10
11
|
export { TableIndexIcon } from './TableIndex';
|
|
11
12
|
export { TopicIcon } from './Topic';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.StreamingQueryIcon = exports.ViewIcon = exports.TransferIcon = exports.TopicIcon = exports.TableIndexIcon = exports.TableIcon = exports.ResourcePoolIcon = exports.FolderOpenIcon = exports.FolderIcon = exports.ExternalTableIcon = exports.ExternalDataSourceIcon = exports.DatabaseIcon = exports.ColumnTableIcon = exports.AsyncReplicationIcon = void 0;
|
|
3
|
+
exports.StreamingQueryIcon = exports.ViewIcon = exports.TransferIcon = exports.TopicIcon = exports.TableIndexIcon = exports.TableIcon = exports.SecretIcon = exports.ResourcePoolIcon = exports.FolderOpenIcon = exports.FolderIcon = exports.ExternalTableIcon = exports.ExternalDataSourceIcon = exports.DatabaseIcon = exports.ColumnTableIcon = exports.AsyncReplicationIcon = void 0;
|
|
4
4
|
var AsyncReplication_1 = require("./AsyncReplication");
|
|
5
5
|
Object.defineProperty(exports, "AsyncReplicationIcon", { enumerable: true, get: function () { return AsyncReplication_1.AsyncReplicationIcon; } });
|
|
6
6
|
var ColumnTable_1 = require("./ColumnTable");
|
|
@@ -17,6 +17,8 @@ var FolderOpen_1 = require("./FolderOpen");
|
|
|
17
17
|
Object.defineProperty(exports, "FolderOpenIcon", { enumerable: true, get: function () { return FolderOpen_1.FolderOpenIcon; } });
|
|
18
18
|
var ResourcePool_1 = require("./ResourcePool");
|
|
19
19
|
Object.defineProperty(exports, "ResourcePoolIcon", { enumerable: true, get: function () { return ResourcePool_1.ResourcePoolIcon; } });
|
|
20
|
+
var Secret_1 = require("./Secret");
|
|
21
|
+
Object.defineProperty(exports, "SecretIcon", { enumerable: true, get: function () { return Secret_1.SecretIcon; } });
|
|
20
22
|
var Table_1 = require("./Table");
|
|
21
23
|
Object.defineProperty(exports, "TableIcon", { enumerable: true, get: function () { return Table_1.TableIcon; } });
|
|
22
24
|
var TableIndex_1 = require("./TableIndex");
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import type { NavigationTreeProps } from './types';
|
|
3
3
|
export type { NavigationTreeProps };
|
|
4
|
-
export
|
|
4
|
+
export interface NavigationTreeHandle {
|
|
5
|
+
/**
|
|
6
|
+
* Scrolls the currently active node into view (if any).
|
|
7
|
+
* Has no effect if there is no active node or it is not present in the tree.
|
|
8
|
+
*/
|
|
9
|
+
scrollToActive: () => void;
|
|
10
|
+
}
|
|
11
|
+
declare function NavigationTreeInner<D = any, M = any>({ rootState: partialRootState, fetchPath, getActions, renderAdditionalNodeElements, activePath, onActionsOpenToggle, onActivePathUpdate, cache, virtualize, }: NavigationTreeProps<D, M>, ref: React.ForwardedRef<NavigationTreeHandle>): JSX.Element;
|
|
12
|
+
/**
|
|
13
|
+
* `React.forwardRef` loses generics, so we cast to preserve the `<D, M>` API.
|
|
14
|
+
*/
|
|
15
|
+
export declare const NavigationTree: <D = any, M = any>(props: NavigationTreeProps<D, M> & {
|
|
16
|
+
ref?: React.ForwardedRef<NavigationTreeHandle> | undefined;
|
|
17
|
+
}) => ReturnType<typeof NavigationTreeInner>;
|
|
@@ -5,7 +5,7 @@ import { EmptyView } from './EmptyView/EmptyView';
|
|
|
5
5
|
import { ErrorView } from './ErrorView/ErrorView';
|
|
6
6
|
import { LoaderView } from './LoaderView/LoaderView';
|
|
7
7
|
import { NavigationTreeNode } from './NavigationTreeNode';
|
|
8
|
-
import { getNodeState, reducer, selectTreeAsList } from './state';
|
|
8
|
+
import { NavigationTreeActionType, getNodeState, reducer, selectTreeAsList } from './state';
|
|
9
9
|
import { isServiceNode } from './utils';
|
|
10
10
|
const renderServiceNode = (node) => {
|
|
11
11
|
const key = `${node.path}|${node.status}`;
|
|
@@ -17,18 +17,81 @@ const renderServiceNode = (node) => {
|
|
|
17
17
|
}
|
|
18
18
|
return _jsx(EmptyView, { level: node.level }, key);
|
|
19
19
|
};
|
|
20
|
-
|
|
20
|
+
function NavigationTreeInner({ rootState: partialRootState, fetchPath, getActions, renderAdditionalNodeElements, activePath, onActionsOpenToggle, onActivePathUpdate, cache = true, virtualize = false, }, ref) {
|
|
21
21
|
const [state, dispatch] = React.useReducer(reducer, {
|
|
22
22
|
[partialRootState.path]: getNodeState(partialRootState),
|
|
23
23
|
});
|
|
24
24
|
const nodesList = React.useMemo(() => selectTreeAsList(state, partialRootState.path), [partialRootState.path, state]);
|
|
25
|
+
const nodesToLoad = React.useMemo(() => nodesList.filter((node) => !isServiceNode(node) &&
|
|
26
|
+
!node.collapsed &&
|
|
27
|
+
!node.loaded &&
|
|
28
|
+
!node.loading &&
|
|
29
|
+
!node.error), [nodesList]);
|
|
30
|
+
const reactListRef = React.useRef(null);
|
|
31
|
+
/**
|
|
32
|
+
* Monotonic counter of in-flight load requests. The current value is captured per
|
|
33
|
+
* dispatched `StartLoading` and forwarded to the resolving `FinishLoading` /
|
|
34
|
+
* `ErrorLoading`, so the reducer can drop responses that no longer match the latest
|
|
35
|
+
* request for a given path (see the stale-result guard in `reducer`).
|
|
36
|
+
*/
|
|
37
|
+
const requestIdRef = React.useRef(0);
|
|
38
|
+
const activeItemRef = React.useRef(null);
|
|
39
|
+
const activeNodeIndex = React.useMemo(() => nodesList.findIndex((node) => !isServiceNode(node) && node.path === activePath), [activePath, nodesList]);
|
|
40
|
+
const scrolledActivePathRef = React.useRef(undefined);
|
|
41
|
+
const scrollToActive = React.useCallback(() => {
|
|
42
|
+
var _a, _b;
|
|
43
|
+
if (activeNodeIndex < 0)
|
|
44
|
+
return;
|
|
45
|
+
if (virtualize) {
|
|
46
|
+
(_a = reactListRef.current) === null || _a === void 0 ? void 0 : _a.scrollAround(activeNodeIndex);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
(_b = activeItemRef.current) === null || _b === void 0 ? void 0 : _b.scrollIntoView({ block: 'nearest' });
|
|
50
|
+
}
|
|
51
|
+
}, [activeNodeIndex, virtualize]);
|
|
52
|
+
React.useImperativeHandle(ref, () => ({ scrollToActive }), [scrollToActive]);
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
nodesToLoad.forEach((node) => {
|
|
55
|
+
const requestId = ++requestIdRef.current;
|
|
56
|
+
dispatch({
|
|
57
|
+
type: NavigationTreeActionType.StartLoading,
|
|
58
|
+
payload: { path: node.path, requestId },
|
|
59
|
+
});
|
|
60
|
+
fetchPath(node.path)
|
|
61
|
+
.then((data) => {
|
|
62
|
+
dispatch({
|
|
63
|
+
type: NavigationTreeActionType.FinishLoading,
|
|
64
|
+
payload: { path: node.path, activePath, data, requestId },
|
|
65
|
+
});
|
|
66
|
+
})
|
|
67
|
+
.catch((error) => {
|
|
68
|
+
dispatch({
|
|
69
|
+
type: NavigationTreeActionType.ErrorLoading,
|
|
70
|
+
payload: { path: node.path, error, requestId },
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}, [activePath, dispatch, fetchPath, nodesToLoad]);
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (scrolledActivePathRef.current === activePath)
|
|
77
|
+
return;
|
|
78
|
+
if (activeNodeIndex < 0)
|
|
79
|
+
return;
|
|
80
|
+
scrollToActive();
|
|
81
|
+
scrolledActivePathRef.current = activePath;
|
|
82
|
+
}, [activeNodeIndex, activePath, scrollToActive]);
|
|
25
83
|
const renderNode = (node) => {
|
|
26
|
-
|
|
84
|
+
const isActive = node.path === activePath;
|
|
85
|
+
return (_jsx(NavigationTreeNode, { state: state, path: node.path, activePath: activePath, dispatch: dispatch, onActivate: onActivePathUpdate, getActions: getActions, onActionsOpenToggle: onActionsOpenToggle, renderAdditionalNodeElements: renderAdditionalNodeElements, cache: cache, level: node.level, activeItemRef: isActive ? activeItemRef : undefined }, node.path));
|
|
27
86
|
};
|
|
28
|
-
const renderVirtualizedTree = () => (_jsx(ReactList, { type: "uniform", length: nodesList.length, useStaticSize: true, itemRenderer: (index) => {
|
|
87
|
+
const renderVirtualizedTree = () => (_jsx(ReactList, { ref: reactListRef, type: "uniform", length: nodesList.length, useStaticSize: true, itemRenderer: (index) => {
|
|
29
88
|
const node = nodesList[index];
|
|
30
89
|
return isServiceNode(node) ? renderServiceNode(node) : renderNode(node);
|
|
31
90
|
} }));
|
|
32
91
|
const renderSimpleTree = () => (_jsx(React.Fragment, { children: nodesList.map((node) => isServiceNode(node) ? renderServiceNode(node) : renderNode(node)) }));
|
|
33
92
|
return virtualize ? renderVirtualizedTree() : renderSimpleTree();
|
|
34
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* `React.forwardRef` loses generics, so we cast to preserve the `<D, M>` API.
|
|
96
|
+
*/
|
|
97
|
+
export const NavigationTree = React.forwardRef(NavigationTreeInner);
|
|
@@ -3,7 +3,6 @@ import type { NavigationTreeAction } from './state';
|
|
|
3
3
|
import type { NavigationTreeProps, NavigationTreeState } from './types';
|
|
4
4
|
export interface NavigationTreeNodeProps {
|
|
5
5
|
path: string;
|
|
6
|
-
fetchPath: NavigationTreeProps['fetchPath'];
|
|
7
6
|
activePath?: string;
|
|
8
7
|
state: NavigationTreeState;
|
|
9
8
|
level?: number;
|
|
@@ -14,5 +13,10 @@ export interface NavigationTreeNodeProps {
|
|
|
14
13
|
onActionsOpenToggle?: NavigationTreeProps['onActionsOpenToggle'];
|
|
15
14
|
renderAdditionalNodeElements?: NavigationTreeProps['renderAdditionalNodeElements'];
|
|
16
15
|
cache?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Ref object attached to the item element when this node is the active one.
|
|
18
|
+
* Set only for the active node so the parent can scroll it into view.
|
|
19
|
+
*/
|
|
20
|
+
activeItemRef?: React.RefObject<HTMLDivElement>;
|
|
17
21
|
}
|
|
18
|
-
export declare function NavigationTreeNode({ path,
|
|
22
|
+
export declare function NavigationTreeNode({ path, activePath, state, level, dispatch, children, onActivate, getActions, onActionsOpenToggle, renderAdditionalNodeElements, cache, activeItemRef, }: NavigationTreeNodeProps): JSX.Element;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { TreeView } from '../TreeView/TreeView';
|
|
4
|
-
import { AsyncReplicationIcon, ColumnTableIcon, DatabaseIcon, ExternalDataSourceIcon, ExternalTableIcon, FolderIcon, FolderOpenIcon, ResourcePoolIcon, StreamingQueryIcon, TableIcon, TableIndexIcon, TopicIcon, TransferIcon, ViewIcon, } from '../icons';
|
|
4
|
+
import { AsyncReplicationIcon, ColumnTableIcon, DatabaseIcon, ExternalDataSourceIcon, ExternalTableIcon, FolderIcon, FolderOpenIcon, ResourcePoolIcon, SecretIcon, StreamingQueryIcon, TableIcon, TableIndexIcon, TopicIcon, TransferIcon, ViewIcon, } from '../icons';
|
|
5
5
|
import { NavigationTreeActionType } from './state';
|
|
6
6
|
function renderIcon(type, collapsed) {
|
|
7
7
|
switch (type) {
|
|
@@ -34,53 +34,22 @@ function renderIcon(type, collapsed) {
|
|
|
34
34
|
return _jsx(ViewIcon, {});
|
|
35
35
|
case 'resource_pool':
|
|
36
36
|
return _jsx(ResourcePoolIcon, {});
|
|
37
|
+
case 'secret':
|
|
38
|
+
return _jsx(SecretIcon, {});
|
|
37
39
|
default:
|
|
38
40
|
return null;
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
|
-
export function NavigationTreeNode({ path,
|
|
43
|
+
export function NavigationTreeNode({ path, activePath, state, level, dispatch, children, onActivate, getActions, onActionsOpenToggle, renderAdditionalNodeElements, cache, activeItemRef, }) {
|
|
42
44
|
const nodeState = state[path];
|
|
43
45
|
React.useEffect(() => {
|
|
44
|
-
if (nodeState.collapsed) {
|
|
45
|
-
if (!cache) {
|
|
46
|
-
dispatch({
|
|
47
|
-
type: NavigationTreeActionType.ResetNode,
|
|
48
|
-
payload: { path },
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (nodeState.loaded || nodeState.loading || nodeState.error) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
dispatch({
|
|
57
|
-
type: NavigationTreeActionType.StartLoading,
|
|
58
|
-
payload: { path },
|
|
59
|
-
});
|
|
60
|
-
fetchPath(path)
|
|
61
|
-
.then((data) => {
|
|
46
|
+
if (nodeState.collapsed && !cache) {
|
|
62
47
|
dispatch({
|
|
63
|
-
type: NavigationTreeActionType.
|
|
64
|
-
payload: { path
|
|
48
|
+
type: NavigationTreeActionType.ResetNode,
|
|
49
|
+
payload: { path },
|
|
65
50
|
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
dispatch({
|
|
69
|
-
type: NavigationTreeActionType.ErrorLoading,
|
|
70
|
-
payload: { path, error },
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
}, [
|
|
74
|
-
activePath,
|
|
75
|
-
cache,
|
|
76
|
-
dispatch,
|
|
77
|
-
fetchPath,
|
|
78
|
-
nodeState.collapsed,
|
|
79
|
-
nodeState.loaded,
|
|
80
|
-
nodeState.loading,
|
|
81
|
-
nodeState.error,
|
|
82
|
-
path,
|
|
83
|
-
]);
|
|
51
|
+
}
|
|
52
|
+
}, [cache, dispatch, nodeState.collapsed, path]);
|
|
84
53
|
const handleClick = React.useCallback(() => {
|
|
85
54
|
if (onActivate) {
|
|
86
55
|
onActivate(path);
|
|
@@ -102,5 +71,5 @@ export function NavigationTreeNode({ path, fetchPath, activePath, state, level,
|
|
|
102
71
|
isOpen,
|
|
103
72
|
});
|
|
104
73
|
}, [nodeState.path, nodeState.type, onActionsOpenToggle]);
|
|
105
|
-
return (_jsx(TreeView, { name: nodeState.name, icon: renderIcon(nodeState.type, nodeState.collapsed), collapsed: nodeState.collapsed, active: nodeState.path === activePath, actions: actions, additionalNodeElements: additionalNodeElements, hasArrow: nodeState.expandable, onClick: handleClick, onArrowClick: handleArrowClick, onActionsOpenToggle: handleActionsOpenToggle, level: level, children: children }));
|
|
74
|
+
return (_jsx(TreeView, { name: nodeState.name, icon: renderIcon(nodeState.type, nodeState.collapsed), collapsed: nodeState.collapsed, active: nodeState.path === activePath, actions: actions, additionalNodeElements: additionalNodeElements, hasArrow: nodeState.expandable, onClick: handleClick, onArrowClick: handleArrowClick, onActionsOpenToggle: handleActionsOpenToggle, level: level, itemRef: activeItemRef, children: children }));
|
|
106
75
|
}
|
|
@@ -15,6 +15,8 @@ export type NavigationTreeAction = {
|
|
|
15
15
|
type: NavigationTreeActionType.StartLoading;
|
|
16
16
|
payload: {
|
|
17
17
|
path: string;
|
|
18
|
+
/** Monotonic id captured at dispatch time; written into node state. */
|
|
19
|
+
requestId: number;
|
|
18
20
|
};
|
|
19
21
|
} | {
|
|
20
22
|
type: NavigationTreeActionType.FinishLoading;
|
|
@@ -22,12 +24,16 @@ export type NavigationTreeAction = {
|
|
|
22
24
|
path: string;
|
|
23
25
|
activePath?: string;
|
|
24
26
|
data: NavigationTreeDataItem[];
|
|
27
|
+
/** Id of the `StartLoading` this result belongs to; stale ones are dropped. */
|
|
28
|
+
requestId: number;
|
|
25
29
|
};
|
|
26
30
|
} | {
|
|
27
31
|
type: NavigationTreeActionType.ErrorLoading;
|
|
28
32
|
payload: {
|
|
29
33
|
path: string;
|
|
30
34
|
error: unknown;
|
|
35
|
+
/** Id of the `StartLoading` this error belongs to; stale ones are dropped. */
|
|
36
|
+
requestId: number;
|
|
31
37
|
};
|
|
32
38
|
} | {
|
|
33
39
|
type: NavigationTreeActionType.ResetNode;
|
|
@@ -41,6 +47,7 @@ export declare function getDefaultNodeState(): {
|
|
|
41
47
|
loaded: boolean;
|
|
42
48
|
error: boolean;
|
|
43
49
|
children: never[];
|
|
50
|
+
requestId: number;
|
|
44
51
|
};
|
|
45
52
|
export declare function getNodeState(partialState: NavigationTreeNodePartialState): NavigationTreeNodeState;
|
|
46
53
|
export declare function reducer(state: NavigationTreeState | undefined, action: NavigationTreeAction): NavigationTreeState;
|
|
@@ -14,6 +14,7 @@ export function getDefaultNodeState() {
|
|
|
14
14
|
loaded: false,
|
|
15
15
|
error: false,
|
|
16
16
|
children: [],
|
|
17
|
+
requestId: 0,
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
export function getNodeState(partialState) {
|
|
@@ -25,9 +26,27 @@ export function reducer(state = {}, action) {
|
|
|
25
26
|
case NavigationTreeActionType.ToggleCollapsed:
|
|
26
27
|
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { collapsed: !state[action.payload.path].collapsed }) });
|
|
27
28
|
case NavigationTreeActionType.StartLoading:
|
|
28
|
-
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { loading: true, loaded: false, error: false, children: [] }) });
|
|
29
|
+
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { loading: true, loaded: false, error: false, children: [], requestId: action.payload.requestId }) });
|
|
29
30
|
case NavigationTreeActionType.FinishLoading: {
|
|
30
|
-
const
|
|
31
|
+
const currentNode = state[action.payload.path];
|
|
32
|
+
// Ignore stale results. A result is stale when:
|
|
33
|
+
// * the node was removed entirely, OR
|
|
34
|
+
// * the node's `requestId` no longer matches the one captured at dispatch time
|
|
35
|
+
// (i.e. `ResetNode` cleared it, or a newer `StartLoading` superseded it).
|
|
36
|
+
// The previous `loading`-only guard couldn't distinguish "still loading the original
|
|
37
|
+
// request" from "loading a second, newer request", which allowed an older response
|
|
38
|
+
// to overwrite a newer one after a collapse-expand cycle with `cache: false`.
|
|
39
|
+
// Note: a plain collapse with `cache: true` keeps `requestId` intact, so the fetch
|
|
40
|
+
// is still allowed to complete and populate the cache — that's intentional.
|
|
41
|
+
if (!currentNode || currentNode.requestId !== action.payload.requestId) {
|
|
42
|
+
return state;
|
|
43
|
+
}
|
|
44
|
+
const newState = Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, currentNode), { loading: false, loaded: Boolean(action.payload.data), error: false,
|
|
45
|
+
// Clear the in-flight marker now that this request has completed.
|
|
46
|
+
// The counter in `NavigationTree` only allocates ids >= 1, so any
|
|
47
|
+
// late stale `FinishLoading` / `ErrorLoading` for this path will
|
|
48
|
+
// still fail the guard above (its id > 0 won't match this 0).
|
|
49
|
+
requestId: 0 }) });
|
|
31
50
|
if (action.payload.data) {
|
|
32
51
|
newState[action.payload.path].children = action.payload.data.map(({ name }) => name);
|
|
33
52
|
for (const item of action.payload.data) {
|
|
@@ -45,8 +64,17 @@ export function reducer(state = {}, action) {
|
|
|
45
64
|
}
|
|
46
65
|
return newState;
|
|
47
66
|
}
|
|
48
|
-
case NavigationTreeActionType.ErrorLoading:
|
|
49
|
-
|
|
67
|
+
case NavigationTreeActionType.ErrorLoading: {
|
|
68
|
+
const currentNode = state[action.payload.path];
|
|
69
|
+
// Ignore stale errors — see the matching note on `FinishLoading` above.
|
|
70
|
+
if (!currentNode || currentNode.requestId !== action.payload.requestId) {
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, currentNode), { loading: false, loaded: false, error: true,
|
|
74
|
+
// See note in `FinishLoading`: clear the in-flight id on completion
|
|
75
|
+
// to keep `requestId === 0` ⇔ "no active request".
|
|
76
|
+
requestId: 0 }) });
|
|
77
|
+
}
|
|
50
78
|
case NavigationTreeActionType.ResetNode:
|
|
51
79
|
return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), getDefaultNodeState()) });
|
|
52
80
|
default:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import type { DropdownMenuItemMixed } from '@gravity-ui/uikit';
|
|
3
|
-
export type NavigationTreeNodeType = 'async_replication' | 'column_table' | 'resource_pool' | 'database' | 'directory' | 'external_data_source' | 'external_table' | 'index_table' | 'index' | 'stream' | 'system_table' | 'streaming_query' | 'table' | 'topic' | 'transfer' | 'view';
|
|
3
|
+
export type NavigationTreeNodeType = 'async_replication' | 'column_table' | 'resource_pool' | 'database' | 'directory' | 'external_data_source' | 'external_table' | 'index_table' | 'index' | 'secret' | 'stream' | 'system_table' | 'streaming_query' | 'table' | 'topic' | 'transfer' | 'view';
|
|
4
4
|
export interface NavigationTreeDataItem<M = unknown> {
|
|
5
5
|
name: string;
|
|
6
6
|
type: NavigationTreeNodeType;
|
|
@@ -24,13 +24,22 @@ export interface NavigationTreeNodeState {
|
|
|
24
24
|
children: string[];
|
|
25
25
|
level?: number;
|
|
26
26
|
meta?: unknown;
|
|
27
|
+
/**
|
|
28
|
+
* Id of the in-flight load request for this node. Assigned by `StartLoading`,
|
|
29
|
+
* cleared back to `0` on `FinishLoading` / `ErrorLoading` / `ResetNode`, so
|
|
30
|
+
* `requestId === 0` always means "no active request". Used to discard stale
|
|
31
|
+
* results: a `FinishLoading` / `ErrorLoading` whose payload id does not match
|
|
32
|
+
* the current value is dropped (so a late response from a request superseded
|
|
33
|
+
* by `ResetNode` + a new `StartLoading` cannot overwrite the newer one).
|
|
34
|
+
*/
|
|
35
|
+
requestId: number;
|
|
27
36
|
}
|
|
28
37
|
export interface NavigationTreeServiceNode {
|
|
29
38
|
path: string;
|
|
30
39
|
status: 'loading' | 'error' | 'empty';
|
|
31
40
|
level?: number;
|
|
32
41
|
}
|
|
33
|
-
export type NavigationTreeNodePartialState = Omit<NavigationTreeNodeState, 'loading' | 'loaded' | 'error' | 'children'>;
|
|
42
|
+
export type NavigationTreeNodePartialState = Omit<NavigationTreeNodeState, 'loading' | 'loaded' | 'error' | 'children' | 'requestId'>;
|
|
34
43
|
export interface NavigationTreeProps<D = any, M = any> {
|
|
35
44
|
rootState: NavigationTreeNodePartialState;
|
|
36
45
|
fetchPath: (path: string) => Promise<NavigationTreeDataItem<M>[]>;
|
|
@@ -15,5 +15,11 @@ export interface TreeViewProps {
|
|
|
15
15
|
actions?: DropdownMenuItemMixed<any>[];
|
|
16
16
|
additionalNodeElements?: JSX.Element;
|
|
17
17
|
level?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Ref object passed to the inner item element.
|
|
20
|
+
* Use this if you need to control scroll-into-view from a parent component
|
|
21
|
+
* (e.g. `itemRef.current?.scrollIntoView({block: 'nearest'})`).
|
|
22
|
+
*/
|
|
23
|
+
itemRef?: React.Ref<HTMLDivElement>;
|
|
18
24
|
}
|
|
19
|
-
export declare function TreeView({ children, name, title, icon, collapsed, active, onClick, onArrowClick, onActionsOpenToggle, hasArrow, actions, additionalNodeElements, level, }: TreeViewProps): JSX.Element;
|
|
25
|
+
export declare function TreeView({ children, name, title, icon, collapsed, active, onClick, onArrowClick, onActionsOpenToggle, hasArrow, actions, additionalNodeElements, level, itemRef, }: TreeViewProps): JSX.Element;
|
|
@@ -5,7 +5,7 @@ import { block } from '../../utils/cn';
|
|
|
5
5
|
import './TreeView.css';
|
|
6
6
|
const TREE_LEVEL_CSS_VAR = '--ydb-tree-view-level';
|
|
7
7
|
const b = block('ydb-tree-view');
|
|
8
|
-
export function TreeView({ children, name, title, icon, collapsed = true, active = false, onClick, onArrowClick, onActionsOpenToggle, hasArrow = false, actions, additionalNodeElements, level, }) {
|
|
8
|
+
export function TreeView({ children, name, title, icon, collapsed = true, active = false, onClick, onArrowClick, onActionsOpenToggle, hasArrow = false, actions, additionalNodeElements, level, itemRef, }) {
|
|
9
9
|
const handleClick = React.useCallback((event) => {
|
|
10
10
|
if (!onClick)
|
|
11
11
|
return;
|
|
@@ -31,7 +31,7 @@ export function TreeView({ children, name, title, icon, collapsed = true, active
|
|
|
31
31
|
}
|
|
32
32
|
const tooltipContent = title !== null && title !== void 0 ? title : (typeof name === 'string' ? name : '');
|
|
33
33
|
const isTooltipDisabled = tooltipContent.trim().length === 0;
|
|
34
|
-
return (_jsx("div", { className: b(), style: { [TREE_LEVEL_CSS_VAR]: level }, children: _jsxs("div", { className: "tree-view", children: [_jsxs("div", { className: `${itemClassName} ${b('item', { active })}`, onClick: handleClick, children: [_jsx("button", { type: "button", className: `${arrowClassName} ${b('arrow', {
|
|
34
|
+
return (_jsx("div", { className: b(), style: { [TREE_LEVEL_CSS_VAR]: level }, children: _jsxs("div", { className: "tree-view", children: [_jsxs("div", { ref: itemRef, className: `${itemClassName} ${b('item', { active })}`, onClick: handleClick, children: [_jsx("button", { type: "button", className: `${arrowClassName} ${b('arrow', {
|
|
35
35
|
collapsed,
|
|
36
36
|
hidden: !hasArrow,
|
|
37
37
|
})}`, disabled: !handleArrowClick, onClick: handleArrowClick }), _jsxs("div", { className: b('content'), children: [_jsx(ActionTooltip, { title: tooltipContent, disabled: isTooltipDisabled, children: _jsxs("div", { className: b('label'), children: [icon && _jsx("div", { className: b('icon'), children: icon }), _jsx("div", { className: b('text'), children: name })] }) }), actions && actions.length > 0 && (_jsxs("div", { className: b('actions'), children: [additionalNodeElements, _jsx(DropdownMenu, { onOpenToggle: onActionsOpenToggle, defaultSwitcherProps: {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export function SecretIcon(props) {
|
|
4
|
+
return (_jsx("svg", Object.assign({ xmlns: "http://www.w3.org/2000/svg", height: 16, width: 16, viewBox: "0 0 16 16", fill: "currentColor" }, props, { children: _jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M8 1a4 4 0 0 1 4 4v1l.154.004A3 3 0 0 1 15 9v3a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V9a3 3 0 0 1 2.846-2.996L4 6V5a4 4 0 0 1 4-4m0 7.75a.75.75 0 0 0-.75.75v2a.75.75 0 0 0 1.5 0v-2A.75.75 0 0 0 8 8.75M8 2.5A2.5 2.5 0 0 0 5.5 5v1h5V5A2.5 2.5 0 0 0 8 2.5" }) })));
|
|
5
|
+
}
|
|
@@ -6,6 +6,7 @@ export { ExternalTableIcon } from './ExternalTable';
|
|
|
6
6
|
export { FolderIcon } from './Folder';
|
|
7
7
|
export { FolderOpenIcon } from './FolderOpen';
|
|
8
8
|
export { ResourcePoolIcon } from './ResourcePool';
|
|
9
|
+
export { SecretIcon } from './Secret';
|
|
9
10
|
export { TableIcon } from './Table';
|
|
10
11
|
export { TableIndexIcon } from './TableIndex';
|
|
11
12
|
export { TopicIcon } from './Topic';
|
|
@@ -6,6 +6,7 @@ export { ExternalTableIcon } from './ExternalTable';
|
|
|
6
6
|
export { FolderIcon } from './Folder';
|
|
7
7
|
export { FolderOpenIcon } from './FolderOpen';
|
|
8
8
|
export { ResourcePoolIcon } from './ResourcePool';
|
|
9
|
+
export { SecretIcon } from './Secret';
|
|
9
10
|
export { TableIcon } from './Table';
|
|
10
11
|
export { TableIndexIcon } from './TableIndex';
|
|
11
12
|
export { TopicIcon } from './Topic';
|