Skip to content

Commit

Permalink
[dashboard] support connect via SSH
Browse files Browse the repository at this point in the history
  • Loading branch information
iQQBot committed Apr 6, 2022
1 parent 686a286 commit 8212c57
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 0 deletions.
20 changes: 20 additions & 0 deletions components/dashboard/src/start/StartWorkspace.tsx
Expand Up @@ -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"));
Expand Down Expand Up @@ -91,6 +92,8 @@ export interface StartWorkspaceState {
clientID?: string;
};
ideOptions?: IDEOptions;
isSSHModalVisible?: boolean;
ownerToken?: string;
}

export default class StartWorkspace extends React.Component<StartWorkspaceProps, StartWorkspaceState> {
Expand Down Expand Up @@ -519,6 +522,15 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
onClick: () =>
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(),
Expand Down Expand Up @@ -556,6 +568,14 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
</a>
.
</div>
{this.state.isSSHModalVisible === true && this.state.ownerToken && (
<ConnectToSSHModal
workspaceId={this.props.workspaceId}
ownerToken={this.state.ownerToken}
ideUrl={this.state.workspaceInstance?.ideUrl.replaceAll("https://", "")}
onClose={() => this.setState({ isSSHModalVisible: false, ownerToken: "" })}
/>
)}
</div>
);
}
Expand Down
109 changes: 109 additions & 0 deletions 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<boolean>(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 (
<div className={`w-full relative ${props.className ?? ""}`}>
<input
disabled={true}
readOnly={true}
autoFocus
className="w-full pr-8 overscroll-none"
type="text"
defaultValue={props.value}
/>
<div className="cursor-pointer" onClick={() => copyToClipboard(props.value)}>
<div className="absolute top-1/3 right-3">
<Tooltip content={copied ? "Copied" : tip}>
<img src={copy} alt="copy icon" title={tip} />
</Tooltip>
</div>
</div>
</div>
);
}

interface SSHProps {
workspaceId: string;
ownerToken: string;
ideUrl: string;
}

function SSHView(props: SSHProps) {
const sshCommand = `ssh ${props.workspaceId}#${props.ownerToken}@${props.ideUrl}`;
return (
<div className="border-t border-b border-gray-200 dark:border-gray-800 mt-2 -mx-6 px-6 py-6">
<div className="mt-1 mb-4">
<p className="text-gray-500 whitespace-normal text-base">
The following shell command can be used to SSH into this workspace.
</p>
<AlertBox className="mt-4">
<p className="text-red-500 whitespace-normal text-base">
<b>Anyone</b> on the internet with this command can access the running workspace. The command
includes a generated access token that resets on every workspace restart.
</p>
</AlertBox>
<InfoBox className="mt-4">
<p className="text-gray-500 whitespace-normal text-base">
Before connecting via SSH, make sure you have an existing SSH private key on your machine. You
can create one using&nbsp;
<a
href="https://en.wikipedia.org/wiki/Ssh-keygen"
target="_blank"
rel="noopener noreferrer"
className="gp-link"
>
ssh-keygen
</a>
.
</p>
</InfoBox>
</div>
<InputWithCopy value={sshCommand} tip="Copy SSH Command" />
</div>
);
}

export default function ConnectToSSHModal(props: {
workspaceId: string;
ownerToken: string;
ideUrl: string;
onClose: () => void;
}) {
return (
<Modal visible={true} onClose={props.onClose}>
<h3 className="mb-4">Connect via SSH</h3>
<SSHView workspaceId={props.workspaceId} ownerToken={props.ownerToken} ideUrl={props.ideUrl} />
<div className="flex justify-end mt-6">
<button className={"ml-2 secondary"} onClick={() => props.onClose()}>
Close
</button>
</div>
</Modal>
);
}
22 changes: 22 additions & 0 deletions components/dashboard/src/workspaces/WorkspaceEntry.tsx
Expand Up @@ -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) {
Expand All @@ -42,13 +43,15 @@ 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<HTMLInputElement>(null);
const [errorMessage, setErrorMessage] = useState("");
const state: WorkspaceInstancePhase = desc.latestInstance?.status?.phase || "stopped";
const currentBranch =
desc.latestInstance?.status.repo?.branch || Workspace.getBranchName(desc.workspace) || "<unknown>";
const ws = desc.workspace;
const [workspaceDescription, setWsDescription] = useState(ws.description);
const [ownerToken, setOwnerToken] = useState("");

const startUrl = new GitpodHostUrl(window.location.href).with({
pathname: "/start/",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -234,6 +245,17 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
</button>
</div>
</Modal>
{isSSHModalVisible && desc.latestInstance && ownerToken !== "" && (
<ConnectToSSHModal
workspaceId={ws.id}
ownerToken={ownerToken}
ideUrl={desc.latestInstance.ideUrl.replaceAll("https://", "")}
onClose={() => {
setSSHModalVisible(false);
setOwnerToken("");
}}
/>
)}
</Item>
);
}
Expand Down

0 comments on commit 8212c57

Please sign in to comment.