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
This commit is contained in:
vchske
2023-09-12 18:03:59 -07:00
committed by GitHub
parent 135a409f0c
commit c07000a5c2
30 changed files with 1013 additions and 306 deletions

View File

@@ -14,6 +14,7 @@ export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
databaseAccount: DataModels.DatabaseAccount;
tabId: string;
username?: string;
}
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
@@ -50,7 +51,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
return;
}
const props: TerminalProps = {
let props: TerminalProps = {
terminalEndpoint: this.tryGetTerminalEndpoint(),
notebookServerEndpoint: this.props.notebookServerInfo?.notebookServerEndpoint,
authToken: this.props.notebookServerInfo?.authToken,
@@ -61,6 +62,13 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
tabId: this.props.tabId,
};
if (this.props.username) {
props = {
...props,
username: this.props.username,
};
}
postRobot.send(this.terminalWindow, "props", props, {
domain: window.location.origin,
});
@@ -78,6 +86,8 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
return this.props.databaseAccount?.properties.postgresqlEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "mongovcore")) {
return this.props.databaseAccount?.properties.vcoreMongoEndpoint;
}
if (terminalEndpoint) {

View File

@@ -1127,6 +1127,10 @@ export default class Explorer {
title = "PSQL Shell";
break;
case ViewModels.TerminalKind.VCoreMongo:
title = "VCoreMongo Shell";
break;
default:
throw new Error("Terminal kind: ${kind} not supported");
}
@@ -1313,7 +1317,7 @@ export default class Explorer {
}
public async refreshExplorer(): Promise<void> {
if (userContext.apiType !== "Postgres") {
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();

View File

@@ -34,8 +34,11 @@ export const CommandBar: React.FC<Props> = ({ 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 (
<div className="commandBarContainer">
<FluentCommandBar

View File

@@ -142,8 +142,8 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
});
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(() => {

View File

@@ -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];
}

View File

@@ -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<NotebookState> = 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<NotebookState> = 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;
}

View File

@@ -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<QuickstartFirewallNotificationProps> = ({
shellName,
screenshot,
messageType,
}: QuickstartFirewallNotificationProps): JSX.Element => (
<Stack style={{ padding: "16px 20px" }}>
<Text block>
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).
</Text>
<Text block>We strongly recommend removing this rule once you finish using the PostgreSQL shell.</Text>
<Image style={{ margin: "20px 0" }} src={FirewallRuleScreenshot} />
<PrimaryButton
style={{ width: 150 }}
onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}
>
<Text block>We strongly recommend removing this rule once you finish using the {shellName} shell.</Text>
<Image style={{ margin: "20px 0" }} src={screenshot} />
<PrimaryButton style={{ width: 150 }} onClick={() => sendMessage({ type: messageType })}>
Add firewall rule
</PrimaryButton>
</Stack>

View File

@@ -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 (
<Stack horizontal verticalAlign="center">
{currentStep > step ? (
<Icon iconName="CompletedSolid" style={{ color: "#57A300", marginRight: 8 }} />
) : (
<Image style={{ marginRight: 8 }} src={getPivotHeaderIcon(step)} />
)}
{defaultRenderer({ ...link, itemIcon: undefined })}
</Stack>
);
};
return (
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
@@ -103,7 +55,9 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {
>
<PivotItem
headerText="Login"
onRenderItemLink={(props, defaultRenderer) => 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 => {
</PivotItem>
<PivotItem
headerText="New table"
onRenderItemLink={(props, defaultRenderer) => 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 => {
</PivotItem>
<PivotItem
headerText="Distribute table"
onRenderItemLink={(props, defaultRenderer) => 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 => {
</PivotItem>
<PivotItem
headerText="Load data"
onRenderItemLink={(props, defaultRenderer) => 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 => {
</PivotItem>
<PivotItem
headerText="Query"
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 4)}
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4)
}
itemKey={GuideSteps[4]}
onClick={() => setCurrentStep(4)}
>

View File

@@ -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 (
<Stack horizontal verticalAlign="center">
{currentStep > newStep ? (
<Icon iconName="CompletedSolid" style={{ color: "#57A300", marginRight: 8 }} />
) : (
<Image style={{ marginRight: 8 }} src={getPivotHeaderIcon(currentStep, newStep)} />
)}
{defaultRenderer({ ...link, itemIcon: undefined })}
</Stack>
);
};

View File

@@ -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})`;

View File

@@ -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<number>(0);
const onCopyBtnClicked = (selector: string): void => {
const textfield: HTMLInputElement = document.querySelector(selector);
textfield.select();
document.execCommand("copy");
};
return (
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
<Text variant="xxLarge">Quick start guide</Text>
<Text variant="small">Getting started in Cosmos DB Mongo DB (vCore)</Text>
{currentStep < 5 && (
<Pivot
style={{ marginTop: 10, width: "100%" }}
selectedKey={GuideSteps[currentStep]}
onLinkClick={(item?: PivotItem) => setCurrentStep(Object.values(GuideSteps).indexOf(item.props.itemKey))}
>
<PivotItem
headerText="Connect"
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 0)
}
itemKey={GuideSteps[0]}
onClick={() => {
setCurrentStep(0);
}}
>
<Stack style={{ marginTop: 20 }}>
<Text>
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.
<br />
<br />
When not in the quick start guide, connecting to Azure Cosmos DB for MongoDB vCore is straightforward
using your connection string.
<br />
<br />
<Link
aria-label="View connection string"
href=""
onClick={() => sendMessage({ type: MessageTypes.OpenVCoreMongoConnectionStringsBlade })}
>
View connection string
</Link>
<br />
<br />
This string contains placeholders for &lt;user&gt; and &lt;password&gt;. 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 &lsquo;Networking&rsquo;
tab of your database settings, or modify your own network&apos;s firewall settings, to successfully
connect.
</Text>
</Stack>
</PivotItem>
<PivotItem
headerText="New collection"
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 1)
}
itemKey={GuideSteps[1]}
onClick={() => setCurrentStep(1)}
>
<Stack style={{ marginTop: 20 }}>
<Text>
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.
<br />
You&apos;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.
</Text>
<DefaultButton
style={{ marginTop: 16, width: 270 }}
onClick={() => useTerminal.getState().sendMessage(newDbAndCollectionCommand)}
>
Create new database and collection
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}>
<TextField
id="newDbAndCollectionCommand"
multiline
rows={5}
readOnly
defaultValue={newDbAndCollectionCommandForDisplay}
styles={{
root: { width: "90%" },
field: {
backgroundColor: "#EEEEEE",
fontFamily:
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
},
}}
/>
<IconButton
iconProps={{
iconName: "Copy",
}}
onClick={() => onCopyBtnClicked("#newDbAndCollectionCommand")}
/>
</Stack>
</Stack>
</PivotItem>
<PivotItem
headerText="Load data"
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 2)
}
itemKey={GuideSteps[2]}
onClick={() => setCurrentStep(2)}
>
<Stack style={{ marginTop: 20 }}>
<Text>
Now that you&apos;ve created your database and collection, it&apos;s time to populate your collection
with data. In MongoDB, data is stored as documents, which are structured as field and value pairs.
<br />
<br />
Let&apos;s populate your sampleCollection with data. We&apos;ll add 10 documents representing books,
each with a title, author, and number of pages, to your sampleCollection in the quickstartDB database.
</Text>
<DefaultButton
style={{ marginTop: 16, width: 200 }}
onClick={() => useTerminal.getState().sendMessage(loadDataCommand)}
>
Create distributed table
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}>
<TextField
id="loadDataCommand"
multiline
rows={5}
readOnly
defaultValue={loadDataCommand}
styles={{
root: { width: "90%" },
field: {
backgroundColor: "#EEEEEE",
fontFamily:
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
},
}}
/>
<IconButton
iconProps={{
iconName: "Copy",
}}
onClick={() => onCopyBtnClicked("#loadDataCommand")}
/>
</Stack>
</Stack>
</PivotItem>
<PivotItem
headerText="Queries"
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 3)
}
itemKey={GuideSteps[3]}
onClick={() => setCurrentStep(3)}
>
<Stack style={{ marginTop: 20 }}>
<Text>
Once youve 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.
</Text>
<DefaultButton
style={{ marginTop: 16, width: 110 }}
onClick={() => useTerminal.getState().sendMessage(queriesCommand)}
>
Load data
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}>
<TextField
id="queriesCommand"
multiline
rows={5}
readOnly
defaultValue={queriesCommandForDisplay}
styles={{
root: { width: "90%" },
field: {
backgroundColor: "#EEEEEE",
fontFamily:
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
},
}}
/>
<IconButton
iconProps={{
iconName: "Copy",
}}
onClick={() => onCopyBtnClicked("#queriesCommand")}
/>
</Stack>
</Stack>
</PivotItem>
<PivotItem
headerText="Integrations"
onRenderItemLink={(props, defaultRenderer) =>
customPivotHeaderRenderer(props, defaultRenderer, currentStep, 4)
}
itemKey={GuideSteps[4]}
onClick={() => setCurrentStep(4)}
>
<Stack>
<Text>
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.
</Text>
<Stack horizontal>
<Stack style={{ marginTop: 20, marginRight: 20 }}>
<Text>
<b>First party integrations</b>
<br />
<br />
<b>Azure Monitor</b>
<br />
Azure monitor provides comprehensive monitoring and diagnostics for Cosmos DB for Mongo DB. Learn
more
<br />
<br />
<b>Azure Networking</b>
<br />
Azure Networking seamlessly integrates with Azure Cosmos DB for Mongo DB for fast and secure data
access. Learn more
<br />
<br />
<b>PowerShell/CLI/ARM</b>
<br />
PowerShell/CLI/ARM seamlessly integrates with Azure Cosmos DB for Mongo DB for efficient
management and automation. Learn more
</Text>
</Stack>
<Stack style={{ marginTop: 20, marginLeft: 20 }}>
<Text>
<b>Application platforms integrations</b>
<br />
<br />
<b>Vercel</b>
<br />
Vercel is a cloud platform for hosting static front ends and serverless functions, with instant
deployments, automated scaling, and Next.js integration. Learn more
</Text>
</Stack>
</Stack>
</Stack>
</PivotItem>
</Pivot>
)}
</Stack>
<Stack horizontal style={{ padding: "16px 20px", boxShadow: "inset 0px 1px 0px rgba(204, 204, 204, 0.8)" }}>
<DefaultButton disabled={currentStep === 0} onClick={() => setCurrentStep(currentStep - 1)}>
Previous
</DefaultButton>
{currentStep < 4 && (
<PrimaryButton onClick={() => setCurrentStep(currentStep + 1)} style={{ marginLeft: 8 }}>
Next
</PrimaryButton>
)}
{currentStep === 4 && (
<PrimaryButton
onClick={() => useTabs.getState().closeReactTab(ReactTabKind.Quickstart)}
style={{ marginLeft: 8 }}
>
Done
</PrimaryButton>
)}
</Stack>
</Stack>
);
};

View File

@@ -260,30 +260,33 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
};
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 (
<div className="connectExplorerContainer">
<form className="connectExplorerFormContainer">
<div className="splashScreenContainer">
<div className="splashScreen">
<h1
className="title"
role="heading"
aria-label={
userContext.apiType === "Postgres"
? "Welcome to Azure Cosmos DB for PostgreSQL"
: "Welcome to Azure Cosmos DB"
}
>
{userContext.apiType === "Postgres"
? "Welcome to Azure Cosmos DB for PostgreSQL"
: "Welcome to Azure Cosmos DB"}
<h1 className="title" role="heading" aria-label={title}>
{title}
<FeaturePanelLauncher />
</h1>
<div className="subtitle">
{userContext.apiType === "Postgres"
? "Get started with our sample datasets, documentation, and additional tools."
: "Globally distributed, multi-model database service for any scale"}
</div>
<div className="subtitle">{subtitle}</div>
{this.getSplashScreenButtons()}
{useCarousel.getState().showCoachMark && (
<Coachmark
@@ -313,7 +316,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
</TeachingBubbleContent>
</Coachmark>
)}
{userContext.apiType === "Postgres" ? (
{userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ? (
<Stack horizontal style={{ margin: "0 auto", width: "84%" }} tokens={{ childrenGap: 16 }}>
<Stack style={{ width: "33%" }}>
<Text
@@ -380,7 +383,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
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<SplashScreenProps> {
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<SplashScreenProps> {
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 PostgreSQLs 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<SplashScreenProps> {
},
];
break;
default:
break;
}
return (
<Stack>
@@ -724,6 +758,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
cdbLiveTv,
];
break;
default:
break;
}
return (
<Stack>
@@ -749,24 +785,46 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
);
}
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 (
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
@@ -785,24 +843,44 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
);
}
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 (
<Stack style={{ minWidth: 124, maxWidth: 296 }}>

View File

@@ -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<QuickstartTabProps> = ({ explorer }: Quicks
forwardingId: notebookServerInfo.forwardingId,
});
const checkFirewallRules = async (): Promise<void> => {
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<QuickstartTabProps> = ({ explorer }: Quicks
<QuickstartGuide />
</Stack>
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
{!isAllPublicIPAddressEnabled && <QuickstartFirewallNotification />}
{!isAllPublicIPAddressEnabled && (
<QuickstartFirewallNotification
messageType={MessageTypes.OpenPostgresNetworkingBlade}
screenshot={FirewallRuleScreenshot}
shellName="PostgreSQL"
/>
)}
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
<NotebookTerminalComponent
notebookServerInfo={getNotebookServerInfo()}

View File

@@ -0,0 +1,44 @@
import { configContext } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { userContext } from "UserContext";
import { armRequest } from "Utils/arm/request";
export async function checkFirewallRules(
apiVersion: string,
firewallRulesPredicate: (rule: DataModels.FirewallRule) => unknown,
isAllPublicIPAddressesEnabled?: ko.Observable<boolean> | React.Dispatch<React.SetStateAction<boolean>>,
setMessageFunc?: (message: string) => void,
message?: string
): Promise<void> {
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
);
}
}

View File

@@ -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 => {
<MessageBar
messageBarType={MessageBarType.warning}
actions={
<MessageBarButton onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}>
<MessageBarButton
onClick={() =>
sendMessage({
type:
userContext.apiType === "VCoreMongo"
? MessageTypes.OpenVCoreMongoNetworkingBlade
: MessageTypes.OpenPostgresNetworkingBlade,
})
}
>
Change network settings
</MessageBarButton>
}
@@ -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" ? <PostgresConnectTab /> : <ConnectTab />;
return userContext.apiType === "VCoreMongo" ? (
<VcoreMongoConnectTab />
) : userContext.apiType === "Postgres" ? (
<PostgresConnectTab />
) : (
<ConnectTab />
);
case ReactTabKind.Home:
return <SplashScreen explorer={explorer} />;
case ReactTabKind.Quickstart:
return <QuickstartTab explorer={explorer} />;
return userContext.apiType === "VCoreMongo" ? (
<VcoreMongoQuickstartTab explorer={explorer} />
) : (
<QuickstartTab explorer={explorer} />
);
case ReactTabKind.QueryCopilot:
return <QueryCopilotTab explorer={explorer} />;
default:

View File

@@ -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<boolean>
) {}
public renderComponent(): JSX.Element {
if (!this.isAllPublicIPAddressesEnabled()) {
return <QuickstartFirewallNotification />;
return (
<QuickstartFirewallNotification
messageType={MessageTypes.OpenPostgresNetworkingBlade}
screenshot={FirewallRuleScreenshot}
shellName="PostgreSQL"
/>
);
}
return this.parameters() ? (
@@ -43,6 +52,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
notebookServerInfo={this.getNotebookServerInfo()}
databaseAccount={this.getDatabaseAccount()}
tabId={this.getTabId()}
username={this.getUsername()}
/>
) : (
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
@@ -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<boolean>(() => {
@@ -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<void> {
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;
}
}

View File

@@ -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("<user>", adminLogin);
const textfieldStyles: Partial<ITextFieldStyles> = {
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 (
<div style={{ width: "100%", padding: 16 }}>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="MongoDB SRV connection URL"
id="mongoSrvConnectionURL"
readOnly
value={displayConnectionString}
styles={textfieldStyles}
/>
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#mongoSrvConnectionURL")} />
</Stack>
</div>
);
};

View File

@@ -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<VCoreMongoQuickstartTabProps> = ({
explorer,
}: VCoreMongoQuickstartTabProps): JSX.Element => {
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(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 (
<Stack style={{ width: "100%" }} horizontal>
<Stack style={{ width: "50%" }}>
<VcoreMongoQuickstartGuide />
</Stack>
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
{!isAllPublicIPAddressEnabled && (
<QuickstartFirewallNotification
messageType={MessageTypes.OpenVCoreMongoNetworkingBlade}
screenshot={FirewallRuleScreenshot}
shellName="MongoDB"
/>
)}
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
<NotebookTerminalComponent
notebookServerInfo={getNotebookServerInfo()}
databaseAccount={userContext.databaseAccount}
tabId="QuickstartVcoreMongoShell"
username={userContext.vcoreMongoConnectionParams.adminLogin}
/>
)}
{isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
<Stack style={{ margin: "auto 0" }}>
<Text block style={{ margin: "auto" }}>
Connecting to the Mongo shell.
</Text>
<Text block style={{ margin: "auto" }}>
If the cluster was just created, this could take up to a minute.
</Text>
<Spinner styles={{ root: { marginTop: 16 } }} size={SpinnerSize.large}></Spinner>
</Stack>
)}
</Stack>
</Stack>
);
};