diff --git a/components/dashboard/src/App.tsx b/components/dashboard/src/App.tsx index 7b9f8ae4d0babf..1fa769fbf6af2f 100644 --- a/components/dashboard/src/App.tsx +++ b/components/dashboard/src/App.tsx @@ -36,6 +36,7 @@ import { settingsPathTeamsJoin, settingsPathTeamsNew, settingsPathVariables, + settingsPathSSHKeys, } from "./settings/settings.routes"; import { projectsPathInstallGitHubApp, @@ -58,6 +59,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")); @@ -364,6 +366,7 @@ function App() { + diff --git a/components/dashboard/src/components/ItemsList.tsx b/components/dashboard/src/components/ItemsList.tsx index 2e7372e9dd5670..3a4db01581f872 100644 --- a/components/dashboard/src/components/ItemsList.tsx +++ b/components/dashboard/src/components/ItemsList.tsx @@ -10,9 +10,11 @@ export function ItemsList(props: { children?: React.ReactNode; className?: strin return
{props.children}
; } -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 (
{props.children}
; } -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 (
diff --git a/components/dashboard/src/components/Modal.tsx b/components/dashboard/src/components/Modal.tsx index 70484bfcb2d0d0..1e14be10bac5c0 100644 --- a/components/dashboard/src/components/Modal.tsx +++ b/components/dashboard/src/components/Modal.tsx @@ -19,7 +19,7 @@ export default function Modal(props: { closeable?: boolean; className?: string; onClose: () => void; - onEnter?: () => boolean; + onEnter?: () => boolean | Promise; }) { const closeModal = (manner: CloseModalManner) => { props.onClose(); @@ -36,7 +36,7 @@ export default function Modal(props: { .then() .catch(console.error); }; - const handler = (evt: KeyboardEvent) => { + const handler = async (evt: KeyboardEvent) => { if (evt.defaultPrevented) { return; } @@ -45,7 +45,7 @@ export default function Modal(props: { } if (evt.key === "Enter") { if (props.onEnter) { - if (props.onEnter()) { + if (await props.onEnter()) { closeModal("enter"); } } else { diff --git a/components/dashboard/src/icons/Key.svg b/components/dashboard/src/icons/Key.svg new file mode 100644 index 00000000000000..09e7beaefeff1a --- /dev/null +++ b/components/dashboard/src/icons/Key.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/dashboard/src/index.css b/components/dashboard/src/index.css index a6f669a7539b43..ba3b102e316adb 100644 --- a/components/dashboard/src/index.css +++ b/components/dashboard/src/index.css @@ -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 { @@ -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; } diff --git a/components/dashboard/src/settings/SSHKeys.tsx b/components/dashboard/src/settings/SSHKeys.tsx new file mode 100644 index 00000000000000..273d71f65f08a8 --- /dev/null +++ b/components/dashboard/src/settings/SSHKeys.tsx @@ -0,0 +1,250 @@ +/** + * 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"; + +interface AddModalProps { + value: SSHPublicKeyValue; + onClose: () => void; + onSave: () => void; +} + +interface DeleteModalProps { + value: KeyData; + onConfirm: () => void; + onClose: () => void; +} + +type KeyData = SSHPublicKeyValue & { id?: string }; + +export function AddSSHKeyModal(props: AddModalProps) { + const [errorMsg, setErrorMsg] = useState(""); + + const [value, setValue] = useState({ ...props.value }); + const update = (pev: Partial) => { + 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); + return false; + } + props.onClose(); + props.onSave(); + return true; + }; + + return ( + + Add Key + + } + visible={true} + onClose={props.onClose} + onEnter={save} + > + <> + {errorMsg.length > 0 && ( + + {errorMsg} + + )} + +
+ Add an SSH key for secure access workspaces via SSH. Learn more +
+ + SSH key are used to connect securely to workspaces.{" "} + + Learn how to create an SSH Key + + +
+

Key

+