Path: blob/master/src/packages/frontend/account/ssh-keys/ssh-key-list.tsx
5950 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Button, Flex, Popconfirm, Typography } from "antd";6import { Map } from "immutable";7import { useIntl } from "react-intl";89import { redux } from "@cocalc/frontend/app-framework";10import {11Gap,12HelpIcon,13Icon,14SettingBox,15TimeAgo,16} from "@cocalc/frontend/components";17import { labels } from "@cocalc/frontend/i18n";18import { CancelText } from "@cocalc/frontend/i18n/components";19import { cmp } from "@cocalc/util/misc";20import SSHKeyAdder from "./ssh-key-adder";2122interface SSHKeyListProps {23ssh_keys?: Map<string, any>;24project_id?: string;25help?: React.JSX.Element;26children?: any;27mode?: "project" | "flyout";28}2930// Children are rendered above the list of SSH Keys31// Takes an optional Help string or node to render as a help modal32export default function SSHKeyList({33ssh_keys,34project_id,35help,36children,37mode = "project",38}: SSHKeyListProps) {39const intl = useIntl();40const isFlyout = mode === "flyout";4142function renderAdder(size?) {43if (project_id) {44return (45<SSHKeyAdder46size={size}47add_ssh_key={(opts) => {48redux49.getActions("projects")50.add_ssh_key_to_project({ ...opts, project_id });51}}52style={{ marginBottom: "10px" }}53extra={54<p>55If you want to use the same SSH key for all your projects and56compute servers, add it using the "SSH Keys" tab under Account57Settings. If you have done that, there is no need to also58configure an SSH key here.59</p>60}61/>62);63} else {64return (65<SSHKeyAdder66size={size}67add_ssh_key={(opts) => redux.getActions("account").add_ssh_key(opts)}68style={{ marginBottom: "0px" }}69/>70);71}72}7374function render_header() {75return (76<Flex style={{ width: "100%" }}>77{project_id ? "Project Specific " : "Global "}78{intl.formatMessage(labels.ssh_keys)} <Gap />79{help && <HelpIcon title="Using SSH Keys">{help}</HelpIcon>}80<div style={{ flex: 1 }} />81{(ssh_keys?.size ?? 0) > 0 ? (82<div style={{ float: "right" }}>{renderAdder()}</div>83) : undefined}84</Flex>85);86}8788function render_keys() {89if (ssh_keys == null || ssh_keys.size == 0) {90return <div style={{ textAlign: "center" }}>{renderAdder("large")}</div>;91}92const v: { date?: Date; fp: string; component: React.JSX.Element }[] = [];9394ssh_keys?.forEach(95(ssh_key: Map<string, any>, fingerprint: string): void => {96if (!ssh_key) {97return;98}99ssh_key = ssh_key.set("fingerprint", fingerprint);100v.push({101date: ssh_key.get("last_use_date"),102fp: fingerprint,103component: (104<OneSSHKey105ssh_key={ssh_key}106key={fingerprint}107project_id={project_id}108mode={mode}109/>110),111});112},113);114// sort in reverse order by last_use_date, then by fingerprint115v.sort(function (a, b) {116if (a.date != null && b.date != null) {117return -cmp(a.date, b.date);118}119if (a.date && b.date == null) {120return -1;121}122if (b.date && a.date == null) {123return +1;124}125return cmp(a.fp, b.fp);126});127if (isFlyout) {128return <div>{v.map((x) => x.component)}</div>;129} else {130return (131<SettingBox style={{ marginBottom: "0px" }} show_header={false}>132{v.map((x) => x.component)}133</SettingBox>134);135}136}137138function renderBody() {139return (140<>141{children}142{render_keys()}143</>144);145}146147if (isFlyout) {148return renderBody();149} else {150return (151<SettingBox title={render_header()} icon={"key"}>152{renderBody()}153</SettingBox>154);155}156}157158interface OneSSHKeyProps {159ssh_key: Map<string, any>;160project_id?: string;161mode?: "project" | "flyout";162}163164function OneSSHKey({ ssh_key, project_id, mode = "project" }: OneSSHKeyProps) {165const isFlyout = mode === "flyout";166167function render_last_use(): React.JSX.Element {168const d = ssh_key.get("last_use_date");169if (d) {170return (171<span style={{ color: "#1e7e34" }}>172Last used <TimeAgo date={new Date(d)} />173</span>174);175} else {176return <span style={{ color: "#333" }}>Never used</span>;177}178}179180function delete_key(): void {181const fingerprint = ssh_key.get("fingerprint");182if (project_id) {183redux.getActions("projects").delete_ssh_key_from_project({184fingerprint,185project_id: project_id,186});187} else {188redux.getActions("account").delete_ssh_key(fingerprint);189}190}191192const key_style: React.CSSProperties = {193fontSize: isFlyout ? "42px" : "72px",194color: ssh_key.get("last_use_date") ? "#1e7e34" : "#888",195};196197return (198<div199style={{200display: "flex",201borderBottom: "1px solid #ccc",202padding: "15px",203}}204>205<div style={{ width: isFlyout ? "48px" : "100px", display: "flex" }}>206<Icon style={key_style} name="key" />207</div>208<div style={{ flex: 1 }}>209<Popconfirm210title={211<div>212Are you sure you want to delete this SSH key? <br />213This CANNOT be undone. <br /> If you want to reuse this key in the214future, you will have to upload it again.215</div>216}217onConfirm={() => delete_key()}218okText={"Yes, delete key"}219cancelText={<CancelText />}220>221<Button222type="link"223size={isFlyout ? "small" : "middle"}224style={{ float: "right" }}225>226<Icon name="trash" /> Delete...227</Button>228</Popconfirm>229<div style={{ fontWeight: 600 }}>{ssh_key.get("title")}</div>230<span style={{ fontWeight: 600 }}>Fingerprint: </span>231<Typography.Text code style={{ fontSize: "80%" }}>232{ssh_key.get("fingerprint")}233</Typography.Text>234<br />235Added on {new Date(ssh_key.get("creation_date")).toLocaleDateString()}236<div> {render_last_use()} (NOTE: not all usage is tracked.)</div>237</div>238</div>239);240}241242243