diff --git a/less/documentDB.less b/less/documentDB.less index 1abbc9b30..79595e776 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2618,6 +2618,7 @@ a:link { .tabPanesContainer { display: flex; flex-grow: 1; + flex-direction: column; overflow: hidden; } diff --git a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx index b6e847d54..56f989c23 100644 --- a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx +++ b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx @@ -1,23 +1,14 @@ /** * React component for Command button component. */ +import Explorer from "Explorer/Explorer"; import { KeyboardAction } from "KeyboardShortcuts"; import * as React from "react"; -import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; -import { KeyCodes } from "../../../Common/Constants"; -import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; -import * as StringUtils from "../../../Utils/StringUtils"; /** * Options for this component */ export interface CommandButtonComponentProps { - /** - * font icon name for the button - */ - iconName?: string; - /** * image source for the button icon */ @@ -31,7 +22,7 @@ export interface CommandButtonComponentProps { /** * Click handler for command button click */ - onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void; + onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent, container: Explorer) => void; /** * Label for the button @@ -120,157 +111,3 @@ export interface CommandButtonComponentProps { */ keyboardAction?: KeyboardAction; } - -export class CommandButtonComponent extends React.Component { - private dropdownElt: HTMLElement; - private expandButtonElt: HTMLElement; - - public componentDidUpdate(): void { - if (!this.dropdownElt || !this.expandButtonElt) { - return; - } - $(this.dropdownElt).offset({ left: $(this.expandButtonElt).offset().left }); - } - - private onKeyPress(event: React.KeyboardEvent): boolean { - if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) { - this.commandClickCallback && this.commandClickCallback(event); - event.stopPropagation(); - return false; - } - return true; - } - - private onLauncherKeyDown(event: React.KeyboardEvent): boolean { - if (event.keyCode === KeyCodes.DownArrow) { - $(this.dropdownElt).hide(); - $(this.dropdownElt).show().focus(); - event.stopPropagation(); - return false; - } - if (event.keyCode === KeyCodes.UpArrow) { - $(this.dropdownElt).hide(); - event.stopPropagation(); - return false; - } - return true; - } - - private getCommandButtonId(): string { - if (this.props.id) { - return this.props.id; - } else { - return `commandButton-${StringUtils.stripSpacesFromString(this.props.commandButtonLabel)}`; - } - } - - public static renderButton(options: CommandButtonComponentProps, key?: string): JSX.Element { - return ; - } - - private commandClickCallback(e: React.SyntheticEvent): void { - if (this.props.disabled) { - return; - } - - // TODO Query component's parent, not document - const el = document.querySelector(".commandDropdownContainer") as HTMLElement; - if (el) { - el.style.display = "none"; - } - this.props.onCommandClick(e); - TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { - commandButtonClicked: this.props.commandButtonLabel, - }); - } - - private renderChildren(): JSX.Element { - if (!this.props.children || this.props.children.length < 1) { - return ; - } - - return ( -
{ - this.expandButtonElt = ref; - }} - onKeyDown={(e: React.KeyboardEvent) => this.onLauncherKeyDown(e)} - > -
- - - - -
-
{ - this.dropdownElt = ref; - }} - > -
- {this.props.children.map((c: CommandButtonComponentProps, index: number): JSX.Element => { - return CommandButtonComponent.renderButton(c, `${index}`); - })} -
-
-
- ); - } - - public static renderLabel( - props: CommandButtonComponentProps, - key?: string, - refct?: (input: HTMLElement) => void, - ): JSX.Element { - if (!props.commandButtonLabel) { - return ; - } - - return ( - - {props.commandButtonLabel} - - ); - } - - public render(): JSX.Element { - let mainClassName = "commandButtonComponent"; - if (this.props.disabled) { - mainClassName += " commandDisabled"; - } - if (this.props.isSelected) { - mainClassName += " selectedButton"; - } - - let contentClassName = "commandContent"; - if (this.props.children && this.props.children.length > 0) { - contentClassName += " hasHiddenItems"; - } - - return ( -
- ) => this.onKeyPress(e)} - title={this.props.tooltipText} - id={this.getCommandButtonId()} - aria-disabled={this.props.disabled} - aria-haspopup={this.props.hasPopup} - aria-label={this.props.ariaLabel} - onClick={(e: React.MouseEvent) => this.commandClickCallback(e)} - > -
- {this.props.iconAlt} - {CommandButtonComponent.renderLabel(this.props)} -
-
- {this.props.children && this.renderChildren()} -
- ); - } -} diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 74f3fb4f6..7a914f535 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -7,6 +7,7 @@ import { ContainerVectorPolicyComponent, ContainerVectorPolicyComponentProps, } from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { useDatabases } from "Explorer/useDatabases"; import { isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { isRunningOnPublicCloud } from "Utils/CloudUtils"; @@ -32,7 +33,6 @@ import { PartitionKeyComponent, PartitionKeyComponentProps, } from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent"; -import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; import { SettingsTabV2 } from "../../Tabs/SettingsTabV2"; import "./SettingsComponent.less"; import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils"; diff --git a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx index 39dbf3c0f..22e3bd624 100644 --- a/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeNodeComponent.tsx @@ -107,7 +107,7 @@ export const TreeNodeComponent: React.FC = ({ const onOpenChange = useCallback( (_: TreeOpenChangeEvent, data: TreeOpenChangeData) => { - if (data.type === "Click" && !isBranch && node.onClick) { + if (data.type === "Click" && node.onClick) { node.onClick(); } if (!node.isExpanded && data.open && node.onExpanded) { @@ -119,7 +119,7 @@ export const TreeNodeComponent: React.FC = ({ node.onCollapsed?.(); } }, - [isBranch, node, setIsLoading], + [node, setIsLoading], ); const onMenuOpenChange = useCallback( diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index da21f0cf8..01502cd76 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -5,6 +5,7 @@ import { Environment, getEnvironment } from "Common/EnvironmentUtility"; import { sendMessage } from "Common/MessageHandler"; import { Platform, configContext } from "ConfigContext"; import { MessageTypes } from "Contracts/ExplorerContracts"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { IGalleryItem } from "Juno/JunoClient"; @@ -47,7 +48,6 @@ import { useTabs } from "../hooks/useTabs"; import "./ComponentRegisterer"; import { DialogProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; -import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; import * as FileSystemUtil from "./Notebook/FileSystemUtil"; import { SnapshotRequest } from "./Notebook/NotebookComponent/types"; import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 9a5f222a3..ace930389 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -4,83 +4,51 @@ * and update any knockout observables passed from the parent. */ import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; +import { + createPlatformButtons, + createStaticCommandBarButtons, +} from "Explorer/Menus/CommandBar/CommandBarComponentButtonFactory"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { useNotebook } from "Explorer/Notebook/useNotebook"; import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; -import { userContext } from "UserContext"; import * as React from "react"; -import create, { UseStore } from "zustand"; import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/StyleConstants"; import { Platform, configContext } from "../../../ConfigContext"; -import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../../Explorer"; import { useSelectedNode } from "../../useSelectedNode"; -import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; import * as CommandBarUtil from "./CommandBarUtil"; interface Props { container: Explorer; } -export interface CommandBarStore { - contextButtons: CommandButtonComponentProps[]; - setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void; - isHidden: boolean; - setIsHidden: (isHidden: boolean) => void; -} - -export const useCommandBar: UseStore = create((set) => ({ - contextButtons: [], - setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })), - isHidden: false, - setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })), -})); - export const CommandBar: React.FC = ({ container }: Props) => { const selectedNodeState = useSelectedNode(); - const buttons = useCommandBar((state) => state.contextButtons); + const contextButtons = useCommandBar((state) => state.contextButtons); const isHidden = useCommandBar((state) => state.isHidden); const backgroundColor = StyleConstants.BaseLight; const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR); + const staticButtons = createStaticCommandBarButtons(selectedNodeState); + const platformButtons = createPlatformButtons(); - if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { - const buttons = - userContext.apiType === "Postgres" - ? CommandBarComponentButtonFactory.createPostgreButtons(container) - : CommandBarComponentButtonFactory.createVCoreMongoButtons(container); - return ( -
- -
- ); - } - - const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState); - const contextButtons = (buttons || []).concat( - CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState), - ); - const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(container); - - const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor); - if (buttons && buttons.length > 0) { + const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor, container); + if (contextButtons?.length > 0) { uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); } - const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor); + const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton( + contextButtons || [], + backgroundColor, + container, + ); if (uiFabricTabsButtons.length > 0) { uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider")); } - const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); - uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); + const uiFabricPlatformButtons = CommandBarUtil.convertButton(platformButtons || [], backgroundColor, container); + uiFabricPlatformButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); const connectionInfo = useNotebook((state) => state.connectionInfo); @@ -88,7 +56,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { (useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) && connectionInfo?.status !== ConnectionStatusType.Connect ) { - uiFabricControlButtons.unshift( + uiFabricPlatformButtons.unshift( CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus"), ); } @@ -107,8 +75,8 @@ export const CommandBar: React.FC = ({ container }: Props) => { }, }; - const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); - const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); + const allButtons = staticButtons.concat(contextButtons).concat(platformButtons); + const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons, container); setKeyboardHandlers(keyboardHandlers); return ( @@ -116,7 +84,7 @@ export const CommandBar: React.FC = ({ container }: Props) => { diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts index 0a4a805d2..bd4002a6e 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts @@ -3,15 +3,12 @@ import { AuthType } from "../../../AuthType"; import { DatabaseAccount } from "../../../Contracts/DataModels"; import { CollectionBase } from "../../../Contracts/ViewModels"; import { updateUserContext } from "../../../UserContext"; -import Explorer from "../../Explorer"; import { useNotebook } from "../../Notebook/useNotebook"; import { useDatabases } from "../../useDatabases"; import { useSelectedNode } from "../../useSelectedNode"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; describe("CommandBarComponentButtonFactory tests", () => { - let mockExplorer: Explorer; - afterEach(() => useSelectedNode.getState().setSelectedNode(undefined)); describe("Enable Azure Synapse Link Button", () => { @@ -19,7 +16,6 @@ describe("CommandBarComponentButtonFactory tests", () => { const selectedNodeState = useSelectedNode.getState(); beforeAll(() => { - mockExplorer = {} as Explorer; updateUserContext({ databaseAccount: { properties: { @@ -30,7 +26,7 @@ describe("CommandBarComponentButtonFactory tests", () => { }); it("Button should be visible", () => { - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState); const enableAzureSynapseLinkBtn = buttons.find( (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel, ); @@ -46,7 +42,7 @@ describe("CommandBarComponentButtonFactory tests", () => { } as DatabaseAccount, }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState); const enableAzureSynapseLinkBtn = buttons.find( (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel, ); @@ -62,7 +58,7 @@ describe("CommandBarComponentButtonFactory tests", () => { } as DatabaseAccount, }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState); const enableAzureSynapseLinkBtn = buttons.find( (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel, ); @@ -75,7 +71,6 @@ describe("CommandBarComponentButtonFactory tests", () => { const selectedNodeState = useSelectedNode.getState(); beforeAll(() => { - mockExplorer = {} as Explorer; updateUserContext({ databaseAccount: { properties: { @@ -108,7 +103,7 @@ describe("CommandBarComponentButtonFactory tests", () => { }, } as DatabaseAccount, }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); expect(openCassandraShellBtn).toBeUndefined(); }); @@ -118,13 +113,13 @@ describe("CommandBarComponentButtonFactory tests", () => { portalEnv: "mooncake", }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); expect(openCassandraShellBtn).toBeUndefined(); }); it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); expect(openCassandraShellBtn).toBeUndefined(); }); @@ -134,12 +129,8 @@ describe("CommandBarComponentButtonFactory tests", () => { const openPostgresShellButtonLabel = "Open PSQL shell"; const openVCoreMongoShellButtonLabel = "Open MongoDB (vCore) shell"; - beforeAll(() => { - mockExplorer = {} as Explorer; - }); - it("creates Postgres shell button", () => { - const buttons = CommandBarComponentButtonFactory.createPostgreButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createPostgreButtons(); const openPostgresShellButton = buttons.find( (button) => button.commandButtonLabel === openPostgresShellButtonLabel, ); @@ -147,7 +138,7 @@ describe("CommandBarComponentButtonFactory tests", () => { }); it("creates vCore Mongo shell button", () => { - const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons(mockExplorer); + const buttons = CommandBarComponentButtonFactory.createVCoreMongoButtons(); const openVCoreMongoShellButton = buttons.find( (button) => button.commandButtonLabel === openVCoreMongoShellButtonLabel, ); @@ -162,8 +153,6 @@ describe("CommandBarComponentButtonFactory tests", () => { const selectedNodeState = useSelectedNode.getState(); beforeAll(() => { - mockExplorer = {} as Explorer; - updateUserContext({ authType: AuthType.ResourceToken, }); @@ -175,7 +164,7 @@ describe("CommandBarComponentButtonFactory tests", () => { kind: "DocumentDB", } as DatabaseAccount, }); - const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(selectedNodeState); expect(buttons.length).toBe(2); expect(buttons[0].commandButtonLabel).toBe("New SQL Query"); expect(buttons[0].disabled).toBe(false); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 8c374a4c1..433a53d24 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -21,7 +21,6 @@ import { userContext } from "../../../UserContext"; import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils"; import { useSidePanel } from "../../../hooks/useSidePanel"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; -import Explorer from "../../Explorer"; import { useNotebook } from "../../Notebook/useNotebook"; import { OpenFullScreen } from "../../OpenFullScreen"; import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane"; @@ -32,19 +31,20 @@ import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode"; let counter = 0; -export function createStaticCommandBarButtons( - container: Explorer, - selectedNodeState: SelectedNodeState, -): CommandButtonComponentProps[] { +export function createStaticCommandBarButtons(selectedNodeState: SelectedNodeState): CommandButtonComponentProps[] { + if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { + return userContext.apiType === "Postgres" ? createPostgreButtons() : createVCoreMongoButtons(); + } + if (userContext.authType === AuthType.ResourceToken) { - return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState); + return createStaticCommandBarButtonsForResourceToken(selectedNodeState); } const buttons: CommandButtonComponentProps[] = []; // Avoid starting with a divider const addDivider = () => { - if (buttons.length > 0) { + if (buttons.length > 0 && !buttons[buttons.length - 1].isDivider) { buttons.push(createDivider()); } }; @@ -54,7 +54,7 @@ export function createStaticCommandBarButtons( userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra" ) { - const addSynapseLink = createOpenSynapseLinkDialogButton(container); + const addSynapseLink = createOpenSynapseLinkDialogButton(); if (addSynapseLink) { addDivider(); buttons.push(addSynapseLink); @@ -67,9 +67,9 @@ export function createStaticCommandBarButtons( const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated); useEffect(() => { - const buttonProps = createLoginForEntraIDButton(container); + const buttonProps = createLoginForEntraIDButton(); setLoginButtonProps(buttonProps); - }, [dataPlaneRbacEnabled, aadTokenUpdated, container]); + }, [dataPlaneRbacEnabled, aadTokenUpdated]); if (loginButtonProps) { addDivider(); @@ -87,8 +87,8 @@ export function createStaticCommandBarButtons( } if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) { - const openQueryBtn = createOpenQueryButton(container); - openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()]; + const openQueryBtn = createOpenQueryButton(); + openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()]; buttons.push(openQueryBtn); } @@ -103,6 +103,7 @@ export function createStaticCommandBarButtons( selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection); }, commandButtonLabel: label, + tooltipText: userContext.features.commandBarV2 ? "New..." : label, ariaLabel: label, hasPopup: true, disabled: @@ -115,21 +116,12 @@ export function createStaticCommandBarButtons( } } - return buttons; -} - -export function createContextCommandBarButtons( - container: Explorer, - selectedNodeState: SelectedNodeState, -): CommandButtonComponentProps[] { - const buttons: CommandButtonComponentProps[] = []; - if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") { const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell"; const newMongoShellBtn: CommandButtonComponentProps = { iconSrc: HostedTerminalIcon, iconAlt: label, - onCommandClick: () => { + onCommandClick: (_, container) => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); if (useNotebook.getState().isShellEnabled) { container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); @@ -141,6 +133,7 @@ export function createContextCommandBarButtons( ariaLabel: label, hasPopup: true, }; + addDivider(); buttons.push(newMongoShellBtn); } @@ -153,25 +146,27 @@ export function createContextCommandBarButtons( const newCassandraShellButton: CommandButtonComponentProps = { iconSrc: HostedTerminalIcon, iconAlt: label, - onCommandClick: () => { + onCommandClick: (_, container) => { container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra); }, commandButtonLabel: label, ariaLabel: label, hasPopup: true, }; + addDivider(); buttons.push(newCassandraShellButton); } return buttons; } -export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { +export function createPlatformButtons(): CommandButtonComponentProps[] { const buttons: CommandButtonComponentProps[] = [ { iconSrc: SettingsIcon, iconAlt: "Settings", - onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", ), + onCommandClick: (_, container) => + useSidePanel.getState().openSidePanel("Settings", ), commandButtonLabel: undefined, ariaLabel: "Settings", tooltipText: "Settings", @@ -207,7 +202,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt const feedbackButtonOptions: CommandButtonComponentProps = { iconSrc: FeedbackIcon, iconAlt: label, - onCommandClick: () => container.openCESCVAFeedbackBlade(), + onCommandClick: (_, container) => container.openCESCVAFeedbackBlade(), commandButtonLabel: undefined, ariaLabel: label, tooltipText: label, @@ -239,7 +234,7 @@ function areScriptsSupported(): boolean { ); } -function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps { +function createOpenSynapseLinkDialogButton(): CommandButtonComponentProps { if (configContext.platform === Platform.Emulator) { return undefined; } @@ -257,7 +252,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo return { iconSrc: SynapseIcon, iconAlt: label, - onCommandClick: () => container.openEnableSynapseLinkDialog(), + onCommandClick: (_, container) => container.openEnableSynapseLinkDialog(), commandButtonLabel: label, hasPopup: false, disabled: @@ -266,12 +261,12 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo }; } -function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps { +function createLoginForEntraIDButton(): CommandButtonComponentProps { if (configContext.platform !== Platform.Portal) { return undefined; } - const handleCommandClick = async () => { + const handleCommandClick: CommandButtonComponentProps["onCommandClick"] = async (_, container) => { await container.openLoginForEntraIDPopUp(); useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true }); }; @@ -398,13 +393,14 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) return buttons; } -function createOpenQueryButton(container: Explorer): CommandButtonComponentProps { +function createOpenQueryButton(): CommandButtonComponentProps { const label = "Open Query"; return { iconSrc: BrowseQueriesIcon, iconAlt: label, + tooltipText: userContext.features.commandBarV2 ? "Open Query..." : "Open Query", keyboardAction: KeyboardAction.OPEN_QUERY, - onCommandClick: () => + onCommandClick: (_, container) => useSidePanel.getState().openSidePanel("Open Saved Queries", ), commandButtonLabel: label, ariaLabel: label, @@ -427,10 +423,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps { }; } -function createOpenTerminalButtonByKind( - container: Explorer, - terminalKind: ViewModels.TerminalKind, -): CommandButtonComponentProps { +function createOpenTerminalButtonByKind(terminalKind: ViewModels.TerminalKind): CommandButtonComponentProps { const terminalFriendlyName = (): string => { switch (terminalKind) { case ViewModels.TerminalKind.Cassandra: @@ -453,7 +446,7 @@ function createOpenTerminalButtonByKind( return { iconSrc: HostedTerminalIcon, iconAlt: label, - onCommandClick: () => { + onCommandClick: (_, container) => { if (useNotebook.getState().isNotebookEnabled) { container.openNotebookTerminal(terminalKind); } @@ -467,11 +460,10 @@ function createOpenTerminalButtonByKind( } function createStaticCommandBarButtonsForResourceToken( - container: Explorer, selectedNodeState: SelectedNodeState, ): CommandButtonComponentProps[] { const newSqlQueryBtn = createNewSQLQueryButton(selectedNodeState); - const openQueryBtn = createOpenQueryButton(container); + const openQueryBtn = createOpenQueryButton(); const resourceTokenCollection: ViewModels.CollectionBase = useDatabases.getState().resourceTokenCollection; const isResourceTokenCollectionNodeSelected: boolean = @@ -484,20 +476,20 @@ function createStaticCommandBarButtonsForResourceToken( openQueryBtn.disabled = !isResourceTokenCollectionNodeSelected; if (!openQueryBtn.disabled) { - openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()]; + openQueryBtn.children = [createOpenQueryButton(), createOpenQueryFromDiskButton()]; } return [newSqlQueryBtn, openQueryBtn]; } -export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] { - const openPostgreShellBtn = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Postgres); +export function createPostgreButtons(): CommandButtonComponentProps[] { + const openPostgreShellBtn = createOpenTerminalButtonByKind(ViewModels.TerminalKind.Postgres); return [openPostgreShellBtn]; } -export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] { - const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo); +export function createVCoreMongoButtons(): CommandButtonComponentProps[] { + const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(ViewModels.TerminalKind.VCoreMongo); return [openVCoreMongoTerminalButton]; } diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.test.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.test.tsx index bcd2b4b26..b91743a0b 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.test.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.test.tsx @@ -1,8 +1,10 @@ import { ICommandBarItemProps } from "@fluentui/react"; +import Explorer from "Explorer/Explorer"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import * as CommandBarUtil from "./CommandBarUtil"; describe("CommandBarUtil tests", () => { + const mockExplorer = {} as Explorer; const createButton = (): CommandButtonComponentProps => { return { iconSrc: "icon", @@ -22,7 +24,7 @@ describe("CommandBarUtil tests", () => { const btn = createButton(); const backgroundColor = "backgroundColor"; - const converteds = CommandBarUtil.convertButton([btn], backgroundColor); + const converteds = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer); expect(converteds.length).toBe(1); const converted = converteds[0]; expect(converted.split).toBe(undefined); @@ -46,7 +48,7 @@ describe("CommandBarUtil tests", () => { btn.children.push(child); } - const converteds = CommandBarUtil.convertButton([btn], "backgroundColor"); + const converteds = CommandBarUtil.convertButton([btn], "backgroundColor", mockExplorer); expect(converteds.length).toBe(1); const converted = converteds[0]; expect(converted.split).toBe(true); @@ -62,7 +64,7 @@ describe("CommandBarUtil tests", () => { btns.push(createButton()); } - const converteds = CommandBarUtil.convertButton(btns, "backgroundColor"); + const converteds = CommandBarUtil.convertButton(btns, "backgroundColor", mockExplorer); const uniqueKeys = converteds .map((btn: ICommandBarItemProps) => btn.key) .filter((value: string, index: number, self: string[]) => self.indexOf(value) === index); @@ -74,10 +76,10 @@ describe("CommandBarUtil tests", () => { const backgroundColor = "backgroundColor"; btn.commandButtonLabel = undefined; - let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0]; + let converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0]; expect(converted.text).toEqual(btn.tooltipText); - converted = CommandBarUtil.convertButton([btn], backgroundColor)[0]; + converted = CommandBarUtil.convertButton([btn], backgroundColor, mockExplorer)[0]; delete btn.commandButtonLabel; expect(converted.text).toEqual(btn.tooltipText); }); diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 7378be453..01bd6efa5 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -25,7 +25,11 @@ import { MemoryTracker } from "./MemoryTrackerComponent"; * Convert our NavbarButtonConfig to UI Fabric buttons * @param btns */ -export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => { +export const convertButton = ( + btns: CommandButtonComponentProps[], + backgroundColor: string, + container: Explorer, +): ICommandBarItemProps[] => { const buttonHeightPx = configContext.platform == Platform.Fabric ? StyleConstants.FabricCommandBarButtonHeight @@ -54,15 +58,14 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol iconProps: { style: { width: StyleConstants.CommandBarIconWidth, // 16 - alignSelf: btn.iconName ? "baseline" : undefined, + alignSelf: undefined, filter: getFilter(btn.disabled), }, imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined, - iconName: btn.iconName, }, onClick: btn.onCommandClick ? (ev?: React.MouseEvent | React.KeyboardEvent) => { - btn.onCommandClick(ev); + btn.onCommandClick(ev, container); let copilotEnabled = false; if (useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotUserDBEnabled) { copilotEnabled = useQueryCopilot.getState().copilotEnabledforExecution; @@ -135,7 +138,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol result.split = true; result.subMenuProps = { - items: convertButton(btn.children, backgroundColor), + items: convertButton(btn.children, backgroundColor, container), styles: { list: { // TODO Figure out how to do it the proper way with subComponentStyles. @@ -186,7 +189,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol option?: IDropdownOption, index?: number, ): void => { - btn.children[index].onCommandClick(event); + btn.children[index].onCommandClick(event, container); TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text }); }; @@ -237,14 +240,17 @@ export const createConnectionStatus = (container: Explorer, poolId: PoolIdType, }; }; -export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap { +export function createKeyboardHandlers( + allButtons: CommandButtonComponentProps[], + container: Explorer, +): KeyboardHandlerMap { const handlers: KeyboardHandlerMap = {}; function createHandlers(buttons: CommandButtonComponentProps[]) { buttons.forEach((button) => { if (!button.disabled && button.keyboardAction) { handlers[button.keyboardAction] = (e) => { - button.onCommandClick(e); + button.onCommandClick(e, container); // If the handler is bound, it means the button is visible and enabled, so we should prevent the default action return true; diff --git a/src/Explorer/Menus/CommandBar/useCommandBar.ts b/src/Explorer/Menus/CommandBar/useCommandBar.ts new file mode 100644 index 000000000..77e95769c --- /dev/null +++ b/src/Explorer/Menus/CommandBar/useCommandBar.ts @@ -0,0 +1,16 @@ +import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; +import create, { UseStore } from "zustand"; + +export interface CommandBarStore { + contextButtons: CommandButtonComponentProps[]; + setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void; + isHidden: boolean; + setIsHidden: (isHidden: boolean) => void; +} + +export const useCommandBar: UseStore = create((set) => ({ + contextButtons: [], + setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })), + isHidden: false, + setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })), +})); diff --git a/src/Explorer/Menus/CommandBarV2/CommandBarV2.tsx b/src/Explorer/Menus/CommandBarV2/CommandBarV2.tsx new file mode 100644 index 000000000..b90a393a4 --- /dev/null +++ b/src/Explorer/Menus/CommandBarV2/CommandBarV2.tsx @@ -0,0 +1,159 @@ +import { + makeStyles, + Menu, + MenuButton, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + Toolbar, + ToolbarButton, + ToolbarDivider, + ToolbarGroup, + Tooltip, +} from "@fluentui/react-components"; +import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; +import Explorer from "Explorer/Explorer"; +import { + createPlatformButtons, + createStaticCommandBarButtons, +} from "Explorer/Menus/CommandBar/CommandBarComponentButtonFactory"; +import { createKeyboardHandlers } from "Explorer/Menus/CommandBar/CommandBarUtil"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; +import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil"; +import { useSelectedNode } from "Explorer/useSelectedNode"; +import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; +import React, { MouseEventHandler } from "react"; + +const useToolbarStyles = makeStyles({ + toolbar: { + height: tokens.layoutRowHeight, + justifyContent: "space-between", // Ensures that the two toolbar groups are at opposite ends of the toolbar + ...cosmosShorthands.borderBottom(), + }, + toolbarGroup: { + display: "flex", + }, +}); + +export interface CommandBarV2Props { + explorer: Explorer; +} + +export const CommandBarV2: React.FC = ({ explorer }: CommandBarV2Props) => { + const styles = useToolbarStyles(); + const selectedNodeState = useSelectedNode(); + const contextButtons = useCommandBar((state) => state.contextButtons); + const isHidden = useCommandBar((state) => state.isHidden); + const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR); + const staticButtons = createStaticCommandBarButtons(selectedNodeState); + const platformButtons = createPlatformButtons(); + + if (isHidden) { + setKeyboardHandlers({}); + return null; + } + + const allButtons = staticButtons.concat(contextButtons).concat(platformButtons); + const keyboardHandlers = createKeyboardHandlers(allButtons, explorer); + setKeyboardHandlers(keyboardHandlers); + + return ( + + + + {staticButtons.map((button, index) => + renderButton(explorer, button, `static-${index}`, contextButtons?.length > 0), + )} + {staticButtons.length > 0 && contextButtons?.length > 0 && } + {contextButtons.map((button, index) => renderButton(explorer, button, `context-${index}`, false))} + + + {platformButtons.map((button, index) => renderButton(explorer, button, `platform-${index}`, true))} + + + + ); +}; + +// This allows us to migrate individual buttons over to using a JSX.Element for the icon, without requiring us to change them all at once. +function renderIcon(iconSrcOrElement: string | JSX.Element, alt?: string): JSX.Element { + if (typeof iconSrcOrElement === "string") { + return {alt}; + } + return iconSrcOrElement; +} + +function renderButton( + explorer: Explorer, + btn: CommandButtonComponentProps, + key: string, + iconOnly: boolean, +): JSX.Element { + if (btn.isDivider) { + return ; + } + + const hasChildren = !!btn.children && btn.children.length > 0; + const label = btn.commandButtonLabel || btn.tooltipText; + const tooltip = btn.tooltipText || (iconOnly ? label : undefined); + const onClick: MouseEventHandler | undefined = + btn.onCommandClick && !hasChildren ? (e) => btn.onCommandClick(e, explorer) : undefined; + + // We don't know which element will be the top-level element, so just slap a key on all of 'em + + let button = hasChildren ? ( + + {!iconOnly && label} + + ) : ( + + {!iconOnly && label} + + ); + + if (tooltip) { + button = ( + + {button} + + ); + } + + if (hasChildren) { + button = ( + + {button} + + {btn.children.map((child, index) => renderMenuItem(explorer, child, index.toString()))} + + + ); + } + + return button; +} + +function renderMenuItem(explorer: Explorer, btn: CommandButtonComponentProps, key: string): JSX.Element { + const hasChildren = !!btn.children && btn.children.length > 0; + const onClick: MouseEventHandler | undefined = btn.onCommandClick + ? (e) => btn.onCommandClick(e, explorer) + : undefined; + const item = ( + + {btn.commandButtonLabel || btn.tooltipText} + + ); + + if (hasChildren) { + return ( + + {item} + + {btn.children.map((child, index) => renderMenuItem(explorer, child, index.toString()))} + + + ); + } + return item; +} diff --git a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx index 199cf7a8f..1d2f9c08a 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotTab.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotTab.tsx @@ -3,7 +3,7 @@ import { Stack } from "@fluentui/react"; import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants"; import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; -import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane"; import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar"; import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities"; diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx index 14c1f16a8..5666e956d 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx @@ -5,7 +5,7 @@ import { Platform, updateConfigContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact"; import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog"; -import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { ButtonsDependencies, DELETE_BUTTON_ID, @@ -461,7 +461,7 @@ describe("Documents tab (noSql API)", () => { useCommandBar .getState() .contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID) - .onCommandClick(undefined); + .onCommandClick(undefined, undefined); }); expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy(); }); @@ -471,7 +471,7 @@ describe("Documents tab (noSql API)", () => { useCommandBar .getState() .contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID) - .onCommandClick(undefined); + .onCommandClick(undefined, undefined); }); expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined(); @@ -483,7 +483,7 @@ describe("Documents tab (noSql API)", () => { await useCommandBar .getState() .contextButtons.find((button) => button.id === DELETE_BUTTON_ID) - .onCommandClick(undefined); + .onCommandClick(undefined, undefined); }); expect(useDialog.getState().showOkCancelModalDialog).toHaveBeenCalled(); @@ -494,7 +494,7 @@ describe("Documents tab (noSql API)", () => { useCommandBar .getState() .contextButtons.find((button) => button.id === DELETE_BUTTON_ID) - .onCommandClick(undefined); + .onCommandClick(undefined, undefined); }); expect(ProgressModalDialog).toHaveBeenCalled(); @@ -508,7 +508,7 @@ describe("Documents tab (noSql API)", () => { useCommandBar .getState() .contextButtons.find((button) => button.id === DELETE_BUTTON_ID) - .onCommandClick(undefined); + .onCommandClick(undefined, undefined); }); // The implementation uses setTimeout, so wait for it to finish diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index c5445c2f9..c662b5f89 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -28,7 +28,7 @@ import { useDialog } from "Explorer/Controls/Dialog"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog"; import Explorer from "Explorer/Explorer"; -import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { ColumnsSelection, diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2Mongo.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2Mongo.test.tsx index 6988f448d..5894fac64 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2Mongo.test.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2Mongo.test.tsx @@ -1,7 +1,7 @@ import { deleteDocuments } from "Common/MongoProxyClient"; import { Platform, updateConfigContext } from "ConfigContext"; import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact"; -import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { DELETE_BUTTON_ID, DISCARD_BUTTON_ID, @@ -163,7 +163,7 @@ describe("Documents tab (Mongo API)", () => { useCommandBar .getState() .contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID) - .onCommandClick(undefined); + .onCommandClick(undefined, undefined); }); expect(wrapper.findWhere((node) => node.text().includes("replace_with_new_document_id")).exists()).toBeTruthy(); }); @@ -173,7 +173,7 @@ describe("Documents tab (Mongo API)", () => { useCommandBar .getState() .contextButtons.find((button) => button.id === NEW_DOCUMENT_BUTTON_ID) - .onCommandClick(undefined); + .onCommandClick(undefined, undefined); }); expect(useCommandBar.getState().contextButtons.find((button) => button.id === SAVE_BUTTON_ID)).toBeDefined(); @@ -188,7 +188,7 @@ describe("Documents tab (Mongo API)", () => { useCommandBar .getState() .contextButtons.find((button) => button.id === DELETE_BUTTON_ID) - .onCommandClick(undefined); + .onCommandClick(undefined, undefined); }); expect(mockDeleteDocuments).toHaveBeenCalled(); diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts index 92e0e6958..23d31645c 100644 --- a/src/Explorer/Tabs/NotebookV2Tab.ts +++ b/src/Explorer/Tabs/NotebookV2Tab.ts @@ -152,7 +152,6 @@ export default class NotebookTabV2 extends NotebookTabBase { ariaLabel: saveLabel, children: saveButtonChildren.length && [ { - iconName: "Save", onCommandClick: () => this.notebookComponentAdapter.notebookSave(), commandButtonLabel: saveLabel, hasPopup: false, diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 5f66836e4..8f6cf9392 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -6,6 +6,7 @@ import { SplitterDirection } from "Common/Splitter"; import { Platform, configContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; import { monaco } from "Explorer/LazyMonaco"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext"; import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar"; @@ -54,7 +55,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { EditorReact } from "../../Controls/Editor/EditorReact"; import Explorer from "../../Explorer"; -import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane"; import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane"; import TabsBase from "../TabsBase"; diff --git a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx index 1f230b305..b0165da77 100644 --- a/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx +++ b/src/Explorer/Tabs/StoredProcedureTab/StoredProcedureTabComponent.tsx @@ -1,5 +1,6 @@ import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; import { Pivot, PivotItem } from "@fluentui/react"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { KeyboardAction } from "KeyboardShortcuts"; import React from "react"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; @@ -15,7 +16,6 @@ import { useTabs } from "../../../hooks/useTabs"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { EditorReact } from "../../Controls/Editor/EditorReact"; import Explorer from "../../Explorer"; -import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; import StoredProcedure from "../../Tree/StoredProcedure"; import { useSelectedNode } from "../../useSelectedNode"; import ScriptTabBase from "../ScriptTabBase"; diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 3ee50992c..996e86727 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -6,7 +6,8 @@ import { IpRule } from "Contracts/DataModels"; import { MessageTypes } from "Contracts/ExplorerContracts"; import { CollectionTabKind } from "Contracts/ViewModels"; import Explorer from "Explorer/Explorer"; -import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; +import { CommandBarV2 } from "Explorer/Menus/CommandBarV2/CommandBarV2"; import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab"; import { SplashScreen } from "Explorer/SplashScreen/SplashScreen"; import { ConnectTab } from "Explorer/Tabs/ConnectTab"; @@ -106,6 +107,7 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
+ {userContext.features.commandBarV2 && } {activeReactTab !== undefined && getReactTabContent(activeReactTab, explorer)} {openedTabs.map((tab) => ( diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts index 029fbfe6e..276d12401 100644 --- a/src/Explorer/Tabs/TabsBase.ts +++ b/src/Explorer/Tabs/TabsBase.ts @@ -1,3 +1,4 @@ +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts"; import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; @@ -9,7 +10,6 @@ import { useNotificationConsole } from "../../hooks/useNotificationConsole"; import { useTabs } from "../../hooks/useTabs"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../Explorer"; -import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel"; import { useSelectedNode } from "../useSelectedNode"; // TODO: Use specific actions for logging telemetry data diff --git a/src/Explorer/Tabs/TriggerTabContent.tsx b/src/Explorer/Tabs/TriggerTabContent.tsx index 28e81ee38..5aac8df33 100644 --- a/src/Explorer/Tabs/TriggerTabContent.tsx +++ b/src/Explorer/Tabs/TriggerTabContent.tsx @@ -1,5 +1,6 @@ import { TriggerDefinition } from "@azure/cosmos"; import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { KeyboardAction } from "KeyboardShortcuts"; import React, { Component } from "react"; import DiscardIcon from "../../../images/discard.svg"; @@ -14,7 +15,6 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { EditorReact } from "../Controls/Editor/EditorReact"; -import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import TriggerTab from "./TriggerTab"; const triggerTypeOptions: IDropdownOption[] = [ diff --git a/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx index 97d83b747..c3b732b7f 100644 --- a/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx +++ b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx @@ -1,5 +1,6 @@ import { UserDefinedFunctionDefinition } from "@azure/cosmos"; import { Label, TextField } from "@fluentui/react"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { KeyboardAction } from "KeyboardShortcuts"; import React, { Component } from "react"; import DiscardIcon from "../../../images/discard.svg"; @@ -13,7 +14,6 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { EditorReact } from "../Controls/Editor/EditorReact"; -import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import UserDefinedFunctionTab from "./UserDefinedFunctionTab"; interface IUserDefinedFunctionTabContentState { diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 26246f1b1..84eb2ef1f 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -1,4 +1,5 @@ import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { useNotebook } from "Explorer/Notebook/useNotebook"; import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import * as ko from "knockout"; @@ -23,7 +24,6 @@ import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types"; import { useTabs } from "../../hooks/useTabs"; import Explorer from "../Explorer"; -import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient"; import ConflictsTab from "../Tabs/ConflictsTab"; import GraphTab from "../Tabs/GraphTab"; diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 76d3e9308..d1e4e5743 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -1,4 +1,5 @@ import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity"; import { shouldShowScriptNodes } from "Explorer/Tree/treeNodeUtil"; import { getItemName } from "Utils/APITypeUtils"; @@ -28,7 +29,6 @@ import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFacto import { useDialog } from "../Controls/Dialog"; import { LegacyTreeComponent, LegacyTreeNode } from "../Controls/TreeComponent/LegacyTreeComponent"; import Explorer from "../Explorer"; -import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem"; import { NotebookUtil } from "../Notebook/NotebookUtil"; import { useNotebook } from "../Notebook/useNotebook"; diff --git a/src/Explorer/Tree/SampleDataTree.tsx b/src/Explorer/Tree/SampleDataTree.tsx index eafd2b862..84d9578a8 100644 --- a/src/Explorer/Tree/SampleDataTree.tsx +++ b/src/Explorer/Tree/SampleDataTree.tsx @@ -1,4 +1,4 @@ -import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import TabsBase from "Explorer/Tabs/TabsBase"; import { useSelectedNode } from "Explorer/useSelectedNode"; import { useTabs } from "hooks/useTabs"; diff --git a/src/Explorer/Tree/treeNodeUtil.test.ts b/src/Explorer/Tree/treeNodeUtil.test.ts index 239b6144d..f8e3526d2 100644 --- a/src/Explorer/Tree/treeNodeUtil.test.ts +++ b/src/Explorer/Tree/treeNodeUtil.test.ts @@ -2,7 +2,7 @@ import { CapabilityNames } from "Common/Constants"; import { Platform, updateConfigContext } from "ConfigContext"; import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import Explorer from "Explorer/Explorer"; -import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { useNotebook } from "Explorer/Notebook/useNotebook"; import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel"; import TabsBase from "Explorer/Tabs/TabsBase"; diff --git a/src/Explorer/Tree/treeNodeUtil.tsx b/src/Explorer/Tree/treeNodeUtil.tsx index 8e6c94559..97635a243 100644 --- a/src/Explorer/Tree/treeNodeUtil.tsx +++ b/src/Explorer/Tree/treeNodeUtil.tsx @@ -1,5 +1,6 @@ import { DatabaseRegular, DocumentMultipleRegular, SettingsRegular } from "@fluentui/react-icons"; import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; +import { useCommandBar } from "Explorer/Menus/CommandBar/useCommandBar"; import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity"; import TabsBase from "Explorer/Tabs/TabsBase"; import StoredProcedure from "Explorer/Tree/StoredProcedure"; @@ -17,7 +18,6 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { userContext } from "../../UserContext"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import Explorer from "../Explorer"; -import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { useNotebook } from "../Notebook/useNotebook"; import { useSelectedNode } from "../useSelectedNode"; diff --git a/src/Main.tsx b/src/Main.tsx index 52972b462..bd9a21140 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -20,9 +20,11 @@ import "../externals/jquery.typeahead.min.css"; import "../externals/jquery.typeahead.min.js"; // Image Dependencies import { Platform } from "ConfigContext"; +import { CommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel"; import { SidebarContainer } from "Explorer/Sidebar"; import { KeyboardShortcutRoot } from "KeyboardShortcuts"; +import { userContext } from "UserContext"; import "allotment/dist/style.css"; import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; @@ -48,7 +50,6 @@ import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less"; import "./Explorer/Controls/TreeComponent/treeComponent.less"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; -import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less"; import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "./Explorer/Menus/NotificationConsole/NotificationConsole.less"; @@ -86,7 +87,7 @@ const App: React.FunctionComponent = () => {
{/* Main Command Bar - Start */} - + {!userContext.features.commandBarV2 && } {/* Collections Tree and Tabs - Begin */} {/* Collections Tree and Tabs - End */} diff --git a/src/Platform/Hosted/Components/FeedbackCommandButton.tsx b/src/Platform/Hosted/Components/FeedbackCommandButton.tsx index 00b6670b9..804828077 100644 --- a/src/Platform/Hosted/Components/FeedbackCommandButton.tsx +++ b/src/Platform/Hosted/Components/FeedbackCommandButton.tsx @@ -1,22 +1,18 @@ import * as React from "react"; -import { CommandButtonComponent } from "../../../Explorer/Controls/CommandButton/CommandButtonComponent"; import FeedbackIcon from "../../../../images/Feedback.svg"; export const FeedbackCommandButton: React.FunctionComponent = () => { + const onClick = () => { + window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback"); + }; + return (
- - window.open("https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback") - } - ariaLabel="feeback button" - tooltipText="Send feedback" - hasPopup={true} - disabled={false} - /> +
+ + Send feedback + +
); }; diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 5bd84516e..6e6bd13fd 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -38,6 +38,7 @@ export type Features = { readonly copilotChatFixedMonacoEditorHeight: boolean; readonly enablePriorityBasedExecution: boolean; readonly disableConnectionStringLogin: boolean; + readonly commandBarV2: boolean; // can be set via both flight and feature flag autoscaleDefault: boolean; @@ -108,6 +109,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"), enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"), disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"), + commandBarV2: "true" === get("commandbarv2"), }; }