Path: blob/master/src/packages/frontend/admin/registration-token-dialog.tsx
5899 views
/*1* This file is part of CoCalc: Copyright © 2025 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import {6Alert,7Button as AntdButton,8Checkbox,9DatePicker,10Form,11Input,12InputNumber,13Modal,14Radio,15Space,16Switch,17} from "antd";18import type { RadioChangeEvent } from "antd";1920import { CancelText } from "@cocalc/frontend/i18n/components";21import { SiteLicenseInput } from "@cocalc/frontend/site-licenses/input";22import {23CUSTOM_PRESET_KEY,24EPHEMERAL_PRESETS,25EPHEMERAL_OFF_KEY,26HOUR_MS,27msToHours,28type Token,29} from "./types";3031interface RegistrationTokenDialogProps {32open: boolean;33isEdit: boolean;34editingToken: Token | null;35onCancel: () => void;36onSave: (values: Token) => Promise<void>;37onReset: () => void;38error?: string;39form: any;40newRandomToken: () => string;41saving: boolean;42licenseInputKey: number;43}4445export default function RegistrationTokenDialog({46open,47isEdit,48editingToken,49onCancel,50onSave,51onReset,52error,53form,54newRandomToken,55saving,56licenseInputKey,57}: RegistrationTokenDialogProps) {58const onFinish = async (values) => {59await onSave(values);60};6162const onRandom = () => form.setFieldsValue({ token: newRandomToken() });63const limitMin = editingToken != null ? (editingToken.counter ?? 0) : 0;6465function renderFooter() {66return [67<AntdButton key="random" onClick={onRandom}>68Randomize69</AntdButton>,70<AntdButton key="reset" onClick={onReset}>71Reset72</AntdButton>,73<AntdButton key="cancel" onClick={onCancel}>74<CancelText />75</AntdButton>,76<AntdButton77key="save"78type="primary"79onClick={() => form.submit()}80loading={saving}81>82Save83</AntdButton>,84];85}8687function renderError() {88if (!error) return null;89return (90<Alert type="error" showIcon style={{ marginTop: 12 }} message={error} />91);92}9394function renderEphemeralControls() {95return (96<Form.Item label="Ephemeral lifetime">97<Form.Item98noStyle99shouldUpdate={(prev, curr) =>100prev.ephemeral !== curr.ephemeral ||101prev._ephemeralMode !== curr._ephemeralMode102}103>104{(formInstance) => {105const ephemeral = formInstance.getFieldValue("ephemeral");106const mode = formInstance.getFieldValue("_ephemeralMode");107const customHours = msToHours(ephemeral);108109const selection =110mode ??111(ephemeral != null ? CUSTOM_PRESET_KEY : EPHEMERAL_OFF_KEY);112113const handleRadioChange = ({114target: { value },115}: RadioChangeEvent) => {116if (value === EPHEMERAL_OFF_KEY) {117formInstance.setFieldsValue({118ephemeral: undefined,119_ephemeralMode: EPHEMERAL_OFF_KEY,120});121return;122}123if (value === CUSTOM_PRESET_KEY) {124formInstance.setFieldsValue({125ephemeral: ephemeral != null ? ephemeral : HOUR_MS,126_ephemeralMode: CUSTOM_PRESET_KEY,127});128return;129}130const preset = EPHEMERAL_PRESETS.find(131(option) => option.key === value,132);133formInstance.setFieldsValue({134ephemeral: preset?.value,135_ephemeralMode: value,136});137};138139const handleCustomHoursChange = (hours: number | string | null) => {140const numeric =141typeof hours === "string" ? parseFloat(hours) : hours;142if (typeof numeric === "number" && !isNaN(numeric)) {143formInstance.setFieldsValue({144ephemeral: numeric >= 1 ? numeric * HOUR_MS : HOUR_MS,145});146} else {147formInstance.setFieldsValue({ ephemeral: HOUR_MS });148}149};150151return (152<>153<Radio.Group value={selection} onChange={handleRadioChange}>154<Radio value={EPHEMERAL_OFF_KEY}>Off</Radio>155{EPHEMERAL_PRESETS.map(({ key, label }) => (156<Radio key={key} value={key}>157{label}158</Radio>159))}160<Radio value={CUSTOM_PRESET_KEY}>Custom</Radio>161</Radio.Group>162{selection === CUSTOM_PRESET_KEY && (163<div style={{ marginTop: "10px" }}>164<InputNumber165min={1}166step={1}167value={customHours ?? 1}168onChange={handleCustomHoursChange}169placeholder="Enter hours"170/>{" "}171hours172</div>173)}174</>175);176}}177</Form.Item>178</Form.Item>179);180}181182function renderRestrictions() {183return (184<Form.Item label="Restrictions">185<Space direction="vertical">186<Form.Item187name={["customize", "disableCollaborators"]}188valuePropName="checked"189noStyle190>191<Checkbox>Disable configuring collaborators</Checkbox>192</Form.Item>193<Form.Item194name={["customize", "disableAI"]}195valuePropName="checked"196noStyle197>198<Checkbox>Disable artificial intelligence</Checkbox>199</Form.Item>200<Form.Item201name={["customize", "disableInternet"]}202valuePropName="checked"203noStyle204>205<Checkbox>Disable internet access</Checkbox>206</Form.Item>207</Space>208</Form.Item>209);210}211212function renderLicense() {213return (214<Form.Item215name={["customize", "license"]}216label="License"217extra="Optional: Apply a site license to projects created via this token"218>219<SiteLicenseInput220key={licenseInputKey}221defaultLicenseId={form.getFieldValue(["customize", "license"])}222onChange={(licenseId) =>223form.setFieldValue(["customize", "license"], licenseId)224}225/>226</Form.Item>227);228}229230function renderForm() {231return (232<Form233form={form}234labelCol={{ span: 6 }}235wrapperCol={{ span: 18 }}236size="middle"237onFinish={onFinish}238>239<Form.Item name="token" label="Token" rules={[{ required: true }]}>240<Input disabled={true} />241</Form.Item>242<Form.Item243name="descr"244label="Description"245rules={[{ required: false }]}246>247<Input />248</Form.Item>249<Form.Item name="expires" label="Expires" rules={[{ required: false }]}>250<DatePicker />251</Form.Item>252<Form.Item name="limit" label="Limit" rules={[{ required: false }]}>253<InputNumber min={limitMin} step={1} />254</Form.Item>255<Form.Item name="ephemeral" hidden>256<InputNumber />257</Form.Item>258<Form.Item name="_ephemeralMode" hidden>259<Input />260</Form.Item>261{renderEphemeralControls()}262{renderRestrictions()}263{renderLicense()}264<Form.Item name="active" label="Active" valuePropName="checked">265<Switch />266</Form.Item>267</Form>268);269}270271return (272<Modal273open={open}274title={isEdit ? "Edit Registration Token" : "Create Registration Token"}275width={800}276destroyOnHidden={true}277maskClosable={false}278onCancel={onCancel}279footer={renderFooter()}280>281{renderForm()}282{renderError()}283</Modal>284);285}286287288