From c07000a5c21407d31834a807179d472cfdf29492 Mon Sep 17 00:00:00 2001 From: vchske Date: Tue, 12 Sep 2023 18:03:59 -0700 Subject: [PATCH] Adding vcore mongo quickstart (#1600) * Safety checkin * Adding vcoremongo to Main * Safety checkin * Adding vcoremongo to Main * Safety commit * Safety checkin * Adding vcoremongo to Main * Safety commit * Integrating mongo shell * Safety checkin * Adding vcoremongo to Main * Safety commit * Integrating mongo shell * Safety checkin * Safety commit * Enable mongo shell in its own tab * Safety checkin * Adding vcoremongo to Main * Safety commit * Integrating mongo shell * Safety checkin * Safety commit * Safety commit * Integrating mongo shell * Safety checkin * Safety commit * Enable mongo shell in its own tab * Adding message * Integrated mongo shell * Moving Juno endpoint back to prod * Fixed command bar unit tests * Fixing spelling --- images/Table.svg | 20 ++ images/vcoreMongoFirewallRule.png | Bin 0 -> 13759 bytes src/Contracts/DataModels.ts | 8 +- src/Contracts/ViewModels.ts | 2 + .../Notebook/NotebookTerminalComponent.tsx | 12 +- src/Explorer/Explorer.tsx | 6 +- .../CommandBar/CommandBarComponentAdapter.tsx | 7 +- .../CommandBarComponentButtonFactory.test.ts | 8 +- .../CommandBarComponentButtonFactory.tsx | 80 ++--- src/Explorer/Notebook/useNotebook.ts | 12 +- .../QuickstartFirewallNotification.tsx | 24 +- src/Explorer/Quickstart/QuickstartGuide.tsx | 70 +--- .../Shared/QuickstartRenderUtilities.tsx | 50 +++ .../VCoreMongoQuickstartCommands.ts | 34 ++ .../Quickstart/VCoreMongoQuickstartGuide.tsx | 310 ++++++++++++++++++ src/Explorer/SplashScreen/SplashScreen.tsx | 238 +++++++++----- src/Explorer/Tabs/QuickstartTab.tsx | 45 +-- .../Tabs/Shared/CheckFirewallRules.ts | 44 +++ src/Explorer/Tabs/Tabs.tsx | 27 +- src/Explorer/Tabs/TerminalTab.tsx | 60 ++-- src/Explorer/Tabs/VCoreMongoConnectTab.tsx | 37 +++ src/Explorer/Tabs/VCoreMongoQuickstartTab.tsx | 80 +++++ src/Main.tsx | 2 +- src/Terminal/JupyterLabAppFactory.ts | 7 + src/Terminal/TerminalProps.ts | 1 + src/Terminal/index.ts | 13 +- src/UserContext.ts | 17 +- src/Utils/APITypeUtils.ts | 6 +- src/Utils/NetworkUtility.ts | 77 +++-- src/hooks/useKnockoutExplorer.ts | 22 +- 30 files changed, 1013 insertions(+), 306 deletions(-) create mode 100644 images/Table.svg create mode 100644 images/vcoreMongoFirewallRule.png create mode 100644 src/Explorer/Quickstart/Shared/QuickstartRenderUtilities.tsx create mode 100644 src/Explorer/Quickstart/VCoreMongoQuickstartCommands.ts create mode 100644 src/Explorer/Quickstart/VCoreMongoQuickstartGuide.tsx create mode 100644 src/Explorer/Tabs/Shared/CheckFirewallRules.ts create mode 100644 src/Explorer/Tabs/VCoreMongoConnectTab.tsx create mode 100644 src/Explorer/Tabs/VCoreMongoQuickstartTab.tsx diff --git a/images/Table.svg b/images/Table.svg new file mode 100644 index 000000000..43bc2fb5f --- /dev/null +++ b/images/Table.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/images/vcoreMongoFirewallRule.png b/images/vcoreMongoFirewallRule.png new file mode 100644 index 0000000000000000000000000000000000000000..f6b161a5961b51c581b45aaafb5b1a0543391968 GIT binary patch literal 13759 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 z literal 0 HcmV?d00001 diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index dd8f95c18..a8e2c1fee 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -1,11 +1,14 @@ import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants"; -export interface DatabaseAccount { +export interface ArmEntity { id: string; name: string; location: string; type: string; kind: string; +} + +export interface DatabaseAccount extends ArmEntity { properties: DatabaseAccountExtendedProperties; systemData?: DatabaseAccountSystemData; } @@ -35,6 +38,7 @@ export interface DatabaseAccountExtendedProperties { locations?: DatabaseAccountResponseLocation[]; postgresqlEndpoint?: string; publicNetworkAccess?: string; + vcoreMongoEndpoint?: string; } export interface DatabaseAccountResponseLocation { @@ -575,7 +579,7 @@ export interface ContainerConnectionInfo { //need to add ram and rom info } -export interface PostgresFirewallRule { +export interface FirewallRule { id: string; name: string; type: string; diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index c9bb351bc..7521647df 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -372,6 +372,7 @@ export enum TerminalKind { Mongo = 1, Cassandra = 2, Postgres = 3, + VCoreMongo = 4, } export interface DataExplorerInputsFrame { @@ -397,6 +398,7 @@ export interface DataExplorerInputsFrame { defaultCollectionThroughput?: CollectionCreationDefaults; isPostgresAccount?: boolean; isReplica?: boolean; + isVCoreMongoAccount?: boolean; clientIpAddress?: string; // TODO: Update this param in the OSS extension to remove isFreeTier, isMarlinServerGroup, and make nodes a flat array instead of an nested array connectionStringParams?: any; diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx index 082979a23..96d41155e 100644 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx +++ b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx @@ -14,6 +14,7 @@ export interface NotebookTerminalComponentProps { notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; databaseAccount: DataModels.DatabaseAccount; tabId: string; + username?: string; } export class NotebookTerminalComponent extends React.Component { @@ -50,7 +51,7 @@ export class NotebookTerminalComponent extends React.Component { - if (userContext.apiType !== "Postgres") { + if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { userContext.authType === AuthType.ResourceToken ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 3eaf0507b..4e4f32e2c 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -34,8 +34,11 @@ export const CommandBar: React.FC = ({ container }: Props) => { const buttons = useCommandBar((state) => state.contextButtons); const backgroundColor = StyleConstants.BaseLight; - if (userContext.apiType === "Postgres") { - const buttons = CommandBarComponentButtonFactory.createPostgreButtons(container); + if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { + const buttons = + userContext.apiType === "Postgres" + ? CommandBarComponentButtonFactory.createPostgreButtons(container) + : CommandBarComponentButtonFactory.createVCoreMongoButtons(container); return (
{ }); }); - describe("Open Mongo Shell button", () => { - const openMongoShellBtnLabel = "Open Mongo Shell"; + describe("Open Mongo shell button", () => { + const openMongoShellBtnLabel = "Open Mongo shell"; const selectedNodeState = useSelectedNode.getState(); beforeAll(() => { @@ -247,8 +247,8 @@ describe("CommandBarComponentButtonFactory tests", () => { }); }); - describe("Open Cassandra Shell button", () => { - const openCassandraShellBtnLabel = "Open Cassandra Shell"; + describe("Open Cassandra shell button", () => { + const openCassandraShellBtnLabel = "Open Cassandra shell"; const selectedNodeState = useSelectedNode.getState(); beforeAll(() => { diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index b39de9c05..c497d295d 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -94,9 +94,9 @@ export function createStaticCommandBarButtons( ) { notebookButtons.push(createDivider()); if (userContext.apiType === "Cassandra") { - notebookButtons.push(createOpenCassandraTerminalButton(container)); + notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Cassandra)); } else { - notebookButtons.push(createOpenMongoTerminalButton(container)); + notebookButtons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Mongo)); } } @@ -499,8 +499,25 @@ function createOpenTerminalButton(container: Explorer): CommandButtonComponentPr }; } -function createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps { - const label = "Open Mongo Shell"; +function createOpenTerminalButtonByKind( + container: Explorer, + terminalKind: ViewModels.TerminalKind +): CommandButtonComponentProps { + const terminalFriendlyName = (): string => { + switch (terminalKind) { + case ViewModels.TerminalKind.Cassandra: + return "Cassandra"; + case ViewModels.TerminalKind.Mongo: + return "Mongo"; + case ViewModels.TerminalKind.Postgres: + return "PSQL"; + case ViewModels.TerminalKind.VCoreMongo: + return "MongoDB (vcore)"; + default: + return ""; + } + }; + const label = `Open ${terminalFriendlyName()} shell`; const tooltip = "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."; const disableButton = @@ -510,7 +527,7 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon iconAlt: label, onCommandClick: () => { if (useNotebook.getState().isNotebookEnabled) { - container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); + container.openNotebookTerminal(terminalKind); } }, commandButtonLabel: label, @@ -521,51 +538,6 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon }; } -function createOpenCassandraTerminalButton(container: Explorer): CommandButtonComponentProps { - const label = "Open Cassandra Shell"; - const tooltip = - "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."; - const disableButton = - !useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled; - return { - iconSrc: HostedTerminalIcon, - iconAlt: label, - onCommandClick: () => { - if (useNotebook.getState().isNotebookEnabled) { - container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra); - } - }, - commandButtonLabel: label, - hasPopup: false, - disabled: disableButton, - ariaLabel: label, - tooltipText: !disableButton ? "" : tooltip, - }; -} - -function createOpenPsqlTerminalButton(container: Explorer): CommandButtonComponentProps { - const label = "Open PSQL Shell"; - const disableButton = - (!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled) || - useSelectedNode.getState().isQueryCopilotCollectionSelected(); - return { - iconSrc: HostedTerminalIcon, - iconAlt: label, - onCommandClick: () => { - if (useNotebook.getState().isNotebookEnabled) { - container.openNotebookTerminal(ViewModels.TerminalKind.Postgres); - } - }, - commandButtonLabel: label, - hasPopup: false, - disabled: disableButton, - ariaLabel: label, - tooltipText: !disableButton - ? "" - : "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.", - }; -} - function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps { const label = "Reset Workspace"; return { @@ -630,7 +602,13 @@ function createStaticCommandBarButtonsForResourceToken( } export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] { - const openPostgreShellBtn = createOpenPsqlTerminalButton(container); + const openPostgreShellBtn = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.Postgres); return [openPostgreShellBtn]; } + +export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] { + const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo); + + return [openVCoreMongoTerminalButton]; +} diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index 4386c4da4..734d1cd79 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -1,6 +1,6 @@ import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; -import { cloneDeep } from "lodash"; import { PhoenixClient } from "Phoenix/PhoenixClient"; +import { cloneDeep } from "lodash"; import create, { UseStore } from "zustand"; import { AuthType } from "../../AuthType"; import * as Constants from "../../Common/Constants"; @@ -10,13 +10,13 @@ import * as Logger from "../../Common/Logger"; import { configContext } from "../../ConfigContext"; import * as DataModels from "../../Contracts/DataModels"; import { ContainerConnectionInfo, ContainerInfo, PhoenixErrorType } from "../../Contracts/DataModels"; -import { useTabs } from "../../hooks/useTabs"; import { IPinnedRepo } from "../../Juno/JunoClient"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; +import { useTabs } from "../../hooks/useTabs"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; import NotebookManager from "./NotebookManager"; @@ -124,7 +124,7 @@ export const useNotebook: UseStore = create((set, get) => ({ } const firstWriteLocation = - userContext.apiType === "Postgres" + userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ? databaseAccount?.location : databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase(); const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`; @@ -316,8 +316,10 @@ export const useNotebook: UseStore = create((set, get) => ({ isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true; isPhoenixFeatures = isPublicInternetAllowed && - // phoenix needs to be enabled for Postgres accounts since the PSQL shell requires phoenix containers - (userContext.features.phoenixFeatures === true || userContext.apiType === "Postgres"); + // phoenix needs to be enabled for Postgres and VCoreMongo accounts since the PSQL and mongo shell requires phoenix containers + (userContext.features.phoenixFeatures === true || + userContext.apiType === "Postgres" || + userContext.apiType === "VCoreMongo"); } else { isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed; } diff --git a/src/Explorer/Quickstart/QuickstartFirewallNotification.tsx b/src/Explorer/Quickstart/QuickstartFirewallNotification.tsx index 3751cbbb5..ecafbcb96 100644 --- a/src/Explorer/Quickstart/QuickstartFirewallNotification.tsx +++ b/src/Explorer/Quickstart/QuickstartFirewallNotification.tsx @@ -2,20 +2,26 @@ import { Image, PrimaryButton, Stack, Text } from "@fluentui/react"; import { sendMessage } from "Common/MessageHandler"; import { MessageTypes } from "Contracts/ExplorerContracts"; import React from "react"; -import FirewallRuleScreenshot from "../../../images/firewallRule.png"; -export const QuickstartFirewallNotification: React.FC = (): JSX.Element => ( +export interface QuickstartFirewallNotificationProps { + shellName: string; + screenshot: string; + messageType: MessageTypes; +} + +export const QuickstartFirewallNotification: React.FC = ({ + shellName, + screenshot, + messageType, +}: QuickstartFirewallNotificationProps): JSX.Element => ( - To use the PostgreSQL shell, you need to add a firewall rule to allow access from all IP addresses + To use the {shellName} shell, you need to add a firewall rule to allow access from all IP addresses (0.0.0.0-255.255.255). - We strongly recommend removing this rule once you finish using the PostgreSQL shell. - - sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })} - > + We strongly recommend removing this rule once you finish using the {shellName} shell. + + sendMessage({ type: messageType })}> Add firewall rule diff --git a/src/Explorer/Quickstart/QuickstartGuide.tsx b/src/Explorer/Quickstart/QuickstartGuide.tsx index 48a7a9a79..b00fce86d 100644 --- a/src/Explorer/Quickstart/QuickstartGuide.tsx +++ b/src/Explorer/Quickstart/QuickstartGuide.tsx @@ -1,9 +1,7 @@ import { DefaultButton, - Icon, IconButton, Image, - IPivotItemProps, Pivot, PivotItem, PrimaryButton, @@ -21,18 +19,10 @@ import { queryCommand, queryCommandForDisplay, } from "Explorer/Quickstart/PostgreQuickstartCommands"; +import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities"; import { useTerminal } from "hooks/useTerminal"; import React, { useState } from "react"; import Youtube from "react-youtube"; -import Pivot1SelectedIcon from "../../../images/Pivot1_selected.svg"; -import Pivot2Icon from "../../../images/Pivot2.svg"; -import Pivot2SelectedIcon from "../../../images/Pivot2_selected.svg"; -import Pivot3Icon from "../../../images/Pivot3.svg"; -import Pivot3SelectedIcon from "../../../images/Pivot3_selected.svg"; -import Pivot4Icon from "../../../images/Pivot4.svg"; -import Pivot4SelectedIcon from "../../../images/Pivot4_selected.svg"; -import Pivot5Icon from "../../../images/Pivot5.svg"; -import Pivot5SelectedIcon from "../../../images/Pivot5_selected.svg"; import CompleteIcon from "../../../images/QuickstartComplete.svg"; import { ReactTabKind, useTabs } from "../../hooks/useTabs"; @@ -53,44 +43,6 @@ export const QuickstartGuide: React.FC = (): JSX.Element => { document.execCommand("copy"); }; - const getPivotHeaderIcon = (step: number): string => { - switch (step) { - case 0: - return Pivot1SelectedIcon; - case 1: - return step === currentStep ? Pivot2SelectedIcon : Pivot2Icon; - case 2: - return step === currentStep ? Pivot3SelectedIcon : Pivot3Icon; - case 3: - return step === currentStep ? Pivot4SelectedIcon : Pivot4Icon; - case 4: - return step === currentStep ? Pivot5SelectedIcon : Pivot5Icon; - default: - return ""; - } - }; - - const customPivotHeaderRenderer = ( - link: IPivotItemProps, - defaultRenderer: (link?: IPivotItemProps) => JSX.Element | null, - step: number - ): JSX.Element | null => { - if (!link || !defaultRenderer) { - return null; - } - - return ( - - {currentStep > step ? ( - - ) : ( - - )} - {defaultRenderer({ ...link, itemIcon: undefined })} - - ); - }; - return ( @@ -103,7 +55,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => { > customPivotHeaderRenderer(props, defaultRenderer, 0)} + onRenderItemLink={(props, defaultRenderer) => + customPivotHeaderRenderer(props, defaultRenderer, currentStep, 0) + } itemKey={GuideSteps[0]} onClick={() => { setCurrentStep(0); @@ -125,7 +79,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => { customPivotHeaderRenderer(props, defaultRenderer, 1)} + onRenderItemLink={(props, defaultRenderer) => + customPivotHeaderRenderer(props, defaultRenderer, currentStep, 1) + } itemKey={GuideSteps[1]} onClick={() => setCurrentStep(1)} > @@ -165,7 +121,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => { customPivotHeaderRenderer(props, defaultRenderer, 2)} + onRenderItemLink={(props, defaultRenderer) => + customPivotHeaderRenderer(props, defaultRenderer, currentStep, 2) + } itemKey={GuideSteps[2]} onClick={() => setCurrentStep(2)} > @@ -210,7 +168,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => { customPivotHeaderRenderer(props, defaultRenderer, 3)} + onRenderItemLink={(props, defaultRenderer) => + customPivotHeaderRenderer(props, defaultRenderer, currentStep, 3) + } itemKey={GuideSteps[3]} onClick={() => setCurrentStep(3)} > @@ -250,7 +210,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => { customPivotHeaderRenderer(props, defaultRenderer, 4)} + onRenderItemLink={(props, defaultRenderer) => + customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4) + } itemKey={GuideSteps[4]} onClick={() => setCurrentStep(4)} > diff --git a/src/Explorer/Quickstart/Shared/QuickstartRenderUtilities.tsx b/src/Explorer/Quickstart/Shared/QuickstartRenderUtilities.tsx new file mode 100644 index 000000000..192beb497 --- /dev/null +++ b/src/Explorer/Quickstart/Shared/QuickstartRenderUtilities.tsx @@ -0,0 +1,50 @@ +import { Icon, Image, IPivotItemProps, Stack } from "@fluentui/react"; +import React from "react"; +import Pivot1SelectedIcon from "../../../../images/Pivot1_selected.svg"; +import Pivot2Icon from "../../../../images/Pivot2.svg"; +import Pivot2SelectedIcon from "../../../../images/Pivot2_selected.svg"; +import Pivot3Icon from "../../../../images/Pivot3.svg"; +import Pivot3SelectedIcon from "../../../../images/Pivot3_selected.svg"; +import Pivot4Icon from "../../../../images/Pivot4.svg"; +import Pivot4SelectedIcon from "../../../../images/Pivot4_selected.svg"; +import Pivot5Icon from "../../../../images/Pivot5.svg"; +import Pivot5SelectedIcon from "../../../../images/Pivot5_selected.svg"; + +const getPivotHeaderIcon = (currentStep: number, newStep: number): string => { + switch (newStep) { + case 0: + return Pivot1SelectedIcon; + case 1: + return newStep === currentStep ? Pivot2SelectedIcon : Pivot2Icon; + case 2: + return newStep === currentStep ? Pivot3SelectedIcon : Pivot3Icon; + case 3: + return newStep === currentStep ? Pivot4SelectedIcon : Pivot4Icon; + case 4: + return newStep === currentStep ? Pivot5SelectedIcon : Pivot5Icon; + default: + return ""; + } +}; + +export const customPivotHeaderRenderer = ( + link: IPivotItemProps, + defaultRenderer: (link?: IPivotItemProps) => JSX.Element | null, + currentStep: number, + newStep: number +): JSX.Element | null => { + if (!link || !defaultRenderer) { + return null; + } + + return ( + + {currentStep > newStep ? ( + + ) : ( + + )} + {defaultRenderer({ ...link, itemIcon: undefined })} + + ); +}; diff --git a/src/Explorer/Quickstart/VCoreMongoQuickstartCommands.ts b/src/Explorer/Quickstart/VCoreMongoQuickstartCommands.ts new file mode 100644 index 000000000..b3d327109 --- /dev/null +++ b/src/Explorer/Quickstart/VCoreMongoQuickstartCommands.ts @@ -0,0 +1,34 @@ +export const newDbAndCollectionCommand = `use quickstartDB +db.createCollection('sampleCollection')`; + +export const newDbAndCollectionCommandForDisplay = `use quickstartDB // Create new database named 'quickstartDB' or switch to it if it already exists + +db.createCollection('sampleCollection') // Create new collection named 'sampleCollection'`; + +export const loadDataCommand = `db.sampleCollection.insertMany([ + {title: "The Great Gatsby", author: "F. Scott Fitzgerald", pages: 180}, + {title: "To Kill a Mockingbird", author: "Harper Lee", pages: 324}, + {title: "1984", author: "George Orwell", pages: 328}, + {title: "The Catcher in the Rye", author: "J.D. Salinger", pages: 277}, + {title: "Moby-Dick", author: "Herman Melville", pages: 720}, + {title: "Pride and Prejudice", author: "Jane Austen", pages: 279}, + {title: "The Hobbit", author: "J.R.R. Tolkien", pages: 310}, + {title: "War and Peace", author: "Leo Tolstoy", pages: 1392}, + {title: "The Odyssey", author: "Homer", pages: 374}, + {title: "Ulysses", author: "James Joyce", pages: 730} + ])`; + +export const queriesCommand = `db.sampleCollection.find({author: "George Orwell"}) + +db.sampleCollection.find({pages: {$gt: 500}}) + +db.sampleCollection.find({}).sort({pages: 1})`; + +export const queriesCommandForDisplay = `// Query to find all books written by "George Orwell" +db.sampleCollection.find({author: "George Orwell"}) + +// Query to find all books with more than 500 pages +db.sampleCollection.find({pages: {$gt: 500}}) + +// Query to find all books and sort them by the number of pages in ascending order +db.sampleCollection.find({}).sort({pages: 1})`; diff --git a/src/Explorer/Quickstart/VCoreMongoQuickstartGuide.tsx b/src/Explorer/Quickstart/VCoreMongoQuickstartGuide.tsx new file mode 100644 index 000000000..219640831 --- /dev/null +++ b/src/Explorer/Quickstart/VCoreMongoQuickstartGuide.tsx @@ -0,0 +1,310 @@ +import { + DefaultButton, + IconButton, + Link, + Pivot, + PivotItem, + PrimaryButton, + Stack, + Text, + TextField, +} from "@fluentui/react"; +import { sendMessage } from "Common/MessageHandler"; +import { MessageTypes } from "Contracts/ExplorerContracts"; +import { customPivotHeaderRenderer } from "Explorer/Quickstart/Shared/QuickstartRenderUtilities"; +import { + loadDataCommand, + newDbAndCollectionCommand, + newDbAndCollectionCommandForDisplay, + queriesCommand, + queriesCommandForDisplay, +} from "Explorer/Quickstart/VCoreMongoQuickstartCommands"; +import { useTerminal } from "hooks/useTerminal"; +import React, { useState } from "react"; +import { ReactTabKind, useTabs } from "../../hooks/useTabs"; + +enum GuideSteps { + Login, + NewTable, + DistributeTable, + LoadData, + Query, +} + +export const VcoreMongoQuickstartGuide: React.FC = (): JSX.Element => { + const [currentStep, setCurrentStep] = useState(0); + + const onCopyBtnClicked = (selector: string): void => { + const textfield: HTMLInputElement = document.querySelector(selector); + textfield.select(); + document.execCommand("copy"); + }; + + return ( + + + Quick start guide + Getting started in Cosmos DB Mongo DB (vCore) + {currentStep < 5 && ( + setCurrentStep(Object.values(GuideSteps).indexOf(item.props.itemKey))} + > + + customPivotHeaderRenderer(props, defaultRenderer, currentStep, 0) + } + itemKey={GuideSteps[0]} + onClick={() => { + setCurrentStep(0); + }} + > + + + A hosted mongosh (mongo shell) is provided for this quick start. You are automatically logged in to + mongosh, allowing you to interact with your database directly. +
+
+ When not in the quick start guide, connecting to Azure Cosmos DB for MongoDB vCore is straightforward + using your connection string. +
+
+ sendMessage({ type: MessageTypes.OpenVCoreMongoConnectionStringsBlade })} + > + View connection string + +
+
+ This string contains placeholders for <user> and <password>. Replace them with your chosen + username and password to establish a secure connection to your cluster. Depending on your environment, + you may need to adjust firewall rules or configure private endpoints in the ‘Networking’ + tab of your database settings, or modify your own network's firewall settings, to successfully + connect. +
+
+
+ + customPivotHeaderRenderer(props, defaultRenderer, currentStep, 1) + } + itemKey={GuideSteps[1]} + onClick={() => setCurrentStep(1)} + > + + + In MongoDB, data is stored in collections, which are analogous to tables in relational databases. + Collections contain documents, each of which consists of field and value pairs. The fields in + documents are similar to the columns in a relational database table. One key advantage of MongoDB is + that these documents within a collection can have different fields. +
+ You're now going to create a new database and a collection within that database using the Mongo + shell. In MongoDB, creating a database or a collection is implicit. This means that databases and + collections are created when you first reference them in a command, so no explicit creation command is + necessary. +
+ useTerminal.getState().sendMessage(newDbAndCollectionCommand)} + > + Create new database and collection + + + + onCopyBtnClicked("#newDbAndCollectionCommand")} + /> + +
+
+ + customPivotHeaderRenderer(props, defaultRenderer, currentStep, 2) + } + itemKey={GuideSteps[2]} + onClick={() => setCurrentStep(2)} + > + + + Now that you've created your database and collection, it's time to populate your collection + with data. In MongoDB, data is stored as documents, which are structured as field and value pairs. +
+
+ Let's populate your sampleCollection with data. We'll add 10 documents representing books, + each with a title, author, and number of pages, to your sampleCollection in the quickstartDB database. +
+ useTerminal.getState().sendMessage(loadDataCommand)} + > + Create distributed table + + + + onCopyBtnClicked("#loadDataCommand")} + /> + +
+
+ + customPivotHeaderRenderer(props, defaultRenderer, currentStep, 3) + } + itemKey={GuideSteps[3]} + onClick={() => setCurrentStep(3)} + > + + + Once you’ve inserted data into your sampleCollection, you can retrieve it using queries. MongoDB + queries can be as simple or as complex as you need them to be, allowing you to filter, sort, and limit + results. + + useTerminal.getState().sendMessage(queriesCommand)} + > + Load data + + + + onCopyBtnClicked("#queriesCommand")} + /> + + + + + customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4) + } + itemKey={GuideSteps[4]} + onClick={() => setCurrentStep(4)} + > + + + Cosmos DB for MongoDB vCore seamlessly integrates with Azure services. These integrations enable + Cosmos DB for MongoDB and its partner products to directly interoperate, ensuring a smooth and unified + experience, that just works. + + + + + First party integrations +
+
+ Azure Monitor +
+ Azure monitor provides comprehensive monitoring and diagnostics for Cosmos DB for Mongo DB. Learn + more +
+
+ Azure Networking +
+ Azure Networking seamlessly integrates with Azure Cosmos DB for Mongo DB for fast and secure data + access. Learn more +
+
+ PowerShell/CLI/ARM +
+ PowerShell/CLI/ARM seamlessly integrates with Azure Cosmos DB for Mongo DB for efficient + management and automation. Learn more +
+
+ + + Application platforms integrations +
+
+ Vercel +
+ Vercel is a cloud platform for hosting static front ends and serverless functions, with instant + deployments, automated scaling, and Next.js integration. Learn more +
+
+
+
+
+
+ )} +
+ + setCurrentStep(currentStep - 1)}> + Previous + + {currentStep < 4 && ( + setCurrentStep(currentStep + 1)} style={{ marginLeft: 8 }}> + Next + + )} + {currentStep === 4 && ( + useTabs.getState().closeReactTab(ReactTabKind.Quickstart)} + style={{ marginLeft: 8 }} + > + Done + + )} + +
+ ); +}; diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index 0e599c65a..6243a5daf 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -260,30 +260,33 @@ export class SplashScreen extends React.Component { }; public render(): JSX.Element { + let title: string; + let subtitle: string; + + switch (userContext.apiType) { + case "Postgres": + title = "Welcome to Azure Cosmos DB for PostgreSQL"; + subtitle = "Get started with our sample datasets, documentation, and additional tools."; + break; + case "VCoreMongo": + title = "Welcome to Azure Cosmos DB for MongoDB (vCore)"; + subtitle = "Get started with our sample datasets, documentation, and additional tools."; + break; + default: + title = "Welcome to Azure Cosmos DB"; + subtitle = "Globally distributed, multi-model database service for any scale"; + } + return (
-

- {userContext.apiType === "Postgres" - ? "Welcome to Azure Cosmos DB for PostgreSQL" - : "Welcome to Azure Cosmos DB"} +

+ {title}

-
- {userContext.apiType === "Postgres" - ? "Get started with our sample datasets, documentation, and additional tools." - : "Globally distributed, multi-model database service for any scale"} -
+
{subtitle}
{this.getSplashScreenButtons()} {useCarousel.getState().showCoachMark && ( { )} - {userContext.apiType === "Postgres" ? ( + {userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ? ( { if ( userContext.apiType === "SQL" || userContext.apiType === "Mongo" || - (userContext.apiType === "Postgres" && !userContext.isReplica) + (userContext.apiType === "Postgres" && !userContext.isReplica) || + userContext.apiType === "VCoreMongo" ) { const launchQuickstartBtn = { id: "quickstartDescription", @@ -388,9 +392,11 @@ export class SplashScreen extends React.Component { title: "Launch quick start", description: "Launch a quick start tutorial to get started with sample data", onClick: () => { - userContext.apiType === "Postgres" - ? useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart) - : this.container.onNewCollectionClicked({ isQuickstart: true }); + if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { + useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart); + } else { + this.container.onNewCollectionClicked({ isQuickstart: true }); + } traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType }); }, }; @@ -405,39 +411,65 @@ export class SplashScreen extends React.Component { heroes.push(newNotebookBtn); } + heroes.push(this.getShellCard()); + heroes.push(this.getThirdCard()); + return heroes; + } + + private getShellCard() { if (userContext.apiType === "Postgres") { - const postgreShellBtn = { + return { iconSrc: PowerShellIcon, title: "PostgreSQL Shell", description: "Create table and interact with data using PostgreSQL’s shell interface", onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres), }; - heroes.push(postgreShellBtn); - } else { - const newContainerBtn = { - iconSrc: ContainersIcon, - title: `New ${getCollectionName()}`, - description: "Create a new container for storage and throughput", - onClick: () => { - this.container.onNewCollectionClicked(); - traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType }); - }, - }; - heroes.push(newContainerBtn); } - const connectBtn = { - iconSrc: ConnectIcon, - title: userContext.apiType === "Postgres" ? "Connect with pgAdmin" : "Connect", - description: - userContext.apiType === "Postgres" - ? "Prefer pgAdmin? Find your connection strings here" - : "Prefer using your own choice of tooling? Find the connection string you need to connect", - onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect), - }; - heroes.push(connectBtn); + if (userContext.apiType === "VCoreMongo") { + return { + iconSrc: PowerShellIcon, + title: "Mongo Shell", + description: "Create a collection and interact with data using MongoDB's shell interface", + onClick: () => this.container.openNotebookTerminal(TerminalKind.VCoreMongo), + }; + } - return heroes; + return { + iconSrc: ContainersIcon, + title: `New ${getCollectionName()}`, + description: "Create a new container for storage and throughput", + onClick: () => { + this.container.onNewCollectionClicked(); + traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType }); + }, + }; + } + + private getThirdCard() { + let icon = ConnectIcon; + let title = "Connect"; + let description = "Prefer using your own choice of tooling? Find the connection string you need to connect"; + let onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect); + + if (userContext.apiType === "Postgres") { + title = "Connect with pgAdmin"; + description = "Prefer pgAdmin? Find your connection strings here"; + } + + if (userContext.apiType === "VCoreMongo") { + icon = ContainersIcon; + title = "Connect with Studio 3T"; + description = "Prefer Studio 3T? Find your connection strings here"; + onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect); + } + + return { + iconSrc: icon, + title: title, + description: description, + onClick: onClick, + }; } private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) { @@ -587,6 +619,8 @@ export class SplashScreen extends React.Component { }, ]; break; + default: + break; } return ( @@ -724,6 +758,8 @@ export class SplashScreen extends React.Component { cdbLiveTv, ]; break; + default: + break; } return ( @@ -749,24 +785,46 @@ export class SplashScreen extends React.Component { ); } + private postgresNextStepItems: { link: string; title: string; description: string }[] = [ + { + link: "https://go.microsoft.com/fwlink/?linkid=2208312", + title: "Data Modeling", + description: "", + }, + { + link: " https://go.microsoft.com/fwlink/?linkid=2206941 ", + title: "How to choose a Distribution Column", + description: "", + }, + { + link: "https://go.microsoft.com/fwlink/?linkid=2207425", + title: "Build Apps with Python/Java/Django", + description: "", + }, + ]; + + private vcoreMongoNextStepItems: { link: string; title: string; description: string }[] = [ + { + link: + "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/how-to-migrate-native-tools?tabs=export-import", + title: "Migrate Data", + description: "", + }, + { + link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search-ai", + title: "Build AI apps with Vector Search", + description: "", + }, + { + link: + "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/tutorial-nodejs-web-app?tabs=github-codespaces", + title: "Build Apps with Nodejs", + description: "", + }, + ]; + private getNextStepItems(): JSX.Element { - const items: { link: string; title: string; description: string }[] = [ - { - link: "https://go.microsoft.com/fwlink/?linkid=2208312", - title: "Data Modeling", - description: "", - }, - { - link: " https://go.microsoft.com/fwlink/?linkid=2206941 ", - title: "How to choose a Distribution Column", - description: "", - }, - { - link: "https://go.microsoft.com/fwlink/?linkid=2207425", - title: "Build Apps with Python/Java/Django", - description: "", - }, - ]; + const items = userContext.apiType === "Postgres" ? this.postgresNextStepItems : this.vcoreMongoNextStepItems; return ( @@ -785,24 +843,44 @@ export class SplashScreen extends React.Component { ); } + private postgresLearnMoreItems: { link: string; title: string; description: string }[] = [ + { + link: "https://go.microsoft.com/fwlink/?linkid=2207226", + title: "Performance Tuning", + description: "", + }, + { + link: "https://go.microsoft.com/fwlink/?linkid=2208037", + title: "Useful Diagnostic Queries", + description: "", + }, + { + link: "https://go.microsoft.com/fwlink/?linkid=2205270", + title: "Distributed SQL Reference", + description: "", + }, + ]; + + private vcoreMongoLearnMoreItems: { link: string; title: string; description: string }[] = [ + { + link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search", + title: "Vector Search", + description: "", + }, + { + link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/how-to-create-text-index", + title: "Text Indexing", + description: "", + }, + { + link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/troubleshoot-common-issues", + title: "Troubleshoot common issues", + description: "", + }, + ]; + private getTipsAndLearnMoreItems(): JSX.Element { - const items: { link: string; title: string; description: string }[] = [ - { - link: "https://go.microsoft.com/fwlink/?linkid=2207226", - title: "Performance Tuning", - description: "", - }, - { - link: "https://go.microsoft.com/fwlink/?linkid=2208037", - title: "Useful Diagnostic Queries", - description: "", - }, - { - link: "https://go.microsoft.com/fwlink/?linkid=2205270", - title: "Distributed SQL Reference", - description: "", - }, - ]; + const items = userContext.apiType === "Postgres" ? this.postgresLearnMoreItems : this.vcoreMongoLearnMoreItems; return ( diff --git a/src/Explorer/Tabs/QuickstartTab.tsx b/src/Explorer/Tabs/QuickstartTab.tsx index 5de5d15f9..39177c215 100644 --- a/src/Explorer/Tabs/QuickstartTab.tsx +++ b/src/Explorer/Tabs/QuickstartTab.tsx @@ -1,16 +1,16 @@ import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react"; import { PoolIdType } from "Common/Constants"; -import { configContext } from "ConfigContext"; -import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels"; +import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels"; +import { MessageTypes } from "Contracts/ExplorerContracts"; import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent"; import Explorer from "Explorer/Explorer"; import { useNotebook } from "Explorer/Notebook/useNotebook"; import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification"; import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide"; -import { ReactTabKind, useTabs } from "hooks/useTabs"; -import React, { useEffect, useState } from "react"; +import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules"; import { userContext } from "UserContext"; -import { armRequest } from "Utils/arm/request"; +import React, { useEffect, useState } from "react"; +import FirewallRuleScreenshot from "../../../images/firewallRule.png"; interface QuickstartTabProps { explorer: Explorer; @@ -26,29 +26,12 @@ export const QuickstartTab: React.FC = ({ explorer }: Quicks forwardingId: notebookServerInfo.forwardingId, }); - const checkFirewallRules = async (): Promise => { - const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const response: any = await armRequest({ - host: configContext.ARM_ENDPOINT, - path: firewallRulesUri, - method: "GET", - apiVersion: "2022-11-08", - }); - const firewallRules: PostgresFirewallRule[] = response?.data?.value || response?.value || []; - const isEnabled = firewallRules.some( - (rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255" - ); - setIsAllPublicIPAddressEnabled(isEnabled); - - // If the firewall rule is not added, check every 30 seconds to see if the user has added the rule - if (!isEnabled && useTabs.getState().activeReactTab === ReactTabKind.Quickstart) { - setTimeout(checkFirewallRules, 30000); - } - }; - useEffect(() => { - checkFirewallRules(); + checkFirewallRules( + "2022-11-08", + (rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255", + setIsAllPublicIPAddressEnabled + ); }); useEffect(() => { @@ -61,7 +44,13 @@ export const QuickstartTab: React.FC = ({ explorer }: Quicks - {!isAllPublicIPAddressEnabled && } + {!isAllPublicIPAddressEnabled && ( + + )} {isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && ( unknown, + isAllPublicIPAddressesEnabled?: ko.Observable | React.Dispatch>, + setMessageFunc?: (message: string) => void, + message?: string +): Promise { + const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const response: any = await armRequest({ + host: configContext.ARM_ENDPOINT, + path: firewallRulesUri, + method: "GET", + apiVersion: apiVersion, + }); + const firewallRules: DataModels.FirewallRule[] = response?.data?.value || response?.value || []; + const isEnabled = firewallRules.some(firewallRulesPredicate); + + if (isAllPublicIPAddressesEnabled) { + isAllPublicIPAddressesEnabled(isEnabled); + } + + if (setMessageFunc) { + if (!isEnabled) { + setMessageFunc(message); + } else { + setMessageFunc(undefined); + } + } + + // If the firewall rule is not added, check every 30 seconds to see if the user has added the rule + if (!isEnabled) { + setTimeout( + () => + checkFirewallRules(apiVersion, firewallRulesPredicate, isAllPublicIPAddressesEnabled, setMessageFunc, message), + 30000 + ); + } +} diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index fc80a4f89..787a3011d 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -8,6 +8,8 @@ import { SplashScreen } from "Explorer/SplashScreen/SplashScreen"; import { ConnectTab } from "Explorer/Tabs/ConnectTab"; import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab"; import { QuickstartTab } from "Explorer/Tabs/QuickstartTab"; +import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; +import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; import { userContext } from "UserContext"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useTeachingBubble } from "hooks/useTeachingBubble"; @@ -35,7 +37,16 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => { sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}> + + sendMessage({ + type: + userContext.apiType === "VCoreMongo" + ? MessageTypes.OpenVCoreMongoNetworkingBlade + : MessageTypes.OpenPostgresNetworkingBlade, + }) + } + > Change network settings } @@ -252,11 +263,21 @@ const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => { const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => { switch (activeReactTab) { case ReactTabKind.Connect: - return userContext.apiType === "Postgres" ? : ; + return userContext.apiType === "VCoreMongo" ? ( + + ) : userContext.apiType === "Postgres" ? ( + + ) : ( + + ); case ReactTabKind.Home: return ; case ReactTabKind.Quickstart: - return ; + return userContext.apiType === "VCoreMongo" ? ( + + ) : ( + + ); case ReactTabKind.QueryCopilot: return ; default: diff --git a/src/Explorer/Tabs/TerminalTab.tsx b/src/Explorer/Tabs/TerminalTab.tsx index a65d1e9da..cd335d3b5 100644 --- a/src/Explorer/Tabs/TerminalTab.tsx +++ b/src/Explorer/Tabs/TerminalTab.tsx @@ -1,9 +1,10 @@ import { Spinner, SpinnerSize } from "@fluentui/react"; -import { configContext } from "ConfigContext"; +import { MessageTypes } from "Contracts/ExplorerContracts"; import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification"; +import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules"; import * as ko from "knockout"; import * as React from "react"; -import { armRequest } from "Utils/arm/request"; +import FirewallRuleScreenshot from "../../../images/firewallRule.png"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; @@ -18,6 +19,7 @@ export interface TerminalTabOptions extends ViewModels.TabOptions { account: DataModels.DatabaseAccount; container: Explorer; kind: ViewModels.TerminalKind; + username?: string; } /** @@ -30,12 +32,19 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo, private getDatabaseAccount: () => DataModels.DatabaseAccount, private getTabId: () => string, + private getUsername: () => string, private isAllPublicIPAddressesEnabled: ko.Observable ) {} public renderComponent(): JSX.Element { if (!this.isAllPublicIPAddressesEnabled()) { - return ; + return ( + + ); } return this.parameters() ? ( @@ -43,6 +52,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { notebookServerInfo={this.getNotebookServerInfo()} databaseAccount={this.getDatabaseAccount()} tabId={this.getTabId()} + username={this.getUsername()} /> ) : ( @@ -64,6 +74,7 @@ export default class TerminalTab extends TabsBase { () => this.getNotebookServerInfo(options), () => userContext?.databaseAccount, () => this.tabId, + () => this.getUsername(), this.isAllPublicIPAddressesEnabled ); this.notebookTerminalComponentAdapter.parameters = ko.computed(() => { @@ -79,7 +90,21 @@ export default class TerminalTab extends TabsBase { }); if (options.kind === ViewModels.TerminalKind.Postgres) { - this.checkPostgresFirewallRules(); + checkFirewallRules( + "2022-11-08", + (rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255", + this.isAllPublicIPAddressesEnabled + ); + } + + if (options.kind === ViewModels.TerminalKind.VCoreMongo) { + checkFirewallRules( + "2023-03-01-preview", + (rule) => + rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") || + (rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"), + this.isAllPublicIPAddressesEnabled + ); } } @@ -115,6 +140,10 @@ export default class TerminalTab extends TabsBase { endpointSuffix = "postgresql"; break; + case ViewModels.TerminalKind.VCoreMongo: + endpointSuffix = "mongovcore"; + break; + default: throw new Error(`Terminal kind: ${options.kind} not supported`); } @@ -127,24 +156,11 @@ export default class TerminalTab extends TabsBase { }; } - private async checkPostgresFirewallRules(): Promise { - const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const response: any = await armRequest({ - host: configContext.ARM_ENDPOINT, - path: firewallRulesUri, - method: "GET", - apiVersion: "2022-11-08", - }); - const firewallRules: DataModels.PostgresFirewallRule[] = response?.data?.value || response?.value || []; - const isEnabled = firewallRules.some( - (rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255" - ); - this.isAllPublicIPAddressesEnabled(isEnabled); - - // If the firewall rule is not added, check every 30 seconds to see if the user has added the rule - if (!isEnabled) { - setTimeout(() => this.checkPostgresFirewallRules(), 30000); + private getUsername(): string { + if (userContext.apiType !== "VCoreMongo" || !userContext?.vcoreMongoConnectionParams?.adminLogin) { + return undefined; } + + return userContext.vcoreMongoConnectionParams.adminLogin; } } diff --git a/src/Explorer/Tabs/VCoreMongoConnectTab.tsx b/src/Explorer/Tabs/VCoreMongoConnectTab.tsx new file mode 100644 index 000000000..0e91d2f3d --- /dev/null +++ b/src/Explorer/Tabs/VCoreMongoConnectTab.tsx @@ -0,0 +1,37 @@ +import { IconButton, ITextFieldStyles, Stack, TextField } from "@fluentui/react"; +import React from "react"; +import { userContext } from "UserContext"; + +export const VcoreMongoConnectTab: React.FC = (): JSX.Element => { + const { adminLogin, connectionString } = userContext.vcoreMongoConnectionParams; + const displayConnectionString = connectionString.replace("", adminLogin); + + const textfieldStyles: Partial = { + root: { width: "100%" }, + field: { backgroundColor: "rgb(230, 230, 230)" }, + fieldGroup: { borderColor: "rgb(138, 136, 134)" }, + subComponentStyles: { label: { fontWeight: 400 } }, + description: { fontWeight: 400 }, + }; + + const onCopyBtnClicked = (selector: string): void => { + const textfield: HTMLInputElement = document.querySelector(selector); + textfield.select(); + document.execCommand("copy"); + }; + + return ( +
+ + + onCopyBtnClicked("#mongoSrvConnectionURL")} /> + +
+ ); +}; diff --git a/src/Explorer/Tabs/VCoreMongoQuickstartTab.tsx b/src/Explorer/Tabs/VCoreMongoQuickstartTab.tsx new file mode 100644 index 000000000..828e7c3b4 --- /dev/null +++ b/src/Explorer/Tabs/VCoreMongoQuickstartTab.tsx @@ -0,0 +1,80 @@ +import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react"; +import { PoolIdType } from "Common/Constants"; +import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels"; +import { MessageTypes } from "Contracts/ExplorerContracts"; +import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent"; +import Explorer from "Explorer/Explorer"; +import { useNotebook } from "Explorer/Notebook/useNotebook"; +import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification"; +import { VcoreMongoQuickstartGuide } from "Explorer/Quickstart/VCoreMongoQuickstartGuide"; +import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules"; +import { userContext } from "UserContext"; +import React, { useEffect, useState } from "react"; +import FirewallRuleScreenshot from "../../../images/vcoreMongoFirewallRule.png"; + +interface VCoreMongoQuickstartTabProps { + explorer: Explorer; +} + +export const VcoreMongoQuickstartTab: React.FC = ({ + explorer, +}: VCoreMongoQuickstartTabProps): JSX.Element => { + const notebookServerInfo = useNotebook((state) => state.notebookServerInfo); + const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState(true); + + const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({ + authToken: notebookServerInfo.authToken, + notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/mongovcore`, + forwardingId: notebookServerInfo.forwardingId, + }); + + useEffect(() => { + checkFirewallRules( + "2023-03-01-preview", + (rule) => + rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") || + (rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"), + setIsAllPublicIPAddressEnabled + ); + }); + + useEffect(() => { + explorer.allocateContainer(PoolIdType.DefaultPoolId); + }, []); + + return ( + + + + + + {!isAllPublicIPAddressEnabled && ( + + )} + {isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && ( + + )} + {isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && ( + + + Connecting to the Mongo shell. + + + If the cluster was just created, this could take up to a minute. + + + + )} + + + ); +}; diff --git a/src/Main.tsx b/src/Main.tsx index 6ea26ba9f..3982e866b 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -91,7 +91,7 @@ const App: React.FunctionComponent = () => { {/* Collections Tree and Tabs - Begin */}
{/* Collections Tree - Start */} - {userContext.apiType !== "Postgres" && ( + {userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
{/* Collections Tree Expanded - Start */} diff --git a/src/Terminal/JupyterLabAppFactory.ts b/src/Terminal/JupyterLabAppFactory.ts index a07bdeb76..c2e48ba16 100644 --- a/src/Terminal/JupyterLabAppFactory.ts +++ b/src/Terminal/JupyterLabAppFactory.ts @@ -28,6 +28,10 @@ export class JupyterLabAppFactory { this.isShellStarted = content?.includes("citus=>"); } + private isVCoreMongoShellStarted(content: string | undefined) { + this.isShellStarted = content?.includes("Enter password"); + } + constructor(closeTab: () => void) { this.onShellExited = closeTab; this.isShellStarted = false; @@ -43,6 +47,9 @@ export class JupyterLabAppFactory { case "Postgres": this.checkShellStarted = this.isPostgresShellStarted; break; + case "VCoreMongo": + this.checkShellStarted = this.isVCoreMongoShellStarted; + break; } } diff --git a/src/Terminal/TerminalProps.ts b/src/Terminal/TerminalProps.ts index 5122a6cb7..e4445c970 100644 --- a/src/Terminal/TerminalProps.ts +++ b/src/Terminal/TerminalProps.ts @@ -11,4 +11,5 @@ export interface TerminalProps { apiType: ApiType; subscriptionId: string; tabId: string; + username?: string; } diff --git a/src/Terminal/index.ts b/src/Terminal/index.ts index 0fe7b77dd..e269ecbfc 100644 --- a/src/Terminal/index.ts +++ b/src/Terminal/index.ts @@ -7,17 +7,24 @@ import { HttpHeaders } from "../Common/Constants"; import { Action } from "../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import { updateUserContext } from "../UserContext"; -import "./index.css"; import { JupyterLabAppFactory } from "./JupyterLabAppFactory"; import { TerminalProps } from "./TerminalProps"; +import "./index.css"; const createServerSettings = (props: TerminalProps): ServerConnection.ISettings => { let body: BodyInit | undefined; let headers: HeadersInit | undefined; if (props.terminalEndpoint) { - body = JSON.stringify({ + let bodyObj: { endpoint: string; username?: string } = { endpoint: props.terminalEndpoint, - }); + }; + if (props.username) { + bodyObj = { + ...bodyObj, + username: props.username, + }; + } + body = JSON.stringify(bodyObj); headers = { [HttpHeaders.contentType]: "application/json", }; diff --git a/src/UserContext.ts b/src/UserContext.ts index 3f7eba765..2cdddf6b8 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -1,12 +1,12 @@ -import { useCarousel } from "hooks/useCarousel"; -import { usePostgres } from "hooks/usePostgres"; import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; +import { useCarousel } from "hooks/useCarousel"; +import { usePostgres } from "hooks/usePostgres"; import { AuthType } from "./AuthType"; import { DatabaseAccount } from "./Contracts/DataModels"; import { SubscriptionType } from "./Contracts/SubscriptionType"; -import { extractFeatures, Features } from "./Platform/Hosted/extractFeatures"; +import { Features, extractFeatures } from "./Platform/Hosted/extractFeatures"; import { CollectionCreation, CollectionCreationDefaults } from "./Shared/Constants"; interface ThroughputDefaults { @@ -41,6 +41,11 @@ export interface PostgresConnectionStrParams { isFreeTier: boolean; } +export interface VCoreMongoConnectionParams { + adminLogin: string; + connectionString: string; +} + interface UserContext { readonly authType?: AuthType; readonly masterKey?: string; @@ -71,9 +76,10 @@ interface UserContext { readonly isReplica?: boolean; collectionCreationDefaults: CollectionCreationDefaults; sampleDataConnectionInfo?: ParsedResourceTokenConnectionString; + readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams; } -export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres"; +export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo"; export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod" | "dev"; const ONE_WEEK_IN_MS = 604800000; @@ -156,6 +162,9 @@ function apiType(account: DatabaseAccount | undefined): ApiType { if (account.kind === "Postgres") { return "Postgres"; } + if (account.kind === "VCoreMongo") { + return "VCoreMongo"; + } return "SQL"; } diff --git a/src/Utils/APITypeUtils.ts b/src/Utils/APITypeUtils.ts index dda1ecf24..b25ca1c29 100644 --- a/src/Utils/APITypeUtils.ts +++ b/src/Utils/APITypeUtils.ts @@ -2,7 +2,6 @@ import { userContext } from "../UserContext"; export const getCollectionName = (isPlural?: boolean): string => { let collectionName: string; - let unknownApiType: never; switch (userContext.apiType) { case "SQL": collectionName = "Container"; @@ -20,8 +19,7 @@ export const getCollectionName = (isPlural?: boolean): string => { case "Postgres": return ""; default: - unknownApiType = userContext.apiType; - throw new Error(`Unknown API type: ${unknownApiType}`); + throw new Error(`Unknown API type: ${userContext.apiType}`); } if (isPlural) { @@ -72,6 +70,8 @@ export const getApiShortDisplayName = (): string => { return "NoSQL API"; case "Tables": return "Table API"; + case "VCoreMongo": + return "MongoDB (vCore) API"; } }; diff --git a/src/Utils/NetworkUtility.ts b/src/Utils/NetworkUtility.ts index 82761198c..0037fec30 100644 --- a/src/Utils/NetworkUtility.ts +++ b/src/Utils/NetworkUtility.ts @@ -1,3 +1,4 @@ +import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules"; import { userContext } from "UserContext"; const PortalIPs: { [key: string]: string[] } = { @@ -10,40 +11,58 @@ const PortalIPs: { [key: string]: string[] } = { usnat: ["7.28.202.68"], }; -export const getNetworkSettingsWarningMessage = (): string => { +export const getNetworkSettingsWarningMessage = async ( + setStateFunc: (warningMessage: string) => void +): Promise => { const accountProperties = userContext.databaseAccount?.properties; + const accessMessage = + "The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure Portal to proceed."; + const publicAccessMessage = + "The Network settings for this account are preventing access from Data Explorer. Please enable public access to proceed."; - if (!accountProperties) { - return ""; - } + if (userContext.apiType === "Postgres") { + checkFirewallRules( + "2022-11-08", + (rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255", + undefined, + setStateFunc, + accessMessage + ); + } else if (userContext.apiType === "VCoreMongo") { + checkFirewallRules( + "2023-03-01-preview", + (rule) => + rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") || + (rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"), + undefined, + setStateFunc, + accessMessage + ); + } else if (accountProperties) { + // public network access is disabled + if ( + accountProperties.publicNetworkAccess !== "Enabled" && + accountProperties.publicNetworkAccess !== "SecuredByPerimeter" + ) { + setStateFunc(publicAccessMessage); + } - // public network access is disabled - if ( - accountProperties.publicNetworkAccess !== "Enabled" && - accountProperties.publicNetworkAccess !== "SecuredByPerimeter" - ) { - return "The Network settings for this account are preventing access from Data Explorer. Please enable public access to proceed."; - } + const ipRules = accountProperties.ipRules; + // public network access is NOT set to "All networks" + if (ipRules.length > 0) { + if (userContext.apiType === "Cassandra" || userContext.apiType === "Mongo") { + const portalIPs = PortalIPs[userContext.portalEnv]; + let numberOfMatches = 0; + ipRules.forEach((ipRule) => { + if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) { + numberOfMatches++; + } + }); - const ipRules = accountProperties.ipRules; - // public network access is set to "All networks" - if (ipRules.length === 0) { - return ""; - } - - if (userContext.apiType === "Cassandra" || userContext.apiType === "Mongo") { - const portalIPs = PortalIPs[userContext.portalEnv]; - let numberOfMatches = 0; - ipRules.forEach((ipRule) => { - if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) { - numberOfMatches++; + if (numberOfMatches !== portalIPs.length) { + setStateFunc(accessMessage); + } } - }); - - if (numberOfMatches !== portalIPs.length) { - return "The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure Portal to proceed."; } } - - return ""; }; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 8e18c5732..4492fe8e9 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -105,7 +105,10 @@ async function configureHosted(): Promise { } if (event.data?.type === MessageTypes.CloseTab) { - if (event.data?.data?.tabId === "QuickstartPSQLShell") { + if ( + event.data?.data?.tabId === "QuickstartPSQLShell" || + event.data?.data?.tabId === "QuickstartVcoreMongoShell" + ) { useTabs.getState().closeReactTab(ReactTabKind.Quickstart); } else { useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId); @@ -303,7 +306,10 @@ async function configurePortal(): Promise { } else if (shouldForwardMessage(message, event.origin)) { sendMessage(message); } else if (event.data?.type === MessageTypes.CloseTab) { - if (event.data?.data?.tabId === "QuickstartPSQLShell") { + if ( + event.data?.data?.tabId === "QuickstartPSQLShell" || + event.data?.data?.tabId === "QuickstartVcoreMongoShell" + ) { useTabs.getState().closeReactTab(ReactTabKind.Quickstart); } else { useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId); @@ -375,8 +381,16 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { } } - const warningMessage = getNetworkSettingsWarningMessage(); - useTabs.getState().setNetworkSettingsWarning(warningMessage); + if (inputs.isVCoreMongoAccount) { + if (inputs.connectionStringParams) { + updateUserContext({ + apiType: "VCoreMongo", + vcoreMongoConnectionParams: inputs.connectionStringParams, + }); + } + } + + getNetworkSettingsWarningMessage(useTabs.getState().setNetworkSettingsWarning); if (inputs.features) { Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));