From ec891671b678336d928be87da728c42f8447e245 Mon Sep 17 00:00:00 2001 From: Sourabh Jain Date: Fri, 28 Feb 2025 07:55:48 +0530 Subject: [PATCH] code refactor --- .../CloudShellTab/CloudShellTabComponent.tsx | 63 ++++++++++++++ src/Explorer/Tabs/CloudShellTab/Commands.tsx | 56 ++++++++++++ src/Explorer/Tabs/CloudShellTab/Data.tsx | 34 +++----- .../Tabs/CloudShellTab/UseTerminal.tsx | 10 +-- .../Tabs/CloudShellTerminalComponent.tsx | 86 ------------------- src/Explorer/Tabs/TerminalTab.tsx | 2 +- 6 files changed, 137 insertions(+), 114 deletions(-) create mode 100644 src/Explorer/Tabs/CloudShellTab/CloudShellTabComponent.tsx create mode 100644 src/Explorer/Tabs/CloudShellTab/Commands.tsx delete mode 100644 src/Explorer/Tabs/CloudShellTerminalComponent.tsx diff --git a/src/Explorer/Tabs/CloudShellTab/CloudShellTabComponent.tsx b/src/Explorer/Tabs/CloudShellTab/CloudShellTabComponent.tsx new file mode 100644 index 000000000..720a77f50 --- /dev/null +++ b/src/Explorer/Tabs/CloudShellTab/CloudShellTabComponent.tsx @@ -0,0 +1,63 @@ +import React, { useEffect, useRef } from "react"; +import { Terminal } from "xterm"; +import { FitAddon } from 'xterm-addon-fit'; +import "xterm/css/xterm.css"; +import { TerminalKind } from "../../../Contracts/ViewModels"; +import { getAuthorizationHeader } from "../../../Utils/AuthorizationUtils"; +import { getCommands } from "./Commands"; +import { startCloudShellterminal } from "./UseTerminal"; + +export interface CloudShellTerminalProps { + shellType: TerminalKind; +} + +export const CloudShellTerminalComponent: React.FC = ({ + shellType +}: CloudShellTerminalProps) => { + const terminalRef = useRef(null); // Reference for terminal container + const xtermRef = useRef(null); // Reference for XTerm instance + const socketRef = useRef(null); // Reference for WebSocket + const fitAddon = new FitAddon(); + + useEffect(() => { + // Initialize XTerm instance + const term = new Terminal({ + cursorBlink: true, + theme: { background: "#1d1f21", foreground: "#c5c8c6" } + }); + + term.loadAddon(fitAddon); + + // Attach terminal to the DOM + if (terminalRef.current) { + term.open(terminalRef.current); + xtermRef.current = term; + } + fitAddon.fit(); + + // Adjust terminal size on window resize + const handleResize = () => fitAddon.fit(); + window.addEventListener('resize', handleResize); + + const authorizationHeader = getAuthorizationHeader() + socketRef.current = startCloudShellterminal(term, getCommands(shellType), authorizationHeader.token); + + term.onData((data) => { + if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { + socketRef.current.send(data); + } + }); + + // Cleanup function to close WebSocket and dispose terminal + return () => { + if (socketRef.current) { + socketRef.current.close(); // Close WebSocket connection + } + window.removeEventListener('resize', handleResize); + term.dispose(); // Clean up XTerm instance + }; + + }, []); + + return
; +}; diff --git a/src/Explorer/Tabs/CloudShellTab/Commands.tsx b/src/Explorer/Tabs/CloudShellTab/Commands.tsx new file mode 100644 index 000000000..0aaf94469 --- /dev/null +++ b/src/Explorer/Tabs/CloudShellTab/Commands.tsx @@ -0,0 +1,56 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + */ + +import { TerminalKind } from "../../../Contracts/ViewModels"; + +export const getCommands = (terminalKind: TerminalKind): string => { + if (!Commands[terminalKind]) { + throw new Error(`Unsupported terminal kind: ${terminalKind}`); + } + return Commands[terminalKind].join("\n").concat("\n"); +}; + +export const Commands: Record = { + [TerminalKind.Postgres]: [ + "curl -s https://ipinfo.io", + "curl -LO https://ftp.postgresql.org/pub/source/v15.2/postgresql-15.2.tar.bz2", + "tar -xvjf postgresql-15.2.tar.bz2", + "cd postgresql-15.2", + "mkdir ~/pgsql", + "curl -LO https://ftp.gnu.org/gnu/readline/readline-8.1.tar.gz", + "tar -xvzf readline-8.1.tar.gz", + "cd readline-8.1", + "./configure --prefix=$HOME/pgsql" + ], + [TerminalKind.Mongo]: [ + "curl -s https://ipinfo.io", + "curl -LO https://downloads.mongodb.com/compass/mongosh-2.3.8-linux-x64.tgz", + "tar -xvzf mongosh-2.3.8-linux-x64.tgz", + "mkdir -p ~/mongosh && mv mongosh-2.3.8-linux-x64/* ~/mongosh/", + "echo 'export PATH=$PATH:$HOME/mongosh/bin' >> ~/.bashrc", + "source ~/.bashrc", + "mongosh --version" + ], + [TerminalKind.VCoreMongo]: [ + "curl -s https://ipinfo.io", + "curl -LO https://downloads.mongodb.com/compass/mongosh-2.3.8-linux-x64.tgz", + "tar -xvzf mongosh-2.3.8-linux-x64.tgz", + "mkdir -p ~/mongosh && mv mongosh-2.3.8-linux-x64/* ~/mongosh/", + "echo 'export PATH=$PATH:$HOME/mongosh/bin' >> ~/.bashrc", + "source ~/.bashrc", + "mongosh --version" + ], + [TerminalKind.Cassandra]: [ + "curl -s https://ipinfo.io", + "curl -LO https://downloads.apache.org/cassandra/4.1.2/apache-cassandra-4.1.2-bin.tar.gz", + "tar -xvzf apache-cassandra-4.1.2-bin.tar.gz", + "cd apache-cassandra-4.1.2", + "mkdir ~/cassandra", + "echo 'export CASSANDRA_HOME=$HOME/cassandra' >> ~/.bashrc", + "source ~/.bashrc" + ], + [TerminalKind.Default]: [ + "echo Unknown Shell" + ], +}; \ No newline at end of file diff --git a/src/Explorer/Tabs/CloudShellTab/Data.tsx b/src/Explorer/Tabs/CloudShellTab/Data.tsx index 1d41dd6d6..a31c9d8d5 100644 --- a/src/Explorer/Tabs/CloudShellTab/Data.tsx +++ b/src/Explorer/Tabs/CloudShellTab/Data.tsx @@ -17,17 +17,7 @@ export const validateUserSettings = (userSettings: Settings) => { } } -// https://stackoverflow.com/q/38598280 (Is it possible to wrap a function and retain its types?) -export const trackedApiCall = , U>(apiCall: (...args: T) => Promise, name: string) => { - return async (...args: T): Promise => { - const startTime = Date.now(); - const result = await apiCall(...args); - const endTime = Date.now(); - return result; - }; -}; - -export const getUserRegion = trackedApiCall(async (subscriptionId: string, resourceGroup: string, accountName: string) => { +export const getUserRegion = async (subscriptionId: string, resourceGroup: string, accountName: string) => { return await armRequest({ host: configContext.ARM_ENDPOINT, path: `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`, @@ -35,9 +25,9 @@ export const getUserRegion = trackedApiCall(async (subscriptionId: string, resou apiVersion: "2022-12-01" }); -}, "getUserRegion"); +}; -export const getUserSettings = trackedApiCall(async (): Promise => { +export const getUserSettings = async (): Promise => { const resp = await armRequest({ host: configContext.ARM_ENDPOINT, path: `/providers/Microsoft.Portal/userSettings/cloudconsole`, @@ -53,9 +43,9 @@ export const getUserSettings = trackedApiCall(async (): Promise => { sessionType: resp?.properties?.sessionType, osType: resp?.properties?.preferredOsType }; -}, "getUserSettings"); +}; -export const putEphemeralUserSettings = trackedApiCall(async (userSubscriptionId: string, userRegion: string) => { +export const putEphemeralUserSettings = async (userSubscriptionId: string, userRegion: string) => { const ephemeralSettings = { properties: { preferredOsType: OsType.Linux, @@ -80,7 +70,7 @@ export const putEphemeralUserSettings = trackedApiCall(async (userSubscriptionId return resp; -}, "putEphemeralUserSettings"); +}; export const verifyCloudshellProviderRegistration = async(subscriptionId: string) => { return await armRequest({ @@ -106,7 +96,7 @@ export const registerCloudShellProvider = async (subscriptionId: string) => { }); }; -export const provisionConsole = trackedApiCall(async (subscriptionId: string, location: string): Promise => { +export const provisionConsole = async (subscriptionId: string, location: string): Promise => { const data = { properties: { osType: OsType.Linux @@ -124,9 +114,9 @@ export const provisionConsole = trackedApiCall(async (subscriptionId: string, lo }, body: data, }); -}, "provisionConsole"); +}; -export const connectTerminal = trackedApiCall(async (consoleUri: string, size: { rows: number, cols: number }): Promise => { +export const connectTerminal = async (consoleUri: string, size: { rows: number, cols: number }): Promise => { const targetUri = consoleUri + `/terminals?cols=${size.cols}&rows=${size.rows}&version=2019-01-01&shell=bash`; const resp = await fetch(targetUri, { method: "post", @@ -141,9 +131,9 @@ export const connectTerminal = trackedApiCall(async (consoleUri: string, size: { body: "{}" // empty body is necessary }); return resp.json(); -}, "connectTerminal"); +}; -export const authorizeSession = trackedApiCall(async (consoleUri: string): Promise => { +export const authorizeSession = async (consoleUri: string): Promise => { const targetUri = consoleUri + "/authorize"; const resp = await fetch(targetUri, { method: "post", @@ -156,7 +146,7 @@ export const authorizeSession = trackedApiCall(async (consoleUri: string): Promi body: "{}" // empty body is necessary }); return resp.json(); -}, "authorizeSession"); +}; export const getLocale = () => { const langLocale = navigator.language; diff --git a/src/Explorer/Tabs/CloudShellTab/UseTerminal.tsx b/src/Explorer/Tabs/CloudShellTab/UseTerminal.tsx index 2f40c3e35..34755cf12 100644 --- a/src/Explorer/Tabs/CloudShellTab/UseTerminal.tsx +++ b/src/Explorer/Tabs/CloudShellTab/UseTerminal.tsx @@ -154,16 +154,17 @@ const provisionCloudShellSession = async( ): Promise<{ socketUri?: string; provisionConsoleResponse?: any; targetUri?: string }> => { return new Promise((resolve, reject) => { // Show consent message inside the terminal - xterminal.writeln(`\x1B[1;33m⚠️ Are you agreeing to continue with cloudshell terminal at ${resolvedRegion}.\x1B[0m`); + xterminal.writeln(`\x1B[1;33m⚠️ Are you agreeing to continue with CloudShell terminal at ${resolvedRegion}.\x1B[0m`); xterminal.writeln("\x1B[1;37mPress 'Y' to continue or 'N' to exit.\x1B[0m"); + xterminal.focus(); // Listen for user input const handleKeyPress = xterminal.onKey(async ({ key }: { key: string }) => { // Remove the event listener after first execution handleKeyPress.dispose(); if (key.toLowerCase() === "y") { - xterminal.writeln("\x1B[1;32m✅ Consent given. Terminal ready!\x1B[0m"); + xterminal.writeln("\x1B[1;32mConsent given. Requesting CloudShell. !\x1B[0m"); try { await putEphemeralUserSettings(userContext.subscriptionId, resolvedRegion); @@ -198,8 +199,7 @@ const provisionCloudShellSession = async( return reject(new Error("Failed to provision console.")); } - xterminal.writeln(LogInfo("Connecting to cloudshell")); - xterminal.writeln(LogInfo("Please wait...")); + xterminal.writeln(LogInfo("Connecting to Cloudshell Terminal...\n\r")); // connect the terminal let connectTerminalResponse; try { @@ -226,7 +226,7 @@ const provisionCloudShellSession = async( } else if (key.toLowerCase() === "n") { - xterminal.writeln("\x1B[1;31m❌ Consent denied. Exiting...\x1B[0m"); + xterminal.writeln("\x1B[1;31m Consent denied. Exiting...\x1B[0m"); setTimeout(() => xterminal.dispose(), 2000); // Close terminal after 2 sec return resolve({}); } diff --git a/src/Explorer/Tabs/CloudShellTerminalComponent.tsx b/src/Explorer/Tabs/CloudShellTerminalComponent.tsx deleted file mode 100644 index 4d2af19bd..000000000 --- a/src/Explorer/Tabs/CloudShellTerminalComponent.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import { Terminal } from "xterm"; -import "xterm/css/xterm.css"; -import { TerminalKind } from "../../Contracts/ViewModels"; -import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; -import { startCloudShellterminal } from "./CloudShellTab/UseTerminal"; - -export interface CloudShellTerminalProps { - shellType: TerminalKind; -} - -export const CloudShellTerminalComponent: React.FC = ({ - shellType -}: CloudShellTerminalProps) => { - const terminalRef = useRef(null); // Reference for terminal container - const xtermRef = useRef(null); // Reference for XTerm instance - const socketRef = useRef(null); // Reference for WebSocket - - useEffect(() => { - // Initialize XTerm instance - const term = new Terminal({ - cursorBlink: true, - fontSize: 14, - theme: { background: "#1d1f21", foreground: "#c5c8c6" }, - }); - - // Attach terminal to the DOM - if (terminalRef.current) { - term.open(terminalRef.current); - xtermRef.current = term; - } - - const authorizationHeader = getAuthorizationHeader() - socketRef.current = startCloudShellterminal(term, getCommands(shellType), authorizationHeader.token); - - term.onData((data) => { - if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { - socketRef.current.send(data); - } - }); - - // Cleanup function to close WebSocket and dispose terminal - return () => { - if (socketRef.current) { - socketRef.current.close(); // Close WebSocket connection - } - term.dispose(); // Clean up XTerm instance - }; - - }, []); - - return
; -}; - -export const getCommands = (terminalKind: TerminalKind): string => { - switch (terminalKind) { - case TerminalKind.Postgres: - return `curl -s https://ipinfo.io \n` + - `curl -LO https://ftp.postgresql.org/pub/source/v15.2/postgresql-15.2.tar.bz2 \n` + - `tar -xvjf postgresql-15.2.tar.bz2 \n` + - `cd postgresql-15.2 \n` + - `mkdir ~/pgsql \n` + - `curl -LO https://ftp.gnu.org/gnu/readline/readline-8.1.tar.gz \n` + - `tar -xvzf readline-8.1.tar.gz \n` + - `cd readline-8.1 \n` + - `./configure --prefix=$HOME/pgsql \n`; - case TerminalKind.Mongo || terminalKind === TerminalKind.VCoreMongo: - return `curl -s https://ipinfo.io \n` + - `curl -LO https://downloads.mongodb.com/compass/mongosh-2.3.8-linux-x64.tgz \n` + - `tar -xvzf mongosh-2.3.8-linux-x64.tgz \n` + - `mkdir -p ~/mongosh && mv mongosh-2.3.8-linux-x64/* ~/mongosh/ \n` + - `echo 'export PATH=$PATH:$HOME/mongosh/bin' >> ~/.bashrc \n` + - `source ~/.bashrc \n` + - `mongosh --version \n`; - case TerminalKind.Cassandra: - return `curl -s https://ipinfo.io \n` + - `curl -OL https://archive.apache.org/dist/cassandra/4.0.0/apache-cassandra-4.0.0-bin.tar.gz \n` + - `tar -xvzf apache-cassandra-4.0.0-bin.tar.gz \n` + - `cd apache-cassandra-4.0.0 \n` + - `mkdir ~/cassandra \n` + - `echo 'export CASSANDRA_HOME=$HOME/cassandra' >> ~/.bashrc \n` + - `source ~/.bashrc \n`; - default: - throw new Error("Unsupported terminal kind"); - } -} \ No newline at end of file diff --git a/src/Explorer/Tabs/TerminalTab.tsx b/src/Explorer/Tabs/TerminalTab.tsx index b88d26cc5..c4450d0c7 100644 --- a/src/Explorer/Tabs/TerminalTab.tsx +++ b/src/Explorer/Tabs/TerminalTab.tsx @@ -13,7 +13,7 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu import { NotebookTerminalComponent } from "../Controls/Notebook/NotebookTerminalComponent"; import Explorer from "../Explorer"; import { useNotebook } from "../Notebook/useNotebook"; -import { CloudShellTerminalComponent } from "./CloudShellTerminalComponent"; +import { CloudShellTerminalComponent } from "./CloudShellTab/CloudShellTabComponent"; import TabsBase from "./TabsBase";