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

Support ssh public keys configuration #10573

Merged
merged 4 commits into from
Jul 5, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions components/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
settingsPathTeamsJoin,
settingsPathTeamsNew,
settingsPathVariables,
settingsPathSSHKeys,
} from "./settings/settings.routes";
import {
projectsPathInstallGitHubApp,
Expand All @@ -56,6 +57,7 @@ const Billing = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/
const Plans = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/Plans"));
const Teams = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/Teams"));
const EnvironmentVariables = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/EnvironmentVariables"));
const SSHKeys = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/SSHKeys"));
const Integrations = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/Integrations"));
const Preferences = React.lazy(() => import(/* webpackPrefetch: true */ "./settings/Preferences"));
const Open = React.lazy(() => import(/* webpackPrefetch: true */ "./start/Open"));
Expand Down Expand Up @@ -352,6 +354,7 @@ function App() {
<Route path={settingsPathBilling} exact component={Billing} />
<Route path={settingsPathPlans} exact component={Plans} />
<Route path={settingsPathVariables} exact component={EnvironmentVariables} />
<Route path={settingsPathSSHKeys} exact component={SSHKeys} />
<Route path={settingsPathPreferences} exact component={Preferences} />
<Route path={projectsPathInstallGitHubApp} exact component={InstallGitHubApp} />
<Route path="/from-referrer" exact component={FromReferrer} />
Expand Down
15 changes: 11 additions & 4 deletions components/dashboard/src/components/ItemsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ export function ItemsList(props: { children?: React.ReactNode; className?: strin
return <div className={`flex flex-col space-y-2 ${props.className || ""}`}>{props.children}</div>;
}

export function Item(props: { children?: React.ReactNode; className?: string; header?: boolean }) {
export function Item(props: { children?: React.ReactNode; className?: string; header?: boolean; solid?: boolean }) {
// cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700
const solidClassName = props.solid ? "bg-gray-50 dark:bg-gray-800" : "hover:bg-gray-100 dark:hover:bg-gray-800";
const headerClassName = "text-sm text-gray-400 border-t border-b border-gray-200 dark:border-gray-800";
const notHeaderClassName = "rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gitpod-kumquat-light";
const notHeaderClassName = "rounded-xl focus:bg-gitpod-kumquat-light " + solidClassName;
return (
<div
className={`flex flex-grow flex-row w-full p-3 justify-between transition ease-in-out ${
Expand All @@ -32,10 +34,15 @@ export function ItemFieldIcon(props: { children?: React.ReactNode; className?: s
return <div className={`flex self-center w-8 ${props.className || ""}`}>{props.children}</div>;
}

export function ItemFieldContextMenu(props: { menuEntries: ContextMenuEntry[]; className?: string }) {
export function ItemFieldContextMenu(props: {
menuEntries: ContextMenuEntry[];
className?: string;
position?: "start" | "center" | "end";
}) {
const cls = "self-" + (props.position ?? "center");
return (
<div
className={`flex self-center hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md cursor-pointer w-8 ${
className={`flex hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md cursor-pointer w-8 ${cls} ${
props.className || ""
}`}
>
Expand Down
14 changes: 10 additions & 4 deletions components/dashboard/src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ export default function Modal(props: {
// specify a key if having the same title and window.location
specify?: string;
title?: string;
hideDivider?: boolean;
buttons?: React.ReactChild[] | React.ReactChild;
children: React.ReactChild[] | React.ReactChild;
visible: boolean;
closeable?: boolean;
className?: string;
onClose: () => void;
onEnter?: () => boolean;
onEnter?: () => boolean | Promise<boolean>;
}) {
const closeModal = (manner: CloseModalManner) => {
props.onClose();
Expand All @@ -36,7 +37,7 @@ export default function Modal(props: {
.then()
.catch(console.error);
};
const handler = (evt: KeyboardEvent) => {
const handler = async (evt: KeyboardEvent) => {
if (!props.visible) {
return;
}
Expand All @@ -48,7 +49,7 @@ export default function Modal(props: {
}
if (evt.key === "Enter") {
if (props.onEnter) {
if (props.onEnter()) {
if (await props.onEnter()) {
closeModal("enter");
}
} else {
Expand Down Expand Up @@ -94,7 +95,12 @@ export default function Modal(props: {
{props.title ? (
<>
<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}
</div>
<div className="flex justify-end mt-6 space-x-2">{props.buttons}</div>
Expand Down
3 changes: 3 additions & 0 deletions components/dashboard/src/icons/Key.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions components/dashboard/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,14 @@
@apply text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500;
}

textarea,
input[type="text"],
input[type="search"],
input[type="password"],
select {
@apply block w-56 text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 rounded-md border border-gray-300 dark:border-gray-500 focus:border-gray-400 dark:focus:border-gray-400 focus:ring-0;
}
textarea::placeholder,
input[type="text"]::placeholder,
input[type="search"]::placeholder,
input[type="password"]::placeholder {
Expand All @@ -93,6 +95,7 @@
select.error {
@apply border-gitpod-red dark:border-gitpod-red focus:border-gitpod-red dark:focus:border-gitpod-red;
}
textarea[disabled],
input[disabled] {
@apply bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 text-gray-400 dark:text-gray-500;
}
Expand Down
258 changes: 258 additions & 0 deletions components/dashboard/src/settings/SSHKeys.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/**
* 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 { useContext, useEffect, useState } from "react";
import { PageWithSubMenu } from "../components/PageWithSubMenu";
import getSettingsMenu from "./settings-menu";
import { PaymentContext } from "../payment-context";
import Modal from "../components/Modal";
import Alert from "../components/Alert";
import { Item, ItemField, ItemFieldContextMenu } from "../components/ItemsList";
import ConfirmationModal from "../components/ConfirmationModal";
import { SSHPublicKeyValue, UserSSHPublicKeyValue } from "@gitpod/gitpod-protocol";
import { getGitpodService } from "../service/service";
import moment from "moment";

interface AddModalProps {
value: SSHPublicKeyValue;
onClose: () => void;
onSave: () => void;
}

interface DeleteModalProps {
value: UserSSHPublicKeyValue;
onConfirm: () => void;
onClose: () => void;
}

export function AddSSHKeyModal(props: AddModalProps) {
const [errorMsg, setErrorMsg] = useState("");

const [value, setValue] = useState({ ...props.value });
const update = (pev: Partial<SSHPublicKeyValue>) => {
setValue({ ...value, ...pev });
setErrorMsg("");
};

useEffect(() => {
setValue({ ...props.value });
setErrorMsg("");
}, [props.value]);

const save = async () => {
const tmp = SSHPublicKeyValue.validate(value);
if (tmp) {
setErrorMsg(tmp);
return false;
}
try {
await getGitpodService().server.addSSHPublicKey(value);
} catch (e) {
setErrorMsg(e.message.replace("Request addSSHPublicKey failed with message: ", ""));
return false;
}
props.onClose();
props.onSave();
return true;
};

return (
<Modal
title="New SSH Key"
buttons={
<button className="ml-2" onClick={save}>
Add SSH Key
</button>
}
visible={true}
onClose={props.onClose}
onEnter={save}
>
<>
{errorMsg.length > 0 && (
<Alert type="error" className="mb-2">
mustard-mh marked this conversation as resolved.
Show resolved Hide resolved
gtsiolis marked this conversation as resolved.
Show resolved Hide resolved
{errorMsg}
</Alert>
)}
</>
<div className="text-gray-500 dark:text-gray-400 text-md">
Add an SSH key for secure access workspaces via SSH.{" "}
<a href="/docs/configure/ssh" target="gitpod-ssh-doc" className="gp-link">
Learn more
</a>
Comment on lines +83 to +85
Copy link
Contributor

Choose a reason for hiding this comment

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

thought: Do we need this help link here? Asking because we also link below to the same page.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can keep that, they are different part link

</div>
<Alert type="info" className="mt-2">
mustard-mh marked this conversation as resolved.
Show resolved Hide resolved
SSH key are used to connect securely to workspaces.{" "}
<a
href="https://www.gitpod.io/docs/configure/ssh#create-an-ssh-key"
target="gitpod-create-ssh-key-doc"
className="gp-link"
>
Learn how to create an SSH Key
</a>
</Alert>
<div className="mt-2">
<h4>Key</h4>
<textarea
autoFocus
style={{ height: "160px" }}
className="w-full resize-none"
value={value.key}
placeholder="Begins with 'ssh-rsa', 'ecdsa-sha2-nistp256',
'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521',
'ssh-ed25519',
'[email protected]', or
'[email protected]'"
onChange={(v) => update({ key: v.target.value })}
/>
</div>
<div className="mt-4">
<h4>Title</h4>
<input
className="w-full"
type="text"
placeholder="e.g. laptop"
value={value.name}
onChange={(v) => {
update({ name: v.target.value });
}}
/>
</div>
</Modal>
);
}

export function DeleteSSHKeyModal(props: DeleteModalProps) {
const confirmDelete = async () => {
await getGitpodService().server.deleteSSHPublicKey(props.value.id!);
props.onConfirm();
props.onClose();
};
return (
<ConfirmationModal
title="Delete SSH Key"
areYouSureText="Are you sure you want to delete this SSH Key?"
buttonText="Delete SSH Key"
onClose={props.onClose}
onConfirm={confirmDelete}
>
<Item solid>
<KeyItem sshKey={props.value}></KeyItem>
</Item>
</ConfirmationModal>
);
}

export default function SSHKeys() {
const { showPaymentUI, showUsageBasedUI } = useContext(PaymentContext);

const [dataList, setDataList] = useState<UserSSHPublicKeyValue[]>([]);
const [currentData, setCurrentData] = useState<SSHPublicKeyValue>({ name: "", key: "" });
const [currentDelData, setCurrentDelData] = useState<UserSSHPublicKeyValue>();
const [showAddModal, setShowAddModal] = useState(false);
const [showDelModal, setShowDelModal] = useState(false);

const loadData = () => {
getGitpodService()
.server.getSSHPublicKeys()
.then((r) => setDataList(r));
};

useEffect(() => {
loadData();
}, []);

const addOne = () => {
setCurrentData({ name: "", key: "" });
setShowAddModal(true);
setShowDelModal(false);
};

const deleteOne = (value: UserSSHPublicKeyValue) => {
setCurrentDelData(value);
setShowAddModal(false);
setShowDelModal(true);
};

return (
<PageWithSubMenu
subMenu={getSettingsMenu({ showPaymentUI, showUsageBasedUI })}
title="SSH Keys"
subtitle="Connect securely to workspaces."
>
{showAddModal && (
<AddSSHKeyModal value={currentData} onSave={loadData} onClose={() => setShowAddModal(false)} />
)}
{showDelModal && (
<DeleteSSHKeyModal
value={currentDelData!}
onConfirm={loadData}
onClose={() => setShowDelModal(false)}
/>
)}
<div className="flex items-start sm:justify-between mb-2">
<div>
<h3>SSH Keys</h3>
<h2 className="text-gray-500">Create and manage SSH keys.</h2>
</div>
{dataList.length !== 0 ? (
<div className="mt-3 flex">
<button onClick={addOne} className="ml-2">
New SSH Key
</button>
</div>
) : null}
</div>
{dataList.length === 0 ? (
<div className="bg-gray-100 dark:bg-gray-800 rounded-xl w-full h-96">
<div className="pt-28 flex flex-col items-center w-112 m-auto">
<h3 className="text-center pb-3 text-gray-500 dark:text-gray-400">No SSH Keys</h3>
<div className="text-center pb-6 text-gray-500">
SSH keys allow you to establish a <b>secure connection</b> between your <b>computer</b> and{" "}
<b>workspaces</b>.
</div>
<button onClick={addOne}>New SSH Key</button>
</div>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4">
{dataList.map((key) => {
return (
<Item solid className="items-start">
<KeyItem sshKey={key}></KeyItem>
<ItemFieldContextMenu
position="start"
menuEntries={[
{
title: "Delete",
customFontStyle:
"text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300",
onClick: () => deleteOne(key),
},
]}
/>
</Item>
);
})}
</div>
)}
</PageWithSubMenu>
);
}

function KeyItem(props: { sshKey: UserSSHPublicKeyValue }) {
const key = props.sshKey;
return (
<ItemField className="flex flex-col gap-y box-border overflow-hidden">
<p className="truncate text-gray-400 dark:text-gray-600">SHA256:{key.fingerprint}</p>
gtsiolis marked this conversation as resolved.
Show resolved Hide resolved
<div className="truncate my-1 text-xl text-gray-800 dark:text-gray-100 font-semibold">{key.name}</div>
<p className="truncate mt-4">Added on {moment(key.creationTime).format("MMM D, YYYY, hh:mm A")}</p>
{!!key.lastUsedTime && (
<p className="truncate">Last used on {moment(key.lastUsedTime).format("MMM D, YYYY, hh:mm A")}</p>
)}
</ItemField>
);
}
Loading