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.
Files changed (27) hide show
  1. package/build/cjs/components/NavigationTree/NavigationTree.d.ts +15 -2
  2. package/build/cjs/components/NavigationTree/NavigationTree.js +66 -4
  3. package/build/cjs/components/NavigationTree/NavigationTreeNode.d.ts +6 -2
  4. package/build/cjs/components/NavigationTree/NavigationTreeNode.js +9 -40
  5. package/build/cjs/components/NavigationTree/state.d.ts +7 -0
  6. package/build/cjs/components/NavigationTree/state.js +32 -4
  7. package/build/cjs/components/NavigationTree/types.d.ts +11 -2
  8. package/build/cjs/components/TreeView/TreeView.d.ts +7 -1
  9. package/build/cjs/components/TreeView/TreeView.js +2 -2
  10. package/build/cjs/components/icons/Secret.d.ts +2 -0
  11. package/build/cjs/components/icons/Secret.js +8 -0
  12. package/build/cjs/components/icons/index.d.ts +1 -0
  13. package/build/cjs/components/icons/index.js +3 -1
  14. package/build/esm/components/NavigationTree/NavigationTree.d.ts +15 -2
  15. package/build/esm/components/NavigationTree/NavigationTree.js +67 -4
  16. package/build/esm/components/NavigationTree/NavigationTreeNode.d.ts +6 -2
  17. package/build/esm/components/NavigationTree/NavigationTreeNode.js +10 -41
  18. package/build/esm/components/NavigationTree/state.d.ts +7 -0
  19. package/build/esm/components/NavigationTree/state.js +32 -4
  20. package/build/esm/components/NavigationTree/types.d.ts +11 -2
  21. package/build/esm/components/TreeView/TreeView.d.ts +7 -1
  22. package/build/esm/components/TreeView/TreeView.js +2 -2
  23. package/build/esm/components/icons/Secret.d.ts +2 -0
  24. package/build/esm/components/icons/Secret.js +5 -0
  25. package/build/esm/components/icons/index.d.ts +1 -0
  26. package/build/esm/components/icons/index.js +1 -0
  27. package/package.json +1 -1
@@ -1,4 +1,17 @@
1
- /// <reference types="react" />
1
+ import React from 'react';
2
2
  import type { NavigationTreeProps } from './types';
3
3
  export type { NavigationTreeProps };
4
- export declare function NavigationTree<D = any, M = any>({ rootState: partialRootState, fetchPath, getActions, renderAdditionalNodeElements, activePath, onActionsOpenToggle, onActivePathUpdate, cache, virtualize, }: NavigationTreeProps<D, M>): JSX.Element;
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 NavigationTree({ rootState: partialRootState, fetchPath, getActions, renderAdditionalNodeElements, activePath, onActionsOpenToggle, onActivePathUpdate, cache = true, virtualize = false, }) {
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
- return ((0, jsx_runtime_1.jsx)(NavigationTreeNode_1.NavigationTreeNode, { state: state, path: node.path, activePath: activePath, fetchPath: fetchPath, dispatch: dispatch, onActivate: onActivePathUpdate, getActions: getActions, onActionsOpenToggle: onActionsOpenToggle, renderAdditionalNodeElements: renderAdditionalNodeElements, cache: cache, level: node.level }, node.path));
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
- exports.NavigationTree = NavigationTree;
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, fetchPath, activePath, state, level, dispatch, children, onActivate, getActions, onActionsOpenToggle, renderAdditionalNodeElements, cache, }: NavigationTreeNodeProps): JSX.Element;
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, fetchPath, activePath, state, level, dispatch, children, onActivate, getActions, onActionsOpenToggle, renderAdditionalNodeElements, cache, }) {
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.FinishLoading,
70
- payload: { path, activePath, data },
54
+ type: state_1.NavigationTreeActionType.ResetNode,
55
+ payload: { path },
71
56
  });
72
- })
73
- .catch((error) => {
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 newState = Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { loading: false, loaded: Boolean(action.payload.data), error: false }) });
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
- return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { loading: false, loaded: false, error: true }) });
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,2 @@
1
+ import React from 'react';
2
+ export declare function SecretIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element;
@@ -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
- /// <reference types="react" />
1
+ import React from 'react';
2
2
  import type { NavigationTreeProps } from './types';
3
3
  export type { NavigationTreeProps };
4
- export declare function NavigationTree<D = any, M = any>({ rootState: partialRootState, fetchPath, getActions, renderAdditionalNodeElements, activePath, onActionsOpenToggle, onActivePathUpdate, cache, virtualize, }: NavigationTreeProps<D, M>): JSX.Element;
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
- export function NavigationTree({ rootState: partialRootState, fetchPath, getActions, renderAdditionalNodeElements, activePath, onActionsOpenToggle, onActivePathUpdate, cache = true, virtualize = false, }) {
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
- return (_jsx(NavigationTreeNode, { state: state, path: node.path, activePath: activePath, fetchPath: fetchPath, dispatch: dispatch, onActivate: onActivePathUpdate, getActions: getActions, onActionsOpenToggle: onActionsOpenToggle, renderAdditionalNodeElements: renderAdditionalNodeElements, cache: cache, level: node.level }, node.path));
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, fetchPath, activePath, state, level, dispatch, children, onActivate, getActions, onActionsOpenToggle, renderAdditionalNodeElements, cache, }: NavigationTreeNodeProps): JSX.Element;
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, fetchPath, activePath, state, level, dispatch, children, onActivate, getActions, onActionsOpenToggle, renderAdditionalNodeElements, cache, }) {
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.FinishLoading,
64
- payload: { path, activePath, data },
48
+ type: NavigationTreeActionType.ResetNode,
49
+ payload: { path },
65
50
  });
66
- })
67
- .catch((error) => {
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 newState = Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { loading: false, loaded: Boolean(action.payload.data), error: false }) });
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
- return Object.assign(Object.assign({}, state), { [action.payload.path]: Object.assign(Object.assign({}, state[action.payload.path]), { loading: false, loaded: false, error: true }) });
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,2 @@
1
+ import React from 'react';
2
+ export declare function SecretIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element;
@@ -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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-ui-components",
3
- "version": "5.4.1",
3
+ "version": "6.1.0",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "exports": {