Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/components/api-keys.tsx
Views: 687
/*1React component for managing a list of api keys.23Applications:45- the keys for a project6- the keys for an account7*/89import {10Alert,11Button,12DatePicker,13Form,14Input,15Modal,16Popconfirm,17Space,18Table,19Typography,20} from "antd";21import { ColumnsType } from "antd/es/table";22import dayjs from "dayjs";23import { useEffect, useState } from "react";24import TimeAgo from "react-timeago"; // so can use from nextjs25const { Text, Paragraph } = Typography; // so can use from nextjs2627import { CancelText } from "@cocalc/frontend/i18n/components";28import type { ApiKey } from "@cocalc/util/db-schema/api-keys";29import { A } from "./A";30import CopyToClipBoard from "./copy-to-clipboard";31import { Icon } from "./icon";3233const { useForm } = Form;3435interface Props {36// Manage is a function that lets you get all api keys, delete a single api key,37// or create an api key.38// - If you call manage with input "get" it will return a Javascript array ApiKey[]39// of all your api keys, with each api key represented as an object {name, id, trunc, last_active?}40// as defined above. The actual key itself is not returned, and trunc is a truncated41// version of the key used for display.42// - If you call manage with input "delete" and id set then that key will get deleted.43// - If you call manage with input "create", then a new api key is created and returned44// as a single string. This is the one and only time the user can see this *secret*.45// - If call with edit and both name and id set, changes the key determined by id46// to have the given name. Similar for expire.47manage: (opts: {48action: "get" | "delete" | "create" | "edit";49id?: number;50name?: string;51expire?: Date;52}) => Promise<ApiKey[] | undefined>;53mode?: "project" | "flyout";54}5556export default function ApiKeys({ manage, mode = "project" }: Props) {57const isFlyout = mode === "flyout";58const size = isFlyout ? "small" : undefined; // for e.g. buttons59const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);60const [loading, setLoading] = useState<boolean>(true);61const [editingKey, setEditingKey] = useState<number | undefined>(undefined);62const [addModalVisible, setAddModalVisible] = useState<boolean>(false);63const [editModalVisible, setEditModalVisible] = useState<boolean>(false);64const [form] = useForm();65const [error, setError] = useState<string | null>(null);6667useEffect(() => {68getAllApiKeys();69}, []);7071const getAllApiKeys = async () => {72setLoading(true);73try {74const response = await manage({ action: "get" });75setApiKeys(response as ApiKey[]);76setLoading(false);77setError(null);78} catch (err) {79setLoading(false);80setError(err.message || "An error occurred");81}82};8384const deleteApiKey = async (id: number) => {85try {86await manage({ action: "delete", id });87getAllApiKeys();88} catch (err) {89setError(err.message || "An error occurred");90}91};9293const deleteAllApiKeys = async () => {94for (const { id } of apiKeys) {95await deleteApiKey(id);96}97};9899const editApiKey = async (id: number, name: string, expire?: Date) => {100try {101await manage({ action: "edit", id, name, expire });102getAllApiKeys();103} catch (err) {104setError(err.message || "An error occurred");105}106};107108const createApiKey = async (name: string, expire?: Date) => {109try {110const response = await manage({111action: "create",112name,113expire,114});115setAddModalVisible(false);116getAllApiKeys();117118Modal.success({119title: "New Secret API Key",120content: (121<>122<div>123Save this secret key somewhere safe.{" "}124<b>You won't be able to view it again here.</b> If you lose this125secret key, you'll need to generate a new one.126</div>127<div style={{ marginTop: 16 }}>128<strong>Secret API Key</strong>{" "}129<CopyToClipBoard130style={{ marginTop: "16px" }}131value={response?.[0].secret ?? "failed to get secret"}132/>133</div>134</>135),136});137setError(null);138} catch (err) {139setError(err.message || "An error occurred");140}141};142143const columns: ColumnsType<ApiKey> = [144{145dataIndex: "name",146title: "Name/Key",147render: (name, record) => {148return (149<>150{name}151<br />152<Text type="secondary">({record.trunc})</Text>153</>154);155},156},157{158dataIndex: "last_active",159title: "Last Used",160render: (last_active) =>161last_active ? <TimeAgo date={last_active} /> : "Never",162},163{164dataIndex: "expire",165title: "Expire",166render: (expire) => (expire ? <TimeAgo date={expire} /> : "Never"),167},168{169dataIndex: "operation",170title: "Operation",171align: "right",172render: (_text, record) => (173<Space.Compact direction={isFlyout ? "vertical" : "horizontal"}>174<Popconfirm175title="Are you sure you want to delete this key?"176onConfirm={() => deleteApiKey(record.id)}177>178<a>Delete</a>179</Popconfirm>180<a181onClick={() => {182// Set the initial form value as the current key name183form.setFieldsValue({ name: record.name });184setEditModalVisible(true);185setEditingKey(record.id);186}}187style={{ marginLeft: "1em" }}188>189Edit190</a>191</Space.Compact>192),193},194];195196if (!isFlyout) {197columns.splice(1, 0, { dataIndex: "id", title: "Id" });198}199200const handleAdd = () => {201setAddModalVisible(true);202};203204const handleModalOK = () => {205const name = form.getFieldValue("name");206const expire = form.getFieldValue("expire");207if (editingKey != null) {208editApiKey(editingKey, name, expire);209setEditModalVisible(false);210setEditingKey(undefined);211form.resetFields();212} else {213createApiKey(name, expire);214form.resetFields();215}216};217218const handleModalCancel = () => {219setAddModalVisible(false);220setEditModalVisible(false);221setEditingKey(undefined);222form.resetFields();223};224225return (226<>227{error && (228<Alert229message={error}230type="error"231closable232onClose={() => setError(null)}233style={{ marginBottom: 16 }}234/>235)}236{apiKeys.length > 0 && (237<Table238style={{ marginBottom: 16 }}239dataSource={apiKeys}240columns={columns}241loading={loading}242rowKey="id"243pagination={false}244/>245)}246<div style={isFlyout ? { padding: "5px" } : undefined}>247<Space.Compact size={size}>248<Button onClick={handleAdd} size={size}>249<Icon name="plus-circle" /> Add API key...250</Button>251<Button onClick={getAllApiKeys} size={size}>252Refresh253</Button>254{apiKeys.length > 0 && (255<Popconfirm256title="Are you sure you want to delete all these api keys?"257onConfirm={deleteAllApiKeys}258>259<Button danger size={size}>260Delete All...261</Button>262</Popconfirm>263)}264</Space.Compact>265<Paragraph style={{ marginTop: "10px" }}>266Read the <A href="https://doc.cocalc.com/api/">API documentation</A>.267</Paragraph>268<Modal269open={addModalVisible || editModalVisible}270title={271editingKey != null ? "Edit API Key Name" : "Create a New API Key"272}273okText={editingKey != null ? "Save" : "Create"}274cancelText={<CancelText />}275onCancel={handleModalCancel}276onOk={handleModalOK}277>278<Form form={form} layout="vertical">279<Form.Item280name="name"281label="Name"282rules={[{ required: true, message: "Please enter a name" }]}283>284<Input />285</Form.Item>286<Form.Item287name="expire"288label="Expire"289rules={[290{291required: false,292message:293"Optional date when key will be automatically deleted",294},295]}296>297<DatePicker298changeOnBlur299showTime300disabledDate={(current) => {301// disable all dates before today302return current && current < dayjs();303}}304/>305</Form.Item>306</Form>307</Modal>308</div>309</>310);311}312313314