Skip to content

Commit 7a708ce

Browse files
iQQBotroboquat
authored andcommitted
[dashboard] support connect via SSH
1 parent 19fe0d3 commit 7a708ce

File tree

3 files changed

+151
-0
lines changed

3 files changed

+151
-0
lines changed

components/dashboard/src/start/StartWorkspace.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import PendingChangesDropdown from "../components/PendingChangesDropdown";
2727
import { watchHeadlessLogs } from "../components/PrebuildLogs";
2828
import { getGitpodService, gitpodHostUrl } from "../service/service";
2929
import { StartPage, StartPhase, StartWorkspaceError } from "./StartPage";
30+
import ConnectToSSHModal from "../workspaces/ConnectToSSHModal";
3031
const sessionId = v4();
3132

3233
const WorkspaceLogs = React.lazy(() => import("../components/WorkspaceLogs"));
@@ -91,6 +92,8 @@ export interface StartWorkspaceState {
9192
clientID?: string;
9293
};
9394
ideOptions?: IDEOptions;
95+
isSSHModalVisible?: boolean;
96+
ownerToken?: string;
9497
}
9598

9699
export default class StartWorkspace extends React.Component<StartWorkspaceProps, StartWorkspaceState> {
@@ -482,6 +485,15 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
482485
onClick: () =>
483486
getGitpodService().server.stopWorkspace(this.props.workspaceId),
484487
},
488+
{
489+
title: "Connect via SSH",
490+
onClick: async () => {
491+
const ownerToken = await getGitpodService().server.getOwnerToken(
492+
this.props.workspaceId,
493+
);
494+
this.setState({ isSSHModalVisible: true, ownerToken });
495+
},
496+
},
485497
{
486498
title: "Go to Dashboard",
487499
href: gitpodHostUrl.asDashboard().toString(),
@@ -519,6 +531,14 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
519531
</a>
520532
.
521533
</div>
534+
{this.state.isSSHModalVisible === true && this.state.ownerToken && (
535+
<ConnectToSSHModal
536+
workspaceId={this.props.workspaceId}
537+
ownerToken={this.state.ownerToken}
538+
ideUrl={this.state.workspaceInstance?.ideUrl.replaceAll("https://", "")}
539+
onClose={() => this.setState({ isSSHModalVisible: false, ownerToken: "" })}
540+
/>
541+
)}
522542
</div>
523543
);
524544
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { useState } from "react";
8+
import Modal from "../components/Modal";
9+
import Tooltip from "../components/Tooltip";
10+
import copy from "../images/copy.svg";
11+
import AlertBox from "../components/AlertBox";
12+
import InfoBox from "../components/InfoBox";
13+
14+
function InputWithCopy(props: { value: string; tip?: string; className?: string }) {
15+
const [copied, setCopied] = useState<boolean>(false);
16+
const copyToClipboard = (text: string) => {
17+
const el = document.createElement("textarea");
18+
el.value = text;
19+
document.body.appendChild(el);
20+
el.select();
21+
try {
22+
document.execCommand("copy");
23+
} finally {
24+
document.body.removeChild(el);
25+
}
26+
setCopied(true);
27+
setTimeout(() => setCopied(false), 2000);
28+
};
29+
const tip = props.tip ?? "Click to copy";
30+
return (
31+
<div className={`w-full relative ${props.className ?? ""}`}>
32+
<input
33+
disabled={true}
34+
readOnly={true}
35+
autoFocus
36+
className="w-full pr-8 overscroll-none"
37+
type="text"
38+
defaultValue={props.value}
39+
/>
40+
<div className="cursor-pointer" onClick={() => copyToClipboard(props.value)}>
41+
<div className="absolute top-1/3 right-3">
42+
<Tooltip content={copied ? "Copied" : tip}>
43+
<img src={copy} alt="copy icon" title={tip} />
44+
</Tooltip>
45+
</div>
46+
</div>
47+
</div>
48+
);
49+
}
50+
51+
interface SSHProps {
52+
workspaceId: string;
53+
ownerToken: string;
54+
ideUrl: string;
55+
}
56+
57+
function SSHView(props: SSHProps) {
58+
const sshCommand = `ssh ${props.workspaceId}#${props.ownerToken}@${props.ideUrl}`;
59+
return (
60+
<div className="border-t border-b border-gray-200 dark:border-gray-800 mt-2 -mx-6 px-6 py-6">
61+
<div className="mt-1 mb-4">
62+
<AlertBox>
63+
<p className="text-red-500 whitespace-normal text-base">
64+
<b>Anyone</b> on the internet with this command can access the running workspace. The command
65+
includes a generated access token that resets on every workspace restart.
66+
</p>
67+
</AlertBox>
68+
<InfoBox className="mt-4">
69+
<p className="text-gray-500 whitespace-normal text-base">
70+
Before connecting via SSH, make sure you have an existing SSH private key on your machine. You
71+
can create one using&nbsp;
72+
<a
73+
href="https://en.wikipedia.org/wiki/Ssh-keygen"
74+
target="_blank"
75+
rel="noopener noreferrer"
76+
className="gp-link"
77+
>
78+
ssh-keygen
79+
</a>
80+
.
81+
</p>
82+
</InfoBox>
83+
<p className="mt-4 text-gray-500 whitespace-normal text-base">
84+
The following shell command can be used to SSH into this workspace.
85+
</p>
86+
</div>
87+
<InputWithCopy value={sshCommand} tip="Copy SSH Command" />
88+
</div>
89+
);
90+
}
91+
92+
export default function ConnectToSSHModal(props: {
93+
workspaceId: string;
94+
ownerToken: string;
95+
ideUrl: string;
96+
onClose: () => void;
97+
}) {
98+
return (
99+
<Modal visible={true} onClose={props.onClose}>
100+
<h3 className="mb-4">Connect via SSH</h3>
101+
<SSHView workspaceId={props.workspaceId} ownerToken={props.ownerToken} ideUrl={props.ideUrl} />
102+
<div className="flex justify-end mt-6">
103+
<button className={"ml-2 secondary"} onClick={() => props.onClose()}>
104+
Close
105+
</button>
106+
</div>
107+
</Modal>
108+
);
109+
}

components/dashboard/src/workspaces/WorkspaceEntry.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import PendingChangesDropdown from "../components/PendingChangesDropdown";
2424
import Tooltip from "../components/Tooltip";
2525
import { WorkspaceModel } from "./workspace-model";
2626
import { getGitpodService } from "../service/service";
27+
import ConnectToSSHModal from "./ConnectToSSHModal";
2728

2829
function getLabel(state: WorkspaceInstancePhase, conditions?: WorkspaceInstanceConditions) {
2930
if (conditions?.failed) {
@@ -42,13 +43,15 @@ interface Props {
4243
export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
4344
const [isDeleteModalVisible, setDeleteModalVisible] = useState(false);
4445
const [isRenameModalVisible, setRenameModalVisible] = useState(false);
46+
const [isSSHModalVisible, setSSHModalVisible] = useState(false);
4547
const renameInputRef = useRef<HTMLInputElement>(null);
4648
const [errorMessage, setErrorMessage] = useState("");
4749
const state: WorkspaceInstancePhase = desc.latestInstance?.status?.phase || "stopped";
4850
const currentBranch =
4951
desc.latestInstance?.status.repo?.branch || Workspace.getBranchName(desc.workspace) || "<unknown>";
5052
const ws = desc.workspace;
5153
const [workspaceDescription, setWsDescription] = useState(ws.description);
54+
const [ownerToken, setOwnerToken] = useState("");
5255

5356
const startUrl = new GitpodHostUrl(window.location.href).with({
5457
pathname: "/start/",
@@ -77,6 +80,14 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
7780
title: "Stop",
7881
onClick: () => stopWorkspace(ws.id),
7982
});
83+
menuEntries.splice(1, 0, {
84+
title: "Connect via SSH",
85+
onClick: async () => {
86+
const ot = await getGitpodService().server.getOwnerToken(ws.id);
87+
setOwnerToken(ot);
88+
setSSHModalVisible(true);
89+
},
90+
});
8091
}
8192
menuEntries.push({
8293
title: "Download",
@@ -234,6 +245,17 @@ export function WorkspaceEntry({ desc, model, isAdmin, stopWorkspace }: Props) {
234245
</button>
235246
</div>
236247
</Modal>
248+
{isSSHModalVisible && desc.latestInstance && ownerToken !== "" && (
249+
<ConnectToSSHModal
250+
workspaceId={ws.id}
251+
ownerToken={ownerToken}
252+
ideUrl={desc.latestInstance.ideUrl.replaceAll("https://", "")}
253+
onClose={() => {
254+
setSSHModalVisible(false);
255+
setOwnerToken("");
256+
}}
257+
/>
258+
)}
237259
</Item>
238260
);
239261
}

0 commit comments

Comments
 (0)