diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 94ca16c27..ea694db40 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -257,6 +257,7 @@ export class Areas { public static ShareDialog: string = "Share Access Dialog"; public static Notebook: string = "Notebook"; public static Copilot: string = "Copilot"; + public static CloudShell: string = "Cloud Shell"; } export class HttpHeaders { diff --git a/src/Explorer/Tabs/CloudShellTab/CloudShellTerminalComponent.tsx b/src/Explorer/Tabs/CloudShellTab/CloudShellTerminalComponent.tsx index 368bf7207..b39958dc6 100644 --- a/src/Explorer/Tabs/CloudShellTab/CloudShellTerminalComponent.tsx +++ b/src/Explorer/Tabs/CloudShellTab/CloudShellTerminalComponent.tsx @@ -17,7 +17,6 @@ export const CloudShellTerminalComponent: React.FC { // Initialize XTerm instance @@ -34,18 +33,33 @@ export const CloudShellTerminalComponent: React.FC { + fitAddon.fit(); + }, 0); - // Adjust terminal size on window resize - const handleResize = () => fitAddon.fit(); - window.addEventListener('resize', handleResize); + // Use ResizeObserver instead of window resize + const resizeObserver = new ResizeObserver(() => { + const container = terminalRef.current; + if ( + container && + container.offsetWidth > 0 && + container.offsetHeight > 0 + ) { + try { + fitAddon.fit(); + } catch (e) { + console.warn("Fit failed on resize:", e); + } + } + }); + resizeObserver.observe(terminalRef.current); socketRef.current = startCloudShellTerminal(term, props.shellType); @@ -55,10 +69,11 @@ export const CloudShellTerminalComponent: React.FC; diff --git a/src/Explorer/Tabs/CloudShellTab/CloudShellTerminalCore.tsx b/src/Explorer/Tabs/CloudShellTab/CloudShellTerminalCore.tsx index 0abd27462..d8acd2821 100644 --- a/src/Explorer/Tabs/CloudShellTab/CloudShellTerminalCore.tsx +++ b/src/Explorer/Tabs/CloudShellTab/CloudShellTerminalCore.tsx @@ -3,8 +3,12 @@ * Core functionality for CloudShell terminal management */ +import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { Terminal } from "xterm"; +import { Areas } from "../../../Common/Constants"; import { TerminalKind } from "../../../Contracts/ViewModels"; +import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../../UserContext"; import { authorizeSession, @@ -14,7 +18,7 @@ import { registerCloudShellProvider, verifyCloudShellProviderRegistration } from "./Data/CloudShellClient"; -import { START_MARKER, AbstractShellHandler } from "./ShellTypes/AbstractShellHandler"; +import { AbstractShellHandler, START_MARKER } from "./ShellTypes/AbstractShellHandler"; import { ShellTypeHandlerFactory } from "./ShellTypes/ShellTypeFactory"; import { AttachAddon } from "./Utils/AttachAddOn"; import { askConfirmation, wait } from "./Utils/CommonUtils"; @@ -37,42 +41,72 @@ const MAX_PING_COUNT = 20 * 60; // 20 minutes (60 seconds/minute) export const startCloudShellTerminal = async (terminal: Terminal, shellType: TerminalKind): Promise => { - await ensureCloudShellProviderRegistered(); + const startKey = TelemetryProcessor.traceStart(Action.CloudShellTerminalSession, { + shellType: TerminalKind[shellType], + dataExplorerArea: Areas.CloudShell + }); - const resolvedRegion = determineCloudShellRegion(); - // Ask for user consent for region - const consentGranted = await askConfirmation(terminal, formatWarningMessage("This shell might be in a different region than the database region. Do you want to proceed?")); - if (!consentGranted) { - return null; // Exit if user declined + try { + await ensureCloudShellProviderRegistered(); + + const resolvedRegion = determineCloudShellRegion(); + // Ask for user consent for region + const consentGranted = await askConfirmation(terminal, formatWarningMessage("This shell might be in a different region than the database region. Do you want to proceed?")); + + // Track user decision + TelemetryProcessor.trace(Action.CloudShellUserConsent, + consentGranted ? ActionModifiers.Success : ActionModifiers.Cancel, + { dataExplorerArea: Areas.CloudShell } + ); + + if (!consentGranted) { + return null; // Exit if user declined + } + + terminal.writeln(formatInfoMessage("Connecting to CloudShell.....")); + + let sessionDetails: { + socketUri?: string; + provisionConsoleResponse?: any; + targetUri?: string; + }; + + sessionDetails = await provisionCloudShellSession(resolvedRegion, terminal); + + if (!sessionDetails.socketUri) { + terminal.writeln(formatErrorMessage("Failed to establish a connection. Please try again later.")); + return null; + } + + // Get the shell handler for this type + const shellHandler = await ShellTypeHandlerFactory.getHandler(shellType); + // Configure WebSocket connection with shell-specific commands + const socket = await establishTerminalConnection( + terminal, + shellHandler, + sessionDetails.socketUri, + sessionDetails.provisionConsoleResponse, + sessionDetails.targetUri + ); + + TelemetryProcessor.traceSuccess(Action.CloudShellTerminalSession, { + shellType: TerminalKind[shellType], + dataExplorerArea: Areas.CloudShell, + region: resolvedRegion + }, startKey); + + return socket; } + catch (err) { + TelemetryProcessor.traceFailure(Action.CloudShellTerminalSession, { + shellType: TerminalKind[shellType], + dataExplorerArea: Areas.CloudShell, + error: getErrorMessage(error), + errorStack: getErrorStack(error) + }, startKey); - terminal.writeln(formatInfoMessage("Connecting to CloudShell.....")); - - let sessionDetails: { - socketUri?: string; - provisionConsoleResponse?: any; - targetUri?: string; - }; - - sessionDetails = await provisionCloudShellSession(resolvedRegion, terminal); - - if (!sessionDetails.socketUri) { - terminal.writeln(formatErrorMessage("Failed to establish a connection. Please try again later.")); - return null; - } - - // Get the shell handler for this type - const shellHandler = await ShellTypeHandlerFactory.getHandler(shellType); - // Configure WebSocket connection with shell-specific commands - const socket = await establishTerminalConnection( - terminal, - shellHandler, - sessionDetails.socketUri, - sessionDetails.provisionConsoleResponse, - sessionDetails.targetUri - ); - - return socket; + terminal.writeln(formatErrorMessage(`Failed with error.${getErrorMessage(error)}`)); + } }; /** diff --git a/src/Explorer/Tabs/CloudShellTab/ShellTypes/AbstractShellHandler.tsx b/src/Explorer/Tabs/CloudShellTab/ShellTypes/AbstractShellHandler.tsx index fcb5cc7de..cacd61305 100644 --- a/src/Explorer/Tabs/CloudShellTab/ShellTypes/AbstractShellHandler.tsx +++ b/src/Explorer/Tabs/CloudShellTab/ShellTypes/AbstractShellHandler.tsx @@ -1,4 +1,5 @@ export const START_MARKER = `echo "START INITIALIZATION" > /dev/null`; +export const DISABLE_HISTORY = `set +o history`; export abstract class AbstractShellHandler { @@ -14,6 +15,7 @@ export abstract class AbstractShellHandler { const allCommands = [ START_MARKER, + DISABLE_HISTORY, ...setupCommands, connectionCommand ]; diff --git a/src/Explorer/Tabs/CloudShellTab/ShellTypes/PostgresShellHandler.tsx b/src/Explorer/Tabs/CloudShellTab/ShellTypes/PostgresShellHandler.tsx index 9e8585347..5eb25c6b5 100644 --- a/src/Explorer/Tabs/CloudShellTab/ShellTypes/PostgresShellHandler.tsx +++ b/src/Explorer/Tabs/CloudShellTab/ShellTypes/PostgresShellHandler.tsx @@ -33,7 +33,7 @@ export class PostgresShellHandler extends AbstractShellHandler { } public getConnectionCommand(): string { - return `psql 'read -p "Enter Database Name: " dbname && read -p "Enter Username: " username && host=${this.getEndpoint()} port=5432 dbname=$dbname user=$username sslmode=require'`; + return `read -p "Enter Database Name: " dbname && read -p "Enter Username: " username && psql -h "${this.getEndpoint()}" -p 5432 -d "$dbname" -U "$username" --set=sslmode=require`; } public getTerminalSuppressedData(): string { diff --git a/src/Explorer/Tabs/CloudShellTab/Utils/AttachAddOn.tsx b/src/Explorer/Tabs/CloudShellTab/Utils/AttachAddOn.tsx index b3a748857..8103a0cde 100644 --- a/src/Explorer/Tabs/CloudShellTab/Utils/AttachAddOn.tsx +++ b/src/Explorer/Tabs/CloudShellTab/Utils/AttachAddOn.tsx @@ -83,11 +83,14 @@ export class AttachAddon implements ITerminalAddon { this._allowTerminalWrite = false; terminal.write(`Preparing ${this._shellHandler.getShellName()} environment...\r\n`); } - if (this._allowTerminalWrite && - this._shellHandler?.getTerminalSuppressedData() && - this._shellHandler?.getTerminalSuppressedData().length > 0 && - !data.includes(this._shellHandler?.getTerminalSuppressedData())) { - terminal.write(data); + + if (this._allowTerminalWrite) { + const suppressedData = this._shellHandler?.getTerminalSuppressedData(); + const hasSuppressedData = suppressedData && suppressedData.length > 0; + + if (!hasSuppressedData || !data.includes(suppressedData)) { + terminal.write(data); + } } if (data.includes(this._shellHandler.getConnectionCommand())) { diff --git a/src/Shared/Telemetry/TelemetryConstants.ts b/src/Shared/Telemetry/TelemetryConstants.ts index 222ca542a..3c7492ab5 100644 --- a/src/Shared/Telemetry/TelemetryConstants.ts +++ b/src/Shared/Telemetry/TelemetryConstants.ts @@ -143,6 +143,8 @@ export enum Action { ReadPersistedTabState, SavePersistedTabState, DeletePersistedTabState, + CloudShellUserConsent, + CloudShellTerminalSession } export const ActionModifiers = {