Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dashboard] support connect via SSH #9003

Merged
merged 1 commit into from Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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",
gtsiolis marked this conversation as resolved.
Show resolved Hide resolved
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 && (
akosyakov marked this conversation as resolved.
Show resolved Hide resolved
<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">
<AlertBox>
<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>
<p className="mt-4 text-gray-500 whitespace-normal text-base">
The following shell command can be used to SSH into this workspace.
</p>
</div>
<InputWithCopy value={sshCommand} tip="Copy SSH Command" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Don't know what you changed but this works like magic. 🔮

</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