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
+
+
+
Title
+ {
+ update({ name: v.target.value });
+ }}
+ />
+
+
+ );
+}
+
+export function DeleteSSHKeyModal(props: DeleteModalProps) {
+ const confirmDelete = async () => {
+ await getGitpodService().server.deleteSSHPublicKey(props.value.id!);
+ props.onConfirm();
+ props.onClose();
+ };
+ return (
+
+ );
+}
+
+export default function SSHKeys() {
+ const { showPaymentUI, showUsageBasedUI } = useContext(PaymentContext);
+
+ const [dataList, setDataList] = useState([]);
+ const [currentData, setCurrentData] = useState({ name: "", key: "" });
+ 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) => {
+ setCurrentData({ id: value.id, name: value.name, key: "" });
+ setShowAddModal(false);
+ setShowDelModal(true);
+ };
+
+ return (
+
+ {showAddModal && (
+ setShowAddModal(false)} />
+ )}
+ {showDelModal && (
+ setShowDelModal(false)} />
+ )}
+
+
+ {dataList.length !== 0 ? (
+
+
+
+ ) : null}
+
+ {dataList.length === 0 ? (
+
+
+
No SSH Keys
+
+ SSH keys allow you to establish a secure connection between your computer and{" "}
+ workspaces.
+
+
+
+
+ ) : (
+
+ {dataList.map((key) => {
+ return (
+
-
+
+
+ {key.fingerprint
+ .toUpperCase()
+ .match(/.{1,2}/g)
+ ?.join(":") ?? key.fingerprint}
+
+
+ {key.name}
+
+ Added on {key.creationTime}
+ {!!key.lastUsedTime && Last used on {key.lastUsedTime}
}
+
+ deleteOne(key),
+ },
+ ]}
+ />
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/components/dashboard/src/settings/settings-menu.ts b/components/dashboard/src/settings/settings-menu.ts
index 7c576cfc3b927c..1c97c0ec14a021 100644
--- a/components/dashboard/src/settings/settings-menu.ts
+++ b/components/dashboard/src/settings/settings-menu.ts
@@ -14,6 +14,7 @@ import {
settingsPathPreferences,
settingsPathTeams,
settingsPathVariables,
+ settingsPathSSHKeys,
} from "./settings.routes";
export default function getSettingsMenu(params: { showPaymentUI?: boolean; showUsageBasedUI?: boolean }) {
@@ -50,6 +51,10 @@ export default function getSettingsMenu(params: { showPaymentUI?: boolean; showU
title: "Variables",
link: [settingsPathVariables],
},
+ {
+ title: "SSH Keys",
+ link: [settingsPathSSHKeys],
+ },
{
title: "Integrations",
link: [settingsPathIntegrations, "/access-control"],
diff --git a/components/dashboard/src/settings/settings.routes.ts b/components/dashboard/src/settings/settings.routes.ts
index 72025cc439b7cc..97e1a38480478b 100644
--- a/components/dashboard/src/settings/settings.routes.ts
+++ b/components/dashboard/src/settings/settings.routes.ts
@@ -18,3 +18,5 @@ export const settingsPathTeamsJoin = [settingsPathTeams, "join"].join("/");
export const settingsPathTeamsNew = [settingsPathTeams, "new"].join("/");
export const settingsPathVariables = "/variables";
+
+export const settingsPathSSHKeys = "/keys";
diff --git a/components/dashboard/tailwind.config.js b/components/dashboard/tailwind.config.js
index 16c6d2d7628c83..2549cb04df39a5 100644
--- a/components/dashboard/tailwind.config.js
+++ b/components/dashboard/tailwind.config.js
@@ -5,16 +5,13 @@
*/
// tailwind.config.js
-const colors = require('tailwindcss/colors');
+const colors = require("tailwindcss/colors");
module.exports = {
jit: true,
- purge: [
- './public/**/*.html',
- './src/**/*.{js,ts,tsx}',
- ],
+ purge: ["./public/**/*.html", "./src/**/*.{js,ts,tsx}"],
important: true,
- darkMode: 'class',
+ darkMode: "class",
theme: {
extend: {
colors: {
@@ -22,79 +19,76 @@ module.exports = {
green: colors.lime,
orange: colors.amber,
blue: {
- light: '#75A9EC',
- DEFAULT: '#5C8DD6',
- dark: '#265583',
+ light: "#75A9EC",
+ DEFAULT: "#5C8DD6",
+ dark: "#265583",
},
- 'gitpod-black': '#161616',
- 'gitpod-gray': '#8E8787',
- 'gitpod-red': '#CE4A3E',
- 'gitpod-kumquat-light': '#FFE4BC',
- 'gitpod-kumquat': '#FFB45B',
- 'gitpod-kumquat-dark': '#FF8A00',
- 'gitpod-kumquat-darker': '#f28300',
- 'gitpod-kumquat-gradient': 'linear-gradient(137.41deg, #FFAD33 14.37%, #FF8A00 91.32%)',
+ "gitpod-black": "#161616",
+ "gitpod-gray": "#8E8787",
+ "gitpod-red": "#CE4A3E",
+ "gitpod-kumquat-light": "#FFE4BC",
+ "gitpod-kumquat": "#FFB45B",
+ "gitpod-kumquat-dark": "#FF8A00",
+ "gitpod-kumquat-darker": "#f28300",
+ "gitpod-kumquat-gradient": "linear-gradient(137.41deg, #FFAD33 14.37%, #FF8A00 91.32%)",
},
container: {
center: true,
},
outline: {
- blue: '1px solid #000033',
+ blue: "1px solid #000033",
+ },
+ width: {
+ 120: "28rem",
+ 128: "32rem",
},
},
fontFamily: {
sans: [
- 'Inter',
- 'system-ui',
- '-apple-system',
- 'BlinkMacSystemFont',
- 'Segoe UI',
- 'Roboto',
- 'Helvetica Neue',
- 'Arial',
- 'Noto Sans',
- 'sans-serif',
- 'Apple Color Emoji',
- 'Segoe UI Emoji',
- 'Segoe UI Symbol',
- 'Noto Color Emoji',
- ],
- mono: [
- 'SF Mono',
- 'Monaco',
- 'Inconsolata',
- 'Fira Mono',
- 'Droid Sans Mono',
- 'Source Code Pro',
- 'monospace'
+ "Inter",
+ "system-ui",
+ "-apple-system",
+ "BlinkMacSystemFont",
+ "Segoe UI",
+ "Roboto",
+ "Helvetica Neue",
+ "Arial",
+ "Noto Sans",
+ "sans-serif",
+ "Apple Color Emoji",
+ "Segoe UI Emoji",
+ "Segoe UI Symbol",
+ "Noto Color Emoji",
],
+ mono: ["SF Mono", "Monaco", "Inconsolata", "Fira Mono", "Droid Sans Mono", "Source Code Pro", "monospace"],
},
underlineThickness: {
- 'thin': '2px',
- 'thick': '5px'
+ thin: "2px",
+ thick: "5px",
},
underlineOffset: {
- 'small': '2px',
- 'medium': '5px',
+ small: "2px",
+ medium: "5px",
},
- filter: { // defaults to {}
+ filter: {
+ // defaults to {}
// https://github.com/benface/tailwindcss-filters#usage
- 'none': 'none',
- 'grayscale': 'grayscale(1)',
- 'invert': 'invert(1)',
- 'brightness-10': 'brightness(10)',
+ none: "none",
+ grayscale: "grayscale(1)",
+ invert: "invert(1)",
+ "brightness-10": "brightness(10)",
},
},
variants: {
extend: {
- opacity: ['disabled'],
- display: ['dark'],
- }
+ opacity: ["disabled"],
+ display: ["dark"],
+ },
},
plugins: [
- require('@tailwindcss/forms'),
- require('tailwind-underline-utils'),
- require('tailwindcss-filters'),
+ require("@tailwindcss/forms"),
+ require("tailwind-underline-utils"),
+ require("tailwindcss-filters"),
// ...
],
-};
\ No newline at end of file
+};