[dashboard] support ssh copy-paste with ssh keys

This commit is contained in:
mustard 2022-06-15 16:58:31 +00:00 committed by Robo Quat
parent 28f9ddc77c
commit 924b20eaee
2 changed files with 91 additions and 38 deletions

View File

@ -13,6 +13,7 @@ export default function Modal(props: {
// specify a key if having the same title and window.location // specify a key if having the same title and window.location
specify?: string; specify?: string;
title?: string; title?: string;
hideDivider?: boolean;
buttons?: React.ReactChild[] | React.ReactChild; buttons?: React.ReactChild[] | React.ReactChild;
children: React.ReactChild[] | React.ReactChild; children: React.ReactChild[] | React.ReactChild;
visible: boolean; visible: boolean;
@ -94,7 +95,12 @@ export default function Modal(props: {
{props.title ? ( {props.title ? (
<> <>
<h3 className="pb-2">{props.title}</h3> <h3 className="pb-2">{props.title}</h3>
<div className="border-t border-b border-gray-200 dark:border-gray-800 mt-2 -mx-6 px-6 py-4"> <div
className={
"border-gray-200 dark:border-gray-800 -mx-6 px-6 " +
(props.hideDivider ? "" : "border-t border-b mt-2 py-4")
}
>
{props.children} {props.children}
</div> </div>
<div className="flex justify-end mt-6 space-x-2">{props.buttons}</div> <div className="flex justify-end mt-6 space-x-2">{props.buttons}</div>

View File

@ -4,12 +4,14 @@
* See License-AGPL.txt in the project root for license information. * See License-AGPL.txt in the project root for license information.
*/ */
import { useState } from "react"; import { useEffect, useState } from "react";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
import Tooltip from "../components/Tooltip"; import Tooltip from "../components/Tooltip";
import copy from "../images/copy.svg"; import copy from "../images/copy.svg";
import AlertBox from "../components/AlertBox"; import Alert from "../components/Alert";
import InfoBox from "../components/InfoBox"; import TabMenuItem from "../components/TabMenuItem";
import { settingsPathSSHKeys } from "../settings/settings.routes";
import { getGitpodService } from "../service/service";
function InputWithCopy(props: { value: string; tip?: string; className?: string }) { function InputWithCopy(props: { value: string; tip?: string; className?: string }) {
const [copied, setCopied] = useState<boolean>(false); const [copied, setCopied] = useState<boolean>(false);
@ -35,7 +37,7 @@ function InputWithCopy(props: { value: string; tip?: string; className?: string
autoFocus autoFocus
className="w-full pr-8 overscroll-none" className="w-full pr-8 overscroll-none"
type="text" type="text"
defaultValue={props.value} value={props.value}
/> />
<div className="cursor-pointer" onClick={() => copyToClipboard(props.value)}> <div className="cursor-pointer" onClick={() => copyToClipboard(props.value)}>
<div className="absolute top-1/3 right-3"> <div className="absolute top-1/3 right-3">
@ -55,40 +57,80 @@ interface SSHProps {
} }
function SSHView(props: SSHProps) { function SSHView(props: SSHProps) {
const sshCommand = `ssh '${props.workspaceId}#${props.ownerToken}@${props.ideUrl.replace( const [hasSSHKey, setHasSSHKey] = useState(true);
props.workspaceId, const [selectSSHKey, setSelectSSHKey] = useState(true);
props.workspaceId + ".ssh",
)}'`; useEffect(() => {
getGitpodService()
.server.hasSSHPublicKey()
.then((d) => {
setHasSSHKey(d);
})
.catch(console.error);
}, []);
const host = props.ideUrl.replace(props.workspaceId, props.workspaceId + ".ssh");
const sshAccessTokenCommand = `ssh '${props.workspaceId}#${props.ownerToken}@${host}'`;
const sshKeyCommand = `ssh '${props.workspaceId}@${host}'`;
return ( 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"> <div className="flex flex-row">
<AlertBox> <TabMenuItem
<p className="text-red-500 whitespace-normal text-base"> key="ssh_key"
name="SSH Key"
selected={selectSSHKey}
onClick={() => {
setSelectSSHKey(true);
}}
/>
<TabMenuItem
key="access_token"
name="Access Token"
selected={!selectSSHKey}
onClick={() => {
setSelectSSHKey(false);
}}
/>
</div>
<div className="border-gray-200 dark:border-gray-800 border-b"></div>
<div className="space-y-4 mt-4">
{!selectSSHKey && (
<Alert type="warning" className="whitespace-normal">
<b>Anyone</b> on the internet with this command can access the running workspace. The command <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. includes a generated access token that resets on every workspace restart.
</p> </Alert>
</AlertBox> )}
<InfoBox className="mt-4"> {!hasSSHKey && selectSSHKey && (
<Alert type="warning" className="whitespace-normal">
You don't have any public SSH keys in your Gitpod account. You can{" "}
<a href={settingsPathSSHKeys} target="setting-keys" className="gp-link">
add a new public key
</a>
, or use a generated access token.
</Alert>
)}
<p className="text-gray-500 whitespace-normal text-base"> <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 {!selectSSHKey ? (
can create one using&nbsp; "The following shell command can be used to SSH into this workspace."
<a ) : (
href="https://en.wikipedia.org/wiki/Ssh-keygen" <>
target="_blank" The following shell command can be used to SSH into this workspace with a{" "}
rel="noopener noreferrer" <a href={settingsPathSSHKeys} target="setting-keys" className="gp-link">
className="gp-link" ssh key
>
ssh-keygen
</a> </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> </p>
</div> </div>
<InputWithCopy value={sshCommand} tip="Copy SSH Command" /> <InputWithCopy
</div> className="my-2"
value={!selectSSHKey ? sshAccessTokenCommand : sshKeyCommand}
tip="Copy SSH Command"
/>
</>
); );
} }
@ -99,14 +141,19 @@ export default function ConnectToSSHModal(props: {
onClose: () => void; onClose: () => void;
}) { }) {
return ( return (
// TODO: Use title and buttons props <Modal
<Modal visible={true} onClose={props.onClose}> title="Connect via SSH"
<h3 className="mb-4">Connect via SSH</h3> hideDivider
<SSHView workspaceId={props.workspaceId} ownerToken={props.ownerToken} ideUrl={props.ideUrl} /> buttons={
<div className="flex justify-end mt-6">
<button className={"ml-2 secondary"} onClick={() => props.onClose()}> <button className={"ml-2 secondary"} onClick={() => props.onClose()}>
Close Close
</button> </button>
}
visible={true}
onClose={props.onClose}
>
<div className="border-gray-200 dark:border-gray-800 -mx-6 px-6 border-b pb-4">
<SSHView workspaceId={props.workspaceId} ownerToken={props.ownerToken} ideUrl={props.ideUrl} />
</div> </div>
</Modal> </Modal>
); );