mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 10:51:30 +00:00
Compare commits
5 Commits
cja/simple
...
cloudshell
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac137c994b | ||
|
|
0817acf404 | ||
|
|
8e2c46301d | ||
|
|
012d043c78 | ||
|
|
3afd74a957 |
@@ -765,3 +765,10 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
|
||||
|
||||
userPrompt: "find all products",
|
||||
};
|
||||
|
||||
export enum MongoGuidRepresentation {
|
||||
Standard = "Standard",
|
||||
CSharpLegacy = "CSharpLegacy",
|
||||
JavaLegacy = "JavaLegacy",
|
||||
PythonLegacy = "PythonLegacy",
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
imgSrc={CosmosDBIcon}
|
||||
imgSize={35}
|
||||
title={"Azure Cosmos DB Samples Gallery"}
|
||||
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={() => {
|
||||
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 });
|
||||
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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import { getHostFromUrl } from "../Utils/CommonUtils";
|
||||
import { filterAndCleanTerminalOutput, getHostFromUrl, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
|
||||
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND } 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,10 @@ export class MongoShellHandler extends AbstractShellHandler {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import { filterAndCleanTerminalOutput, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
|
||||
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND } 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,7 @@ 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;
|
||||
updateTerminalData(data: string): string {
|
||||
return filterAndCleanTerminalOutput(data, this._removeInfoText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
13
src/Explorer/Tabs/CloudShellTab/Utils/CloudShellIPUtils.ts
Normal file
13
src/Explorer/Tabs/CloudShellTab/Utils/CloudShellIPUtils.ts
Normal 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"],
|
||||
};
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -8,6 +8,10 @@ const validCloudShellRegions = new Set([
|
||||
"centralindia",
|
||||
"southeastasia",
|
||||
"westcentralus",
|
||||
"usgovvirginia",
|
||||
"usgovarizona",
|
||||
"centraluseuap",
|
||||
"eastus2euap",
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -39,8 +43,8 @@ export const getNormalizedRegion = (region: string, defaultCloudshellRegion: str
|
||||
}
|
||||
|
||||
const regionMap: Record<string, string> = {
|
||||
centralus: "westcentralus",
|
||||
eastus2: "eastus",
|
||||
centralus: "centraluseuap",
|
||||
eastus2: "eastus2euap",
|
||||
};
|
||||
|
||||
const normalizedRegion = regionMap[region.toLowerCase()] || region;
|
||||
|
||||
@@ -106,6 +106,6 @@ describe("QueryTabComponent", () => {
|
||||
<QueryTabCopilotComponent {...propsMock} />
|
||||
</CopilotProvider>,
|
||||
);
|
||||
expect(container.find(QueryCopilotPromptbar).exists()).toBe(true);
|
||||
expect(container.find(QueryCopilotPromptbar).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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()}
|
||||
|
||||
40
src/Explorer/Tabs/Shared/CloudShellIPChecker.ts
Normal file
40
src/Explorer/Tabs/Shared/CloudShellIPChecker.ts
Normal 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 || [];
|
||||
}
|
||||
@@ -22,9 +22,19 @@ export abstract class BaseTerminalComponentAdapter implements ReactAdapter {
|
||||
protected getUsername: () => string,
|
||||
protected isAllPublicIPAddressesEnabled: ko.Observable<boolean>,
|
||||
protected kind: ViewModels.TerminalKind,
|
||||
) {}
|
||||
protected isCloudShellIPsConfigured?: ko.Observable<boolean>,
|
||||
) { }
|
||||
|
||||
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()) {
|
||||
return (
|
||||
<QuickstartFirewallNotification
|
||||
|
||||
@@ -8,6 +8,7 @@ import { userContext } from "../../UserContext";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../Explorer";
|
||||
import { useNotebook } from "../Notebook/useNotebook";
|
||||
import { checkCloudShellIPsConfigured } from "./Shared/CloudShellIPChecker";
|
||||
import { NotebookTerminalComponentAdapter } from "./ShellAdapters/NotebookTerminalComponentAdapter";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
@@ -23,11 +24,13 @@ export default class TerminalTab extends TabsBase {
|
||||
private container: Explorer;
|
||||
private notebookTerminalComponentAdapter: ReactAdapter;
|
||||
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>;
|
||||
private isCloudShellIPsConfigured: ko.Observable<boolean>;
|
||||
|
||||
constructor(options: TerminalTabOptions) {
|
||||
super(options);
|
||||
this.container = options.container;
|
||||
this.isAllPublicIPAddressesEnabled = ko.observable(true);
|
||||
this.isCloudShellIPsConfigured = ko.observable(true);
|
||||
|
||||
const commonArgs: [
|
||||
() => DataModels.DatabaseAccount,
|
||||
@@ -44,11 +47,29 @@ export default class TerminalTab extends TabsBase {
|
||||
];
|
||||
|
||||
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>(() => {
|
||||
if (options.kind === ViewModels.TerminalKind.VCoreMongo) {
|
||||
const cloudShellConfigured = this.isCloudShellIPsConfigured();
|
||||
return this.isTemplateReady() && cloudShellConfigured;
|
||||
}
|
||||
return this.isTemplateReady() && this.isAllPublicIPAddressesEnabled();
|
||||
});
|
||||
|
||||
if (options.kind === ViewModels.TerminalKind.VCoreMongo) {
|
||||
(async () => {
|
||||
const result = await checkCloudShellIPsConfigured();
|
||||
this.isCloudShellIPsConfigured(result);
|
||||
})();
|
||||
}
|
||||
} else {
|
||||
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
||||
() => this.getNotebookServerInfo(options),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user