From db250a6eddd728a774ae1dd337109d00a13e0161 Mon Sep 17 00:00:00 2001 From: Sakshi Gupta Date: Mon, 9 Mar 2026 19:41:45 +0530 Subject: [PATCH] added pin(fav) and sorting in local in sidebar tree of DE --- images/Pin.svg | 3 + src/Explorer/ContextMenuButtonFactory.tsx | 37 +- .../CommandBarComponentButtonFactory.tsx | 2 +- .../DeleteCollectionConfirmationPane.tsx | 31 +- ...teCollectionConfirmationPane.test.tsx.snap | 28 + .../Panes/DeleteDatabaseConfirmationPanel.tsx | 28 +- ...eteDatabaseConfirmationPanel.test.tsx.snap | 66 + src/Explorer/Tree/ResourceTree.tsx | 123 +- .../__snapshots__/treeNodeUtil.test.ts.snap | 1993 +++++++++-------- src/Explorer/Tree/treeNodeUtil.tsx | 57 +- src/Explorer/useDatabases.ts | 106 +- src/Main.tsx | 34 +- src/Shared/StorageUtility.ts | 2 + 13 files changed, 1421 insertions(+), 1089 deletions(-) create mode 100644 images/Pin.svg diff --git a/images/Pin.svg b/images/Pin.svg new file mode 100644 index 000000000..f2f3fb845 --- /dev/null +++ b/images/Pin.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index 76b75dda8..9ba6ef4b5 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -23,7 +23,9 @@ import DeleteSprocIcon from "../../images/DeleteSproc.svg"; import DeleteTriggerIcon from "../../images/DeleteTrigger.svg"; import DeleteUDFIcon from "../../images/DeleteUDF.svg"; import HostedTerminalIcon from "../../images/Hosted-Terminal.svg"; +import PinIcon from "../../images/Pin.svg"; import * as ViewModels from "../Contracts/ViewModels"; +import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { userContext } from "../UserContext"; import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; import { useSidePanel } from "../hooks/useSidePanel"; @@ -35,7 +37,6 @@ import StoredProcedure from "./Tree/StoredProcedure"; import Trigger from "./Tree/Trigger"; import UserDefinedFunction from "./Tree/UserDefinedFunction"; import { useSelectedNode } from "./useSelectedNode"; -import { extractFeatures } from "../Platform/Hosted/extractFeatures"; export interface CollectionContextMenuButtonParams { databaseId: string; @@ -52,8 +53,14 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin if (isFabric() && userContext.fabricContext?.isReadOnly) { return undefined; } + const isPinned = useDatabases.getState().isPinned(databaseId); const items: TreeNodeMenuItem[] = [ + { + iconSrc: PinIcon, + onClick: () => useDatabases.getState().togglePinDatabase(databaseId), + label: isPinned ? "Unpin from top" : "Pin to top", + }, { iconSrc: AddCollectionIcon, onClick: () => container.onNewCollectionClicked({ databaseId }), @@ -76,13 +83,13 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin items.push({ iconSrc: DeleteDatabaseIcon, onClick: (lastFocusedElement?: React.RefObject) => { - (useSidePanel.getState().getRef = lastFocusedElement), - useSidePanel - .getState() - .openSidePanel( - "Delete " + getDatabaseName(), - container.refreshAllDatabases()} />, - ); + useSidePanel.getState().getRef = lastFocusedElement; + useSidePanel + .getState() + .openSidePanel( + "Delete " + getDatabaseName(), + container.refreshAllDatabases()} />, + ); }, label: `Delete ${getDatabaseName()}`, styleClass: "deleteDatabaseMenuItem", @@ -175,13 +182,13 @@ export const createCollectionContextMenuButton = ( iconSrc: DeleteCollectionIcon, onClick: (lastFocusedElement?: React.RefObject) => { useSelectedNode.getState().setSelectedNode(selectedCollection); - (useSidePanel.getState().getRef = lastFocusedElement), - useSidePanel - .getState() - .openSidePanel( - "Delete " + getCollectionName(), - container.refreshAllDatabases()} />, - ); + useSidePanel.getState().getRef = lastFocusedElement; + useSidePanel + .getState() + .openSidePanel( + "Delete " + getCollectionName(), + container.refreshAllDatabases()} />, + ); }, label: `Delete ${getCollectionName()}`, styleClass: "deleteCollectionMenuItem", diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 471f20048..792024c9b 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -167,7 +167,7 @@ export function createContextCommandBarButtons( export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { const buttons: CommandButtonComponentProps[] = [ - ThemeToggleButton(), + ...(configContext.platform !== Platform.Portal ? [ThemeToggleButton()] : []), { iconSrc: SettingsIcon, iconAlt: "Settings", diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx index 05eff191f..b8b9a138e 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx @@ -17,6 +17,24 @@ import { useDatabases } from "../../useDatabases"; import { useSelectedNode } from "../../useSelectedNode"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; +const themedTextFieldStyles = { + fieldGroup: { + width: 300, + backgroundColor: "var(--colorNeutralBackground1)", + borderColor: "var(--colorNeutralStroke1)", + selectors: { + ":hover": { borderColor: "var(--colorNeutralStroke1Hover)" }, + }, + }, + field: { + color: "var(--colorNeutralForeground1)", + backgroundColor: "var(--colorNeutralBackground1)", + }, + subComponentStyles: { + label: { root: { color: "var(--colorNeutralForeground1)" } }, + }, +}; + export interface DeleteCollectionConfirmationPaneProps { refreshDatabases: () => Promise; } @@ -111,18 +129,21 @@ export const DeleteCollectionConfirmationPane: FunctionComponent
* - Confirm by typing the {collectionName.toLowerCase()} id + + Confirm by typing the {collectionName.toLowerCase()} id + { setInputCollectionName(newInput); }} @@ -132,15 +153,15 @@ export const DeleteCollectionConfirmationPane: FunctionComponent {shouldRecordFeedback() && (
- + Help us improve Azure Cosmos DB! - + What is the reason why you are deleting this {collectionName}? Confirm by typing the container @@ -47,9 +57,27 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect required={true} styles={ { + "field": { + "backgroundColor": "var(--colorNeutralBackground1)", + "color": "var(--colorNeutralForeground1)", + }, "fieldGroup": { + "backgroundColor": "var(--colorNeutralBackground1)", + "borderColor": "var(--colorNeutralStroke1)", + "selectors": { + ":hover": { + "borderColor": "var(--colorNeutralStroke1Hover)", + }, + }, "width": 300, }, + "subComponentStyles": { + "label": { + "root": { + "color": "var(--colorNeutralForeground1)", + }, + }, + }, } } value="" diff --git a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx index 04bc117ca..9da33928d 100644 --- a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx +++ b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx @@ -19,6 +19,24 @@ import { useSelectedNode } from "../useSelectedNode"; import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm"; +const themedTextFieldStyles = { + fieldGroup: { + width: 300, + backgroundColor: "var(--colorNeutralBackground1)", + borderColor: "var(--colorNeutralStroke1)", + selectors: { + ":hover": { borderColor: "var(--colorNeutralStroke1Hover)" }, + }, + }, + field: { + color: "var(--colorNeutralForeground1)", + backgroundColor: "var(--colorNeutralBackground1)", + }, + subComponentStyles: { + label: { root: { color: "var(--colorNeutralForeground1)" } }, + }, +}; + interface DeleteDatabaseConfirmationPanelProps { refreshDatabases: () => Promise; } @@ -132,12 +150,12 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent
* - {confirmDatabase} + {confirmDatabase} { setDatabaseInput(newInput); }} @@ -147,15 +165,15 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent {isLastNonEmptyDatabase() && (
- + Help us improve Azure Cosmos DB! - + What is the reason why you are deleting this {getDatabaseName()}? { diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index d391b5ae3..cdc0e273a 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -356,10 +356,20 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` * Confirm by typing the Database id (name) @@ -373,9 +383,27 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` required={true} styles={ { + "field": { + "backgroundColor": "var(--colorNeutralBackground1)", + "color": "var(--colorNeutralForeground1)", + }, "fieldGroup": { + "backgroundColor": "var(--colorNeutralBackground1)", + "borderColor": "var(--colorNeutralStroke1)", + "selectors": { + ":hover": { + "borderColor": "var(--colorNeutralStroke1Hover)", + }, + }, "width": 300, }, + "subComponentStyles": { + "label": { + "root": { + "color": "var(--colorNeutralForeground1)", + }, + }, + }, } } > @@ -699,20 +727,40 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` > Help us improve Azure Cosmos DB! What is the reason why you are deleting this Database @@ -727,9 +775,27 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` rows={3} styles={ { + "field": { + "backgroundColor": "var(--colorNeutralBackground1)", + "color": "var(--colorNeutralForeground1)", + }, "fieldGroup": { + "backgroundColor": "var(--colorNeutralBackground1)", + "borderColor": "var(--colorNeutralStroke1)", + "selectors": { + ":hover": { + "borderColor": "var(--colorNeutralStroke1Hover)", + }, + }, "width": 300, }, + "subComponentStyles": { + "label": { + "root": { + "color": "var(--colorNeutralForeground1)", + }, + }, + }, } } > diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index ee928152c..0f08de63a 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,5 +1,17 @@ -import { Input, Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components"; -import { Home16Regular, Search20Regular } from "@fluentui/react-icons"; +import { + Button, + Input, + Tree, + TreeItemValue, + TreeOpenChangeData, + TreeOpenChangeEvent, +} from "@fluentui/react-components"; +import { + ArrowSortDown20Regular, + ArrowSortUp20Regular, + Home16Regular, + Search20Regular, +} from "@fluentui/react-icons"; import { AuthType } from "AuthType"; import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles"; import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; @@ -57,6 +69,9 @@ export const ResourceTree: React.FC = ({ explorer }: Resource const databasesFetchedSuccessfully = useDatabases((state) => state.databasesFetchedSuccessfully); const searchText = useDatabases((state) => state.searchText); const setSearchText = useDatabases((state) => state.setSearchText); + const sortOrder = useDatabases((state) => state.sortOrder); + const setSortOrder = useDatabases((state) => state.setSortOrder); + const pinnedDatabaseIds = useDatabases((state) => state.pinnedDatabaseIds); const { isCopilotEnabled, isCopilotSampleDBEnabled } = useQueryCopilot((state) => ({ isCopilotEnabled: state.copilotEnabled, isCopilotSampleDBEnabled: state.copilotSampleDBEnabled, @@ -65,8 +80,24 @@ export const ResourceTree: React.FC = ({ explorer }: Resource const databaseTreeNodes = useMemo(() => { return userContext.authType === AuthType.ResourceToken ? createResourceTokenTreeNodes(resourceTokenCollection) - : createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab, searchText); - }, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab, searchText]); + : createDatabaseTreeNodes( + explorer, + isNotebookEnabled, + databases, + refreshActiveTab, + searchText, + sortOrder, + pinnedDatabaseIds, + ); + }, [ + resourceTokenCollection, + databases, + isNotebookEnabled, + refreshActiveTab, + searchText, + sortOrder, + pinnedDatabaseIds, + ]); const isSampleDataEnabled = isCopilotEnabled && @@ -80,22 +111,26 @@ export const ResourceTree: React.FC = ({ explorer }: Resource : []; }, [isSampleDataEnabled, sampleDataResourceTokenCollection]); - const headerNodes: TreeNode[] = isFabricMirrored() - ? [] - : [ - { - id: "home", - iconSrc: , - label: "Home", - isSelected: () => - useSelectedNode.getState().selectedNode === undefined && - useTabs.getState().activeReactTab === ReactTabKind.Home, - onClick: () => { - useSelectedNode.getState().setSelectedNode(undefined); - useTabs.getState().openAndActivateReactTab(ReactTabKind.Home); - }, - }, - ]; + const headerNodes: TreeNode[] = useMemo( + () => + isFabricMirrored() + ? [] + : [ + { + id: "home", + iconSrc: , + 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) { @@ -116,54 +151,60 @@ export const ResourceTree: React.FC = ({ explorer }: Resource } else { return [...headerNodes, ...databaseTreeNodes]; } - }, [databaseTreeNodes, sampleDataNodes]); + }, [headerNodes, databaseTreeNodes, sampleDataNodes]); // Track complete DatabaseLoad scenario (start, tree rendered, interactive) useDatabaseLoadScenario(databaseTreeNodes, databasesFetchedSuccessfully); 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 expandedIds: TreeItemValue[] = []; + const collectExpandedIds = (node: TreeNode, parentNodeId: string | undefined): void => { 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]); - } + expandedIds.push(globalId); } - if (node.children) { for (const child of node.children) { - updateOpenItems(child, globalId); + collectExpandedIds(child, globalId); } } }; - rootNodes.forEach((n) => updateOpenItems(n, undefined)); - }, [rootNodes, openItems, setOpenItems]); + rootNodes.forEach((n) => collectExpandedIds(n, undefined)); + + if (expandedIds.length > 0) { + setOpenItems((prevOpenItems) => { + const prevSet = new Set(prevOpenItems); + const newIds = expandedIds.filter((id) => !prevSet.has(id)); + return newIds.length > 0 ? [...prevOpenItems, ...newIds] : prevOpenItems; + }); + } + }, [rootNodes]); const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => setOpenItems(Array.from(data.openItems)); + const toggleSortOrder = () => { + setSortOrder(sortOrder === "az" ? "za" : "az"); + }; + return (
{userContext.authType !== AuthType.ResourceToken && databases.length > 0 && ( -
+
setSearchText(data?.value || "")} size="small" contentBefore={} + style={{ flex: 1 }} + /> +
)} diff --git a/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap b/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap index 1955d296a..213e991e8 100644 --- a/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap +++ b/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap @@ -2,215 +2,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Cassandra API, serverless, on Hosted 1`] = ` [ - { - "children": [ - { - "children": [ - { - "contextMenu": [ - { - "iconSrc": {}, - "label": "Open Cassandra Shell", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Table", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "id": "", - "isSelected": [Function], - "label": "Rows", - "onClick": [Function], - }, - ], - "className": "collectionNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "Open Cassandra Shell", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Table", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "standardCollection", - "onClick": [Function], - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - { - "children": [ - { - "contextMenu": [ - { - "iconSrc": {}, - "label": "Open Cassandra Shell", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Table", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "id": "", - "isSelected": [Function], - "label": "Rows", - "onClick": [Function], - }, - { - "isSelected": [Function], - "label": "Conflicts", - "onClick": [Function], - }, - ], - "className": "collectionNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "Open Cassandra Shell", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Table", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "conflictsCollection", - "onClick": [Function], - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - ], - "className": "databaseNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "New Table", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Keyspace", - "onClick": [Function], - "styleClass": "deleteDatabaseMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "standardDb", - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - { - "children": [ - { - "iconSrc": , - "id": "", - "isSelected": [Function], - "label": "Scale", - "onClick": [Function], - }, - { - "children": [ - { - "contextMenu": [ - { - "iconSrc": {}, - "label": "Open Cassandra Shell", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Table", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "id": "sampleItems", - "isSelected": [Function], - "label": "Rows", - "onClick": [Function], - }, - ], - "className": "collectionNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "Open Cassandra Shell", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Table", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "sampleItemsCollection", - "onClick": [Function], - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - ], - "className": "databaseNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "New Table", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Keyspace", - "onClick": [Function], - "styleClass": "deleteDatabaseMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "sharedDatabase", - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, { "children": [ { @@ -323,6 +114,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca ], "className": "databaseNode", "contextMenu": [ + { + "iconSrc": {}, + "label": "Pin to top", + "onClick": [Function], + }, { "iconSrc": {}, "label": "New Table", @@ -345,6 +141,225 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca "onContextMenuOpen": [Function], "onExpanded": [Function], }, + { + "children": [ + { + "iconSrc": , + "id": "", + "isSelected": [Function], + "label": "Scale", + "onClick": [Function], + }, + { + "children": [ + { + "contextMenu": [ + { + "iconSrc": {}, + "label": "Open Cassandra Shell", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Table", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "id": "sampleItems", + "isSelected": [Function], + "label": "Rows", + "onClick": [Function], + }, + ], + "className": "collectionNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "Open Cassandra Shell", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Table", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "sampleItemsCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + ], + "className": "databaseNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "Pin to top", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Table", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Keyspace", + "onClick": [Function], + "styleClass": "deleteDatabaseMenuItem", + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "sharedDatabase", + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + { + "children": [ + { + "children": [ + { + "contextMenu": [ + { + "iconSrc": {}, + "label": "Open Cassandra Shell", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Table", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "id": "", + "isSelected": [Function], + "label": "Rows", + "onClick": [Function], + }, + ], + "className": "collectionNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "Open Cassandra Shell", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Table", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "standardCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + { + "children": [ + { + "contextMenu": [ + { + "iconSrc": {}, + "label": "Open Cassandra Shell", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Table", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "id": "", + "isSelected": [Function], + "label": "Rows", + "onClick": [Function], + }, + { + "isSelected": [Function], + "label": "Conflicts", + "onClick": [Function], + }, + ], + "className": "collectionNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "Open Cassandra Shell", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Table", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "conflictsCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + ], + "className": "databaseNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "Pin to top", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Table", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Keyspace", + "onClick": [Function], + "styleClass": "deleteDatabaseMenuItem", + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "standardDb", + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, ] `; @@ -384,6 +399,61 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo "label": "Scale & Settings", "onClick": [Function], }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "label": "string", + }, + { + "label": "HasNulls: false", + }, + ], + "label": "street", + }, + { + "children": [ + { + "label": "string", + }, + { + "label": "HasNulls: true", + }, + ], + "label": "line2", + }, + { + "children": [ + { + "label": "number", + }, + { + "label": "HasNulls: false", + }, + ], + "label": "zip", + }, + ], + "label": "address", + }, + { + "children": [ + { + "label": "string", + }, + { + "label": "HasNulls: false", + }, + ], + "label": "orderId", + }, + ], + "label": "Schema", + "onClick": [Function], + }, ], "className": "collectionNode", "contextMenu": [ @@ -409,83 +479,25 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo />, "isExpanded": true, "isSelected": [Function], - "label": "standardCollection", + "label": "schemaCollection", "onClick": [Function], "onCollapsed": [Function], "onContextMenuOpen": [Function], "onExpanded": [Function], }, { - "children": [ - { - "contextMenu": [ - { - "iconSrc": {}, - "label": "New Query", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Open Mongo Shell", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Collection", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "id": "", - "isSelected": [Function], - "label": "Documents", - "onClick": [Function], - }, - { - "id": "", - "isSelected": [Function], - "label": "Scale & Settings", - "onClick": [Function], - }, - { - "isSelected": [Function], - "label": "Conflicts", - "onClick": [Function], - }, - ], - "className": "collectionNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "New Query", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Open Mongo Shell", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Collection", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "conflictsCollection", + "className": "loadMoreNode", + "label": "load more", "onClick": [Function], - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], }, ], "className": "databaseNode", "contextMenu": [ + { + "iconSrc": {}, + "label": "Pin to top", + "onClick": [Function], + }, { "iconSrc": {}, "label": "New Collection", @@ -503,7 +515,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo />, "isExpanded": true, "isSelected": [Function], - "label": "standardDb", + "label": "giganticDatabase", "onCollapsed": [Function], "onContextMenuOpen": [Function], "onExpanded": [Function], @@ -585,6 +597,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo ], "className": "databaseNode", "contextMenu": [ + { + "iconSrc": {}, + "label": "Pin to top", + "onClick": [Function], + }, { "iconSrc": {}, "label": "New Collection", @@ -641,6 +658,169 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo "label": "Scale & Settings", "onClick": [Function], }, + ], + "className": "collectionNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "New Query", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Open Mongo Shell", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Collection", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "standardCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + { + "children": [ + { + "contextMenu": [ + { + "iconSrc": {}, + "label": "New Query", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Open Mongo Shell", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Collection", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "id": "", + "isSelected": [Function], + "label": "Documents", + "onClick": [Function], + }, + { + "id": "", + "isSelected": [Function], + "label": "Scale & Settings", + "onClick": [Function], + }, + { + "isSelected": [Function], + "label": "Conflicts", + "onClick": [Function], + }, + ], + "className": "collectionNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "New Query", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Open Mongo Shell", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Collection", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "conflictsCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + ], + "className": "databaseNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "Pin to top", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Collection", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Database", + "onClick": [Function], + "styleClass": "deleteDatabaseMenuItem", + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "standardDb", + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, +] +`; + +exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric non read-only (native) 1`] = ` +[ + { + "children": [ + { + "children": [ + { + "contextMenu": [ + { + "iconSrc": {}, + "label": "New SQL Query", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Container", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "id": "", + "isSelected": [Function], + "label": "Items", + "onClick": [Function], + }, + { + "id": "", + "isSelected": [Function], + "label": "Settings", + "onClick": [Function], + }, { "children": [ { @@ -701,17 +881,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo "contextMenu": [ { "iconSrc": {}, - "label": "New Query", + "label": "New SQL Query", "onClick": [Function], }, { "iconSrc": {}, - "label": "Open Mongo Shell", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Collection", + "label": "Delete Container", "onClick": [Function], "styleClass": "deleteCollectionMenuItem", }, @@ -737,14 +912,13 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo "contextMenu": [ { "iconSrc": {}, - "label": "New Collection", + "label": "Pin to top", "onClick": [Function], }, { "iconSrc": {}, - "label": "Delete Database", + "label": "New Container", "onClick": [Function], - "styleClass": "deleteDatabaseMenuItem", }, ], "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "sampleItemsCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + ], + "className": "databaseNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "Pin to top", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Container", + "onClick": [Function], + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "sharedDatabase", + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, { "children": [ { @@ -878,6 +1126,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ ], "className": "databaseNode", "contextMenu": [ + { + "iconSrc": {}, + "label": "Pin to top", + "onClick": [Function], + }, { "iconSrc": {}, "label": "New Container", @@ -894,36 +1147,15 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "onContextMenuOpen": [Function], "onExpanded": [Function], }, +] +`; + +exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric read-only (mirrored) 1`] = ` +[ { "children": [ { - "children": [ - { - "contextMenu": [ - { - "iconSrc": {}, - "label": "New SQL Query", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Container", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "id": "sampleItems", - "isSelected": [Function], - "label": "Items", - "onClick": [Function], - }, - { - "id": "sampleSettings", - "isSelected": [Function], - "label": "Settings", - "onClick": [Function], - }, - ], + "children": undefined, "className": "collectionNode", "contextMenu": [ { @@ -931,141 +1163,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "label": "New SQL Query", "onClick": [Function], }, - { - "iconSrc": {}, - "label": "Delete Container", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "sampleItemsCollection", - "onClick": [Function], - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - ], - "className": "databaseNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "New Container", - "onClick": [Function], - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "sharedDatabase", - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - { - "children": [ - { - "children": [ - { - "contextMenu": [ - { - "iconSrc": {}, - "label": "New SQL Query", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Container", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "id": "", - "isSelected": [Function], - "label": "Items", - "onClick": [Function], - }, - { - "id": "", - "isSelected": [Function], - "label": "Settings", - "onClick": [Function], - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "label": "string", - }, - { - "label": "HasNulls: false", - }, - ], - "label": "street", - }, - { - "children": [ - { - "label": "string", - }, - { - "label": "HasNulls: true", - }, - ], - "label": "line2", - }, - { - "children": [ - { - "label": "number", - }, - { - "label": "HasNulls: false", - }, - ], - "label": "zip", - }, - ], - "label": "address", - }, - { - "children": [ - { - "label": "string", - }, - { - "label": "HasNulls: false", - }, - ], - "label": "orderId", - }, - ], - "label": "Schema", - "onClick": [Function], - }, - ], - "className": "collectionNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "New SQL Query", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Container", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, ], "iconSrc": , @@ -1102,11 +1193,42 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "onContextMenuOpen": [Function], "onExpanded": [Function], }, -] -`; - -exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric read-only (mirrored) 1`] = ` -[ + { + "children": [ + { + "children": undefined, + "className": "collectionNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "New SQL Query", + "onClick": [Function], + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "sampleItemsCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + ], + "className": "databaseNode", + "contextMenu": undefined, + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "sharedDatabase", + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, { "children": [ { @@ -1164,10 +1286,126 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "onContextMenuOpen": [Function], "onExpanded": [Function], }, +] +`; + +exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Portal 1`] = ` +[ { "children": [ { - "children": undefined, + "children": [ + { + "contextMenu": [ + { + "iconSrc": {}, + "label": "New SQL Query", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Stored Procedure", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New UDF", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Trigger", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Container", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "id": "", + "isSelected": [Function], + "label": "Items", + "onClick": [Function], + }, + { + "id": "", + "isSelected": [Function], + "label": "Scale & Settings", + "onClick": [Function], + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "label": "string", + }, + { + "label": "HasNulls: false", + }, + ], + "label": "street", + }, + { + "children": [ + { + "label": "string", + }, + { + "label": "HasNulls: true", + }, + ], + "label": "line2", + }, + { + "children": [ + { + "label": "number", + }, + { + "label": "HasNulls: false", + }, + ], + "label": "zip", + }, + ], + "label": "address", + }, + { + "children": [ + { + "label": "string", + }, + { + "label": "HasNulls: false", + }, + ], + "label": "orderId", + }, + ], + "label": "Schema", + "onClick": [Function], + }, + { + "children": [], + "label": "Stored Procedures", + "onExpanded": [Function], + }, + { + "children": [], + "label": "User Defined Functions", + "onExpanded": [Function], + }, + { + "children": [], + "label": "Triggers", + "onExpanded": [Function], + }, + ], "className": "collectionNode", "contextMenu": [ { @@ -1175,42 +1413,27 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "label": "New SQL Query", "onClick": [Function], }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "sampleItemsCollection", - "onClick": [Function], - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - ], - "className": "databaseNode", - "contextMenu": undefined, - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "sharedDatabase", - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - { - "children": [ - { - "children": undefined, - "className": "collectionNode", - "contextMenu": [ { "iconSrc": {}, - "label": "New SQL Query", + "label": "New Stored Procedure", "onClick": [Function], }, + { + "iconSrc": {}, + "label": "New UDF", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Trigger", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Container", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, ], "iconSrc": , @@ -1241,11 +1481,145 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "onContextMenuOpen": [Function], "onExpanded": [Function], }, -] -`; - -exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Portal 1`] = ` -[ + { + "children": [ + { + "iconSrc": , + "id": "", + "isSelected": [Function], + "label": "Scale", + "onClick": [Function], + }, + { + "children": [ + { + "contextMenu": [ + { + "iconSrc": {}, + "label": "New SQL Query", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Stored Procedure", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New UDF", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Trigger", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Container", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "id": "sampleItems", + "isSelected": [Function], + "label": "Items", + "onClick": [Function], + }, + { + "id": "sampleSettings", + "isSelected": [Function], + "label": "Settings", + "onClick": [Function], + }, + { + "children": [], + "label": "Stored Procedures", + "onExpanded": [Function], + }, + { + "children": [], + "label": "User Defined Functions", + "onExpanded": [Function], + }, + { + "children": [], + "label": "Triggers", + "onExpanded": [Function], + }, + ], + "className": "collectionNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "New SQL Query", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Stored Procedure", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New UDF", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Trigger", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Container", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "sampleItemsCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + ], + "className": "databaseNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "Pin to top", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Container", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Database", + "onClick": [Function], + "styleClass": "deleteDatabaseMenuItem", + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "sharedDatabase", + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, { "children": [ { @@ -1491,6 +1865,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ ], "className": "databaseNode", "contextMenu": [ + { + "iconSrc": {}, + "label": "Pin to top", + "onClick": [Function], + }, { "iconSrc": {}, "label": "New Container", @@ -1513,140 +1892,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "onContextMenuOpen": [Function], "onExpanded": [Function], }, - { - "children": [ - { - "iconSrc": , - "id": "", - "isSelected": [Function], - "label": "Scale", - "onClick": [Function], - }, - { - "children": [ - { - "contextMenu": [ - { - "iconSrc": {}, - "label": "New SQL Query", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New Stored Procedure", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New UDF", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New Trigger", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Container", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "id": "sampleItems", - "isSelected": [Function], - "label": "Items", - "onClick": [Function], - }, - { - "id": "sampleSettings", - "isSelected": [Function], - "label": "Settings", - "onClick": [Function], - }, - { - "children": [], - "label": "Stored Procedures", - "onExpanded": [Function], - }, - { - "children": [], - "label": "User Defined Functions", - "onExpanded": [Function], - }, - { - "children": [], - "label": "Triggers", - "onExpanded": [Function], - }, - ], - "className": "collectionNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "New SQL Query", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New Stored Procedure", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New UDF", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New Trigger", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Container", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "sampleItemsCollection", - "onClick": [Function], - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - ], - "className": "databaseNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "New Container", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Database", - "onClick": [Function], - "styleClass": "deleteDatabaseMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "sharedDatabase", - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, +] +`; + +exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expected tree 1`] = ` +[ { "children": [ { @@ -1810,6 +2060,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ ], "className": "databaseNode", "contextMenu": [ + { + "iconSrc": {}, + "label": "Pin to top", + "onClick": [Function], + }, { "iconSrc": {}, "label": "New Container", @@ -1832,11 +2087,145 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "onContextMenuOpen": [Function], "onExpanded": [Function], }, -] -`; - -exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expected tree 1`] = ` -[ + { + "children": [ + { + "iconSrc": , + "id": "", + "isSelected": [Function], + "label": "Scale", + "onClick": [Function], + }, + { + "children": [ + { + "contextMenu": [ + { + "iconSrc": {}, + "label": "New SQL Query", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Stored Procedure", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New UDF", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Trigger", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Container", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "id": "sampleItems", + "isSelected": [Function], + "label": "Items", + "onClick": [Function], + }, + { + "id": "sampleSettings", + "isSelected": [Function], + "label": "Settings", + "onClick": [Function], + }, + { + "children": [], + "label": "Stored Procedures", + "onExpanded": [Function], + }, + { + "children": [], + "label": "User Defined Functions", + "onExpanded": [Function], + }, + { + "children": [], + "label": "Triggers", + "onExpanded": [Function], + }, + ], + "className": "collectionNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "New SQL Query", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Stored Procedure", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New UDF", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Trigger", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Container", + "onClick": [Function], + "styleClass": "deleteCollectionMenuItem", + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "sampleItemsCollection", + "onClick": [Function], + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, + ], + "className": "databaseNode", + "contextMenu": [ + { + "iconSrc": {}, + "label": "Pin to top", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "New Container", + "onClick": [Function], + }, + { + "iconSrc": {}, + "label": "Delete Database", + "onClick": [Function], + "styleClass": "deleteDatabaseMenuItem", + }, + ], + "iconSrc": , + "isExpanded": true, + "isSelected": [Function], + "label": "sharedDatabase", + "onCollapsed": [Function], + "onContextMenuOpen": [Function], + "onExpanded": [Function], + }, { "children": [ { @@ -2079,138 +2468,9 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe "contextMenu": [ { "iconSrc": {}, - "label": "New Container", + "label": "Pin to top", "onClick": [Function], }, - { - "iconSrc": {}, - "label": "Delete Database", - "onClick": [Function], - "styleClass": "deleteDatabaseMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "standardDb", - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - { - "children": [ - { - "iconSrc": , - "id": "", - "isSelected": [Function], - "label": "Scale", - "onClick": [Function], - }, - { - "children": [ - { - "contextMenu": [ - { - "iconSrc": {}, - "label": "New SQL Query", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New Stored Procedure", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New UDF", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New Trigger", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Container", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "id": "sampleItems", - "isSelected": [Function], - "label": "Items", - "onClick": [Function], - }, - { - "id": "sampleSettings", - "isSelected": [Function], - "label": "Settings", - "onClick": [Function], - }, - { - "children": [], - "label": "Stored Procedures", - "onExpanded": [Function], - }, - { - "children": [], - "label": "User Defined Functions", - "onExpanded": [Function], - }, - { - "children": [], - "label": "Triggers", - "onExpanded": [Function], - }, - ], - "className": "collectionNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "New SQL Query", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New Stored Procedure", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New UDF", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New Trigger", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Container", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "sampleItemsCollection", - "onClick": [Function], - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - ], - "className": "databaseNode", - "contextMenu": [ { "iconSrc": {}, "label": "New Container", @@ -2228,192 +2488,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe />, "isExpanded": true, "isSelected": [Function], - "label": "sharedDatabase", - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - { - "children": [ - { - "children": [ - { - "contextMenu": [ - { - "iconSrc": {}, - "label": "New SQL Query", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New Stored Procedure", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New UDF", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New Trigger", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Container", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "id": "", - "isSelected": [Function], - "label": "Items", - "onClick": [Function], - }, - { - "id": "", - "isSelected": [Function], - "label": "Scale & Settings", - "onClick": [Function], - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "label": "string", - }, - { - "label": "HasNulls: false", - }, - ], - "label": "street", - }, - { - "children": [ - { - "label": "string", - }, - { - "label": "HasNulls: true", - }, - ], - "label": "line2", - }, - { - "children": [ - { - "label": "number", - }, - { - "label": "HasNulls: false", - }, - ], - "label": "zip", - }, - ], - "label": "address", - }, - { - "children": [ - { - "label": "string", - }, - { - "label": "HasNulls: false", - }, - ], - "label": "orderId", - }, - ], - "label": "Schema", - "onClick": [Function], - }, - { - "children": [], - "label": "Stored Procedures", - "onExpanded": [Function], - }, - { - "children": [], - "label": "User Defined Functions", - "onExpanded": [Function], - }, - { - "children": [], - "label": "Triggers", - "onExpanded": [Function], - }, - ], - "className": "collectionNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "New SQL Query", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New Stored Procedure", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New UDF", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "New Trigger", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Container", - "onClick": [Function], - "styleClass": "deleteCollectionMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "schemaCollection", - "onClick": [Function], - "onCollapsed": [Function], - "onContextMenuOpen": [Function], - "onExpanded": [Function], - }, - { - "className": "loadMoreNode", - "label": "load more", - "onClick": [Function], - }, - ], - "className": "databaseNode", - "contextMenu": [ - { - "iconSrc": {}, - "label": "New Container", - "onClick": [Function], - }, - { - "iconSrc": {}, - "label": "Delete Database", - "onClick": [Function], - "styleClass": "deleteDatabaseMenuItem", - }, - ], - "iconSrc": , - "isExpanded": true, - "isSelected": [Function], - "label": "giganticDatabase", + "label": "standardDb", "onCollapsed": [Function], "onContextMenuOpen": [Function], "onExpanded": [Function], diff --git a/src/Explorer/Tree/treeNodeUtil.tsx b/src/Explorer/Tree/treeNodeUtil.tsx index 63aefcd28..af51e00d2 100644 --- a/src/Explorer/Tree/treeNodeUtil.tsx +++ b/src/Explorer/Tree/treeNodeUtil.tsx @@ -1,11 +1,11 @@ -import { DatabaseRegular, DocumentMultipleRegular, EyeRegular, SettingsRegular } from "@fluentui/react-icons"; +import { DatabaseRegular, DocumentMultipleRegular, EyeRegular, Pin16Filled, SettingsRegular } from "@fluentui/react-icons"; import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity"; import TabsBase from "Explorer/Tabs/TabsBase"; import StoredProcedure from "Explorer/Tree/StoredProcedure"; import Trigger from "Explorer/Tree/Trigger"; import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction"; -import { useDatabases } from "Explorer/useDatabases"; +import { DatabaseSortOrder, useDatabases } from "Explorer/useDatabases"; import { isFabric, isFabricMirrored, isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil"; import { getItemName } from "Utils/APITypeUtils"; import { isServerlessAccount } from "Utils/CapabilityUtils"; @@ -27,7 +27,10 @@ export const shouldShowScriptNodes = (): boolean => { const TreeDatabaseIcon = ; const TreeSettingsIcon = ; const TreeCollectionIcon = ; -const GlobalSecondaryIndexCollectionIcon = ; //check icon +const GlobalSecondaryIndexCollectionIcon = ; + +const pinnedIconStyle: React.CSSProperties = { display: "inline-flex", alignItems: "center", gap: "2px" }; +const pinnedBadgeStyle: React.CSSProperties = { color: "#0078D4" }; export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => { const updatedSampleTree: TreeNode = { @@ -132,13 +135,28 @@ export const createDatabaseTreeNodes = ( databases: ViewModels.Database[], refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void, searchText = "", + sortOrder: DatabaseSortOrder = "az", + pinnedDatabaseIds: Set = new Set(), ): TreeNode[] => { - // Filter databases based on search text + // Filter databases based on search text (cache lowercase to avoid repeated conversion) + const lowerSearch = searchText.toLowerCase(); const filteredDatabases = searchText - ? databases.filter((db) => db.id().toLowerCase().includes(searchText.toLowerCase())) + ? databases.filter((db) => db.id().toLowerCase().includes(lowerSearch)) : databases; - const databaseTreeNodes: TreeNode[] = filteredDatabases.map((database: ViewModels.Database) => { + // Sort: pinned first, then by name (A-Z or Z-A) within each group + const orderedDatabases = [...filteredDatabases].sort((first, second) => { + const isFirstPinned = pinnedDatabaseIds.has(first.id()); + const isSecondPinned = pinnedDatabaseIds.has(second.id()); + if (isFirstPinned !== isSecondPinned) return isFirstPinned ? -1 : 1; + const firstName = first.id(); + const secondName = second.id(); + return sortOrder === "az" + ? firstName.localeCompare(secondName, undefined, { sensitivity: "base" }) + : secondName.localeCompare(firstName, undefined, { sensitivity: "base" }); + }); + + const databaseTreeNodes: TreeNode[] = orderedDatabases.map((database: ViewModels.Database) => { const buildDatabaseChildNodes = (databaseNode: TreeNode) => { databaseNode.children = []; if (database.isDatabaseShared() && configContext.platform !== Platform.Fabric) { @@ -176,13 +194,24 @@ export const createDatabaseTreeNodes = ( } }; + const isPinned = pinnedDatabaseIds.has(database.id()); + + const databaseIcon = isPinned ? ( + + + + + ) : ( + TreeDatabaseIcon + ); + const databaseNode: TreeNode = { label: database.id(), className: "databaseNode", children: [], isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()), contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()), - iconSrc: TreeDatabaseIcon, + iconSrc: databaseIcon, onExpanded: async () => { useSelectedNode.getState().setSelectedNode(database); if (!databaseNode.children || databaseNode.children?.length === 0) { @@ -198,7 +227,6 @@ export const createDatabaseTreeNodes = ( isExpanded: database.isDatabaseExpanded(), onCollapsed: () => { database.collapseDatabase(); - // useCommandBar.getState().setContextButtons([]); useDatabases.getState().updateDatabase(database); }, }; @@ -248,13 +276,13 @@ export const buildCollectionNode = ( (tab: TabsBase) => tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId, ); - useDatabases.getState().updateDatabase(database); - - // If we're showing script nodes, start loading them. + // If we're showing script nodes, start loading them in parallel. if (shouldShowScriptNodes()) { - await collection.loadStoredProcedures(); - await collection.loadUserDefinedFunctions(); - await collection.loadTriggers(); + await Promise.all([ + collection.loadStoredProcedures(), + collection.loadUserDefinedFunctions(), + collection.loadTriggers(), + ]); } useDatabases.getState().updateDatabase(database); @@ -263,7 +291,6 @@ export const buildCollectionNode = ( onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection), onCollapsed: () => { collection.collapseCollection(); - // useCommandBar.getState().setContextButtons([]); useDatabases.getState().updateDatabase(database); }, isExpanded: collection.isCollectionExpanded(), diff --git a/src/Explorer/useDatabases.ts b/src/Explorer/useDatabases.ts index 1753eee55..f5f3e2cb1 100644 --- a/src/Explorer/useDatabases.ts +++ b/src/Explorer/useDatabases.ts @@ -1,17 +1,25 @@ -import _ from "underscore"; import create, { UseStore } from "zustand"; import * as Constants from "../Common/Constants"; import * as ViewModels from "../Contracts/ViewModels"; +import * as LocalStorageUtility from "../Shared/LocalStorageUtility"; +import { StorageKey } from "../Shared/StorageUtility"; import { userContext } from "../UserContext"; import { useSelectedNode } from "./useSelectedNode"; +export type DatabaseSortOrder = "az" | "za"; + interface DatabasesState { databases: ViewModels.Database[]; resourceTokenCollection: ViewModels.CollectionBase; sampleDataResourceTokenCollection: ViewModels.CollectionBase; databasesFetchedSuccessfully: boolean; // Track if last database fetch was successful searchText: string; + sortOrder: DatabaseSortOrder; + pinnedDatabaseIds: Set; setSearchText: (searchText: string) => void; + setSortOrder: (sortOrder: DatabaseSortOrder) => void; + togglePinDatabase: (databaseId: string) => void; + isPinned: (databaseId: string) => boolean; updateDatabase: (database: ViewModels.Database) => void; addDatabases: (databases: ViewModels.Database[]) => void; deleteDatabase: (database: ViewModels.Database) => void; @@ -29,13 +37,41 @@ interface DatabasesState { validateCollectionId: (databaseId: string, collectionId: string) => Promise; } +const loadPinnedDatabases = (): Set => { + const stored = LocalStorageUtility.getEntryObject(StorageKey.PinnedDatabases); + return new Set(Array.isArray(stored) ? stored : []); +}; + +const loadSortOrder = (): DatabaseSortOrder => { + const stored = LocalStorageUtility.getEntryString(StorageKey.DatabaseSortOrder); + return stored === "az" || stored === "za" ? stored : "az"; +}; + export const useDatabases: UseStore = create((set, get) => ({ databases: [], resourceTokenCollection: undefined, sampleDataResourceTokenCollection: undefined, databasesFetchedSuccessfully: false, searchText: "", + sortOrder: loadSortOrder(), + pinnedDatabaseIds: loadPinnedDatabases(), setSearchText: (searchText: string) => set({ searchText }), + setSortOrder: (sortOrder: DatabaseSortOrder) => { + LocalStorageUtility.setEntryString(StorageKey.DatabaseSortOrder, sortOrder); + set({ sortOrder }); + }, + togglePinDatabase: (databaseId: string) => { + const current = get().pinnedDatabaseIds; + const updated = new Set(current); + if (updated.has(databaseId)) { + updated.delete(databaseId); + } else { + updated.add(databaseId); + } + LocalStorageUtility.setEntryObject(StorageKey.PinnedDatabases, [...updated]); + set({ pinnedDatabaseIds: updated }); + }, + isPinned: (databaseId: string) => get().pinnedDatabaseIds.has(databaseId), updateDatabase: (updatedDatabase: ViewModels.Database) => set((state) => { const updatedDatabases = state.databases.map((database: ViewModels.Database) => { @@ -49,29 +85,27 @@ export const useDatabases: UseStore = create((set, get) => ({ }), addDatabases: (databases: ViewModels.Database[]) => set((state) => ({ - databases: [...state.databases, ...databases].sort((db1, db2) => db1.id().localeCompare(db2.id())), + databases: [...state.databases, ...databases], })), deleteDatabase: (database: ViewModels.Database) => - set((state) => ({ databases: state.databases.filter((db) => database.id() !== db.id()) })), + set((state) => { + const updated = new Set(state.pinnedDatabaseIds); + if (updated.delete(database.id())) { + LocalStorageUtility.setEntryObject(StorageKey.PinnedDatabases, [...updated]); + } + return { + databases: state.databases.filter((db) => database.id() !== db.id()), + pinnedDatabaseIds: updated, + }; + }), clearDatabases: () => set(() => ({ databases: [] })), isSaveQueryEnabled: () => { - const savedQueriesDatabase: ViewModels.Database = _.find( - get().databases, - (database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName, + const savedQueriesDatabase = get().databases.find( + (database) => database.id() === Constants.SavedQueries.DatabaseName, ); - if (!savedQueriesDatabase) { - return false; - } - const savedQueriesCollection: ViewModels.Collection = - savedQueriesDatabase && - _.find( - savedQueriesDatabase.collections(), - (collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName, - ); - if (!savedQueriesCollection) { - return false; - } - return true; + return !!savedQueriesDatabase + ?.collections() + ?.find((collection) => collection.id() === Constants.SavedQueries.CollectionName); }, findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => { return isSampleDatabase === undefined @@ -105,43 +139,27 @@ export const useDatabases: UseStore = create((set, get) => ({ }, loadDatabaseOffers: async () => { await Promise.all( - get().databases?.map(async (database: ViewModels.Database) => { - await database.loadOffer(); - }), + get().databases.map((database: ViewModels.Database) => database.loadOffer()), ); }, loadAllOffers: async () => { await Promise.all( - get().databases?.map(async (database: ViewModels.Database) => { - await database.loadOffer(); - await database.loadCollections(); + get().databases.map(async (database: ViewModels.Database) => { + await Promise.all([database.loadOffer(), database.loadCollections()]); await Promise.all( - (database.collections() || []).map(async (collection: ViewModels.Collection) => { - await collection.loadOffer(); - }), + (database.collections() || []).map((collection: ViewModels.Collection) => collection.loadOffer()), ); }), ); }, isFirstResourceCreated: () => { const databases = get().databases; - - if (!databases || databases.length === 0) { + if (databases.length === 0) { return false; } - - return databases.some((database) => { - // user has created at least one collection - if (database.collections()?.length > 0) { - return true; - } - // user has created a database with shared throughput - if (database.offer()) { - return true; - } - // use has created an empty database without shared throughput - return false; - }); + return databases.some( + (database) => database.collections()?.length > 0 || !!database.offer(), + ); }, findSelectedDatabase: (): ViewModels.Database => { const selectedNode = useSelectedNode.getState().selectedNode; @@ -149,7 +167,7 @@ export const useDatabases: UseStore = create((set, get) => ({ return undefined; } if (selectedNode.nodeKind === "Database") { - return _.find(get().databases, (database: ViewModels.Database) => database.id() === selectedNode.id()); + return get().databases.find((database) => database.id() === selectedNode.id()); } if (selectedNode.nodeKind === "Collection") { diff --git a/src/Main.tsx b/src/Main.tsx index 43e1a90f6..4b06d0c53 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -82,6 +82,32 @@ const useStyles = makeStyles({ backgroundColor: "var(--colorNeutralBackground1)", color: "var(--colorNeutralForeground1)", }, + splashContainer: { + zIndex: 5, + position: "absolute", + left: 0, + top: 0, + width: "100%", + height: "100%", + backgroundColor: "var(--colorNeutralBackground1)", + opacity: "0.7", + }, + splashContent: { + display: "flex", + flexDirection: "column", + height: "100%", + textAlign: "center", + justifyContent: "center", + }, + splashTitle: { + fontSize: "13px", + color: "var(--colorNeutralForeground1)", + margin: "6px 6px 12px 6px", + }, + splashText: { + marginTop: "12px", + color: "var(--colorNeutralForeground2)", + }, }); const App = (): JSX.Element => { @@ -234,15 +260,15 @@ function LoadingExplorer(): JSX.Element { const styles = useStyles(); return (
-
-
+
+

Azure Cosmos DB

-

+

Welcome to Azure Cosmos DB

-
diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index 7f5c275ce..598f1d6a6 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -36,6 +36,8 @@ export enum StorageKey { AppState, MongoGuidRepresentation, IgnorePartitionKeyOnDocumentUpdate, + PinnedDatabases, + DatabaseSortOrder, } export const hasRUThresholdBeenConfigured = (): boolean => {