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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1013 additions and 306 deletions

20
images/Table.svg Normal file
View File

@ -0,0 +1,20 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.11108 12.8623H38.8889V33.9245C38.8889 34.2593 38.7559 34.5803 38.5192 34.8171C38.2825 35.0538 37.9614 35.1867 37.6266 35.1867H2.37331C2.03854 35.1867 1.71749 35.0538 1.48078 34.8171C1.24407 34.5803 1.11108 34.2593 1.11108 33.9245V12.8623Z" fill="url(#paint0_linear_307_12833)"/>
<path d="M2.37997 4.81349H37.62C37.7857 4.81349 37.9499 4.84614 38.103 4.90958C38.2561 4.97301 38.3953 5.06598 38.5125 5.18319C38.6297 5.3004 38.7227 5.43955 38.7861 5.59269C38.8495 5.74582 38.8822 5.90996 38.8822 6.07572V12.8624H1.11108V6.07572C1.11108 5.9094 1.14395 5.74472 1.2078 5.59114C1.27165 5.43756 1.36522 5.29812 1.48313 5.18083C1.60105 5.06353 1.74098 4.97069 1.89489 4.90766C2.0488 4.84462 2.21366 4.81262 2.37997 4.81349V4.81349Z" fill="#0078D4"/>
<path opacity="0.9" d="M12.6021 15.8242H5.74876C5.40144 15.8242 5.11987 16.1058 5.11987 16.4531V19.3064C5.11987 19.6538 5.40144 19.9353 5.74876 19.9353H12.6021C12.9494 19.9353 13.231 19.6538 13.231 19.3064V16.4531C13.231 16.1058 12.9494 15.8242 12.6021 15.8242Z" fill="white"/>
<path opacity="0.9" d="M23.4821 15.7666H16.6288C16.2814 15.7666 15.9999 16.0482 15.9999 16.3955V19.2488C15.9999 19.5961 16.2814 19.8777 16.6288 19.8777H23.4821C23.8294 19.8777 24.111 19.5961 24.111 19.2488V16.3955C24.111 16.0482 23.8294 15.7666 23.4821 15.7666Z" fill="white"/>
<path opacity="0.9" d="M34.3621 15.7666H27.5088C27.1614 15.7666 26.8799 16.0482 26.8799 16.3955V19.2488C26.8799 19.5961 27.1614 19.8777 27.5088 19.8777H34.3621C34.7094 19.8777 34.991 19.5961 34.991 19.2488V16.3955C34.991 16.0482 34.7094 15.7666 34.3621 15.7666Z" fill="white"/>
<path opacity="0.9" d="M12.7221 21.7051H5.86876C5.52143 21.7051 5.23987 21.9866 5.23987 22.334V25.1873C5.23987 25.5346 5.52143 25.8162 5.86876 25.8162H12.7221C13.0694 25.8162 13.351 25.5346 13.351 25.1873V22.334C13.351 21.9866 13.0694 21.7051 12.7221 21.7051Z" fill="white"/>
<path d="M23.6021 21.6465H16.7488C16.4014 21.6465 16.1199 21.928 16.1199 22.2754V25.1287C16.1199 25.476 16.4014 25.7576 16.7488 25.7576H23.6021C23.9494 25.7576 24.231 25.476 24.231 25.1287V22.2754C24.231 21.928 23.9494 21.6465 23.6021 21.6465Z" fill="#ECF4FD"/>
<path d="M34.4821 21.6465H27.6288C27.2814 21.6465 26.9999 21.928 26.9999 22.2754V25.1287C26.9999 25.476 27.2814 25.7576 27.6288 25.7576H34.4821C34.8294 25.7576 35.111 25.476 35.111 25.1287V22.2754C35.111 21.928 34.8294 21.6465 34.4821 21.6465Z" fill="#ECF4FD"/>
<path d="M12.7221 27.6426H5.86876C5.52143 27.6426 5.23987 27.9241 5.23987 28.2715V31.1248C5.23987 31.4721 5.52143 31.7537 5.86876 31.7537H12.7221C13.0694 31.7537 13.351 31.4721 13.351 31.1248V28.2715C13.351 27.9241 13.0694 27.6426 12.7221 27.6426Z" fill="#ECF4FD"/>
<path d="M23.6021 27.585H16.7488C16.4014 27.585 16.1199 27.8665 16.1199 28.2139V31.0672C16.1199 31.4145 16.4014 31.6961 16.7488 31.6961H23.6021C23.9494 31.6961 24.231 31.4145 24.231 31.0672V28.2139C24.231 27.8665 23.9494 27.585 23.6021 27.585Z" fill="#ECF4FD"/>
<path d="M34.4821 27.585H27.6288C27.2814 27.585 26.9999 27.8665 26.9999 28.2139V31.0672C26.9999 31.4145 27.2814 31.6961 27.6288 31.6961H34.4821C34.8294 31.6961 35.111 31.4145 35.111 31.0672V28.2139C35.111 27.8665 34.8294 27.585 34.4821 27.585Z" fill="#ECF4FD"/>
<defs>
<linearGradient id="paint0_linear_307_12833" x1="20" y1="35.1867" x2="20" y2="12.8623" gradientUnits="userSpaceOnUse">
<stop stop-color="#0078D4"/>
<stop offset="0.502" stop-color="#4093E6"/>
<stop offset="0.775" stop-color="#5EA0EF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

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

View File

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

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,16 +411,31 @@ 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 = {
}
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 {
iconSrc: ContainersIcon,
title: `New ${getCollectionName()}`,
description: "Create a new container for storage and throughput",
@ -423,21 +444,32 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
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);
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);
return heroes;
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,8 +785,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
);
}
private getNextStepItems(): JSX.Element {
const items: { link: string; title: string; description: string }[] = [
private postgresNextStepItems: { link: string; title: string; description: string }[] = [
{
link: "https://go.microsoft.com/fwlink/?linkid=2208312",
title: "Data Modeling",
@ -768,6 +803,29 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
},
];
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 = userContext.apiType === "Postgres" ? this.postgresNextStepItems : this.vcoreMongoNextStepItems;
return (
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
{items.map((item, i) => (
@ -785,8 +843,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
);
}
private getTipsAndLearnMoreItems(): JSX.Element {
const items: { link: string; title: string; description: string }[] = [
private postgresLearnMoreItems: { link: string; title: string; description: string }[] = [
{
link: "https://go.microsoft.com/fwlink/?linkid=2207226",
title: "Performance Tuning",
@ -804,6 +861,27 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
},
];
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 = userContext.apiType === "Postgres" ? this.postgresLearnMoreItems : this.vcoreMongoLearnMoreItems;
return (
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
{items.map((item, i) => (

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);
private getUsername(): string {
if (userContext.apiType !== "VCoreMongo" || !userContext?.vcoreMongoConnectionParams?.adminLogin) {
return undefined;
}
// 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);
}
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>
);
};

View File

@ -91,7 +91,7 @@ const App: React.FunctionComponent = () => {
{/* Collections Tree and Tabs - Begin */}
<div className="resourceTreeAndTabs">
{/* Collections Tree - Start */}
{userContext.apiType !== "Postgres" && (
{userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" && (
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
<div className="collectionsTreeWithSplitter">
{/* Collections Tree Expanded - Start */}

View File

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

View File

@ -11,4 +11,5 @@ export interface TerminalProps {
apiType: ApiType;
subscriptionId: string;
tabId: string;
username?: string;
}

View File

@ -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",
};

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
import { userContext } from "UserContext";
const PortalIPs: { [key: string]: string[] } = {
@ -10,27 +11,45 @@ const PortalIPs: { [key: string]: string[] } = {
usnat: ["7.28.202.68"],
};
export const getNetworkSettingsWarningMessage = (): string => {
export const getNetworkSettingsWarningMessage = async (
setStateFunc: (warningMessage: string) => void
): Promise<void> => {
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"
) {
return "The Network settings for this account are preventing access from Data Explorer. Please enable public access to proceed.";
setStateFunc(publicAccessMessage);
}
const ipRules = accountProperties.ipRules;
// public network access is set to "All networks"
if (ipRules.length === 0) {
return "";
}
// 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;
@ -41,9 +60,9 @@ export const getNetworkSettingsWarningMessage = (): string => {
});
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.";
setStateFunc(accessMessage);
}
}
}
}
return "";
};

View File

@ -105,7 +105,10 @@ async function configureHosted(): Promise<Explorer> {
}
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<Explorer> {
} 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)));