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 <nishtha17354@iiittd.ac.in>
Co-authored-by: sakshigupta12feb <sakshigupta12feb1@gmail.com>
Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
This commit is contained in:
Nishtha Ahuja
2026-03-16 20:23:44 +05:30
committed by GitHub
parent 454a02bc53
commit 8cce0a4802
19 changed files with 1493 additions and 1080 deletions

3
images/Pin.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.25 1.5C9.25 1.22386 9.47386 1 9.75 1H10.25C10.5261 1 10.75 1.22386 10.75 1.5V5.5L13 7.5V9H8.75V14L8 15L7.25 14V9H3V7.5L5.25 5.5V1.5C5.25 1.22386 5.47386 1 5.75 1H6.25C6.52614 1 6.75 1.22386 6.75 1.5V5.25H9.25V1.5Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@@ -185,9 +185,10 @@ describe("CommandBar Utils", () => {
it("should respect disabled state when provided", () => { it("should respect disabled state when provided", () => {
const buttons = getCommandBarButtons(mockExplorer, false); const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => { // Theme toggle (index 2) is disabled in Portal mode, others are not
expect(button.disabled).toBe(false); 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", () => { it("should return CommandButtonComponentProps with all required properties", () => {

View File

@@ -14,6 +14,7 @@ import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] { function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] {
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref); const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
const isPortal = configContext.platform === Platform.Portal;
const buttons: CopyJobCommandBarBtnType[] = [ const buttons: CopyJobCommandBarBtnType[] = [
{ {
key: "createCopyJob", key: "createCopyJob",
@@ -33,8 +34,13 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand
key: "themeToggle", key: "themeToggle",
iconSrc: isDarkMode ? SunIcon : MoonIcon, iconSrc: isDarkMode ? SunIcon : MoonIcon,
label: isDarkMode ? "Light Theme" : "Dark Theme", label: isDarkMode ? "Light Theme" : "Dark Theme",
ariaLabel: isDarkMode ? "Switch to Light Theme" : "Switch to Dark Theme", ariaLabel: isPortal
onClick: () => useThemeStore.getState().toggleTheme(), ? "Dark Mode is managed in Azure Portal Settings"
: isDarkMode
? "Switch to Light Theme"
: "Switch to Dark Theme",
disabled: isPortal,
onClick: isPortal ? () => {} : () => useThemeStore.getState().toggleTheme(),
}, },
]; ];

View File

@@ -24,6 +24,7 @@ import DeleteSprocIcon from "../../images/DeleteSproc.svg";
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg"; import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg"; import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg"; import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import PinIcon from "../../images/Pin.svg";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
@@ -53,8 +54,14 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
if (isFabric() && userContext.fabricContext?.isReadOnly) { if (isFabric() && userContext.fabricContext?.isReadOnly) {
return undefined; return undefined;
} }
const isPinned = useDatabases.getState().isPinned(databaseId);
const items: TreeNodeMenuItem[] = [ const items: TreeNodeMenuItem[] = [
{
iconSrc: PinIcon,
onClick: () => useDatabases.getState().togglePinDatabase(databaseId),
label: isPinned ? "Unpin from top" : "Pin to top",
},
{ {
iconSrc: AddCollectionIcon, iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked({ databaseId }), onClick: () => container.onNewCollectionClicked({ databaseId }),
@@ -77,13 +84,13 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
items.push({ items.push({
iconSrc: DeleteDatabaseIcon, iconSrc: DeleteDatabaseIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
(useSidePanel.getState().getRef = lastFocusedElement), useSidePanel.getState().getRef = lastFocusedElement;
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }), "Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />, <DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
); );
}, },
label: t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }), label: t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }),
styleClass: "deleteDatabaseMenuItem", styleClass: "deleteDatabaseMenuItem",
@@ -176,13 +183,13 @@ export const createCollectionContextMenuButton = (
iconSrc: DeleteCollectionIcon, iconSrc: DeleteCollectionIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
useSelectedNode.getState().setSelectedNode(selectedCollection); useSelectedNode.getState().setSelectedNode(selectedCollection);
(useSidePanel.getState().getRef = lastFocusedElement), useSidePanel.getState().getRef = lastFocusedElement;
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }), "Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />, <DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
); );
}, },
label: t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }), label: t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }),
styleClass: "deleteCollectionMenuItem", styleClass: "deleteCollectionMenuItem",

View File

@@ -58,6 +58,11 @@ export interface CommandButtonComponentProps {
*/ */
tooltipText?: string; 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 * Custom styles to apply to the button using Fluent UI theme tokens
*/ */

View File

@@ -167,7 +167,7 @@ export function createContextCommandBarButtons(
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [ const buttons: CommandButtonComponentProps[] = [
ThemeToggleButton(), ThemeToggleButton(configContext.platform === Platform.Portal),
{ {
iconSrc: SettingsIcon, iconSrc: SettingsIcon,
iconAlt: "Settings", iconAlt: "Settings",

View File

@@ -5,6 +5,7 @@ import {
IconType, IconType,
IDropdownOption, IDropdownOption,
IDropdownStyles, IDropdownStyles,
TooltipHost,
} from "@fluentui/react"; } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { KeyboardHandlerMap } from "KeyboardShortcuts"; import { KeyboardHandlerMap } from "KeyboardShortcuts";
@@ -154,6 +155,21 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
id: btn.id, id: btn.id,
}; };
if (btn.tooltipContent) {
result.title = undefined;
result.commandBarButtonAs = (props: IComponentAsProps<ICommandBarItemProps>) => {
const { defaultRender: DefaultRender, ...rest } = props;
return React.createElement(
TooltipHost,
{
content: btn.tooltipContent as JSX.Element,
calloutProps: { gapSpace: 0 },
},
React.createElement(DefaultRender, rest),
);
};
}
if (isSplit) { if (isSplit) {
// It's a split button // It's a split button
result.split = true; result.split = true;

View File

@@ -1,10 +1,13 @@
import { Link, Text } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import MoonIcon from "../../../../images/MoonIcon.svg"; import MoonIcon from "../../../../images/MoonIcon.svg";
import SunIcon from "../../../../images/SunIcon.svg"; import SunIcon from "../../../../images/SunIcon.svg";
import { useThemeStore } from "../../../hooks/useTheme"; import { useThemeStore } from "../../../hooks/useTheme";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; 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); const [darkMode, setDarkMode] = React.useState(useThemeStore.getState().isDarkMode);
React.useEffect(() => { React.useEffect(() => {
@@ -16,6 +19,34 @@ export const ThemeToggleButton = (): CommandButtonComponentProps => {
const tooltipText = darkMode ? "Switch to Light Theme" : "Switch to Dark Theme"; 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 { return {
iconSrc: darkMode ? SunIcon : MoonIcon, iconSrc: darkMode ? SunIcon : MoonIcon,
iconAlt: "Theme Toggle", iconAlt: "Theme Toggle",

View File

@@ -18,6 +18,24 @@ import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; 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 { export interface DeleteCollectionConfirmationPaneProps {
refreshDatabases: () => Promise<void>; refreshDatabases: () => Promise<void>;
} }
@@ -126,12 +144,14 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
<span className="mandatoryStar">* </span> <span className="mandatoryStar">* </span>
<Text variant="small">{confirmContainer}</Text> <Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{confirmContainer}
</Text>
<TextField <TextField
id="confirmCollectionId" id="confirmCollectionId"
autoFocus autoFocus
value={inputCollectionName} value={inputCollectionName}
styles={{ fieldGroup: { width: 300 } }} styles={themedTextFieldStyles}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
setInputCollectionName(newInput); setInputCollectionName(newInput);
}} }}
@@ -141,15 +161,15 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
</div> </div>
{shouldRecordFeedback() && ( {shouldRecordFeedback() && (
<div className="deleteCollectionFeedback"> <div className="deleteCollectionFeedback">
<Text variant="small" block> <Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.deleteCollection.feedbackTitle)} {t(Keys.panes.deleteCollection.feedbackTitle)}
</Text> </Text>
<Text variant="small" block> <Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.deleteCollection.feedbackReason, { collectionName })} {t(Keys.panes.deleteCollection.feedbackReason, { collectionName })}
</Text> </Text>
<TextField <TextField
id="deleteCollectionFeedbackInput" id="deleteCollectionFeedbackInput"
styles={{ fieldGroup: { width: 300 } }} styles={themedTextFieldStyles}
multiline multiline
value={deleteCollectionFeedback} value={deleteCollectionFeedback}
rows={3} rows={3}

View File

@@ -29,10 +29,20 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
* *
</span> </span>
<Text <Text
style={
{
"color": "var(--colorNeutralForeground1)",
}
}
variant="small" variant="small"
> >
<span <span
className="css-109" className="css-109"
style={
{
"color": "var(--colorNeutralForeground1)",
}
}
> >
Confirm by typing the container id Confirm by typing the container id
</span> </span>
@@ -45,9 +55,27 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
required={true} required={true}
styles={ styles={
{ {
"field": {
"backgroundColor": "var(--colorNeutralBackground1)",
"color": "var(--colorNeutralForeground1)",
},
"fieldGroup": { "fieldGroup": {
"backgroundColor": "var(--colorNeutralBackground1)",
"borderColor": "var(--colorNeutralStroke1)",
"selectors": {
":hover": {
"borderColor": "var(--colorNeutralStroke1Hover)",
},
},
"width": 300, "width": 300,
}, },
"subComponentStyles": {
"label": {
"root": {
"color": "var(--colorNeutralForeground1)",
},
},
},
} }
} }
value="" value=""

View File

@@ -20,6 +20,24 @@ import { useSelectedNode } from "../useSelectedNode";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm"; 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 { interface DeleteDatabaseConfirmationPanelProps {
refreshDatabases: () => Promise<void>; refreshDatabases: () => Promise<void>;
} }
@@ -143,12 +161,14 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
<span className="mandatoryStar">* </span> <span className="mandatoryStar">* </span>
<Text variant="small">{confirmDatabase}</Text> <Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{confirmDatabase}
</Text>
<TextField <TextField
id="confirmDatabaseId" id="confirmDatabaseId"
data-test="Input:confirmDatabaseId" data-test="Input:confirmDatabaseId"
autoFocus autoFocus
styles={{ fieldGroup: { width: 300 } }} styles={themedTextFieldStyles}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
setDatabaseInput(newInput); setDatabaseInput(newInput);
}} }}
@@ -158,15 +178,15 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
</div> </div>
{isLastNonEmptyDatabase() && ( {isLastNonEmptyDatabase() && (
<div className="deleteDatabaseFeedback"> <div className="deleteDatabaseFeedback">
<Text variant="small" block> <Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.deleteDatabase.feedbackTitle)} {t(Keys.panes.deleteDatabase.feedbackTitle)}
</Text> </Text>
<Text variant="small" block> <Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() })} {t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() })}
</Text> </Text>
<TextField <TextField
id="deleteDatabaseFeedbackInput" id="deleteDatabaseFeedbackInput"
styles={{ fieldGroup: { width: 300 } }} styles={themedTextFieldStyles}
multiline multiline
rows={3} rows={3}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {

View File

@@ -356,10 +356,20 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
* *
</span> </span>
<Text <Text
style={
{
"color": "var(--colorNeutralForeground1)",
}
}
variant="small" variant="small"
> >
<span <span
className="css-113" className="css-113"
style={
{
"color": "var(--colorNeutralForeground1)",
}
}
> >
Confirm by typing the Database id (name) Confirm by typing the Database id (name)
</span> </span>
@@ -373,9 +383,27 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
required={true} required={true}
styles={ styles={
{ {
"field": {
"backgroundColor": "var(--colorNeutralBackground1)",
"color": "var(--colorNeutralForeground1)",
},
"fieldGroup": { "fieldGroup": {
"backgroundColor": "var(--colorNeutralBackground1)",
"borderColor": "var(--colorNeutralStroke1)",
"selectors": {
":hover": {
"borderColor": "var(--colorNeutralStroke1Hover)",
},
},
"width": 300, "width": 300,
}, },
"subComponentStyles": {
"label": {
"root": {
"color": "var(--colorNeutralForeground1)",
},
},
},
} }
} }
> >
@@ -699,20 +727,40 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<Text <Text
block={true} block={true}
style={
{
"color": "var(--colorNeutralForeground1)",
}
}
variant="small" variant="small"
> >
<span <span
className="css-126" className="css-126"
style={
{
"color": "var(--colorNeutralForeground1)",
}
}
> >
Help us improve Azure Cosmos DB! Help us improve Azure Cosmos DB!
</span> </span>
</Text> </Text>
<Text <Text
block={true} block={true}
style={
{
"color": "var(--colorNeutralForeground1)",
}
}
variant="small" variant="small"
> >
<span <span
className="css-126" className="css-126"
style={
{
"color": "var(--colorNeutralForeground1)",
}
}
> >
What is the reason why you are deleting this Database? What is the reason why you are deleting this Database?
</span> </span>
@@ -725,9 +773,27 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
rows={3} rows={3}
styles={ styles={
{ {
"field": {
"backgroundColor": "var(--colorNeutralBackground1)",
"color": "var(--colorNeutralForeground1)",
},
"fieldGroup": { "fieldGroup": {
"backgroundColor": "var(--colorNeutralBackground1)",
"borderColor": "var(--colorNeutralStroke1)",
"selectors": {
":hover": {
"borderColor": "var(--colorNeutralStroke1Hover)",
},
},
"width": 300, "width": 300,
}, },
"subComponentStyles": {
"label": {
"root": {
"color": "var(--colorNeutralForeground1)",
},
},
},
} }
} }
> >

View File

@@ -1,5 +1,12 @@
import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components"; import {
import { Home16Regular } from "@fluentui/react-icons"; Button,
Input,
Tree,
TreeItemValue,
TreeOpenChangeData,
TreeOpenChangeEvent,
} from "@fluentui/react-components";
import { ArrowSortDown20Regular, ArrowSortUp20Regular, Home16Regular, Search20Regular } from "@fluentui/react-icons";
import { AuthType } from "AuthType"; import { AuthType } from "AuthType";
import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles"; import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles";
import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
@@ -55,6 +62,11 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
sampleDataResourceTokenCollection: state.sampleDataResourceTokenCollection, sampleDataResourceTokenCollection: state.sampleDataResourceTokenCollection,
})); }));
const databasesFetchedSuccessfully = useDatabases((state) => state.databasesFetchedSuccessfully); 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) => ({ const { isCopilotEnabled, isCopilotSampleDBEnabled } = useQueryCopilot((state) => ({
isCopilotEnabled: state.copilotEnabled, isCopilotEnabled: state.copilotEnabled,
isCopilotSampleDBEnabled: state.copilotSampleDBEnabled, isCopilotSampleDBEnabled: state.copilotSampleDBEnabled,
@@ -63,8 +75,24 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
const databaseTreeNodes = useMemo(() => { const databaseTreeNodes = useMemo(() => {
return userContext.authType === AuthType.ResourceToken return userContext.authType === AuthType.ResourceToken
? createResourceTokenTreeNodes(resourceTokenCollection) ? createResourceTokenTreeNodes(resourceTokenCollection)
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab); : createDatabaseTreeNodes(
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]); explorer,
isNotebookEnabled,
databases,
refreshActiveTab,
searchText,
sortOrder,
pinnedDatabaseIds,
);
}, [
resourceTokenCollection,
databases,
isNotebookEnabled,
refreshActiveTab,
searchText,
sortOrder,
pinnedDatabaseIds,
]);
const isSampleDataEnabled = const isSampleDataEnabled =
isCopilotEnabled && isCopilotEnabled &&
@@ -114,46 +142,65 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
} else { } else {
return [...headerNodes, ...databaseTreeNodes]; 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]); }, [databaseTreeNodes, sampleDataNodes]);
// Track complete DatabaseLoad scenario (start, tree rendered, interactive) // Track complete DatabaseLoad scenario (start, tree rendered, interactive)
useDatabaseLoadScenario(databaseTreeNodes, databasesFetchedSuccessfully); useDatabaseLoadScenario(databaseTreeNodes, databasesFetchedSuccessfully);
useEffect(() => { useEffect(() => {
// Compute open items based on node.isExpanded const expandedIds: TreeItemValue[] = [];
const updateOpenItems = (node: TreeNode, parentNodeId: string): void => { const collectExpandedIds = (node: TreeNode, parentNodeId: string | undefined): void => {
// This will look for ANY expanded node, event if its parent node isn't expanded
// and add it to the openItems list
const globalId = parentNodeId === undefined ? node.label : `${parentNodeId}/${node.label}`; const globalId = parentNodeId === undefined ? node.label : `${parentNodeId}/${node.label}`;
if (node.isExpanded) { if (node.isExpanded) {
let found = false; expandedIds.push(globalId);
for (const id of openItems) {
if (id === globalId) {
found = true;
break;
}
}
if (!found) {
setOpenItems((prevOpenItems) => [...prevOpenItems, globalId]);
}
} }
if (node.children) { if (node.children) {
for (const child of node.children) { for (const child of node.children) {
updateOpenItems(child, globalId); collectExpandedIds(child, globalId);
} }
} }
}; };
rootNodes.forEach((n) => updateOpenItems(n, undefined)); rootNodes.forEach((n) => collectExpandedIds(n, undefined));
}, [rootNodes, openItems, setOpenItems]);
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) => const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) =>
setOpenItems(Array.from(data.openItems)); setOpenItems(Array.from(data.openItems));
const toggleSortOrder = () => {
setSortOrder(sortOrder === "az" ? "za" : "az");
};
return ( return (
<div className={treeStyles.treeContainer}> <div className={treeStyles.treeContainer}>
{userContext.authType !== AuthType.ResourceToken && databases.length > 0 && (
<div style={{ padding: "8px", display: "flex", gap: "4px", alignItems: "center" }}>
<Input
placeholder="Search databases only"
value={searchText}
onChange={(_, data) => setSearchText(data?.value || "")}
size="small"
contentBefore={<Search20Regular />}
style={{ flex: 1 }}
/>
<Button
appearance="subtle"
size="small"
icon={sortOrder === "az" ? <ArrowSortDown20Regular /> : <ArrowSortUp20Regular />}
onClick={toggleSortOrder}
/>
</div>
)}
<Tree <Tree
aria-label="CosmosDB resources" aria-label="CosmosDB resources"
openItems={openItems} openItems={openItems}

File diff suppressed because it is too large Load Diff

View File

@@ -363,7 +363,7 @@ describe("createDatabaseTreeNodes", () => {
}, },
} as never, } as never,
}); });
nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab); nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, "");
}); });
it("creates expected tree", () => { it("creates expected tree", () => {
@@ -445,6 +445,7 @@ describe("createDatabaseTreeNodes", () => {
isNotebookEnabled, isNotebookEnabled,
useDatabases.getState().databases, useDatabases.getState().databases,
refreshActiveTab, refreshActiveTab,
"",
); );
expect(nodes).toMatchSnapshot(); 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. // 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", () => { 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()); const giganticDbNode = nodes.find((node) => node.label === giganticDb.id());
expect(giganticDbNode).toBeDefined(); expect(giganticDbNode).toBeDefined();
expect(giganticDbNode.children.map((node) => node.label)).toStrictEqual(["schemaCollection", "load more"]); expect(giganticDbNode.children.map((node) => node.label)).toStrictEqual(["schemaCollection", "load more"]);
@@ -487,7 +488,7 @@ describe("createDatabaseTreeNodes", () => {
}, },
} as unknown as DataModels.DatabaseAccount, } 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()); standardDbNode = nodes.find((node) => node.label === standardDb.id());
sharedDbNode = nodes.find((node) => node.label === sharedDb.id()); sharedDbNode = nodes.find((node) => node.label === sharedDb.id());
giganticDbNode = nodes.find((node) => node.label === giganticDb.id()); giganticDbNode = nodes.find((node) => node.label === giganticDb.id());
@@ -642,7 +643,7 @@ describe("createDatabaseTreeNodes", () => {
setup(); setup();
// Rebuild the nodes after changing the user/config context. // 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()); standardDbNode = nodes.find((node) => node.label === standardDb.id());
standardCollectionNode = standardDbNode.children.find((node) => node.label === standardCollection.id()); standardCollectionNode = standardDbNode.children.find((node) => node.label === standardCollection.id());

View File

@@ -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 { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity"; import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
import TabsBase from "Explorer/Tabs/TabsBase"; import TabsBase from "Explorer/Tabs/TabsBase";
import StoredProcedure from "Explorer/Tree/StoredProcedure"; import StoredProcedure from "Explorer/Tree/StoredProcedure";
import Trigger from "Explorer/Tree/Trigger"; import Trigger from "Explorer/Tree/Trigger";
import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction"; 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 { isFabric, isFabricMirrored, isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
import { getItemName } from "Utils/APITypeUtils"; import { getItemName } from "Utils/APITypeUtils";
import { isServerlessAccount } from "Utils/CapabilityUtils"; import { isServerlessAccount } from "Utils/CapabilityUtils";
@@ -27,7 +33,10 @@ export const shouldShowScriptNodes = (): boolean => {
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />; const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
const TreeSettingsIcon = <SettingsRegular fontSize={16} />; const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />; const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
const GlobalSecondaryIndexCollectionIcon = <EyeRegular fontSize={16} />; //check icon const GlobalSecondaryIndexCollectionIcon = <EyeRegular fontSize={16} />;
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[] => { export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
const updatedSampleTree: TreeNode = { const updatedSampleTree: TreeNode = {
@@ -131,8 +140,31 @@ export const createDatabaseTreeNodes = (
isNotebookEnabled: boolean, isNotebookEnabled: boolean,
databases: ViewModels.Database[], databases: ViewModels.Database[],
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void, refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
searchText = "",
sortOrder: DatabaseSortOrder = "az",
pinnedDatabaseIds: Set<string> = new Set(),
): TreeNode[] => { ): 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) => { const buildDatabaseChildNodes = (databaseNode: TreeNode) => {
databaseNode.children = []; databaseNode.children = [];
if (database.isDatabaseShared() && configContext.platform !== Platform.Fabric) { if (database.isDatabaseShared() && configContext.platform !== Platform.Fabric) {
@@ -170,13 +202,24 @@ export const createDatabaseTreeNodes = (
} }
}; };
const isPinned = pinnedDatabaseIds.has(database.id());
const databaseIcon = isPinned ? (
<span style={pinnedIconStyle}>
<DatabaseRegular fontSize={16} />
<Pin16Filled fontSize={10} style={pinnedBadgeStyle} />
</span>
) : (
TreeDatabaseIcon
);
const databaseNode: TreeNode = { const databaseNode: TreeNode = {
label: database.id(), label: database.id(),
className: "databaseNode", className: "databaseNode",
children: [], children: [],
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()), isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()), contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
iconSrc: TreeDatabaseIcon, iconSrc: databaseIcon,
onExpanded: async () => { onExpanded: async () => {
useSelectedNode.getState().setSelectedNode(database); useSelectedNode.getState().setSelectedNode(database);
if (!databaseNode.children || databaseNode.children?.length === 0) { if (!databaseNode.children || databaseNode.children?.length === 0) {
@@ -192,7 +235,6 @@ export const createDatabaseTreeNodes = (
isExpanded: database.isDatabaseExpanded(), isExpanded: database.isDatabaseExpanded(),
onCollapsed: () => { onCollapsed: () => {
database.collapseDatabase(); database.collapseDatabase();
// useCommandBar.getState().setContextButtons([]);
useDatabases.getState().updateDatabase(database); useDatabases.getState().updateDatabase(database);
}, },
}; };
@@ -242,13 +284,13 @@ export const buildCollectionNode = (
(tab: TabsBase) => (tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId, tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId,
); );
useDatabases.getState().updateDatabase(database); // If we're showing script nodes, start loading them in parallel.
// If we're showing script nodes, start loading them.
if (shouldShowScriptNodes()) { if (shouldShowScriptNodes()) {
await collection.loadStoredProcedures(); await Promise.all([
await collection.loadUserDefinedFunctions(); collection.loadStoredProcedures(),
await collection.loadTriggers(); collection.loadUserDefinedFunctions(),
collection.loadTriggers(),
]);
} }
useDatabases.getState().updateDatabase(database); useDatabases.getState().updateDatabase(database);
@@ -257,7 +299,6 @@ export const buildCollectionNode = (
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection), onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
onCollapsed: () => { onCollapsed: () => {
collection.collapseCollection(); collection.collapseCollection();
// useCommandBar.getState().setContextButtons([]);
useDatabases.getState().updateDatabase(database); useDatabases.getState().updateDatabase(database);
}, },
isExpanded: collection.isCollectionExpanded(), isExpanded: collection.isCollectionExpanded(),

View File

@@ -1,15 +1,25 @@
import _ from "underscore";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import * as LocalStorageUtility from "../Shared/LocalStorageUtility";
import { StorageKey } from "../Shared/StorageUtility";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { useSelectedNode } from "./useSelectedNode"; import { useSelectedNode } from "./useSelectedNode";
export type DatabaseSortOrder = "az" | "za";
interface DatabasesState { interface DatabasesState {
databases: ViewModels.Database[]; databases: ViewModels.Database[];
resourceTokenCollection: ViewModels.CollectionBase; resourceTokenCollection: ViewModels.CollectionBase;
sampleDataResourceTokenCollection: ViewModels.CollectionBase; sampleDataResourceTokenCollection: ViewModels.CollectionBase;
databasesFetchedSuccessfully: boolean; // Track if last database fetch was successful databasesFetchedSuccessfully: boolean; // Track if last database fetch was successful
searchText: string;
sortOrder: DatabaseSortOrder;
pinnedDatabaseIds: Set<string>;
setSearchText: (searchText: string) => void;
setSortOrder: (sortOrder: DatabaseSortOrder) => void;
togglePinDatabase: (databaseId: string) => void;
isPinned: (databaseId: string) => boolean;
updateDatabase: (database: ViewModels.Database) => void; updateDatabase: (database: ViewModels.Database) => void;
addDatabases: (databases: ViewModels.Database[]) => void; addDatabases: (databases: ViewModels.Database[]) => void;
deleteDatabase: (database: ViewModels.Database) => void; deleteDatabase: (database: ViewModels.Database) => void;
@@ -27,11 +37,41 @@ interface DatabasesState {
validateCollectionId: (databaseId: string, collectionId: string) => Promise<boolean>; validateCollectionId: (databaseId: string, collectionId: string) => Promise<boolean>;
} }
const loadPinnedDatabases = (): Set<string> => {
const stored = LocalStorageUtility.getEntryObject<string[]>(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<DatabasesState> = create((set, get) => ({ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
databases: [], databases: [],
resourceTokenCollection: undefined, resourceTokenCollection: undefined,
sampleDataResourceTokenCollection: undefined, sampleDataResourceTokenCollection: undefined,
databasesFetchedSuccessfully: false, 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) => updateDatabase: (updatedDatabase: ViewModels.Database) =>
set((state) => { set((state) => {
const updatedDatabases = state.databases.map((database: ViewModels.Database) => { const updatedDatabases = state.databases.map((database: ViewModels.Database) => {
@@ -45,29 +85,27 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
}), }),
addDatabases: (databases: ViewModels.Database[]) => addDatabases: (databases: ViewModels.Database[]) =>
set((state) => ({ set((state) => ({
databases: [...state.databases, ...databases].sort((db1, db2) => db1.id().localeCompare(db2.id())), databases: [...state.databases, ...databases],
})), })),
deleteDatabase: (database: ViewModels.Database) => 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: [] })), clearDatabases: () => set(() => ({ databases: [] })),
isSaveQueryEnabled: () => { isSaveQueryEnabled: () => {
const savedQueriesDatabase: ViewModels.Database = _.find( const savedQueriesDatabase = get().databases.find(
get().databases, (database) => database.id() === Constants.SavedQueries.DatabaseName,
(database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName,
); );
if (!savedQueriesDatabase) { return !!savedQueriesDatabase
return false; ?.collections()
} ?.find((collection) => collection.id() === Constants.SavedQueries.CollectionName);
const savedQueriesCollection: ViewModels.Collection =
savedQueriesDatabase &&
_.find(
savedQueriesDatabase.collections(),
(collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName,
);
if (!savedQueriesCollection) {
return false;
}
return true;
}, },
findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => { findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => {
return isSampleDatabase === undefined return isSampleDatabase === undefined
@@ -100,44 +138,24 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
return true; return true;
}, },
loadDatabaseOffers: async () => { loadDatabaseOffers: async () => {
await Promise.all( await Promise.all(get().databases.map((database: ViewModels.Database) => database.loadOffer()));
get().databases?.map(async (database: ViewModels.Database) => {
await database.loadOffer();
}),
);
}, },
loadAllOffers: async () => { loadAllOffers: async () => {
await Promise.all( await Promise.all(
get().databases?.map(async (database: ViewModels.Database) => { get().databases.map(async (database: ViewModels.Database) => {
await database.loadOffer(); await Promise.all([database.loadOffer(), database.loadCollections()]);
await database.loadCollections();
await Promise.all( await Promise.all(
(database.collections() || []).map(async (collection: ViewModels.Collection) => { (database.collections() || []).map((collection: ViewModels.Collection) => collection.loadOffer()),
await collection.loadOffer();
}),
); );
}), }),
); );
}, },
isFirstResourceCreated: () => { isFirstResourceCreated: () => {
const databases = get().databases; const databases = get().databases;
if (databases.length === 0) {
if (!databases || databases.length === 0) {
return false; return false;
} }
return databases.some((database) => database.collections()?.length > 0 || !!database.offer());
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;
});
}, },
findSelectedDatabase: (): ViewModels.Database => { findSelectedDatabase: (): ViewModels.Database => {
const selectedNode = useSelectedNode.getState().selectedNode; const selectedNode = useSelectedNode.getState().selectedNode;
@@ -145,7 +163,7 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
return undefined; return undefined;
} }
if (selectedNode.nodeKind === "Database") { 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") { if (selectedNode.nodeKind === "Collection") {

View File

@@ -82,6 +82,32 @@ const useStyles = makeStyles({
backgroundColor: "var(--colorNeutralBackground1)", backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)", 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 => { const App = (): JSX.Element => {
@@ -234,15 +260,15 @@ function LoadingExplorer(): JSX.Element {
const styles = useStyles(); const styles = useStyles();
return ( return (
<div className={styles.root}> <div className={styles.root}>
<div className="splashLoaderContainer"> <div className={styles.splashContainer}>
<div className="splashLoaderContentContainer"> <div className={styles.splashContent}>
<p className="connectExplorerContent"> <p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" /> <img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p> </p>
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle"> <p className={styles.splashTitle} id="explorerLoadingStatusTitle">
Welcome to Azure Cosmos DB Welcome to Azure Cosmos DB
</p> </p>
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert"> <p className={styles.splashText} id="explorerLoadingStatusText" role="alert">
Connecting... Connecting...
</p> </p>
</div> </div>

View File

@@ -36,6 +36,8 @@ export enum StorageKey {
AppState, AppState,
MongoGuidRepresentation, MongoGuidRepresentation,
IgnorePartitionKeyOnDocumentUpdate, IgnorePartitionKeyOnDocumentUpdate,
PinnedDatabases,
DatabaseSortOrder,
} }
export const hasRUThresholdBeenConfigured = (): boolean => { export const hasRUThresholdBeenConfigured = (): boolean => {