Compare commits

...

5 Commits

Author SHA1 Message Date
nishthaAhujaa
ac137c994b partly 2025-09-02 12:40:58 +05:30
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
20 changed files with 356 additions and 119 deletions

View File

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

View File

@@ -199,6 +199,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true", 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 styles = useStyles();
const explorerVersion = configContext.gitSha; const explorerVersion = configContext.gitSha;
@@ -261,6 +267,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
useDatabases.getState().sampleDataResourceTokenCollection && useDatabases.getState().sampleDataResourceTokenCollection &&
!isEmulator; !isEmulator;
const shouldShowMongoGuidRepresentationOption = userContext.apiType === "Mongo";
const handlerOnSubmit = async () => { const handlerOnSubmit = async () => {
setIsExecuting(true); setIsExecuting(true);
@@ -412,6 +420,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
); );
} }
if (shouldShowMongoGuidRepresentationOption) {
LocalStorageUtility.setEntryString(StorageKey.MongoGuidRepresentation, mongoGuidRepresentation);
}
setIsExecuting(false); setIsExecuting(false);
logConsoleInfo( logConsoleInfo(
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`, `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()); refreshExplorer && (await explorer.refreshExplorer());
closeSidePanel(); closeSidePanel();
}; };
@@ -477,6 +497,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{ key: SplitterDirection.Horizontal, text: "Horizontal" }, { 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 = ( const handleOnPriorityLevelOptionChange = (
ev: React.FormEvent<HTMLInputElement>, ev: React.FormEvent<HTMLInputElement>,
option: IChoiceGroupOption, option: IChoiceGroupOption,
@@ -559,6 +586,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
setRefreshExplorer(false); setRefreshExplorer(false);
}; };
const handleOnMongoGuidRepresentationOptionChange = (
ev: React.FormEvent<HTMLInputElement>,
option: IDropdownOption,
): void => {
setMongoGuidRepresentation(option.key as Constants.MongoGuidRepresentation);
};
const choiceButtonStyles = { const choiceButtonStyles = {
root: { root: {
clear: "both", clear: "both",
@@ -1065,15 +1099,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
This is a sample database and collection with synthetic product data you can use to explore using 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 NoSQL queries. This will appear as another database in the Data Explorer UI, and is created by,
is created by, and maintained by Microsoft at no cost to you. and maintained by Microsoft at no cost to you.
</div> </div>
<Checkbox <Checkbox
styles={{ styles={{
label: { padding: 0 }, label: { padding: 0 },
}} }}
className="padding" className="padding"
ariaLabel="Enable sample db for Query Advisor" ariaLabel="Enable sample db for query exploration"
checked={copilotSampleDBEnabled} checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange} onChange={handleSampleDatabaseChange}
label="Enable sample database" label="Enable sample database"
@@ -1082,6 +1116,27 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel> </AccordionPanel>
</AccordionItem> </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> </Accordion>
)} )}

View File

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

View File

@@ -24,6 +24,7 @@ import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import ConnectIcon from "../../../images/Connect_color.svg"; import ConnectIcon from "../../../images/Connect_color.svg";
import ContainersIcon from "../../../images/Containers.svg"; import ContainersIcon from "../../../images/Containers.svg";
import CosmosDBIcon from "../../../images/CosmosDB-logo.svg";
import LinkIcon from "../../../images/Link_blue.svg"; import LinkIcon from "../../../images/Link_blue.svg";
import PowerShellIcon from "../../../images/PowerShell.svg"; import PowerShellIcon from "../../../images/PowerShell.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
@@ -120,11 +121,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
}; };
private getSplashScreenButtons = (): JSX.Element => { private getSplashScreenButtons = (): JSX.Element => {
if ( if (userContext.apiType === "SQL") {
userContext.apiType === "SQL" &&
useQueryCopilot.getState().copilotEnabled &&
useDatabases.getState().sampleDataResourceTokenCollection
) {
return ( return (
<Stack <Stack
className="splashStackContainer" className="splashStackContainer"
@@ -152,25 +149,18 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
/> />
</Stack> </Stack>
<Stack className="splashStackRow" horizontal> <Stack className="splashStackRow" horizontal>
{useQueryCopilot.getState().copilotEnabled && ( <SplashScreenButton
<SplashScreenButton imgSrc={CosmosDBIcon}
imgSrc={CopilotIcon} imgSize={35}
title={"Query faster with Query Advisor"} title={"Azure Cosmos DB Samples Gallery"}
description={ 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!" "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={() => { onClick={() => {
const copilotVersion = userContext.features.copilotVersion; window.open("https://azurecosmosdb.github.io/gallery/?tags=example", "_blank");
if (copilotVersion === "v1.0") { traceOpen(Action.LearningResourcesClicked, { apiType: userContext.apiType });
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 <SplashScreenButton
imgSrc={ConnectIcon} imgSrc={ConnectIcon}
title={"Connect"} title={"Connect"}
@@ -212,6 +202,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
sample data, query. sample data, query.
</TeachingBubble> </TeachingBubble>
)} )}
{/*TODO: convert below to use SplashScreenButton */}
{mainItems.map((item) => ( {mainItems.map((item) => (
<Stack <Stack
id={`mainButton-${item.id}`} 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) { private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
return { return {
iconSrc: CollectionIcon, iconSrc: CollectionIcon,

View File

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

View File

@@ -22,6 +22,7 @@ import { formatErrorMessage, formatInfoMessage, formatWarningMessage } from "./U
// Constants // Constants
const DEFAULT_CLOUDSHELL_REGION = "westus"; const DEFAULT_CLOUDSHELL_REGION = "westus";
const DEFAULT_FAIRFAX_CLOUDSHELL_REGION = "usgovvirginia";
const POLLING_INTERVAL_MS = 2000; const POLLING_INTERVAL_MS = 2000;
const MAX_RETRY_COUNT = 10; const MAX_RETRY_COUNT = 10;
const MAX_PING_COUNT = 120 * 60; // 120 minutes (60 seconds/minute) 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 * Determines the appropriate CloudShell region
*/ */
export const determineCloudShellRegion = (): string => { 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

@@ -33,6 +33,7 @@ jest.mock("../../../../UserContext", () => ({
})); }));
jest.mock("../Utils/CommonUtils", () => ({ jest.mock("../Utils/CommonUtils", () => ({
...jest.requireActual("../Utils/CommonUtils"),
getHostFromUrl: jest.fn().mockReturnValue("test-mongo.documents.azure.com"), getHostFromUrl: jest.fn().mockReturnValue("test-mongo.documents.azure.com"),
})); }));
@@ -124,7 +125,10 @@ describe("MongoShellHandler", () => {
describe("getTerminalSuppressedData", () => { describe("getTerminalSuppressedData", () => {
it("should return the correct warning message", () => { 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 { userContext } from "../../../../UserContext";
import { getHostFromUrl } from "../Utils/CommonUtils"; import { filterAndCleanTerminalOutput, getHostFromUrl, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND } from "./AbstractShellHandler"; import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND } from "./AbstractShellHandler";
export class MongoShellHandler extends AbstractShellHandler { export class MongoShellHandler extends AbstractShellHandler {
private _key: string; private _key: string;
private _endpoint: string | undefined; private _endpoint: string | undefined;
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
constructor(private key: string) { constructor(private key: string) {
super(); super();
this._key = key; this._key = key;
@@ -44,6 +45,10 @@ export class MongoShellHandler extends AbstractShellHandler {
} }
public getTerminalSuppressedData(): string[] { public getTerminalSuppressedData(): string[] {
return ["Warning: Non-Genuine MongoDB Detected"]; return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
}
updateTerminalData(data: string): string {
return filterAndCleanTerminalOutput(data, this._removeInfoText);
} }
} }

View File

@@ -1,13 +1,10 @@
import { userContext } from "../../../../UserContext"; import { userContext } from "../../../../UserContext";
import { filterAndCleanTerminalOutput, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND } from "./AbstractShellHandler"; import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND } from "./AbstractShellHandler";
export class VCoreMongoShellHandler extends AbstractShellHandler { export class VCoreMongoShellHandler extends AbstractShellHandler {
private _endpoint: string | undefined; private _endpoint: string | undefined;
private _textFilterRules: string[] = [ private _removeInfoText: string[] = getMongoShellRemoveInfoText();
"For mongosh info see: https://www.mongodb.com/docs/mongodb-shell/",
"disableTelemetry() command",
"https://www.mongodb.com/legal/privacy-policy",
];
constructor() { constructor() {
super(); super();
@@ -38,12 +35,7 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."]; return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
} }
updateTerminalData(content: string): string { updateTerminalData(data: string): string {
const updatedContent = content return filterAndCleanTerminalOutput(data, this._removeInfoText);
.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;
} }
} }

View File

@@ -135,7 +135,11 @@ export class AttachAddon implements ITerminalAddon {
} }
if (this._allowTerminalWrite) { 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 suppressedData = this._shellHandler?.getTerminalSuppressedData();
const shouldNotWrite = suppressedData.filter(Boolean).some((item) => updatedData.includes(item)); const shouldNotWrite = suppressedData.filter(Boolean).some((item) => updatedData.includes(item));

View File

@@ -0,0 +1,13 @@
export const CLOUDSHELL_IP_RECOMMENDATIONS = {
centralindia: ["4.247.135.109", "74.225.207.63"],
southeastasia: ["4.194.56.6", "4.194.213.10", "4.194.144.127", "4.194.5.74"],
centraluseuap: ["52.158.186.182", "172.215.26.246", "134.138.154.177", "134.138.129.52", "172.215.31.177"],
eastus2euap: ["135.18.43.51", "20.252.175.33", "40.89.88.111", "135.18.17.187", "135.18.67.251"],
eastus: ["40.71.199.151", "20.42.18.188", "52.190.17.9", "20.120.96.152"],
northeurope: ["74.234.65.146", "52.169.70.113"],
southcentralus: ["4.151.247.81", "20.225.211.35", "4.151.48.133", "4.151.247.225"],
westeurope: ["52.166.126.216", "108.142.162.20", "52.178.13.125", "172.201.33.160"],
westus: ["20.245.161.131", "57.154.182.51", "40.118.133.244", "20.253.192.12", "20.43.245.209", "20.66.22.66"],
usgovarizona: ["62.10.232.179"],
usgovvirginia: ["62.10.26.85"],
};

View File

@@ -50,3 +50,34 @@ export const getShellNameForDisplay = (terminalKind: TerminalKind): string => {
return ""; 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

@@ -8,6 +8,10 @@ const validCloudShellRegions = new Set([
"centralindia", "centralindia",
"southeastasia", "southeastasia",
"westcentralus", "westcentralus",
"usgovvirginia",
"usgovarizona",
"centraluseuap",
"eastus2euap",
]); ]);
/** /**
@@ -39,8 +43,8 @@ export const getNormalizedRegion = (region: string, defaultCloudshellRegion: str
} }
const regionMap: Record<string, string> = { const regionMap: Record<string, string> = {
centralus: "westcentralus", centralus: "centraluseuap",
eastus2: "eastus", eastus2: "eastus2euap",
}; };
const normalizedRegion = regionMap[region.toLowerCase()] || region; const normalizedRegion = regionMap[region.toLowerCase()] || region;

View File

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

View File

@@ -0,0 +1,40 @@
import { configContext } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { userContext } from "UserContext";
import { armRequest } from "Utils/arm/request";
import { CLOUDSHELL_IP_RECOMMENDATIONS } from "../CloudShellTab/Utils/CloudShellIPUtils";
import { getNormalizedRegion } from "../CloudShellTab/Utils/RegionUtils";
export async function checkCloudShellIPsConfigured() {
const databaseRegion = userContext.databaseAccount?.location;
console.log("db region", databaseRegion);
const normalizedRegion = getNormalizedRegion(databaseRegion, "westus");
const cloudShellIPs = getCloudShellIPsForRegion(normalizedRegion);
console.log("CloudShell IPs for region", normalizedRegion, cloudShellIPs);
if (!cloudShellIPs || cloudShellIPs.length === 0) {
return false;
}
const firewallRules = await getFirewallRules();
console.log("firewall rules", firewallRules);
return false;
}
function getCloudShellIPsForRegion(region: string): string[] {
const regionKey = region.toLowerCase();
const ips = CLOUDSHELL_IP_RECOMMENDATIONS[regionKey as keyof typeof CLOUDSHELL_IP_RECOMMENDATIONS];
return ips ? [...ips] : [];
}
async function getFirewallRules(): Promise<DataModels.FirewallRule[]> {
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
const response: any = await armRequest({
host: configContext.ARM_ENDPOINT,
path: firewallRulesUri,
method: "GET",
apiVersion: "2023-03-01-preview",
});
return response?.data?.value || response?.value || [];
}

View File

@@ -22,9 +22,19 @@ export abstract class BaseTerminalComponentAdapter implements ReactAdapter {
protected getUsername: () => string, protected getUsername: () => string,
protected isAllPublicIPAddressesEnabled: ko.Observable<boolean>, protected isAllPublicIPAddressesEnabled: ko.Observable<boolean>,
protected kind: ViewModels.TerminalKind, protected kind: ViewModels.TerminalKind,
) {} protected isCloudShellIPsConfigured?: ko.Observable<boolean>,
) { }
public renderComponent(): JSX.Element { public renderComponent(): JSX.Element {
if (this.kind === ViewModels.TerminalKind.VCoreMongo && this.isCloudShellIPsConfigured && !this.isCloudShellIPsConfigured()) {
return (
<QuickstartFirewallNotification
messageType={this.getMessageType()}
screenshot={VcoreFirewallRuleScreenshot}
shellName={getShellNameForDisplay(this.kind)}
/>
);
}
if (!this.isAllPublicIPAddressesEnabled()) { if (!this.isAllPublicIPAddressesEnabled()) {
return ( return (
<QuickstartFirewallNotification <QuickstartFirewallNotification

View File

@@ -8,6 +8,7 @@ import { userContext } from "../../UserContext";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useNotebook } from "../Notebook/useNotebook"; import { useNotebook } from "../Notebook/useNotebook";
import { checkCloudShellIPsConfigured } from "./Shared/CloudShellIPChecker";
import { NotebookTerminalComponentAdapter } from "./ShellAdapters/NotebookTerminalComponentAdapter"; import { NotebookTerminalComponentAdapter } from "./ShellAdapters/NotebookTerminalComponentAdapter";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
@@ -23,11 +24,13 @@ export default class TerminalTab extends TabsBase {
private container: Explorer; private container: Explorer;
private notebookTerminalComponentAdapter: ReactAdapter; private notebookTerminalComponentAdapter: ReactAdapter;
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>; private isAllPublicIPAddressesEnabled: ko.Observable<boolean>;
private isCloudShellIPsConfigured: ko.Observable<boolean>;
constructor(options: TerminalTabOptions) { constructor(options: TerminalTabOptions) {
super(options); super(options);
this.container = options.container; this.container = options.container;
this.isAllPublicIPAddressesEnabled = ko.observable(true); this.isAllPublicIPAddressesEnabled = ko.observable(true);
this.isCloudShellIPsConfigured = ko.observable(true);
const commonArgs: [ const commonArgs: [
() => DataModels.DatabaseAccount, () => DataModels.DatabaseAccount,
@@ -36,19 +39,37 @@ export default class TerminalTab extends TabsBase {
ko.Observable<boolean>, ko.Observable<boolean>,
ViewModels.TerminalKind, ViewModels.TerminalKind,
] = [ ] = [
() => userContext?.databaseAccount, () => userContext?.databaseAccount,
() => this.tabId, () => this.tabId,
() => this.getUsername(), () => this.getUsername(),
this.isAllPublicIPAddressesEnabled, this.isAllPublicIPAddressesEnabled,
options.kind, options.kind,
]; ];
if (userContext.features.enableCloudShell) { if (userContext.features.enableCloudShell) {
this.notebookTerminalComponentAdapter = new CloudShellTerminalComponentAdapter(...commonArgs); this.notebookTerminalComponentAdapter = new CloudShellTerminalComponentAdapter(
() => userContext?.databaseAccount,
() => this.tabId,
() => this.getUsername(),
this.isAllPublicIPAddressesEnabled,
options.kind,
options.kind === ViewModels.TerminalKind.VCoreMongo ? this.isCloudShellIPsConfigured : undefined,
);
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => { this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
if (options.kind === ViewModels.TerminalKind.VCoreMongo) {
const cloudShellConfigured = this.isCloudShellIPsConfigured();
return this.isTemplateReady() && cloudShellConfigured;
}
return this.isTemplateReady() && this.isAllPublicIPAddressesEnabled(); return this.isTemplateReady() && this.isAllPublicIPAddressesEnabled();
}); });
if (options.kind === ViewModels.TerminalKind.VCoreMongo) {
(async () => {
const result = await checkCloudShellIPsConfigured();
this.isCloudShellIPsConfigured(result);
})();
}
} else { } else {
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter( this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
() => this.getNotebookServerInfo(options), () => this.getNotebookServerInfo(options),

View File

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