diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index aece6a04a..c017ec412 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -20,6 +20,8 @@ export interface DatabaseAccountExtendedProperties { writeLocations?: DatabaseAccountResponseLocation[]; enableFreeTier?: boolean; enableAnalyticalStorage?: boolean; + isVirtualNetworkFilterEnabled?: boolean; + ipRules?: IpRule[]; } export interface DatabaseAccountResponseLocation { @@ -31,6 +33,10 @@ export interface DatabaseAccountResponseLocation { provisioningState: string; } +export interface IpRule { + ipAddressOrRange: string; +} + export interface ConfigurationOverrides { EnableBsonSchema: string; } diff --git a/src/Explorer/ContextMenuButtonFactory.ts b/src/Explorer/ContextMenuButtonFactory.ts index 1940d025f..ed0ef9a59 100644 --- a/src/Explorer/ContextMenuButtonFactory.ts +++ b/src/Explorer/ContextMenuButtonFactory.ts @@ -73,9 +73,13 @@ export class ResourceTreeContextMenuButtonFactory { iconSrc: HostedTerminalIcon, onClick: () => { const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); - selectedCollection && selectedCollection.onNewMongoShellClick(); + if (container.isShellEnabled()) { + container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); + } else { + selectedCollection && selectedCollection.onNewMongoShellClick(); + } }, - label: "New Shell", + label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell", }); } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 6a8979379..1fa13d27b 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -842,6 +842,7 @@ exports[`SettingsComponent renders 1`] = ` "isResourceTokenCollectionNodeSelected": [Function], "isSchemaEnabled": [Function], "isServerlessEnabled": [Function], + "isShellEnabled": [Function], "isSparkEnabled": [Function], "isSparkEnabledForAccount": [Function], "isSynapseLinkUpdating": [Function], @@ -1737,6 +1738,7 @@ exports[`SettingsComponent renders 1`] = ` "isResourceTokenCollectionNodeSelected": [Function], "isSchemaEnabled": [Function], "isServerlessEnabled": [Function], + "isShellEnabled": [Function], "isSparkEnabled": [Function], "isSparkEnabledForAccount": [Function], "isSynapseLinkUpdating": [Function], @@ -2645,6 +2647,7 @@ exports[`SettingsComponent renders 1`] = ` "isResourceTokenCollectionNodeSelected": [Function], "isSchemaEnabled": [Function], "isServerlessEnabled": [Function], + "isShellEnabled": [Function], "isSparkEnabled": [Function], "isSparkEnabledForAccount": [Function], "isSynapseLinkUpdating": [Function], @@ -3540,6 +3543,7 @@ exports[`SettingsComponent renders 1`] = ` "isResourceTokenCollectionNodeSelected": [Function], "isSchemaEnabled": [Function], "isServerlessEnabled": [Function], + "isShellEnabled": [Function], "isSparkEnabled": [Function], "isSparkEnabledForAccount": [Function], "isSynapseLinkUpdating": [Function], diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index ba8f563c4..6793e1194 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -177,6 +177,8 @@ export default class Explorer { public openDialog: ExplorerParams["openDialog"]; public closeDialog: ExplorerParams["closeDialog"]; + public isShellEnabled: ko.Observable; + private _isInitializingNotebooks: boolean; private notebookBasePath: ko.Observable; private _arcadiaManager: ArcadiaResourceManager; @@ -222,6 +224,7 @@ export default class Explorer { }); } }); + this.isShellEnabled = ko.observable(false); this.isNotebooksEnabledForAccount = ko.observable(false); this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); this.isSparkEnabledForAccount = ko.observable(false); @@ -248,6 +251,12 @@ export default class Explorer { ((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) || userContext.features.enableNotebooks) ); + this.isShellEnabled( + this.isNotebookEnabled() && + !userContext.databaseAccount.properties.isVirtualNetworkFilterEnabled && + userContext.databaseAccount.properties.ipRules.length === 0 + ); + TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { isNotebookEnabled: this.isNotebookEnabled(), dataExplorerArea: Constants.Areas.Notebook, @@ -1715,32 +1724,27 @@ export default class Explorer { throw new Error("Terminal kind: ${kind} not supported"); } - const terminalTabs: TerminalTab[] = this.tabsManager.getTabs( - ViewModels.CollectionTabKind.Terminal, - (tab) => tab.hashLocation() == hashLocation + const terminalTabs: TerminalTab[] = this.tabsManager.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => + tab.hashLocation().startsWith(hashLocation) ) as TerminalTab[]; - let terminalTab: TerminalTab = terminalTabs && terminalTabs[0]; - if (terminalTab) { - this.tabsManager.activateTab(terminalTab); - } else { - const newTab = new TerminalTab({ - account: userContext.databaseAccount, - tabKind: ViewModels.CollectionTabKind.Terminal, - node: null, - title: title, - tabPath: title, - collection: null, - hashLocation: hashLocation, - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: null, - onUpdateTabsButtons: this.onUpdateTabsButtons, - container: this, - kind: kind, - }); + const index = terminalTabs.length + 1; + const newTab = new TerminalTab({ + account: userContext.databaseAccount, + tabKind: ViewModels.CollectionTabKind.Terminal, + node: null, + title: `${title} ${index}`, + tabPath: `${title} ${index}`, + collection: null, + hashLocation: `${hashLocation} ${index}`, + isTabsContentExpanded: ko.observable(true), + onLoadStartKey: null, + onUpdateTabsButtons: this.onUpdateTabsButtons, + container: this, + kind: kind, + }); - this.tabsManager.activateNewTab(newTab); - } + this.tabsManager.activateNewTab(newTab); } public async openGallery( diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts index 268f149b7..63172c8d5 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts @@ -139,6 +139,7 @@ describe("CommandBarComponentButtonFactory tests", () => { mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isServerlessEnabled = ko.computed(() => false); + mockExplorer.isShellEnabled = ko.observable(true); }); afterAll(() => { @@ -154,6 +155,7 @@ describe("CommandBarComponentButtonFactory tests", () => { mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); mockExplorer.isRunningOnNationalCloud = ko.observable(false); + mockExplorer.isShellEnabled = ko.observable(true); }); it("Mongo Api not available - button should be hidden", () => { @@ -173,24 +175,18 @@ describe("CommandBarComponentButtonFactory tests", () => { expect(openMongoShellBtn).toBeUndefined(); }); - it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { + it("Notebooks is not enabled and is unavailable - button should be hidden", () => { const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeDefined(); - expect(openMongoShellBtn.disabled).toBe(true); - expect(openMongoShellBtn.tooltipText).toBe( - "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks." - ); + expect(openMongoShellBtn).toBeUndefined(); }); - it("Notebooks is not enabled and is available - button should be shown and enabled", () => { + it("Notebooks is not enabled and is available - button should be hidden", () => { mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); - expect(openMongoShellBtn).toBeDefined(); - expect(openMongoShellBtn.disabled).toBe(false); - expect(openMongoShellBtn.tooltipText).toBe(""); + expect(openMongoShellBtn).toBeUndefined(); }); it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { @@ -213,6 +209,16 @@ describe("CommandBarComponentButtonFactory tests", () => { expect(openMongoShellBtn.disabled).toBe(false); expect(openMongoShellBtn.tooltipText).toBe(""); }); + + it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => { + mockExplorer.isNotebookEnabled = ko.observable(true); + mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); + mockExplorer.isShellEnabled = ko.observable(false); + + const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); + const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); + expect(openMongoShellBtn).toBeUndefined(); + }); }); describe("Open Cassandra Shell button", () => { @@ -273,11 +279,7 @@ describe("CommandBarComponentButtonFactory tests", () => { it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); - expect(openCassandraShellBtn).toBeDefined(); - expect(openCassandraShellBtn.disabled).toBe(true); - expect(openCassandraShellBtn.tooltipText).toBe( - "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks." - ); + expect(openCassandraShellBtn).toBeUndefined(); }); it("Notebooks is not enabled and is available - button should be shown and enabled", () => { @@ -285,9 +287,7 @@ describe("CommandBarComponentButtonFactory tests", () => { const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); - expect(openCassandraShellBtn).toBeDefined(); - expect(openCassandraShellBtn.disabled).toBe(false); - expect(openCassandraShellBtn.tooltipText).toBe(""); + expect(openCassandraShellBtn).toBeUndefined(); }); it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index da8b4251d..41c1bb9e9 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -61,48 +61,40 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto if (container.notebookManager?.gitHubOAuthService) { buttons.push(createManageGitHubAccountButton(container)); } - } - if (!container.isRunningOnNationalCloud()) { - if (!container.isNotebookEnabled()) { - buttons.push(createEnableNotebooksButton(container)); - } - - if (userContext.apiType === "Mongo") { - buttons.push(createOpenMongoTerminalButton(container)); - } - - if (userContext.apiType === "Cassandra") { - buttons.push(createOpenCassandraTerminalButton(container)); - } - } - - if (container.isNotebookEnabled()) { buttons.push(createOpenTerminalButton(container)); buttons.push(createNotebookWorkspaceResetButton(container)); + if ( + (userContext.apiType === "Mongo" && container.isShellEnabled() && container.isDatabaseNodeOrNoneSelected()) || + userContext.apiType === "Cassandra" + ) { + buttons.push(createDivider()); + if (userContext.apiType === "Cassandra") { + buttons.push(createOpenCassandraTerminalButton(container)); + } else { + buttons.push(createOpenMongoTerminalButton(container)); + } + } + } else { + if (!container.isRunningOnNationalCloud()) { + buttons.push(createEnableNotebooksButton(container)); + } } if (!container.isDatabaseNodeOrNoneSelected()) { - if (container.isNotebookEnabled()) { - buttons.push(createDivider()); - } + const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; - const isSqlQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; - if (isSqlQuerySupported) { + if (isQuerySupported) { + buttons.push(createDivider()); const newSqlQueryBtn = createNewSQLQueryButton(container); buttons.push(newSqlQueryBtn); } - const isSupportedOpenQueryApi = - userContext.apiType === "SQL" || userContext.apiType === "Mongo" || userContext.apiType === "Gremlin"; - const isSupportedOpenQueryFromDiskApi = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; - if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) { + if (isQuerySupported && container.selectedNode() && container.findSelectedCollection()) { const openQueryBtn = createOpenQueryButton(container); openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)]; buttons.push(openQueryBtn); - } else if (isSupportedOpenQueryFromDiskApi && container.selectedNode() && container.findSelectedCollection()) { - buttons.push(createOpenQueryFromDiskButton(container)); } if (areScriptsSupported()) { @@ -132,13 +124,17 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt const buttons: CommandButtonComponentProps[] = []; if (!container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") { - const label = "New Shell"; + const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell"; const newMongoShellBtn: CommandButtonComponentProps = { iconSrc: HostedTerminalIcon, iconAlt: label, onCommandClick: () => { const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); - selectedCollection && selectedCollection.onNewMongoShellClick(); + if (container.isShellEnabled()) { + container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); + } else { + selectedCollection && selectedCollection.onNewMongoShellClick(); + } }, commandButtonLabel: label, ariaLabel: label, diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index fddba4c4d..571e69152 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -831,6 +831,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "isResourceTokenCollectionNodeSelected": [Function], "isSchemaEnabled": [Function], "isServerlessEnabled": [Function], + "isShellEnabled": [Function], "isSparkEnabled": [Function], "isSparkEnabledForAccount": [Function], "isSynapseLinkUpdating": [Function], diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index dc2e79ad0..b12f75260 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -821,6 +821,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "isResourceTokenCollectionNodeSelected": [Function], "isSchemaEnabled": [Function], "isServerlessEnabled": [Function], + "isShellEnabled": [Function], "isSparkEnabled": [Function], "isSparkEnabledForAccount": [Function], "isSynapseLinkUpdating": [Function], diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index 339baae7d..afd2bc908 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -822,6 +822,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database "isSchemaEnabled": [Function], "isSelectedDatabaseShared": [Function], "isServerlessEnabled": [Function], + "isShellEnabled": [Function], "isSparkEnabled": [Function], "isSparkEnabledForAccount": [Function], "isSynapseLinkUpdating": [Function],