mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-04-17 03:49:23 +01:00
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:
3
images/Pin.svg
Normal file
3
images/Pin.svg
Normal 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 |
@@ -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", () => {
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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=""
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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
@@ -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());
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
34
src/Main.tsx
34
src/Main.tsx
@@ -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>
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export enum StorageKey {
|
|||||||
AppState,
|
AppState,
|
||||||
MongoGuidRepresentation,
|
MongoGuidRepresentation,
|
||||||
IgnorePartitionKeyOnDocumentUpdate,
|
IgnorePartitionKeyOnDocumentUpdate,
|
||||||
|
PinnedDatabases,
|
||||||
|
DatabaseSortOrder,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasRUThresholdBeenConfigured = (): boolean => {
|
export const hasRUThresholdBeenConfigured = (): boolean => {
|
||||||
|
|||||||
Reference in New Issue
Block a user