cosmos-explorer/src/Explorer/Tree/ResourceTree.tsx
2025-10-01 17:18:51 +05:30

166 lines
5.6 KiB
TypeScript

import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components";
import { Home16Regular } from "@fluentui/react-icons";
import { AuthType } from "AuthType";
import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles";
import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import {
createDatabaseTreeNodes,
createResourceTokenTreeNodes,
createSampleDataTreeNodes,
} from "Explorer/Tree/treeNodeUtil";
import { useDatabases } from "Explorer/useDatabases";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import { useEffect, useMemo } from "react";
import Explorer from "../Explorer";
import { useNotebook } from "../Notebook/useNotebook";
export const MyNotebooksTitle = "My Notebooks";
export const GitHubReposTitle = "GitHub repos";
interface ResourceTreeProps {
explorer: Explorer;
}
export const DATA_TREE_LABEL = "DATA";
export const MY_DATA_TREE_LABEL = "MY DATA";
export const SAMPLE_DATA_TREE_LABEL = "SAMPLE DATA";
/**
* Top-level tree that has no label, but contains all subtrees
*/
export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: ResourceTreeProps): JSX.Element => {
const [openItems, setOpenItems] = React.useState<TreeItemValue[]>([]);
const treeStyles = useTreeStyles();
const isNotebookEnabled = useNotebook((state) => state.isNotebookEnabled)
// We intentionally avoid using a state selector here because we want to re-render the tree if the active tab changes.
const { refreshActiveTab } = useTabs();
const { databases, resourceTokenCollection, sampleDataResourceTokenCollection } = useDatabases((state) => ({
databases: state.databases,
resourceTokenCollection: state.resourceTokenCollection,
sampleDataResourceTokenCollection: state.sampleDataResourceTokenCollection,
}));
const { isCopilotEnabled, isCopilotSampleDBEnabled } = useQueryCopilot((state) => ({
isCopilotEnabled: state.copilotEnabled,
isCopilotSampleDBEnabled: state.copilotSampleDBEnabled,
}));
const databaseTreeNodes = useMemo(() => {
return userContext.authType === AuthType.ResourceToken
? createResourceTokenTreeNodes(resourceTokenCollection)
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab);
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]);
const isSampleDataEnabled =
isCopilotEnabled &&
isCopilotSampleDBEnabled &&
userContext.sampleDataConnectionInfo &&
userContext.apiType === "SQL";
const sampleDataNodes = useMemo<TreeNode[]>(() => {
return isSampleDataEnabled && sampleDataResourceTokenCollection
? createSampleDataTreeNodes(sampleDataResourceTokenCollection)
: [];
}, [isSampleDataEnabled, sampleDataResourceTokenCollection]);
const headerNodes: TreeNode[] = isFabricMirrored()
? []
: [
{
id: "home",
iconSrc: <Home16Regular />,
label: "Home",
isSelected: () =>
useSelectedNode.getState().selectedNode === undefined &&
useTabs.getState().activeReactTab === ReactTabKind.Home,
onClick: () => {
useSelectedNode.getState().setSelectedNode(undefined);
useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
},
},
];
const rootNodes: TreeNode[] = useMemo(() => {
if (sampleDataNodes.length > 0) {
return [
...headerNodes,
{
id: "data",
label: MY_DATA_TREE_LABEL,
children: databaseTreeNodes,
isScrollable: true,
},
{
id: "sampleData",
label: SAMPLE_DATA_TREE_LABEL,
children: sampleDataNodes,
},
];
} else {
return [...headerNodes, ...databaseTreeNodes];
}
}, [databaseTreeNodes, sampleDataNodes]);
useEffect(() => {
// Compute open items based on node.isExpanded
const updateOpenItems = (node: TreeNode, parentNodeId: string): void => {
// This will look for ANY expanded node, event if its parent node isn't expanded
// and add it to the openItems list
const globalId = parentNodeId === undefined ? node.label : `${parentNodeId}/${node.label}`;
if (node.isExpanded) {
let found = false;
for (const id of openItems) {
if (id === globalId) {
found = true;
break;
}
}
if (!found) {
setOpenItems((prevOpenItems) => [...prevOpenItems, globalId]);
}
}
if (node.children) {
for (const child of node.children) {
updateOpenItems(child, globalId);
}
}
};
rootNodes.forEach((n) => updateOpenItems(n, undefined));
}, [rootNodes, openItems, setOpenItems]);
const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) =>
setOpenItems(Array.from(data.openItems));
return (
<div className={treeStyles.treeContainer}>
<Tree
aria-label="CosmosDB resources"
openItems={openItems}
className={treeStyles.tree}
onOpenChange={handleOpenChange}
size="medium"
>
{rootNodes.map((node) => (
<TreeNodeComponent
key={node.label}
openItems={openItems}
className="dataResourceTree"
node={node}
treeNodeId={node.label}
/>
))}
</Tree>
</div>
);
};