Path: blob/main/components/dashboard/src/user-settings/SSHKeys.tsx
2500 views
/**1* Copyright (c) 2022 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import { useCallback, useEffect, useState } from "react";7import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal";8import Alert from "../components/Alert";9import { Item, ItemField, ItemFieldContextMenu } from "../components/ItemsList";10import ConfirmationModal from "../components/ConfirmationModal";11import { SSHPublicKeyValue } from "@gitpod/gitpod-protocol";12import dayjs from "dayjs";13import { PageWithSettingsSubMenu } from "./PageWithSettingsSubMenu";14import { Heading2, Subheading } from "../components/typography/headings";15import { EmptyMessage } from "../components/EmptyMessage";16import { Button } from "@podkit/buttons/Button";17import { sshClient } from "../service/public-api";18import { SSHPublicKey } from "@gitpod/public-api/lib/gitpod/v1/ssh_pb";19import { InputField } from "../components/forms/InputField";20import { TextInputField } from "../components/forms/TextInputField";2122interface AddModalProps {23value: SSHPublicKeyValue;24onClose: () => void;25onSave: () => void;26}2728interface DeleteModalProps {29value: SSHPublicKey;30onConfirm: () => void;31onClose: () => void;32}3334export function AddSSHKeyModal(props: AddModalProps) {35const [errorMsg, setErrorMsg] = useState("");3637const [value, setValue] = useState({ ...props.value });38const update = (pev: Partial<SSHPublicKeyValue>) => {39setValue({ ...value, ...pev });40setErrorMsg("");41};4243useEffect(() => {44setValue({ ...props.value });45setErrorMsg("");46}, [props.value]);4748const save = async () => {49const tmp = SSHPublicKeyValue.validate(value);50if (tmp) {51setErrorMsg(tmp);52return;53}54try {55await sshClient.createSSHPublicKey(value);56props.onClose();57props.onSave();58} catch (e) {59setErrorMsg(e.message.replace("Request addSSHPublicKey failed with message: ", ""));60return;61}62};6364return (65<Modal visible onClose={props.onClose} onSubmit={save}>66<ModalHeader>New SSH Key</ModalHeader>67<ModalBody>68{errorMsg.length > 0 && (69<Alert type="error" className="mb-2">70{errorMsg}71</Alert>72)}73<div className="text-gray-500 dark:text-gray-400 text-md">74Add an SSH key for secure access to workspaces via SSH.75</div>76<Alert type="info" className="mt-2">77SSH key are used to connect securely to workspaces.{" "}78<a79href="https://www.gitpod.io/docs/configure/user-settings/ssh#create-an-ssh-key"80target="gitpod-create-ssh-key-doc"81className="gp-link"82>83Learn how to create an SSH Key84</a>85</Alert>86<InputField label="Key">87<textarea88autoFocus89style={{ height: "160px" }}90className="w-full resize-none"91value={value.key}92placeholder="Begins with 'ssh-rsa', 'ecdsa-sha2-nistp256',93'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521',94'ssh-ed25519',95'[email protected]', or96'[email protected]'"97onChange={(v) => update({ key: v.target.value })}98/>99</InputField>100101<TextInputField102label="Title"103placeholder="e.g. laptop"104type="text"105value={value.name}106onChange={(val) => update({ name: val })}107/>108</ModalBody>109<ModalFooter>110<Button variant="secondary" onClick={props.onClose}>111Cancel112</Button>113<Button type="submit">Add SSH Key</Button>114</ModalFooter>115</Modal>116);117}118119export function DeleteSSHKeyModal(props: DeleteModalProps) {120const confirmDelete = useCallback(async () => {121await sshClient.deleteSSHPublicKey({ sshKeyId: props.value.id! });122props.onConfirm();123props.onClose();124}, [props]);125126return (127<ConfirmationModal128title="Delete SSH Key"129areYouSureText="Are you sure you want to delete this SSH Key?"130buttonText="Delete SSH Key"131onClose={props.onClose}132onConfirm={confirmDelete}133>134<Item solid>135<KeyItem sshKey={props.value}></KeyItem>136</Item>137</ConfirmationModal>138);139}140141export default function SSHKeys() {142const [dataList, setDataList] = useState<SSHPublicKey[]>([]);143const [currentData, setCurrentData] = useState<SSHPublicKeyValue>({ name: "", key: "" });144const [currentDelData, setCurrentDelData] = useState<SSHPublicKey>();145const [showAddModal, setShowAddModal] = useState(false);146const [showDelModal, setShowDelModal] = useState(false);147148const loadData = () => {149sshClient.listSSHPublicKeys({}).then((r) => setDataList(r.sshKeys));150};151152useEffect(() => {153loadData();154}, []);155156const addOne = () => {157setCurrentData({ name: "", key: "" });158setShowAddModal(true);159setShowDelModal(false);160};161162const deleteOne = (value: SSHPublicKey) => {163setCurrentDelData(value);164setShowAddModal(false);165setShowDelModal(true);166};167168return (169<PageWithSettingsSubMenu>170{showAddModal && (171<AddSSHKeyModal value={currentData} onSave={loadData} onClose={() => setShowAddModal(false)} />172)}173{showDelModal && (174<DeleteSSHKeyModal175value={currentDelData!}176onConfirm={loadData}177onClose={() => setShowDelModal(false)}178/>179)}180<div className="flex items-start sm:justify-between mb-2">181<div>182<Heading2>SSH Keys</Heading2>183<Subheading>184Create and manage SSH keys.{" "}185<a186className="gp-link"187href="https://www.gitpod.io/docs/configure/user-settings/ssh"188target="_blank"189rel="noreferrer"190>191Learn more192</a>193</Subheading>194</div>195{dataList.length !== 0 ? (196<div className="mt-3 flex">197<Button onClick={addOne} className="ml-2">198New SSH Key199</Button>200</div>201) : null}202</div>203{dataList.length === 0 ? (204<EmptyMessage205title="No SSH Keys"206subtitle={207<span>208SSH keys allow you to establish a <b>secure connection</b> between your <b>computer</b> and{" "}209<b>workspaces</b>.210</span>211}212buttonText="New SSH Key"213onClick={addOne}214/>215) : (216<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4">217{dataList.map((key) => {218return (219<Item key={key.id} solid className="items-start">220<KeyItem sshKey={key}></KeyItem>221<ItemFieldContextMenu222position="start"223menuEntries={[224{225title: "Delete",226customFontStyle:227"text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300",228onClick: () => deleteOne(key),229},230]}231/>232</Item>233);234})}235</div>236)}237</PageWithSettingsSubMenu>238);239}240241function KeyItem(props: { sshKey: SSHPublicKey }) {242const key = props.sshKey;243return (244<ItemField className="flex flex-col gap-y box-border overflow-hidden">245<p className="truncate text-gray-400 dark:text-gray-600">SHA256:{key.fingerprint}</p>246<div className="truncate my-1 text-xl text-gray-800 dark:text-gray-100 font-semibold">{key.name}</div>247<p className="truncate mt-4">Added on {dayjs(key.creationTime!.toDate()).format("MMM D, YYYY, hh:mm A")}</p>248{!!key.lastUsedTime && (249<p className="truncate">250Last used on {dayjs(key.lastUsedTime!.toDate()).format("MMM D, YYYY, hh:mm A")}251</p>252)}253</ItemField>254);255}256257258