Path: blob/master/src/packages/frontend/account/ssh-keys/ssh-key-list.tsx
1503 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";8import { redux } from "@cocalc/frontend/app-framework";9import {10Gap,11HelpIcon,12Icon,13SettingBox,14TimeAgo,15} from "@cocalc/frontend/components";16import { labels } from "@cocalc/frontend/i18n";17import { CancelText } from "@cocalc/frontend/i18n/components";18import { cmp } from "@cocalc/util/misc";19import SSHKeyAdder from "./ssh-key-adder";2021interface SSHKeyListProps {22ssh_keys?: Map<string, any>;23project_id?: string;24help?: React.JSX.Element;25children?: any;26mode?: "project" | "flyout";27}2829// Children are rendered above the list of SSH Keys30// Takes an optional Help string or node to render as a help modal31export default function SSHKeyList({32ssh_keys,33project_id,34help,35children,36mode = "project",37}: SSHKeyListProps) {38const intl = useIntl();39const isFlyout = mode === "flyout";4041function renderAdder(size?) {42if (project_id) {43return (44<SSHKeyAdder45size={size}46add_ssh_key={(opts) => {47redux48.getActions("projects")49.add_ssh_key_to_project({ ...opts, project_id });50}}51style={{ marginBottom: "10px" }}52extra={53<p>54If you want to use the same SSH key for all your projects and55compute servers, add it using the "SSH Keys" tab under Account56Settings. If you have done that, there is no need to also57configure an SSH key here.58</p>59}60/>61);62} else {63return (64<SSHKeyAdder65size={size}66add_ssh_key={(opts) => redux.getActions("account").add_ssh_key(opts)}67style={{ marginBottom: "0px" }}68/>69);70}71}7273function render_header() {74return (75<Flex style={{ width: "100%" }}>76{project_id ? "Project Specific " : "Global "}77{intl.formatMessage(labels.ssh_keys)} <Gap />78{help && <HelpIcon title="Using SSH Keys">{help}</HelpIcon>}79<div style={{ flex: 1 }} />80{(ssh_keys?.size ?? 0) > 0 ? (81<div style={{ float: "right" }}>{renderAdder()}</div>82) : undefined}83</Flex>84);85}8687function render_keys() {88if (ssh_keys == null || ssh_keys.size == 0) {89return <div style={{ textAlign: "center" }}>{renderAdder("large")}</div>;90}91const v: { date?: Date; fp: string; component: React.JSX.Element }[] = [];9293ssh_keys?.forEach(94(ssh_key: Map<string, any>, fingerprint: string): void => {95if (!ssh_key) {96return;97}98ssh_key = ssh_key.set("fingerprint", fingerprint);99v.push({100date: ssh_key.get("last_use_date"),101fp: fingerprint,102component: (103<OneSSHKey104ssh_key={ssh_key}105key={fingerprint}106project_id={project_id}107mode={mode}108/>109),110});111},112);113// sort in reverse order by last_use_date, then by fingerprint114v.sort(function (a, b) {115if (a.date != null && b.date != null) {116return -cmp(a.date, b.date);117}118if (a.date && b.date == null) {119return -1;120}121if (b.date && a.date == null) {122return +1;123}124return cmp(a.fp, b.fp);125});126if (isFlyout) {127return <div>{v.map((x) => x.component)}</div>;128} else {129return (130<SettingBox style={{ marginBottom: "0px" }} show_header={false}>131{v.map((x) => x.component)}132</SettingBox>133);134}135}136137function renderBody() {138return (139<>140{children}141{render_keys()}142</>143);144}145146if (isFlyout) {147return renderBody();148} else {149return (150<SettingBox title={render_header()} icon={"list-ul"}>151{renderBody()}152</SettingBox>153);154}155}156157interface OneSSHKeyProps {158ssh_key: Map<string, any>;159project_id?: string;160mode?: "project" | "flyout";161}162163function OneSSHKey({ ssh_key, project_id, mode = "project" }: OneSSHKeyProps) {164const isFlyout = mode === "flyout";165166function render_last_use(): React.JSX.Element {167const d = ssh_key.get("last_use_date");168if (d) {169return (170<span style={{ color: "#1e7e34" }}>171Last used <TimeAgo date={new Date(d)} />172</span>173);174} else {175return <span style={{ color: "#333" }}>Never used</span>;176}177}178179function delete_key(): void {180const fingerprint = ssh_key.get("fingerprint");181if (project_id) {182redux.getActions("projects").delete_ssh_key_from_project({183fingerprint,184project_id: project_id,185});186} else {187redux.getActions("account").delete_ssh_key(fingerprint);188}189}190191const key_style: React.CSSProperties = {192fontSize: isFlyout ? "42px" : "72px",193color: ssh_key.get("last_use_date") ? "#1e7e34" : "#888",194};195196return (197<div198style={{199display: "flex",200borderBottom: "1px solid #ccc",201padding: "15px",202}}203>204<div style={{ width: isFlyout ? "48px" : "100px", display: "flex" }}>205<Icon style={key_style} name="key" />206</div>207<div style={{ flex: 1 }}>208<Popconfirm209title={210<div>211Are you sure you want to delete this SSH key? <br />212This CANNOT be undone. <br /> If you want to reuse this key in the213future, you will have to upload it again.214</div>215}216onConfirm={() => delete_key()}217okText={"Yes, delete key"}218cancelText={<CancelText />}219>220<Button221type="link"222size={isFlyout ? "small" : "middle"}223style={{ float: "right" }}224>225<Icon name="trash" /> Delete...226</Button>227</Popconfirm>228<div style={{ fontWeight: 600 }}>{ssh_key.get("title")}</div>229<span style={{ fontWeight: 600 }}>Fingerprint: </span>230<Typography.Text code style={{ fontSize: "80%" }}>231{ssh_key.get("fingerprint")}232</Typography.Text>233<br />234Added on {new Date(ssh_key.get("creation_date")).toLocaleDateString()}235<div> {render_last_use()} (NOTE: not all usage is tracked.)</div>236</div>237</div>238);239}240241242