Compare commits

...

9 Commits

Author SHA1 Message Date
Sung-Hyun Kang
98d7e89712 Added VS Code Exnteion for DE 2025-09-18 08:03:06 -05:00
Sung-Hyun Kang
d45e10f5ac Added VS Code Exnteion for DE 2025-09-18 08:01:01 -05:00
Nishtha Ahuja
cfb5db4df6 Removed screenshot for mongo cloudshell (#2211)
Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
2025-09-16 12:16:19 +05:30
Dmitry Shilov
922ca5c523 chore: Update help link in FabricHome component to point to the new documentation (#2206) 2025-09-04 11:58:07 +02:00
Dmitry Shilov
bafe002fa3 chore: Enhance accessibility (#2208)
- Add tabIndex to button
- Add aria attributes to icons and headings
2025-09-04 11:39:11 +02:00
vchske
0817acf404 Commenting or deleting UI references to Query Advisor (#2209)
* Commenting or deleting UI references to Query Advisor

* Removing (commenting out) QueryTabComponent from two views

* Added new splash screen button, commented out copilot prompt bar

* Fixing unit test
2025-08-28 15:47:29 -07:00
asier-isayas
8e2c46301d Allow Mongo users to change thee Guid Representation when conducting CRUD operations for documents (#2204)
* mongo guid representation

* format

* fix return type

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-08-18 12:30:04 -07:00
BChoudhury-ms
012d043c78 Fix CloudShell terminal hanging for Mongo and Cassandra shells due to missing updateTerminalData method (#2199) 2025-08-13 13:02:27 -07:00
Mike Krüger
3afd74a957 Fix faifax default cloud shell region. (#2201) 2025-08-13 11:25:18 -07:00
22 changed files with 368 additions and 164 deletions

View File

@@ -765,3 +765,10 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
userPrompt: "find all products",
};
export enum MongoGuidRepresentation {
Standard = "Standard",
CSharpLegacy = "CSharpLegacy",
JavaLegacy = "JavaLegacy",
PythonLegacy = "PythonLegacy",
}

View File

@@ -1,4 +1,5 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import { getMongoGuidRepresentation } from "Shared/StorageUtility";
import { AuthType } from "../AuthType";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
@@ -139,6 +140,9 @@ export function readDocument(
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperties?.[0]
: "",
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
@@ -181,6 +185,9 @@ export function createDocument(
partitionKey:
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
documentContent: JSON.stringify(documentContent),
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
@@ -228,6 +235,9 @@ export function updateDocument(
? documentId.partitionKeyProperties?.[0]
: "",
documentContent,
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
@@ -274,6 +284,9 @@ export function deleteDocuments(
subscriptionID: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name,
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);

View File

@@ -44,6 +44,12 @@ export interface DatabaseAccountExtendedProperties {
publicNetworkAccess?: string;
enablePriorityBasedExecution?: boolean;
vcoreMongoEndpoint?: string;
apiProperties?: ApiProperties;
}
export interface ApiProperties {
/* Describes the version of the MongoDB account. */
serverVersion?: "3.2" | "3.6" | "4.0" | "4.2" | "5.0" | "6.0" | "7.0";
}
export interface DatabaseAccountResponseLocation {

View File

@@ -18,6 +18,7 @@ import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
import { getVSCodeUrl } from "Utils/VSCodeExtensionUtils";
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout";
@@ -289,40 +290,8 @@ export default class Explorer {
}
}
/**
* Generates a VS Code DocumentDB connection URL using the current user's MongoDB connection parameters.
* Double-encodes the updated connection string for safe usage in VS Code URLs.
*
* The DocumentDB VS Code extension requires double encoding for connection strings.
* See: https://microsoft.github.io/vscode-documentdb/manual/how-to-construct-url.html#double-encoding
*
* @returns {string} The encoded VS Code DocumentDB connection URL.
*/
private getDocumentDbUrl() {
const { adminLogin: adminLoginuserName = "", connectionString = "" } = userContext.vcoreMongoConnectionParams;
const updatedConnectionString = connectionString.replace(/<(user|username)>:<password>/i, adminLoginuserName);
const encodedUpdatedConnectionString = encodeURIComponent(encodeURIComponent(updatedConnectionString));
const documentDbUrl = `vscode://ms-azuretools.vscode-documentdb?connectionString=${encodedUpdatedConnectionString}`;
return documentDbUrl;
}
private getCosmosDbUrl() {
const activeTab = useTabs.getState().activeTab;
const resourceId = encodeURIComponent(userContext.databaseAccount.id);
const database = encodeURIComponent(activeTab?.collection?.databaseId);
const container = encodeURIComponent(activeTab?.collection?.id());
const baseUrl = `vscode://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`;
const vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl;
return vscodeUrl;
}
private getVSCodeUrl(): string {
const isvCore = (userContext.apiType || userContext.databaseAccount.kind) === "VCoreMongo";
return isvCore ? this.getDocumentDbUrl() : this.getCosmosDbUrl();
}
public openInVsCode(): void {
const vscodeUrl = this.getVSCodeUrl();
const vscodeUrl = getVSCodeUrl();
const openVSCodeDialogProps: DialogProps = {
linkProps: {
linkText: "Download Visual Studio Code",

View File

@@ -199,6 +199,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
);
const [mongoGuidRepresentation, setMongoGuidRepresentation] = useState<Constants.MongoGuidRepresentation>(
LocalStorageUtility.hasItem(StorageKey.MongoGuidRepresentation)
? (LocalStorageUtility.getEntryString(StorageKey.MongoGuidRepresentation) as Constants.MongoGuidRepresentation)
: Constants.MongoGuidRepresentation.CSharpLegacy,
);
const styles = useStyles();
const explorerVersion = configContext.gitSha;
@@ -261,6 +267,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
useDatabases.getState().sampleDataResourceTokenCollection &&
!isEmulator;
const shouldShowMongoGuidRepresentationOption = userContext.apiType === "Mongo";
const handlerOnSubmit = async () => {
setIsExecuting(true);
@@ -412,6 +420,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
);
}
if (shouldShowMongoGuidRepresentationOption) {
LocalStorageUtility.setEntryString(StorageKey.MongoGuidRepresentation, mongoGuidRepresentation);
}
setIsExecuting(false);
logConsoleInfo(
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
@@ -433,6 +445,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
);
}
if (shouldShowMongoGuidRepresentationOption) {
logConsoleInfo(
`Updated Mongo Guid Representation to ${LocalStorageUtility.getEntryString(
StorageKey.MongoGuidRepresentation,
)}`,
);
}
refreshExplorer && (await explorer.refreshExplorer());
closeSidePanel();
};
@@ -477,6 +497,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
];
const mongoGuidRepresentationDropdownOptions: IDropdownOption[] = [
{ key: Constants.MongoGuidRepresentation.CSharpLegacy, text: Constants.MongoGuidRepresentation.CSharpLegacy },
{ key: Constants.MongoGuidRepresentation.JavaLegacy, text: Constants.MongoGuidRepresentation.JavaLegacy },
{ key: Constants.MongoGuidRepresentation.PythonLegacy, text: Constants.MongoGuidRepresentation.PythonLegacy },
{ key: Constants.MongoGuidRepresentation.Standard, text: Constants.MongoGuidRepresentation.Standard },
];
const handleOnPriorityLevelOptionChange = (
ev: React.FormEvent<HTMLInputElement>,
option: IChoiceGroupOption,
@@ -559,6 +586,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
setRefreshExplorer(false);
};
const handleOnMongoGuidRepresentationOptionChange = (
ev: React.FormEvent<HTMLInputElement>,
option: IDropdownOption,
): void => {
setMongoGuidRepresentation(option.key as Constants.MongoGuidRepresentation);
};
const choiceButtonStyles = {
root: {
clear: "both",
@@ -1065,15 +1099,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
This is a sample database and collection with synthetic product data you can use to explore using
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and
is created by, and maintained by Microsoft at no cost to you.
NoSQL queries. This will appear as another database in the Data Explorer UI, and is created by,
and maintained by Microsoft at no cost to you.
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel="Enable sample db for Query Advisor"
ariaLabel="Enable sample db for query exploration"
checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange}
label="Enable sample database"
@@ -1082,6 +1116,27 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
)}
{shouldShowMongoGuidRepresentationOption && (
<AccordionItem value="14">
<AccordionHeader>
<div className={styles.header}>Guid Representation</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
GuidRepresentation in MongoDB refers to how Globally Unique Identifiers (GUIDs) are serialized and
deserialized when stored in BSON documents. This will apply to all document operations.
</div>
<Dropdown
aria-labelledby="mongoGuidRepresentation"
selectedKey={mongoGuidRepresentation}
options={mongoGuidRepresentationDropdownOptions}
onChange={handleOnMongoGuidRepresentationOptionChange}
/>
</div>
</AccordionPanel>
</AccordionItem>
)}
</Accordion>
)}

View File

@@ -1,11 +1,9 @@
/* eslint-disable no-console */
import { Stack } from "@fluentui/react";
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
@@ -13,7 +11,6 @@ import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotRe
import { userContext } from "UserContext";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
import React, { useState } from "react";
import SplitterLayout from "react-splitter-layout";
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
@@ -26,7 +23,8 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
readCopilotToggleStatus(userContext.databaseAccount),
);
const [tabActive, setTabActive] = useState<boolean>(true);
//TODO: Uncomment this useState when query copilot is reinstated in DE
// const [tabActive, setTabActive] = useState<boolean>(true);
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
@@ -70,17 +68,18 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
useCommandBar.getState().setContextButtons(getCommandbarButtons());
}, [query, selectedQuery, copilotActive]);
React.useEffect(() => {
return () => {
useTabs.subscribe((state: TabsState) => {
if (state.activeReactTab === ReactTabKind.QueryCopilot) {
setTabActive(true);
} else {
setTabActive(false);
}
});
};
}, []);
//TODO: Uncomment this effect when query copilot is reinstated in DE
// React.useEffect(() => {
// return () => {
// useTabs.subscribe((state: TabsState) => {
// if (state.activeReactTab === ReactTabKind.QueryCopilot) {
// setTabActive(true);
// } else {
// setTabActive(false);
// }
// });
// };
// }, []);
const toggleCopilot = (toggle: boolean) => {
setCopilotActive(toggle);
@@ -90,6 +89,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
return (
<Stack className="tab-pane" style={{ width: "100%" }}>
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
{/*TODO: Uncomment this section when query copilot is reinstated in DE
{tabActive && copilotActive && (
<QueryCopilotPromptbar
explorer={explorer}
@@ -97,7 +97,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
databaseId={QueryCopilotSampleDatabaseId}
containerId={QueryCopilotSampleContainerId}
></QueryCopilotPromptbar>
)}
)} */}
<Stack className="tabPaneContentContainer">
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
<EditorReact

View File

@@ -1,8 +1,8 @@
/**
* Accordion top class
*/
import { makeStyles, tokens } from "@fluentui/react-components";
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
import { Link, makeStyles, tokens } from "@fluentui/react-components";
import { DocumentAddRegular, LinkMultipleRegular, OpenRegular } from "@fluentui/react-icons";
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
@@ -119,7 +119,7 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
}) => {
const styles = useStyles();
return (
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick} tabIndex={0}>
<div className={styles.buttonUpperPart}>{icon}</div>
<div aria-label={title} className={styles.buttonLowerPart}>
<div>{title}</div>
@@ -147,7 +147,7 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
{
title: "Sample data",
description: "Automatically load sample data in your database",
icon: <img src={CosmosDbBlackIcon} />,
icon: <img src={CosmosDbBlackIcon} alt={"Azure Cosmos DB icon"} aria-hidden="true" />,
onClick: () => setOpenSampleDataImportDialog(true),
},
{
@@ -181,16 +181,18 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
explorer={props.explorer}
databaseName={userContext.fabricContext?.databaseName}
/>
<div className={styles.title} role="heading" aria-label={title}>
<div className={styles.title} role="heading" aria-label={title} aria-level={1}>
{title}
</div>
{getSplashScreenButtons()}
{/* <div className={styles.footer}>
Need help?{" "}
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
Learn more <img src={LinkIcon} alt="Learn more" />
</Link>
</div> */}
{
<div className={styles.footer}>
Need help?{" "}
<Link href="https://learn.microsoft.com/fabric/database/cosmos-db/overview" target="_blank">
Learn more <OpenRegular />
</Link>
</div>
}
</CosmosFluentProvider>
</>
);

View File

@@ -24,6 +24,7 @@ import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import ConnectIcon from "../../../images/Connect_color.svg";
import ContainersIcon from "../../../images/Containers.svg";
import CosmosDBIcon from "../../../images/CosmosDB-logo.svg";
import LinkIcon from "../../../images/Link_blue.svg";
import PowerShellIcon from "../../../images/PowerShell.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
@@ -120,11 +121,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
};
private getSplashScreenButtons = (): JSX.Element => {
if (
userContext.apiType === "SQL" &&
useQueryCopilot.getState().copilotEnabled &&
useDatabases.getState().sampleDataResourceTokenCollection
) {
if (userContext.apiType === "SQL") {
return (
<Stack
className="splashStackContainer"
@@ -152,25 +149,18 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
/>
</Stack>
<Stack className="splashStackRow" horizontal>
{useQueryCopilot.getState().copilotEnabled && (
<SplashScreenButton
imgSrc={CopilotIcon}
title={"Query faster with Query Advisor"}
description={
"Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
}
onClick={() => {
const copilotVersion = userContext.features.copilotVersion;
if (copilotVersion === "v1.0") {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
sampleCollection.onNewQueryClick(sampleCollection, undefined);
}
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
}}
/>
)}
<SplashScreenButton
imgSrc={CosmosDBIcon}
imgSize={35}
title={"Azure Cosmos DB Samples Gallery"}
description={
"Discover samples that showcase scalable, intelligent app patterns. Try one now to see how fast you can go from concept to code with Cosmos DB"
}
onClick={() => {
window.open("https://azurecosmosdb.github.io/gallery/?tags=example", "_blank");
traceOpen(Action.LearningResourcesClicked, { apiType: userContext.apiType });
}}
/>
<SplashScreenButton
imgSrc={ConnectIcon}
title={"Connect"}
@@ -212,6 +202,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
sample data, query.
</TeachingBubble>
)}
{/*TODO: convert below to use SplashScreenButton */}
{mainItems.map((item) => (
<Stack
id={`mainButton-${item.id}`}
@@ -477,6 +468,34 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
};
}
//TODO: Re-enable lint rule when query copilot is reinstated in DE
/* eslint-disable-next-line no-unused-vars */
private getQueryCopilotCard = (): JSX.Element => {
return (
<>
{useQueryCopilot.getState().copilotEnabled && (
<SplashScreenButton
imgSrc={CopilotIcon}
title={"Query faster with Query Advisor"}
description={
"Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
}
onClick={() => {
const copilotVersion = userContext.features.copilotVersion;
if (copilotVersion === "v1.0") {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
sampleCollection.onNewQueryClick(sampleCollection, undefined);
}
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
}}
/>
)}
</>
);
};
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
return {
iconSrc: CollectionIcon,

View File

@@ -7,6 +7,7 @@ interface SplashScreenButtonProps {
title: string;
description: string;
onClick: () => void;
imgSize?: number;
}
export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
@@ -14,6 +15,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
title,
description,
onClick,
imgSize,
}: SplashScreenButtonProps): JSX.Element => {
return (
<Stack
@@ -39,7 +41,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
role="button"
>
<div>
<img src={imgSrc} alt={title} aria-hidden="true" />
<img src={imgSrc} alt={title} aria-hidden="true" {...(imgSize ? { height: imgSize, width: imgSize } : {})} />
</div>
<Stack style={{ marginLeft: 16 }}>
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>

View File

@@ -22,6 +22,7 @@ import { formatErrorMessage, formatInfoMessage, formatWarningMessage } from "./U
// Constants
const DEFAULT_CLOUDSHELL_REGION = "westus";
const DEFAULT_FAIRFAX_CLOUDSHELL_REGION = "usgovvirginia";
const POLLING_INTERVAL_MS = 2000;
const MAX_RETRY_COUNT = 10;
const MAX_PING_COUNT = 120 * 60; // 120 minutes (60 seconds/minute)
@@ -153,7 +154,9 @@ export const ensureCloudShellProviderRegistered = async (): Promise<void> => {
* Determines the appropriate CloudShell region
*/
export const determineCloudShellRegion = (): string => {
return getNormalizedRegion(userContext.databaseAccount?.location, DEFAULT_CLOUDSHELL_REGION);
const defaultRegion =
userContext.portalEnv === "fairfax" ? DEFAULT_FAIRFAX_CLOUDSHELL_REGION : DEFAULT_CLOUDSHELL_REGION;
return getNormalizedRegion(userContext.databaseAccount?.location, defaultRegion);
};
/**

View File

@@ -14,6 +14,11 @@ export const DISABLE_HISTORY = `set +o history`;
* Used when shell initialization or connection fails.
*/
export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && disown -a && exit`;
/**
* Command that displays error message with MongoDB networking guidance and exits the shell session.
* Used when MongoDB shell connection fails due to networking issues.
*/
export const EXIT_COMMAND_MONGO = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && printf "\\033[1;36mPlease use the 'Add Azure Cloud Shell IPs' button in the Networking blade to allow Cloud Shell access, if not already configured.\\033[0m\\n" && disown -a && exit`;
/**
* This command runs mongosh in no-database and quiet mode,
@@ -40,6 +45,14 @@ export abstract class AbstractShellHandler {
abstract getTerminalSuppressedData(): string[];
updateTerminalData?(data: string): string;
/**
* Gets the exit command to use when connection fails.
* Can be overridden by subclasses to provide custom exit commands.
*/
protected getExitCommand(): string {
return EXIT_COMMAND;
}
/**
* Constructs the complete initialization command sequence for the shell.
*
@@ -64,7 +77,7 @@ export abstract class AbstractShellHandler {
START_MARKER,
DISABLE_HISTORY,
...setupCommands,
`{ ${connectionCommand}; } || true;${EXIT_COMMAND}`,
`{ ${connectionCommand}; } || true;${this.getExitCommand()}`,
];
return allCommands.join("\n").concat("\n");

View File

@@ -33,6 +33,7 @@ jest.mock("../../../../UserContext", () => ({
}));
jest.mock("../Utils/CommonUtils", () => ({
...jest.requireActual("../Utils/CommonUtils"),
getHostFromUrl: jest.fn().mockReturnValue("test-mongo.documents.azure.com"),
}));
@@ -124,7 +125,10 @@ describe("MongoShellHandler", () => {
describe("getTerminalSuppressedData", () => {
it("should return the correct warning message", () => {
expect(mongoShellHandler.getTerminalSuppressedData()).toEqual(["Warning: Non-Genuine MongoDB Detected"]);
expect(mongoShellHandler.getTerminalSuppressedData()).toEqual([
"Warning: Non-Genuine MongoDB Detected",
"Telemetry is now disabled.",
]);
});
});
});

View File

@@ -1,10 +1,11 @@
import { userContext } from "../../../../UserContext";
import { getHostFromUrl } from "../Utils/CommonUtils";
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND } from "./AbstractShellHandler";
import { filterAndCleanTerminalOutput, getHostFromUrl, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
export class MongoShellHandler extends AbstractShellHandler {
private _key: string;
private _endpoint: string | undefined;
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
constructor(private key: string) {
super();
this._key = key;
@@ -44,6 +45,14 @@ export class MongoShellHandler extends AbstractShellHandler {
}
public getTerminalSuppressedData(): string[] {
return ["Warning: Non-Genuine MongoDB Detected"];
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
}
protected getExitCommand(): string {
return EXIT_COMMAND_MONGO;
}
updateTerminalData(data: string): string {
return filterAndCleanTerminalOutput(data, this._removeInfoText);
}
}

View File

@@ -1,13 +1,10 @@
import { userContext } from "../../../../UserContext";
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND } from "./AbstractShellHandler";
import { filterAndCleanTerminalOutput, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
export class VCoreMongoShellHandler extends AbstractShellHandler {
private _endpoint: string | undefined;
private _textFilterRules: string[] = [
"For mongosh info see: https://www.mongodb.com/docs/mongodb-shell/",
"disableTelemetry() command",
"https://www.mongodb.com/legal/privacy-policy",
];
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
constructor() {
super();
@@ -38,12 +35,14 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
}
updateTerminalData(content: string): string {
const updatedContent = content
.split("\n")
.filter((line) => !this._textFilterRules.some((part) => line.includes(part)))
.filter((line, idx, arr) => (arr.length > 3 && idx <= arr.length - 3 ? !["", "\r"].includes(line) : true)) // Filter out empty lines and carriage returns, but keep the last 3 lines if they exist
.join("\n");
return updatedContent;
/**
* Override getExitCommand to include MongoDB networking guidance
*/
protected getExitCommand(): string {
return EXIT_COMMAND_MONGO;
}
updateTerminalData(data: string): string {
return filterAndCleanTerminalOutput(data, this._removeInfoText);
}
}

View File

@@ -135,7 +135,11 @@ export class AttachAddon implements ITerminalAddon {
}
if (this._allowTerminalWrite) {
const updatedData = this._shellHandler?.updateTerminalData(data) ?? data;
const updatedData =
typeof this._shellHandler?.updateTerminalData === "function"
? this._shellHandler.updateTerminalData(data)
: data;
const suppressedData = this._shellHandler?.getTerminalSuppressedData();
const shouldNotWrite = suppressedData.filter(Boolean).some((item) => updatedData.includes(item));

View File

@@ -50,3 +50,34 @@ export const getShellNameForDisplay = (terminalKind: TerminalKind): string => {
return "";
}
};
/**
* Get MongoDB shell information text that should be removed from terminal output
*/
export const getMongoShellRemoveInfoText = (): string[] => {
return [
"For mongosh info see: https://www.mongodb.com/docs/mongodb-shell/",
"disableTelemetry() command",
"https://www.mongodb.com/legal/privacy-policy",
];
};
export const filterAndCleanTerminalOutput = (data: string, removeInfoText: string[]): string => {
if (!data || removeInfoText.length === 0) {
return data;
}
const lines = data.split("\n");
const filteredLines: string[] = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const shouldRemove = removeInfoText.some((text) => line.includes(text));
if (!shouldRemove) {
filteredLines.push(line);
}
}
return filteredLines.join("\n").replace(/((\r\n)|\n|\r){2,}/g, "\r\n");
};

View File

@@ -7,7 +7,8 @@ const validCloudShellRegions = new Set([
"westeurope",
"centralindia",
"southeastasia",
"westcentralus",
"usgovvirginia",
"usgovarizona",
]);
/**
@@ -39,7 +40,6 @@ export const getNormalizedRegion = (region: string, defaultCloudshellRegion: str
}
const regionMap: Record<string, string> = {
centralus: "westcentralus",
eastus2: "eastus",
};

View File

@@ -106,6 +106,6 @@ describe("QueryTabComponent", () => {
<QueryTabCopilotComponent {...propsMock} />
</CopilotProvider>,
);
expect(container.find(QueryCopilotPromptbar).exists()).toBe(true);
expect(container.find(QueryCopilotPromptbar).exists()).toBe(false);
});
});

View File

@@ -9,7 +9,6 @@ import { useDialog } from "Explorer/Controls/Dialog";
import { monaco } from "Explorer/LazyMonaco";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
@@ -28,8 +27,9 @@ import { TabsState, useTabs } from "hooks/useTabs";
import React, { Fragment, createRef } from "react";
import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format";
import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
//TODO: Uncomment next two lines when query copilot is reinstated in DE
// import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
// import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
@@ -494,53 +494,55 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
});
}
if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
const mainButtonLabel = "Launch Copilot";
const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
const copilotSettingLabel = "Copilot settings";
//TODO: Uncomment next section when query copilot is reinstated in DE
// if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
// const mainButtonLabel = "Launch Copilot";
// const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
// const copilotSettingLabel = "Copilot settings";
const openCopilotChatButton: CommandButtonComponentProps = {
iconAlt: chatPaneLabel,
onCommandClick: this.launchQueryCopilotChat,
commandButtonLabel: chatPaneLabel,
ariaLabel: chatPaneLabel,
hasPopup: false,
};
// const openCopilotChatButton: CommandButtonComponentProps = {
// iconAlt: chatPaneLabel,
// onCommandClick: this.launchQueryCopilotChat,
// commandButtonLabel: chatPaneLabel,
// ariaLabel: chatPaneLabel,
// hasPopup: false,
// };
const copilotSettingsButton: CommandButtonComponentProps = {
iconAlt: copilotSettingLabel,
onCommandClick: () => undefined,
commandButtonLabel: copilotSettingLabel,
ariaLabel: copilotSettingLabel,
hasPopup: false,
};
// const copilotSettingsButton: CommandButtonComponentProps = {
// iconAlt: copilotSettingLabel,
// onCommandClick: () => undefined,
// commandButtonLabel: copilotSettingLabel,
// ariaLabel: copilotSettingLabel,
// hasPopup: false,
// };
const launchCopilotButton: CommandButtonComponentProps = {
iconSrc: LaunchCopilot,
iconAlt: mainButtonLabel,
onCommandClick: this.launchQueryCopilotChat,
commandButtonLabel: mainButtonLabel,
ariaLabel: mainButtonLabel,
hasPopup: false,
children: [openCopilotChatButton, copilotSettingsButton],
};
buttons.push(launchCopilotButton);
}
// const launchCopilotButton: CommandButtonComponentProps = {
// iconSrc: LaunchCopilot,
// iconAlt: mainButtonLabel,
// onCommandClick: this.launchQueryCopilotChat,
// commandButtonLabel: mainButtonLabel,
// ariaLabel: mainButtonLabel,
// hasPopup: false,
// children: [openCopilotChatButton, copilotSettingsButton],
// };
// buttons.push(launchCopilotButton);
// }
if (this.props.copilotEnabled) {
const toggleCopilotButton: CommandButtonComponentProps = {
iconSrc: QueryCommandIcon,
iconAlt: "Query Advisor",
keyboardAction: KeyboardAction.TOGGLE_COPILOT,
onCommandClick: () => {
this._toggleCopilot(!this.state.copilotActive);
},
commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
hasPopup: false,
};
buttons.push(toggleCopilotButton);
}
//TODO: Uncomment next section when query copilot is reinstated in DE
// if (this.props.copilotEnabled) {
// const toggleCopilotButton: CommandButtonComponentProps = {
// iconSrc: QueryCommandIcon,
// iconAlt: "Query Advisor",
// keyboardAction: KeyboardAction.TOGGLE_COPILOT,
// onCommandClick: () => {
// this._toggleCopilot(!this.state.copilotActive);
// },
// commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
// ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
// hasPopup: false,
// };
// buttons.push(toggleCopilotButton);
// }
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
const label = "Cancel query";
@@ -725,6 +727,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
return (
<Fragment>
<CosmosFluentProvider id={this.props.tabId} className={this.props.styles.queryTab} role="tabpanel">
{/*TODO: Uncomment this section when query copilot is reinstated in DE
{this.props.copilotEnabled && this.state.currentTabActive && this.state.copilotActive && (
<QueryCopilotPromptbar
explorer={this.props.collection.container}
@@ -732,7 +735,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
></QueryCopilotPromptbar>
)}
)} */}
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
<Allotment
key={vertical.toString()}

View File

@@ -4,7 +4,6 @@ import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFi
import { getShellNameForDisplay } from "Explorer/Tabs/CloudShellTab/Utils/CommonUtils";
import * as React from "react";
import FirewallRuleScreenshot from "../../../../images/firewallRule.png";
import VcoreFirewallRuleScreenshot from "../../../../images/vcoreMongoFirewallRule.png";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
@@ -25,15 +24,15 @@ export abstract class BaseTerminalComponentAdapter implements ReactAdapter {
) {}
public renderComponent(): JSX.Element {
if (this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo) {
return this.renderTerminalComponent();
}
if (!this.isAllPublicIPAddressesEnabled()) {
return (
<QuickstartFirewallNotification
messageType={this.getMessageType()}
screenshot={
this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo
? VcoreFirewallRuleScreenshot
: FirewallRuleScreenshot
}
screenshot={FirewallRuleScreenshot}
shellName={getShellNameForDisplay(this.kind)}
/>
);

View File

@@ -1,3 +1,4 @@
import { MongoGuidRepresentation } from "Common/Constants";
import { SplitterDirection } from "Common/Splitter";
import * as LocalStorageUtility from "./LocalStorageUtility";
import * as SessionStorageUtility from "./SessionStorageUtility";
@@ -33,6 +34,7 @@ export enum StorageKey {
DocumentsTabPrefs,
DefaultQueryResultsView,
AppState,
MongoGuidRepresentation,
}
export const hasRUThresholdBeenConfigured = (): boolean => {
@@ -65,4 +67,13 @@ export const getDefaultQueryResultsView = (): SplitterDirection => {
return SplitterDirection.Horizontal;
};
export const getMongoGuidRepresentation = (): MongoGuidRepresentation => {
const mongoGuidRepresentation: string | null = LocalStorageUtility.getEntryString(StorageKey.MongoGuidRepresentation);
if (mongoGuidRepresentation) {
return mongoGuidRepresentation as MongoGuidRepresentation;
}
return MongoGuidRepresentation.CSharpLegacy;
};
export const DefaultRUThreshold = 5000;

View File

@@ -0,0 +1,55 @@
import { useTabs } from "hooks/useTabs";
import { userContext } from "UserContext";
import { getHostFromUrl } from "./../Explorer/Tabs/CloudShellTab/Utils/CommonUtils";
export const DOCUMENTDB_VSCODE_EXTENSION_BASEURL = "vscode://ms-azuretools.vscode-documentdb";
export const COSMOSDB_VSCODE_EXTENSION_BASEURL = "vscode://ms-azuretools.vscode-cosmosdb";
/**
* Generates a VS Code DocumentDB connection URL using the current user's MongoDB connection parameters.
* Double-encodes the updated connection string for safe usage in VS Code URLs.
*
* The DocumentDB VS Code extension requires double encoding for connection strings.
* See: https://microsoft.github.io/vscode-documentdb/manual/how-to-construct-url.html#double-encoding
*
* @returns {string} The encoded VS Code DocumentDB connection URL.
*/
export const getVSCodeUrl = (): string => {
const isvCore = (userContext.apiType || userContext.databaseAccount.kind) === "VCoreMongo";
const isMongo =
userContext.apiType === "Mongo" && userContext.databaseAccount?.properties?.apiProperties?.serverVersion !== "3.2";
return isvCore ? getDocumentDbUrl() : isMongo ? getMongoRUUrl() : getCosmosDbUrl();
};
export const getCosmosDbUrl = () => {
const activeTab = useTabs.getState().activeTab;
const resourceId = encodeURIComponent(userContext.databaseAccount.id);
const database = encodeURIComponent(activeTab?.collection?.databaseId);
const container = encodeURIComponent(activeTab?.collection?.id());
const baseUrl = `${COSMOSDB_VSCODE_EXTENSION_BASEURL}?resourceId=${resourceId}`;
const vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl;
return vscodeUrl;
};
export const getMongoRUUrl = () => {
const activeTab = useTabs.getState().activeTab;
const databaseAccount = userContext.databaseAccount;
const host = getHostFromUrl(databaseAccount.properties?.mongoEndpoint);
const port = 10255;
const database = activeTab?.collection?.databaseId;
const container = activeTab?.collection?.id();
const encodedUpdatedConnectionString = encodeURIComponent(`mongodb://${databaseAccount?.name}@${host}:${port}`);
const documentDbUrl = `${DOCUMENTDB_VSCODE_EXTENSION_BASEURL}?connectionString=${encodedUpdatedConnectionString}${
database ? `&database=${database}` : ""
}${container ? `&collection=${container}` : ""}`;
return documentDbUrl;
};
export const getDocumentDbUrl = () => {
const { adminLogin: adminLoginuserName = "", connectionString = "" } = userContext.vcoreMongoConnectionParams;
const updatedConnectionString = connectionString.replace(/<(user|username)>:<password>/i, adminLoginuserName);
const encodedUpdatedConnectionString = encodeURIComponent(encodeURIComponent(updatedConnectionString));
const documentDbUrl = `${DOCUMENTDB_VSCODE_EXTENSION_BASEURL}?connectionString=${encodedUpdatedConnectionString}`;
return documentDbUrl;
};