ui and functional changes

This commit is contained in:
Sourabh Jain
2025-03-25 00:24:17 +05:30
parent 10b0da2190
commit 8b4eaa95ea
4 changed files with 156 additions and 99 deletions

View File

@@ -18,10 +18,18 @@ export const CloudShellTerminalComponent: React.FC<CloudShellTerminalProps> = ({
const fitAddon = new FitAddon();
useEffect(() => {
// Initialize XTerm instance
const term = new Terminal({
cursorBlink: true,
theme: { background: "#1d1f21", foreground: "#c5c8c6" }
// Initialize XTerm instance
const term = new Terminal({
cursorBlink: true,
cursorStyle: 'bar',
fontFamily: 'Courier New, monospace',
fontSize: 14,
theme: {
background: "#1e1e1e",
foreground: "#d4d4d4",
cursor: "#ffcc00"
},
scrollback: 1000
});
term.loadAddon(fitAddon);
@@ -30,8 +38,23 @@ export const CloudShellTerminalComponent: React.FC<CloudShellTerminalProps> = ({
if (terminalRef.current) {
term.open(terminalRef.current);
xtermRef.current = term;
// Ensure the CSS is injected only once
if (!document.getElementById("xterm-custom-style")) {
const style = document.createElement("style");
style.id = "xterm-custom-style"; // Unique ID to prevent duplicates
style.innerHTML = `
.xterm-text-layer {
transform: translateX(10px); /* Adds left padding */
}
`;
document.head.appendChild(style);
}
}
if (fitAddon) {
fitAddon.fit();
}
fitAddon.fit();
// Adjust terminal size on window resize
const handleResize = () => fitAddon.fit();
@@ -47,14 +70,20 @@ export const CloudShellTerminalComponent: React.FC<CloudShellTerminalProps> = ({
// Cleanup function to close WebSocket and dispose terminal
return () => {
if (!socketRef.current) return; // Prevent errors if WebSocket is not initialized
if (socketRef.current) {
socketRef.current.close(); // Close WebSocket connection
}
window.removeEventListener('resize', handleResize);
term.dispose(); // Clean up XTerm instance
const styleElement = document.getElementById("xterm-custom-style");
if (styleElement) {
styleElement.remove(); // Clean up CSS on unmount
}
};
}, []);
return <div ref={terminalRef} style={{ width: "100%", height: "500px" }} />;
return <div ref={terminalRef} style={{ width: "100%", height: "500px"}} />;
};

View File

@@ -62,7 +62,7 @@ export const putEphemeralUserSettings = async (userSubscriptionId: string, userR
preferredOsType: OsType.Linux,
preferredShellType: ShellType.Bash,
preferredLocation: userRegion,
networkType: NetworkType.Default,
networkType: (!vNetSettings || Object.keys(vNetSettings).length === 0) ? NetworkType.Default : (vNetSettings ? NetworkType.Isolated : NetworkType.Default),
sessionType: SessionType.Ephemeral,
userSubscription: userSubscriptionId,
vnetSettings: vNetSettings ?? {}

View File

@@ -38,9 +38,9 @@ export type UserSettingProperties = {
}
export type VnetSettings = {
networkProfileResourceId: string;
relayNamespaceResourceId: string;
location: string;
networkProfileResourceId?: string;
relayNamespaceResourceId?: string;
location?: string;
}
export type ProvisionConsoleResponse = {

View File

@@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
import { RelayNamespaceResponse, VnetModel } from "Explorer/Tabs/CloudShellTab/DataModels";
import { RelayNamespaceResponse, VnetModel, VnetSettings } from "Explorer/Tabs/CloudShellTab/DataModels";
import { listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { Terminal } from "xterm";
import { TerminalKind } from "../../../Contracts/ViewModels";
@@ -15,22 +15,37 @@ import { LogError, LogInfo } from "./LogFormatter";
export const startCloudShellTerminal = async (terminal: Terminal, shellType: TerminalKind) => {
// validate that the subscription id is registered in the CloudShell namespace
terminal.writeln("");
try {
const response: any = await verifyCloudShellProviderRegistration(userContext.subscriptionId);
if (response.registrationState !== "Registered") {
await registerCloudShellProvider(userContext.subscriptionId);
}
terminal.writeln("\x1B[34mVerifying CloudShell provider registration...\x1B[0m");
const response: any = await verifyCloudShellProviderRegistration(userContext.subscriptionId);
if (response.registrationState !== "Registered") {
terminal.writeln("\x1B[33mCloudShell provider registration is not found. Registering now...\x1B[0m");
await registerCloudShellProvider(userContext.subscriptionId);
terminal.writeln("\x1B[32mCloudShell provider registration completed successfully.\x1B[0m");
}
} catch (err) {
terminal.writeln(LogError('Unable to verify CloudShell provider registration.'));
terminal.writeln("\x1B[31mError: Unable to verify CloudShell provider registration.\x1B[0m");
throw err;
}
const settings = await getUserSettings();
let vNetSettings = {};
terminal.writeln("");
terminal.writeln("\x1B[34mFetching user settings...\x1B[0m");
let settings;
try {
settings = await getUserSettings();
}
catch (err) {
terminal.writeln("\x1B[33mNo user settings found. Proceeding with default settings.\x1B[0m");
}
let vNetSettings: VnetSettings;
if(settings?.properties?.vnetSettings && Object.keys(settings?.properties?.vnetSettings).length > 0) {
terminal.writeln(" Network Profile Resource ID: " + settings.properties.vnetSettings.networkProfileResourceId);
terminal.writeln(" Relay Namespace Resource ID: " + settings.properties.vnetSettings.relayNamespaceResourceId);
terminal.writeln("\x1B[1mUsing existing VNet settings:\x1B[0m");
terminal.writeln(" - Network Profile Resource ID: \x1B[32m" + settings.properties.vnetSettings.networkProfileResourceId + "\x1B[0m");
terminal.writeln(" - Relay Namespace Resource ID: \x1B[32m" + settings.properties.vnetSettings.relayNamespaceResourceId + "\x1B[0m");
terminal.writeln(" - VNet Location: \x1B[32m" + settings.properties.vnetSettings.location + "\x1B[0m");
vNetSettings = {
networkProfileResourceId: settings.properties.vnetSettings.networkProfileResourceId,
@@ -40,103 +55,101 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
}
else
{
terminal.writeln("\x1B[1;31m No VNet settings found. Continuing with default settings\x1B[0m");
terminal.writeln("\x1B[33mNo existing VNet settings found. Proceeding with default settings.\x1B[0m");
}
terminal.writeln("\x1B[1;37m Press '1' to continue with current or default setting.\x1B[0m");
terminal.writeln("\x1B[1;37m Press '2' to configure new VNet to CloudShell.\x1B[0m");
terminal.writeln("\x1B[1;37m Press '3' to Reset CloudShell VNet Settings.\x1B[0m");
terminal.writeln("\x1B[1;37m Note: To learn how to configure VNet for CloudShell, go to this link https://aka.ms/cloudhellvnetsetup \x1B[0m");
terminal.writeln("");
terminal.writeln("\x1B[1mSelect an option to continue:\x1B[0m");
terminal.writeln("\x1B[33m1 - Use current/default VNet settings\x1B[0m");
terminal.writeln("\x1B[33m2 - Configure a new VNet for CloudShell\x1B[0m");
terminal.writeln("\x1B[33m3 - Reset CloudShell VNet settings to default\x1B[0m");
terminal.writeln("\x1B[34mFor details on configuring VNet for CloudShell, visit: https://aka.ms/cloudshellvnetsetup\x1B[0m");
terminal.focus();
let isDefaultSetting = false;
const handleKeyPress = terminal.onKey(async ({ key }: { key: string }) => {
terminal.writeln("")
if (key === "1") {
terminal.writeln("\x1B[1;32m Pressed 1, Continuing with current/default settings.\x1B[0m");
isDefaultSetting = true;
terminal.writeln("\x1B[34mYou selected option 1: Proceeding with current/default settings.\x1B[0m");
handleKeyPress.dispose();
}
else if (key === "2") {
isDefaultSetting = false;
terminal.writeln("\x1B[34mYou selected option 2: Please provide the following details.\x1B[0m");
handleKeyPress.dispose();
const subscriptionId = await askQuestion(terminal, "Enter VNet Subscription ID");
const resourceGroup = await askQuestion(terminal, "Enter VNet Resource Group");
const vNetName = await askQuestion(terminal, "Enter VNet Name");
terminal.writeln("\x1B[34mFetching VNet details...\x1B[0m");
const vNetConfig = await getARMInfo<VnetModel>(`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/virtualNetworks/${vNetName}`);
terminal.writeln(" - VNet Location: \x1B[32m" + vNetConfig.location + "\x1B[0m");
terminal.writeln(" - Available Network Profiles:");
const ipConfigurationProfiles = vNetConfig.properties.subnets.reduce<{ id: string }[]>(
(acc, subnet) => acc.concat(subnet.properties.ipConfigurationProfiles || []),
[]);
for (let i = 0; i < ipConfigurationProfiles.length; i++) {
const match = ipConfigurationProfiles[i].id.match(/\/networkProfiles\/([^/]+)/);
const result = match ? `${match[1]}` : null;
terminal.writeln(" - \x1B[32m" + result + "\x1B[0m");
}
const networkProfile = await askQuestion(terminal, "Network Profile to use");
const relays = await getARMInfo<RelayNamespaceResponse>(
`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Relay/namespaces`,
"2024-01-01");
terminal.writeln(" - Available Network Relays:");
for (let i = 0; i < relays.value.length; i++) {
terminal.writeln(" - \x1B[32m" + relays.value[i].name + "\x1B[0m");
}
const relayName = await askQuestion(terminal, "Network Relay to use:");
const networkProfileResourceId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/networkProfiles/${networkProfile.replace(/[\n\r]/g, "")}`;
const relayResourceId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Relay/namespaces/${relayName.replace(/[\n\r]/g, "")}`;
vNetSettings = {
networkProfileResourceId: networkProfileResourceId,
relayNamespaceResourceId: relayResourceId,
location: vNetConfig.location
};
}
else if (key === "3") {
isDefaultSetting = true;
terminal.writeln("\x1B[34mYou selected option 3: Resetting VNet settings to default.\x1B[0m");
vNetSettings = {};
handleKeyPress.dispose();
}
else {
terminal.writeln("\x1B[1;31m Entered Wrong Input, only 1 or 2 are allowed. Exiting...\x1B[0m");
terminal.writeln("\x1B[31mInvalid selection. Only options 1, 2, and 3 are allowed. Exiting...\x1B[0m");
setTimeout(() => terminal.dispose(), 2000); // Close terminal after 2 sec
handleKeyPress.dispose();
return;
}
if (!isDefaultSetting) {
terminal.writeln("\x1B[1;32m Pressed 2, Enter below details:\x1B[0m");
const subscriptionId = await askQuestion(terminal, "Existing VNet Subscription ID");
const resourceGroup = await askQuestion(terminal, "Existing VNet Resource Group");
const vNetName = await askQuestion(terminal, "Existing VNet Name");
terminal.writeln("\x1B[34mDetermining CloudShell region...\x1B[0m");
const region = vNetSettings?.location ?? userContext.databaseAccount?.location;
terminal.writeln(" - Identified Database Account Region: \x1B[32m" + region + "\x1B[0m");
const vNetConfig = await getARMInfo<VnetModel>(`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/virtualNetworks/${vNetName}`);
terminal.writeln("Suggested Network Profiles:");
const ipConfigurationProfiles = vNetConfig.properties.subnets.reduce<{ id: string }[]>(
(acc, subnet) => acc.concat(subnet.properties.ipConfigurationProfiles || []),
[]
);
for (let i = 0; i < ipConfigurationProfiles.length; i++) {
const match = ipConfigurationProfiles[i].id.match(/\/networkProfiles\/([^/]+)/);
const result = match ? `/${match[1]}` : null;
terminal.writeln(result);
}
const networkProfile = await askQuestion(terminal, "Associated Network Profile");
const relays = await getARMInfo<RelayNamespaceResponse>(
`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Relay/namespaces`,
"2024-01-01");
terminal.writeln("Suggested Network Relays:");
for (let i = 0; i < relays.value.length; i++) {
terminal.writeln(relays.value[i].name);
}
const relayName = await askQuestion(terminal, "Network Relay");
const networkProfileResourceId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/networkProfiles/${networkProfile}`;
const relayResourceId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Relay/namespaces/${relayName}`;
// const vNetRules = userContext.databaseAccount.properties?.virtualNetworkRules;
// if(vNetRules && vNetRules.length > 0) {
// for (let i = 0; i < vNetRules.length; i++) {
// const vNetName = vNetRules[i].id;
// terminal.writeln(vNetName);
// }
// }
vNetSettings = {
networkProfileResourceId: networkProfileResourceId,
relayNamespaceResourceId: relayResourceId,
location: userContext.databaseAccount.location
};
}
const region = userContext.databaseAccount?.location;
terminal.writeln(LogInfo(` Database Account Region identified as '${region}'`));
const defaultCloudShellRegion = "westus";
const resolvedRegion = getNormalizedRegion(region, defaultCloudShellRegion);
terminal.writeln("\x1B[33mAttempting to provision CloudShell in region: \x1B[1m" + resolvedRegion + "\x1B[0m");
try {
terminal.writeln("\x1B[34mProvisioning CloudShell session...\x1B[0m");
var { socketUri, provisionConsoleResponse, targetUri } = await provisionCloudShellSession(resolvedRegion, terminal, vNetSettings);
}
catch (err) {
terminal.writeln(LogError(err));
terminal.writeln(LogError(`Unable to provision console in request region, Falling back to default region i.e. ${defaultCloudShellRegion}`));
terminal.writeln("\x1B[31mError: Unable to provision CloudShell in the requested region.\x1B[0m");
terminal.writeln("\x1B[33mFalling back to default region: " + defaultCloudShellRegion + "\x1B[0m");
terminal.writeln("\x1B[34mAttempting to provision CloudShell in the default region...\x1B[0m");
var { socketUri, provisionConsoleResponse, targetUri } = await provisionCloudShellSession(defaultCloudShellRegion, terminal, vNetSettings);
}
@@ -162,12 +175,15 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
// authorize the session
try {
terminal.writeln("\x1B[34mAuthorizing CloudShell session...\x1B[0m");
const authorizeResponse = await authorizeSession(provisionConsoleResponse.properties.uri);
const cookieToken = authorizeResponse.token;
const a = document.createElement("img");
a.src = targetUri + "&token=" + encodeURIComponent(cookieToken);
terminal.writeln("\x1B[32mAuthorization successful. Establishing connection...\x1B[0m");
} catch (err) {
terminal.writeln(LogError('Unable to authorize the session'));
terminal.writeln("\x1B[31mError: Unable to authorize CloudShell session.\x1B[0m");
socket.close();
throw err;
}
@@ -182,7 +198,8 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
const askQuestion = (terminal: any, question: string) => {
return new Promise<string>((resolve) => {
terminal.write(`\x1B[1;34m${question}: \x1B[0m`);
const prompt = `\x1B[1;34m${question}: \x1B[0m`;
terminal.writeln(prompt);
terminal.focus();
let response = "";
@@ -194,11 +211,18 @@ const askQuestion = (terminal: any, question: string) => {
} else if (data === "\u007F" || data === "\b") { // Handle backspace
if (response.length > 0) {
response = response.slice(0, -1);
terminal.write("\b \b"); // Move cursor back, clear character
terminal.write("\x1B[D \x1B[D");// Move cursor back, clear character
}
} else {
} else if (data.charCodeAt(0) >= 32) { // Ignore control characters
response += data;
terminal.write(data); // Display the typed or pasted characters
terminal.write(data); // Display typed characters
}
});
// Prevent cursor movement beyond the prompt
terminal.onKey(({ domEvent} : { domEvent: KeyboardEvent }) => {
if (domEvent.key === "ArrowLeft" && response.length === 0) {
domEvent.preventDefault(); // Stop moving left beyond the question
}
});
});
@@ -299,10 +323,12 @@ const provisionCloudShellSession = async(
vNetSettings: object
): Promise<{ socketUri?: string; provisionConsoleResponse?: any; targetUri?: string }> => {
return new Promise((resolve, reject) => {
terminal.writeln("");
// Show consent message inside the terminal
terminal.writeln(`\x1B[1;33m ⚠️ Are you agreeing to continue with CloudShell terminal at ${resolvedRegion}.\x1B[0m`);
terminal.writeln("\x1B[1;37m Press 'Y' to continue or 'N' to exit.\x1B[0m");
terminal.writeln(`\x1B[1;33m⚠ CloudShell is not available in your database account region.\x1B[0m`);
terminal.writeln(`\x1B[1;33mWould you like to continue with CloudShell in ${resolvedRegion} instead?\x1B[0m`);
terminal.writeln("\x1B[1;37mPress 'Y' to proceed or 'N' to cancel.\x1B[0m");
terminal.focus();
// Listen for user input
const handleKeyPress = terminal.onKey(async ({ key }: { key: string }) => {
@@ -310,8 +336,9 @@ const provisionCloudShellSession = async(
handleKeyPress.dispose();
if (key.toLowerCase() === "y") {
terminal.writeln("\x1B[1;32mConsent given. Requesting CloudShell. !\x1B[0m");
terminal.writeln(LogInfo('Applying fresh user settings...'));
terminal.writeln("\x1B[1;32mConsent received. Requesting CloudShell...\x1B[0m");
terminal.writeln("");
terminal.writeln(LogInfo('Applying User Settings...'));
try {
await putEphemeralUserSettings(userContext.subscriptionId, resolvedRegion, vNetSettings);
} catch (err) {
@@ -327,19 +354,20 @@ const provisionCloudShellSession = async(
terminal.writeln(LogError('Unable to provision console.'));
return reject(err);
}
// Add check for provisioning state
if (provisionConsoleResponse.properties.provisioningState !== "Succeeded") {
terminal.writeln(LogError("Failed to provision console."));
return reject(new Error("Failed to provision console."));
}
terminal.writeln(LogInfo("Connecting to CloudShell Terminal..."));
terminal.writeln("\x1B[34mConnecting to CloudShell Terminal...\x1B[0m");
// connect the terminal
let connectTerminalResponse;
try {
connectTerminalResponse = await connectTerminal(provisionConsoleResponse.properties.uri, { rows: terminal.rows, cols: terminal.cols });
} catch (err) {
terminal.writeln(LogError('Unable to connect terminal.'));
terminal.writeln(LogError(`Unable to connect terminal. ${err}`));
return reject(err);
}
@@ -359,7 +387,7 @@ const provisionCloudShellSession = async(
} else if (key.toLowerCase() === "n") {
terminal.writeln("\x1B[1;31m Consent denied. Exiting...\x1B[0m");
terminal.writeln("\x1B[1;31mConsent denied. Exiting...\x1B[0m");
setTimeout(() => terminal.dispose(), 2000); // Close terminal after 2 sec
return resolve({});
}