From 8212c573516c12a150c0e3a0af98ea9316d6f3af Mon Sep 17 00:00:00 2001 From: Pudong Zheng Date: Wed, 6 Apr 2022 07:35:45 +0000 Subject: [PATCH] [dashboard] support connect via SSH --- .../dashboard/src/start/StartWorkspace.tsx | 20 ++++ .../src/workspaces/ConnectToSSHModal.tsx | 109 ++++++++++++++++++ .../src/workspaces/WorkspaceEntry.tsx | 22 ++++ 3 files changed, 151 insertions(+) create mode 100644 components/dashboard/src/workspaces/ConnectToSSHModal.tsx diff --git a/components/dashboard/src/start/StartWorkspace.tsx b/components/dashboard/src/start/StartWorkspace.tsx index f3350c03f8f421..13af77faf74d17 100644 --- a/components/dashboard/src/start/StartWorkspace.tsx +++ b/components/dashboard/src/start/StartWorkspace.tsx @@ -27,6 +27,7 @@ import PendingChangesDropdown from "../components/PendingChangesDropdown"; import { watchHeadlessLogs } from "../components/PrebuildLogs"; import { getGitpodService, gitpodHostUrl } from "../service/service"; import { StartPage, StartPhase, StartWorkspaceError } from "./StartPage"; +import ConnectToSSHModal from "../workspaces/ConnectToSSHModal"; const sessionId = v4(); const WorkspaceLogs = React.lazy(() => import("../components/WorkspaceLogs")); @@ -91,6 +92,8 @@ export interface StartWorkspaceState { clientID?: string; }; ideOptions?: IDEOptions; + isSSHModalVisible?: boolean; + ownerToken?: string; } export default class StartWorkspace extends React.Component { @@ -519,6 +522,15 @@ export default class StartWorkspace extends React.Component getGitpodService().server.stopWorkspace(this.props.workspaceId), }, + { + title: "Connect via SSH", + onClick: async () => { + const ownerToken = await getGitpodService().server.getOwnerToken( + this.props.workspaceId, + ); + this.setState({ isSSHModalVisible: true, ownerToken }); + }, + }, { title: "Go to Dashboard", href: gitpodHostUrl.asDashboard().toString(), @@ -556,6 +568,14 @@ export default class StartWorkspace extends React.Component . + {this.state.isSSHModalVisible === true && this.state.ownerToken && ( + this.setState({ isSSHModalVisible: false, ownerToken: "" })} + /> + )} ); } diff --git a/components/dashboard/src/workspaces/ConnectToSSHModal.tsx b/components/dashboard/src/workspaces/ConnectToSSHModal.tsx new file mode 100644 index 00000000000000..6e45de5fd65b28 --- /dev/null +++ b/components/dashboard/src/workspaces/ConnectToSSHModal.tsx @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2022 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + +import { useState } from "react"; +import Modal from "../components/Modal"; +import Tooltip from "../components/Tooltip"; +import copy from "../images/copy.svg"; +import AlertBox from "../components/AlertBox"; +import InfoBox from "../components/InfoBox"; + +function InputWithCopy(props: { value: string; tip?: string; className?: string }) { + const [copied, setCopied] = useState(false); + const copyToClipboard = (text: string) => { + const el = document.createElement("textarea"); + el.value = text; + document.body.appendChild(el); + el.select(); + try { + document.execCommand("copy"); + } finally { + document.body.removeChild(el); + } + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + const tip = props.tip ?? "Click to copy"; + return ( +
+ +
copyToClipboard(props.value)}> +
+ + copy icon + +
+
+
+ ); +} + +interface SSHProps { + workspaceId: string; + ownerToken: string; + ideUrl: string; +} + +function SSHView(props: SSHProps) { + const sshCommand = `ssh ${props.workspaceId}#${props.ownerToken}@${props.ideUrl}`; + return ( +
+
+

+ The following shell command can be used to SSH into this workspace. +

+ +

+ Anyone on the internet with this command can access the running workspace. The command + includes a generated access token that resets on every workspace restart. +

+
+ +

+ Before connecting via SSH, make sure you have an existing SSH private key on your machine. You + can create one using  + + ssh-keygen + + . +

+
+
+ +
+ ); +} + +export default function ConnectToSSHModal(props: { + workspaceId: string; + ownerToken: string; + ideUrl: string; + onClose: () => void; +}) { + return ( + +

Connect via SSH

+ +
+ +
+
+ ); +} diff --git a/components/dashboard/src/workspaces/WorkspaceEntry.tsx b/components/dashboard/src/workspaces/WorkspaceEntry.tsx index 9b515e22ce55a2..f0522e34b0a11e 100644 --- a/components/dashboard/src/workspaces/WorkspaceEntry.tsx +++ b/components/dashboard/src/workspaces/WorkspaceEntry.tsx @@ -24,6 +24,7 @@ import PendingChangesDropdown from "../components/PendingChangesDropdown"; import Tooltip from "../components/Tooltip"; import { WorkspaceModel } from "./workspace-model"; import { getGitpodService } from "../service/service"; +import ConnectToSSHModal from "./ConnectToSSHModal"; function getLabel(state: WorkspaceInstancePhase, conditions?: WorkspaceInstanceConditions) { if (conditions?.failed) { @@ -42,6 +43,7 @@ interface Props { export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) { const [isDeleteModalVisible, setDeleteModalVisible] = useState(false); const [isRenameModalVisible, setRenameModalVisible] = useState(false); + const [isSSHModalVisible, setSSHModalVisible] = useState(false); const renameInputRef = useRef(null); const [errorMessage, setErrorMessage] = useState(""); const state: WorkspaceInstancePhase = desc.latestInstance?.status?.phase || "stopped"; @@ -49,6 +51,7 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) { desc.latestInstance?.status.repo?.branch || Workspace.getBranchName(desc.workspace) || ""; const ws = desc.workspace; const [workspaceDescription, setWsDescription] = useState(ws.description); + const [ownerToken, setOwnerToken] = useState(""); const startUrl = new GitpodHostUrl(window.location.href).with({ pathname: "/start/", @@ -77,6 +80,14 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) { title: "Stop", onClick: () => stopWorkspace(ws.id), }); + menuEntries.splice(1, 0, { + title: "Connect via SSH", + onClick: async () => { + const ot = await getGitpodService().server.getOwnerToken(ws.id); + setOwnerToken(ot); + setSSHModalVisible(true); + }, + }); } menuEntries.push({ title: "Download", @@ -234,6 +245,17 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) { + {isSSHModalVisible && desc.latestInstance && ownerToken !== "" && ( + { + setSSHModalVisible(false); + setOwnerToken(""); + }} + /> + )} ); }