From 8cce0a4802c1e83acda5e859117d76aeb6da8101 Mon Sep 17 00:00:00 2001 From: Nishtha Ahuja <45535788+nishthaAhujaa@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:23:44 +0530 Subject: [PATCH] Filtering DBs and option for pin(fav)(#2301) * implemented search bar * formatting corrected * added pin(fav) and sorting in local in sidebar tree of DE * reverted changes * fixed lint and formatting issues * fixed lint and formatting issues * theme toggle button is disabled if in portal * fixed lint error * added link on disabled theme toggle button * updated the variable for pin icon * removed en-us from url --------- Co-authored-by: nishthaAhujaa Co-authored-by: sakshigupta12feb Co-authored-by: Sakshi Gupta --- images/Pin.svg | 3 + .../ContainerCopy/CommandBar/Utils.test.ts | 7 +- .../ContainerCopy/CommandBar/Utils.ts | 10 +- src/Explorer/ContextMenuButtonFactory.tsx | 35 +- .../CommandButton/CommandButtonComponent.tsx | 5 + .../CommandBarComponentButtonFactory.tsx | 2 +- .../Menus/CommandBar/CommandBarUtil.tsx | 16 + .../Menus/CommandBar/ThemeToggleButton.tsx | 33 +- .../DeleteCollectionConfirmationPane.tsx | 30 +- ...teCollectionConfirmationPane.test.tsx.snap | 28 + .../Panes/DeleteDatabaseConfirmationPanel.tsx | 30 +- ...eteDatabaseConfirmationPanel.test.tsx.snap | 66 + src/Explorer/Tree/ResourceTree.tsx | 93 +- .../__snapshots__/treeNodeUtil.test.ts.snap | 1993 +++++++++-------- src/Explorer/Tree/treeNodeUtil.test.ts | 9 +- src/Explorer/Tree/treeNodeUtil.tsx | 67 +- src/Explorer/useDatabases.ts | 110 +- src/Main.tsx | 34 +- src/Shared/StorageUtility.ts | 2 + 19 files changed, 1493 insertions(+), 1080 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/ContainerCopy/CommandBar/Utils.test.ts b/src/Explorer/ContainerCopy/CommandBar/Utils.test.ts index 003dc22a2..e180194b4 100644 --- a/src/Explorer/ContainerCopy/CommandBar/Utils.test.ts +++ b/src/Explorer/ContainerCopy/CommandBar/Utils.test.ts @@ -185,9 +185,10 @@ describe("CommandBar Utils", () => { it("should respect disabled state when provided", () => { const buttons = getCommandBarButtons(mockExplorer, false); - buttons.forEach((button) => { - expect(button.disabled).toBe(false); - }); + // Theme toggle (index 2) is disabled in Portal mode, others are not + const expectedDisabled = buttons.map((_, index) => index === 2); + const actualDisabled = buttons.map((button) => button.disabled); + expect(actualDisabled).toEqual(expectedDisabled); }); it("should return CommandButtonComponentProps with all required properties", () => { diff --git a/src/Explorer/ContainerCopy/CommandBar/Utils.ts b/src/Explorer/ContainerCopy/CommandBar/Utils.ts index d00b3788d..5b96a6c37 100644 --- a/src/Explorer/ContainerCopy/CommandBar/Utils.ts +++ b/src/Explorer/ContainerCopy/CommandBar/Utils.ts @@ -14,6 +14,7 @@ import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes"; function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] { const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref); + const isPortal = configContext.platform === Platform.Portal; const buttons: CopyJobCommandBarBtnType[] = [ { key: "createCopyJob", @@ -33,8 +34,13 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand key: "themeToggle", iconSrc: isDarkMode ? SunIcon : MoonIcon, label: isDarkMode ? "Light Theme" : "Dark Theme", - ariaLabel: isDarkMode ? "Switch to Light Theme" : "Switch to Dark Theme", - onClick: () => useThemeStore.getState().toggleTheme(), + ariaLabel: isPortal + ? "Dark Mode is managed in Azure Portal Settings" + : isDarkMode + ? "Switch to Light Theme" + : "Switch to Dark Theme", + disabled: isPortal, + onClick: isPortal ? () => {} : () => useThemeStore.getState().toggleTheme(), }, ]; diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index c92098211..3da0a4e00 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -24,6 +24,7 @@ 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"; @@ -53,8 +54,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 }), @@ -77,13 +84,13 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin items.push({ iconSrc: DeleteDatabaseIcon, onClick: (lastFocusedElement?: React.RefObject) => { - (useSidePanel.getState().getRef = lastFocusedElement), - useSidePanel - .getState() - .openSidePanel( - t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }), - container.refreshAllDatabases()} />, - ); + useSidePanel.getState().getRef = lastFocusedElement; + useSidePanel + .getState() + .openSidePanel( + "Delete " + getDatabaseName(), + container.refreshAllDatabases()} />, + ); }, label: t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }), styleClass: "deleteDatabaseMenuItem", @@ -176,13 +183,13 @@ export const createCollectionContextMenuButton = ( iconSrc: DeleteCollectionIcon, onClick: (lastFocusedElement?: React.RefObject) => { useSelectedNode.getState().setSelectedNode(selectedCollection); - (useSidePanel.getState().getRef = lastFocusedElement), - useSidePanel - .getState() - .openSidePanel( - t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }), - container.refreshAllDatabases()} />, - ); + useSidePanel.getState().getRef = lastFocusedElement; + useSidePanel + .getState() + .openSidePanel( + "Delete " + getCollectionName(), + container.refreshAllDatabases()} />, + ); }, label: t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }), styleClass: "deleteCollectionMenuItem", diff --git a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx index 168962312..d45e7043e 100644 --- a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx +++ b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx @@ -58,6 +58,11 @@ export interface CommandButtonComponentProps { */ tooltipText?: string; + /** + * Rich JSX content for tooltip (used instead of tooltipText when provided) + */ + tooltipContent?: React.ReactNode; + /** * Custom styles to apply to the button using Fluent UI theme tokens */ diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 471f20048..7b798eb13 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(), + ThemeToggleButton(configContext.platform === Platform.Portal), { iconSrc: SettingsIcon, iconAlt: "Settings", diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 1f061b913..3cfd53ef6 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -5,6 +5,7 @@ import { IconType, IDropdownOption, IDropdownStyles, + TooltipHost, } from "@fluentui/react"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { KeyboardHandlerMap } from "KeyboardShortcuts"; @@ -154,6 +155,21 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol id: btn.id, }; + if (btn.tooltipContent) { + result.title = undefined; + result.commandBarButtonAs = (props: IComponentAsProps) => { + const { defaultRender: DefaultRender, ...rest } = props; + return React.createElement( + TooltipHost, + { + content: btn.tooltipContent as JSX.Element, + calloutProps: { gapSpace: 0 }, + }, + React.createElement(DefaultRender, rest), + ); + }; + } + if (isSplit) { // It's a split button result.split = true; diff --git a/src/Explorer/Menus/CommandBar/ThemeToggleButton.tsx b/src/Explorer/Menus/CommandBar/ThemeToggleButton.tsx index 8fb6e8113..475286608 100644 --- a/src/Explorer/Menus/CommandBar/ThemeToggleButton.tsx +++ b/src/Explorer/Menus/CommandBar/ThemeToggleButton.tsx @@ -1,10 +1,13 @@ +import { Link, Text } from "@fluentui/react"; import * as React from "react"; import MoonIcon from "../../../../images/MoonIcon.svg"; import SunIcon from "../../../../images/SunIcon.svg"; import { useThemeStore } from "../../../hooks/useTheme"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; -export const ThemeToggleButton = (): CommandButtonComponentProps => { +const PORTAL_SETTINGS_URL = "https://learn.microsoft.com/azure/azure-portal/set-preferences"; + +export const ThemeToggleButton = (isPortal?: boolean): CommandButtonComponentProps => { const [darkMode, setDarkMode] = React.useState(useThemeStore.getState().isDarkMode); React.useEffect(() => { @@ -16,6 +19,34 @@ export const ThemeToggleButton = (): CommandButtonComponentProps => { const tooltipText = darkMode ? "Switch to Light Theme" : "Switch to Dark Theme"; + if (isPortal) { + return { + iconSrc: darkMode ? SunIcon : MoonIcon, + iconAlt: "Theme Toggle", + onCommandClick: undefined, + commandButtonLabel: undefined, + ariaLabel: "Dark Mode is managed in Azure Portal Settings", + tooltipText: undefined, + tooltipContent: React.createElement( + "div", + { style: { padding: "4px 0" } }, + React.createElement(Text, { block: true, variant: "small" }, "Dark Mode is managed in Azure Portal Settings"), + React.createElement( + Link, + { + href: PORTAL_SETTINGS_URL, + target: "_blank", + rel: "noopener noreferrer", + style: { display: "inline-block", marginTop: "4px", fontSize: "12px" }, + }, + "Open settings", + ), + ), + hasPopup: false, + disabled: true, + }; + } + return { iconSrc: darkMode ? SunIcon : MoonIcon, iconAlt: "Theme Toggle", diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx index be592fb2a..3f3fae58d 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx @@ -18,6 +18,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; } @@ -126,12 +144,14 @@ export const DeleteCollectionConfirmationPane: FunctionComponent
* - {confirmContainer} + + {confirmContainer} + { setInputCollectionName(newInput); }} @@ -141,15 +161,15 @@ export const DeleteCollectionConfirmationPane: FunctionComponent {shouldRecordFeedback() && (
- + {t(Keys.panes.deleteCollection.feedbackTitle)} - + {t(Keys.panes.deleteCollection.feedbackReason, { collectionName })} Confirm by typing the container id @@ -45,9 +55,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 c8823766f..a9473efcb 100644 --- a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx +++ b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx @@ -20,6 +20,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; } @@ -143,12 +161,14 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent
* - {confirmDatabase} + + {confirmDatabase} + { setDatabaseInput(newInput); }} @@ -158,15 +178,15 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent {isLastNonEmptyDatabase() && (
- + {t(Keys.panes.deleteDatabase.feedbackTitle)} - + {t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() })} { diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index ed87acb0a..db4b69e21 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? @@ -725,9 +773,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 2405be01e..47d442687 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,5 +1,12 @@ -import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components"; -import { Home16Regular } 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"; @@ -55,6 +62,11 @@ export const ResourceTree: React.FC = ({ explorer }: Resource sampleDataResourceTokenCollection: state.sampleDataResourceTokenCollection, })); 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, @@ -63,8 +75,24 @@ export const ResourceTree: React.FC = ({ explorer }: Resource const databaseTreeNodes = useMemo(() => { return userContext.authType === AuthType.ResourceToken ? createResourceTokenTreeNodes(resourceTokenCollection) - : createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab); - }, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]); + : createDatabaseTreeNodes( + explorer, + isNotebookEnabled, + databases, + refreshActiveTab, + searchText, + sortOrder, + pinnedDatabaseIds, + ); + }, [ + resourceTokenCollection, + databases, + isNotebookEnabled, + refreshActiveTab, + searchText, + sortOrder, + pinnedDatabaseIds, + ]); const isSampleDataEnabled = isCopilotEnabled && @@ -114,46 +142,65 @@ export const ResourceTree: React.FC = ({ explorer }: Resource } else { return [...headerNodes, ...databaseTreeNodes]; } + // headerNodes is intentionally excluded — it depends only on isFabricMirrored() which is stable. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [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 }} + /> +
+ )} , - "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.test.ts b/src/Explorer/Tree/treeNodeUtil.test.ts index 64cc3a6c2..c64f9276e 100644 --- a/src/Explorer/Tree/treeNodeUtil.test.ts +++ b/src/Explorer/Tree/treeNodeUtil.test.ts @@ -363,7 +363,7 @@ describe("createDatabaseTreeNodes", () => { }, } as never, }); - nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab); + nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, ""); }); it("creates expected tree", () => { @@ -445,6 +445,7 @@ describe("createDatabaseTreeNodes", () => { isNotebookEnabled, useDatabases.getState().databases, refreshActiveTab, + "", ); expect(nodes).toMatchSnapshot(); }, @@ -455,7 +456,7 @@ describe("createDatabaseTreeNodes", () => { // The goal is to cover some key behaviors like loading child nodes, opening tabs/side panels, etc. it("adds new collections to database as they appear", () => { - const nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab); + const nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, ""); const giganticDbNode = nodes.find((node) => node.label === giganticDb.id()); expect(giganticDbNode).toBeDefined(); expect(giganticDbNode.children.map((node) => node.label)).toStrictEqual(["schemaCollection", "load more"]); @@ -487,7 +488,7 @@ describe("createDatabaseTreeNodes", () => { }, } as unknown as DataModels.DatabaseAccount, }); - nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab); + nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, ""); standardDbNode = nodes.find((node) => node.label === standardDb.id()); sharedDbNode = nodes.find((node) => node.label === sharedDb.id()); giganticDbNode = nodes.find((node) => node.label === giganticDb.id()); @@ -642,7 +643,7 @@ describe("createDatabaseTreeNodes", () => { setup(); // Rebuild the nodes after changing the user/config context. - nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab); + nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, ""); standardDbNode = nodes.find((node) => node.label === standardDb.id()); standardCollectionNode = standardDbNode.children.find((node) => node.label === standardCollection.id()); diff --git a/src/Explorer/Tree/treeNodeUtil.tsx b/src/Explorer/Tree/treeNodeUtil.tsx index 04eafed3f..224542755 100644 --- a/src/Explorer/Tree/treeNodeUtil.tsx +++ b/src/Explorer/Tree/treeNodeUtil.tsx @@ -1,11 +1,17 @@ -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 +33,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: "var(--colorBrandForeground1)" }; export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => { const updatedSampleTree: TreeNode = { @@ -131,8 +140,31 @@ export const createDatabaseTreeNodes = ( isNotebookEnabled: boolean, databases: ViewModels.Database[], refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void, + searchText = "", + sortOrder: DatabaseSortOrder = "az", + pinnedDatabaseIds: Set = new Set(), ): TreeNode[] => { - const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => { + // 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(lowerSearch)) + : databases; + + // 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) { @@ -170,13 +202,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) { @@ -192,7 +235,6 @@ export const createDatabaseTreeNodes = ( isExpanded: database.isDatabaseExpanded(), onCollapsed: () => { database.collapseDatabase(); - // useCommandBar.getState().setContextButtons([]); useDatabases.getState().updateDatabase(database); }, }; @@ -242,13 +284,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); @@ -257,7 +299,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 b6420647e..1d24a545f 100644 --- a/src/Explorer/useDatabases.ts +++ b/src/Explorer/useDatabases.ts @@ -1,15 +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; @@ -27,11 +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) => { @@ -45,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 @@ -100,44 +138,24 @@ export const useDatabases: UseStore = create((set, get) => ({ return true; }, loadDatabaseOffers: async () => { - await Promise.all( - get().databases?.map(async (database: ViewModels.Database) => { - await database.loadOffer(); - }), - ); + await Promise.all(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; @@ -145,7 +163,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 => {