This commit is contained in:
Justin Kolasa (from Dev Box)
2025-05-08 08:55:42 -04:00
10 changed files with 129 additions and 82 deletions

View File

@@ -1,7 +1,7 @@
/**
* Accordion top class
*/
import { Link, makeStyles, tokens } from "@fluentui/react-components";
import { makeStyles, tokens } from "@fluentui/react-components";
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
@@ -9,7 +9,6 @@ import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUt
import * as React from "react";
import { userContext } from "UserContext";
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
import LinkIcon from "../../../images/Link_blue.svg";
import Explorer from "../Explorer";
export interface SplashScreenProps {
@@ -186,12 +185,12 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
{title}
</div>
{getSplashScreenButtons()}
<div className={styles.footer}>
{/* <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> */}
</CosmosFluentProvider>
</>
);

View File

@@ -43,32 +43,52 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
await ensureCloudShellProviderRegistered();
resolvedRegion = determineCloudShellRegion();
// Ask for user consent for region
const consentGranted = await askConfirmation(
terminal,
formatWarningMessage(
"The shell environment may be operating in a region different from that of the database, which could impact performance or data compliance. Do you wish to proceed?",
resolvedRegion = determineCloudShellRegion();
terminal.writeln(formatWarningMessage("⚠️ IMPORTANT: Azure Cloud Shell Region Notice ⚠️"));
terminal.writeln(
formatInfoMessage(
"The Cloud Shell environment will operate in a region that may differ from your database's region.",
),
);
terminal.writeln(formatInfoMessage("This has two potential implications:"));
terminal.writeln(formatInfoMessage("1. Performance Impact:"));
terminal.writeln(
formatInfoMessage(" Commands may experience higher latency due to geographic distance between regions."),
);
terminal.writeln(formatInfoMessage("2. Data Compliance Considerations:"));
terminal.writeln(
formatInfoMessage(
" Data processed through this shell could temporarily reside in a different geographic region,",
),
);
terminal.writeln(
formatInfoMessage(" which may affect compliance with data residency requirements or regulations specific"),
);
terminal.writeln(formatInfoMessage(" to your organization."));
terminal.writeln("");
terminal.writeln("\x1b[94mFor more information on Azure Cosmos DB data governance and compliance, please visit:");
terminal.writeln("\x1b[94mhttps://learn.microsoft.com/en-us/azure/cosmos-db/data-residency\x1b[0m");
// Ask for user consent for region
const consentGranted = await askConfirmation(terminal, formatWarningMessage("Do you wish to proceed?"));
// Track user decision
TelemetryProcessor.trace(
Action.CloudShellUserConsent,
consentGranted ? ActionModifiers.Success : ActionModifiers.Cancel,
{ dataExplorerArea: Areas.CloudShell },
{
dataExplorerArea: Areas.CloudShell,
shellType: TerminalKind[shellType],
isConsent: consentGranted,
region: resolvedRegion,
},
startKey,
);
if (!consentGranted) {
TelemetryProcessor.traceCancel(
Action.CloudShellTerminalSession,
{
shellType: TerminalKind[shellType],
dataExplorerArea: Areas.CloudShell,
region: resolvedRegion,
isConsent: false,
},
startKey,
);
terminal.writeln(
formatErrorMessage("Session ended. Please close this tab and initiate a new shell session if needed."),
);
@@ -262,28 +282,27 @@ export const configureSocketConnection = async (
};
export const sendTerminalStartupCommands = (socket: WebSocket, initCommands: string): void => {
// ensures connections don't remain open indefinitely by implementing an automatic timeout after 120 minutes.
const keepSocketAlive = (socket: WebSocket) => {
if (socket.readyState === WebSocket.OPEN) {
if (pingCount >= MAX_PING_COUNT) {
socket.close();
} else {
pingCount++;
// The code uses a recursive setTimeout pattern rather than setInterval,
// which ensures each new ping only happens after the previous one completes
// and naturally stops if the socket closes.
keepAliveID = setTimeout(() => keepSocketAlive(socket), 1000);
}
}
};
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(initCommands);
keepSocketAlive(socket);
} else {
socket.onopen = () => {
socket.send(initCommands);
// ensures connections don't remain open indefinitely by implementing an automatic timeout after 20 minutes.
const keepSocketAlive = (socket: WebSocket) => {
if (socket.readyState === WebSocket.OPEN) {
if (pingCount >= MAX_PING_COUNT) {
socket.close();
} else {
socket.send("");
pingCount++;
// The code uses a recursive setTimeout pattern rather than setInterval,
// which ensures each new ping only happens after the previous one completes
// and naturally stops if the socket closes.
keepAliveID = setTimeout(() => keepSocketAlive(socket), 1000);
}
}
};
keepSocketAlive(socket);
};
}

View File

@@ -22,6 +22,12 @@ export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close thi
* the required methods.
*/
export abstract class AbstractShellHandler {
/**
* The name of the application using this shell handler.
* This is used for telemetry and logging purposes.
*/
protected APP_NAME = "CosmosExplorerTerminal";
abstract getShellName(): string;
abstract getSetUpCommands(): string[];
abstract getConnectionCommand(): string;
@@ -56,4 +62,30 @@ export abstract class AbstractShellHandler {
return allCommands.join("\n").concat("\n");
}
/**
* Setup commands for MongoDB shell:
*
* 1. Check if mongosh is already installed
* 2. Download mongosh package if not installed
* 3. Extract the package to access mongosh binaries
* 4. Move extracted files to ~/mongosh directory
* 5. Add mongosh binary path to system PATH
* 6. Apply PATH changes by sourcing .bashrc
*
* Each command runs conditionally only if mongosh
* is not already present in the environment.
*/
protected mongoShellSetupCommands(): string[] {
const PACKAGE_VERSION: string = "2.5.0";
return [
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
`if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${PACKAGE_VERSION}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`,
`if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${PACKAGE_VERSION}-linux-x64 mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
"source ~/.bashrc",
];
}
}

View File

@@ -87,7 +87,7 @@ describe("CassandraShellHandler", () => {
});
test("should return correct connection command", () => {
const expectedCommand = "cqlsh test-endpoint.cassandra.cosmos.azure.com 10350 -u test-account -p test-key --ssl";
const expectedCommand = `cqlsh test-endpoint.cassandra.cosmos.azure.com 10350 -u test-account -p test-key --ssl`;
expect(handler.getConnectionCommand()).toBe(expectedCommand);
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-endpoint.cassandra.cosmos.azure.com:443/");

View File

@@ -68,7 +68,7 @@ describe("MongoShellHandler", () => {
const commands = mongoShellHandler.getSetUpCommands();
expect(Array.isArray(commands)).toBe(true);
expect(commands.length).toBe(6);
expect(commands.length).toBe(7);
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
});
});
@@ -91,7 +91,7 @@ describe("MongoShellHandler", () => {
const command = mongoShellHandler.getConnectionCommand();
expect(command).toBe(
"mongosh --host test-mongo.documents.azure.com --port 10255 --username test-account --password test-key --tls --tlsAllowInvalidCertificates",
"mongosh mongodb://test-mongo.documents.azure.com:10255?appName=CosmosExplorerTerminal --username test-account --password test-key --tls --tlsAllowInvalidCertificates",
);
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-mongo.documents.azure.com:443/");

View File

@@ -2,8 +2,6 @@ import { userContext } from "../../../../UserContext";
import { getHostFromUrl } from "../Utils/CommonUtils";
import { AbstractShellHandler } from "./AbstractShellHandler";
const PACKAGE_VERSION: string = "2.5.0";
export class MongoShellHandler extends AbstractShellHandler {
private _key: string;
private _endpoint: string | undefined;
@@ -18,14 +16,7 @@ export class MongoShellHandler extends AbstractShellHandler {
}
public getSetUpCommands(): string[] {
return [
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
`if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh && mv mongosh-${PACKAGE_VERSION}-linux-x64/* ~/mongosh/; fi`,
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
"source ~/.bashrc",
];
return this.mongoShellSetupCommands();
}
public getConnectionCommand(): string {
@@ -37,9 +28,17 @@ export class MongoShellHandler extends AbstractShellHandler {
if (!dbName) {
return "echo 'Database name not found.'";
}
return `mongosh --host ${getHostFromUrl(this._endpoint)} --port 10255 --username ${dbName} --password ${
this._key
} --tls --tlsAllowInvalidCertificates`;
return (
"mongosh mongodb://" +
getHostFromUrl(this._endpoint) +
":10255?appName=" +
this.APP_NAME +
" --username " +
dbName +
" --password " +
this._key +
" --tls --tlsAllowInvalidCertificates"
);
}
public getTerminalSuppressedData(): string {

View File

@@ -54,7 +54,7 @@ export class PostgresShellHandler extends AbstractShellHandler {
// All Azure Cosmos DB PostgreSQL deployments follow this convention.
// Ref. https://learn.microsoft.com/en-us/azure/cosmos-db/postgresql/reference-limits#database-creation
const loginName = userContext.postgresConnectionStrParams.adminLogin;
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require`;
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require --set=application_name=${this.APP_NAME}`;
}
public getTerminalSuppressedData(): string {

View File

@@ -44,7 +44,7 @@ describe("VCoreMongoShellHandler", () => {
const commands = vcoreMongoShellHandler.getSetUpCommands();
expect(Array.isArray(commands)).toBe(true);
expect(commands.length).toBe(6);
expect(commands.length).toBe(7);
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
expect(commands[0]).toContain("mongosh not found");
});

View File

@@ -1,8 +1,6 @@
import { userContext } from "../../../../UserContext";
import { AbstractShellHandler } from "./AbstractShellHandler";
const PACKAGE_VERSION: string = "2.5.0";
export class VCoreMongoShellHandler extends AbstractShellHandler {
private _endpoint: string | undefined;
@@ -15,28 +13,8 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
return "MongoDB VCore";
}
/**
* Setup commands for MongoDB VCore shell:
*
* 1. Check if mongosh is already installed
* 2. Download mongosh package if not installed
* 3. Extract the package to access mongosh binaries
* 4. Move extracted files to ~/mongosh directory
* 5. Add mongosh binary path to system PATH
* 6. Apply PATH changes by sourcing .bashrc
*
* Each command runs conditionally only if mongosh
* is not already present in the environment.
*/
public getSetUpCommands(): string[] {
return [
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
`if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh && mv mongosh-${PACKAGE_VERSION}-linux-x64/* ~/mongosh/; fi`,
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
"source ~/.bashrc",
];
return this.mongoShellSetupCommands();
}
public getConnectionCommand(): string {
@@ -45,7 +23,7 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
}
const userName = userContext.vcoreMongoConnectionParams.adminLogin;
return `mongosh "mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000"`;
return `mongosh "mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000&appName=${this.APP_NAME}"`;
}
public getTerminalSuppressedData(): string {

View File

@@ -1,5 +1,6 @@
import { AbstractShellHandler } from "Explorer/Tabs/CloudShellTab/ShellTypes/AbstractShellHandler";
import { IDisposable, ITerminalAddon, Terminal } from "@xterm/xterm";
import { AbstractShellHandler } from "../ShellTypes/AbstractShellHandler";
import { formatErrorMessage } from "./TerminalLogFormats";
interface IAttachOptions {
bidirectional?: boolean;
@@ -56,8 +57,27 @@ export class AttachAddon implements ITerminalAddon {
this._disposables.push(terminal.onBinary((data) => this._sendBinary(data)));
}
this._disposables.push(addSocketListener(this._socket, "close", () => this.dispose()));
this._disposables.push(addSocketListener(this._socket, "error", () => this.dispose()));
this._disposables.push(addSocketListener(this._socket, "close", () => this._handleSocketClose(terminal)));
this._disposables.push(addSocketListener(this._socket, "error", () => this._handleSocketClose(terminal)));
}
/**
* Handles socket close events by terminating processes and showing a message
*/
private _handleSocketClose(terminal: Terminal): void {
if (terminal) {
terminal.writeln(
formatErrorMessage("Session ended. Please close this tab and initiate a new shell session if needed."),
);
// Send exit command to terminal
if (this._bidirectional) {
terminal.write(formatErrorMessage("exit\r\n"));
}
}
// Clean up resources
this.dispose();
}
/**