mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-03-06 09:58:25 +00:00
code refactor
This commit is contained in:
parent
942de980c3
commit
ec891671b6
63
src/Explorer/Tabs/CloudShellTab/CloudShellTabComponent.tsx
Normal file
63
src/Explorer/Tabs/CloudShellTab/CloudShellTabComponent.tsx
Normal file
@ -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<CloudShellTerminalProps> = ({
|
||||||
|
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 <div ref={terminalRef} style={{ width: "100%", height: "500px" }} />;
|
||||||
|
};
|
56
src/Explorer/Tabs/CloudShellTab/Commands.tsx
Normal file
56
src/Explorer/Tabs/CloudShellTab/Commands.tsx
Normal file
@ -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, string[]> = {
|
||||||
|
[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"
|
||||||
|
],
|
||||||
|
};
|
@ -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 getUserRegion = async (subscriptionId: string, resourceGroup: string, accountName: string) => {
|
||||||
export const trackedApiCall = <T extends Array<any>, U>(apiCall: (...args: T) => Promise<U>, name: string) => {
|
|
||||||
return async (...args: T): Promise<U> => {
|
|
||||||
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) => {
|
|
||||||
return await armRequest({
|
return await armRequest({
|
||||||
host: configContext.ARM_ENDPOINT,
|
host: configContext.ARM_ENDPOINT,
|
||||||
path: `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`,
|
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"
|
apiVersion: "2022-12-01"
|
||||||
});
|
});
|
||||||
|
|
||||||
}, "getUserRegion");
|
};
|
||||||
|
|
||||||
export const getUserSettings = trackedApiCall(async (): Promise<Settings> => {
|
export const getUserSettings = async (): Promise<Settings> => {
|
||||||
const resp = await armRequest<any>({
|
const resp = await armRequest<any>({
|
||||||
host: configContext.ARM_ENDPOINT,
|
host: configContext.ARM_ENDPOINT,
|
||||||
path: `/providers/Microsoft.Portal/userSettings/cloudconsole`,
|
path: `/providers/Microsoft.Portal/userSettings/cloudconsole`,
|
||||||
@ -53,9 +43,9 @@ export const getUserSettings = trackedApiCall(async (): Promise<Settings> => {
|
|||||||
sessionType: resp?.properties?.sessionType,
|
sessionType: resp?.properties?.sessionType,
|
||||||
osType: resp?.properties?.preferredOsType
|
osType: resp?.properties?.preferredOsType
|
||||||
};
|
};
|
||||||
}, "getUserSettings");
|
};
|
||||||
|
|
||||||
export const putEphemeralUserSettings = trackedApiCall(async (userSubscriptionId: string, userRegion: string) => {
|
export const putEphemeralUserSettings = async (userSubscriptionId: string, userRegion: string) => {
|
||||||
const ephemeralSettings = {
|
const ephemeralSettings = {
|
||||||
properties: {
|
properties: {
|
||||||
preferredOsType: OsType.Linux,
|
preferredOsType: OsType.Linux,
|
||||||
@ -80,7 +70,7 @@ export const putEphemeralUserSettings = trackedApiCall(async (userSubscriptionId
|
|||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
|
|
||||||
}, "putEphemeralUserSettings");
|
};
|
||||||
|
|
||||||
export const verifyCloudshellProviderRegistration = async(subscriptionId: string) => {
|
export const verifyCloudshellProviderRegistration = async(subscriptionId: string) => {
|
||||||
return await armRequest({
|
return await armRequest({
|
||||||
@ -106,7 +96,7 @@ export const registerCloudShellProvider = async (subscriptionId: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const provisionConsole = trackedApiCall(async (subscriptionId: string, location: string): Promise<ProvisionConsoleResponse> => {
|
export const provisionConsole = async (subscriptionId: string, location: string): Promise<ProvisionConsoleResponse> => {
|
||||||
const data = {
|
const data = {
|
||||||
properties: {
|
properties: {
|
||||||
osType: OsType.Linux
|
osType: OsType.Linux
|
||||||
@ -124,9 +114,9 @@ export const provisionConsole = trackedApiCall(async (subscriptionId: string, lo
|
|||||||
},
|
},
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
}, "provisionConsole");
|
};
|
||||||
|
|
||||||
export const connectTerminal = trackedApiCall(async (consoleUri: string, size: { rows: number, cols: number }): Promise<ConnectTerminalResponse> => {
|
export const connectTerminal = async (consoleUri: string, size: { rows: number, cols: number }): Promise<ConnectTerminalResponse> => {
|
||||||
const targetUri = consoleUri + `/terminals?cols=${size.cols}&rows=${size.rows}&version=2019-01-01&shell=bash`;
|
const targetUri = consoleUri + `/terminals?cols=${size.cols}&rows=${size.rows}&version=2019-01-01&shell=bash`;
|
||||||
const resp = await fetch(targetUri, {
|
const resp = await fetch(targetUri, {
|
||||||
method: "post",
|
method: "post",
|
||||||
@ -141,9 +131,9 @@ export const connectTerminal = trackedApiCall(async (consoleUri: string, size: {
|
|||||||
body: "{}" // empty body is necessary
|
body: "{}" // empty body is necessary
|
||||||
});
|
});
|
||||||
return resp.json();
|
return resp.json();
|
||||||
}, "connectTerminal");
|
};
|
||||||
|
|
||||||
export const authorizeSession = trackedApiCall(async (consoleUri: string): Promise<Authorization> => {
|
export const authorizeSession = async (consoleUri: string): Promise<Authorization> => {
|
||||||
const targetUri = consoleUri + "/authorize";
|
const targetUri = consoleUri + "/authorize";
|
||||||
const resp = await fetch(targetUri, {
|
const resp = await fetch(targetUri, {
|
||||||
method: "post",
|
method: "post",
|
||||||
@ -156,7 +146,7 @@ export const authorizeSession = trackedApiCall(async (consoleUri: string): Promi
|
|||||||
body: "{}" // empty body is necessary
|
body: "{}" // empty body is necessary
|
||||||
});
|
});
|
||||||
return resp.json();
|
return resp.json();
|
||||||
}, "authorizeSession");
|
};
|
||||||
|
|
||||||
export const getLocale = () => {
|
export const getLocale = () => {
|
||||||
const langLocale = navigator.language;
|
const langLocale = navigator.language;
|
||||||
|
@ -154,16 +154,17 @@ const provisionCloudShellSession = async(
|
|||||||
): Promise<{ socketUri?: string; provisionConsoleResponse?: any; targetUri?: string }> => {
|
): Promise<{ socketUri?: string; provisionConsoleResponse?: any; targetUri?: string }> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Show consent message inside the terminal
|
// 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.writeln("\x1B[1;37mPress 'Y' to continue or 'N' to exit.\x1B[0m");
|
||||||
|
|
||||||
|
xterminal.focus();
|
||||||
// Listen for user input
|
// Listen for user input
|
||||||
const handleKeyPress = xterminal.onKey(async ({ key }: { key: string }) => {
|
const handleKeyPress = xterminal.onKey(async ({ key }: { key: string }) => {
|
||||||
// Remove the event listener after first execution
|
// Remove the event listener after first execution
|
||||||
handleKeyPress.dispose();
|
handleKeyPress.dispose();
|
||||||
|
|
||||||
if (key.toLowerCase() === "y") {
|
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 {
|
try {
|
||||||
await putEphemeralUserSettings(userContext.subscriptionId, resolvedRegion);
|
await putEphemeralUserSettings(userContext.subscriptionId, resolvedRegion);
|
||||||
@ -198,8 +199,7 @@ const provisionCloudShellSession = async(
|
|||||||
return reject(new Error("Failed to provision console."));
|
return reject(new Error("Failed to provision console."));
|
||||||
}
|
}
|
||||||
|
|
||||||
xterminal.writeln(LogInfo("Connecting to cloudshell"));
|
xterminal.writeln(LogInfo("Connecting to Cloudshell Terminal...\n\r"));
|
||||||
xterminal.writeln(LogInfo("Please wait..."));
|
|
||||||
// connect the terminal
|
// connect the terminal
|
||||||
let connectTerminalResponse;
|
let connectTerminalResponse;
|
||||||
try {
|
try {
|
||||||
@ -226,7 +226,7 @@ const provisionCloudShellSession = async(
|
|||||||
|
|
||||||
} else if (key.toLowerCase() === "n") {
|
} 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
|
setTimeout(() => xterminal.dispose(), 2000); // Close terminal after 2 sec
|
||||||
return resolve({});
|
return resolve({});
|
||||||
}
|
}
|
||||||
|
@ -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<CloudShellTerminalProps> = ({
|
|
||||||
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 <div ref={terminalRef} style={{ width: "100%", height: "500px" }} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,7 +13,7 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu
|
|||||||
import { NotebookTerminalComponent } from "../Controls/Notebook/NotebookTerminalComponent";
|
import { NotebookTerminalComponent } from "../Controls/Notebook/NotebookTerminalComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
import { CloudShellTerminalComponent } from "./CloudShellTerminalComponent";
|
import { CloudShellTerminalComponent } from "./CloudShellTab/CloudShellTabComponent";
|
||||||
import TabsBase from "./TabsBase";
|
import TabsBase from "./TabsBase";
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user