Skip to content

Commit 5c2ef21

Browse files
Merge branch 'master' into wcag
2 parents 550b929 + 70a0a43 commit 5c2ef21

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+761
-108
lines changed

src/packages/api-client/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"scripts": {
77
"preinstall": "npx only-allow pnpm",
88
"build": "../node_modules/.bin/tsc --build",
9-
"tsc": "../node_modules/.bin/tsc --watch --pretty --preserveWatchOutput",
109
"depcheck": "pnpx depcheck --ignores @cocalc/api-client "
1110
},
1211
"files": ["dist/**", "bin/**", "README.md", "package.json"],

src/packages/backend/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
"preinstall": "npx only-allow pnpm",
1919
"clean": "rm -rf dist node_modules",
2020
"build": "pnpm exec tsc --build",
21-
"tsc": "pnpm exec tsc --watch --pretty --preserveWatchOutput",
2221
"test": "pnpm exec jest --forceExit",
2322
"test-conat": " pnpm exec jest --forceExit conat",
2423
"testp": "pnpm exec jest --forceExit",

src/packages/comm/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
"scripts": {
1818
"preinstall": "npx only-allow pnpm",
1919
"build": "../node_modules/.bin/tsc --build",
20-
"tsc": "../node_modules/.bin/tsc --watch --pretty --preserveWatchOutput",
2120
"depcheck": "pnpx depcheck --ignores @types/node"
2221
},
2322
"author": "SageMath, Inc.",

src/packages/conat/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
"preinstall": "npx only-allow pnpm",
2121
"build": "pnpm exec tsc --build",
2222
"clean": "rm -rf dist node_modules",
23-
"tsc": "pnpm exec tsc --watch --pretty --preserveWatchOutput",
2423
"test": "pnpm exec jest",
2524
"depcheck": "pnpx depcheck --ignores events,bufferutil,utf-8-validate"
2625
},

src/packages/database/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
"preinstall": "npx only-allow pnpm",
4545
"build": "../node_modules/.bin/tsc --build && coffee -c -o dist/ ./",
4646
"clean": "rm -rf dist",
47-
"tsc": "../node_modules/.bin/tsc --watch --pretty --preserveWatchOutput",
4847
"test": "pnpm exec jest --forceExit --runInBand",
4948
"depcheck": "pnpx depcheck | grep -Ev '\\.coffee|coffee$'",
5049
"prepublishOnly": "pnpm test"

src/packages/database/postgres/registration-tokens.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ interface Query {
1111
expires?: Date;
1212
limit?: number;
1313
disabled?: boolean;
14+
ephemeral?: boolean;
15+
customize?;
1416
}
1517

1618
export default async function registrationTokensQuery(
1719
db: PostgreSQL,
1820
options: { delete?: boolean }[],
19-
query: Query
21+
query: Query,
2022
) {
2123
if (isDelete(options) && query.token) {
2224
// delete if option is set and there is a token which is defined and not an empty string
@@ -37,22 +39,27 @@ export default async function registrationTokensQuery(
3739
return rows;
3840
} else if (query.token) {
3941
// upsert an existing one
40-
const { token, descr, expires, limit, disabled } = query;
42+
const { token, descr, expires, limit, disabled, ephemeral, customize } =
43+
query;
4144
const { rows } = await callback2(db._query, {
42-
query: `INSERT INTO registration_tokens ("token","descr","expires","limit","disabled")
43-
VALUES ($1, $2, $3, $4, $5) ON CONFLICT (token)
45+
query: `INSERT INTO registration_tokens ("token","descr","expires","limit","disabled","ephemeral","customize")
46+
VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (token)
4447
DO UPDATE SET
4548
"token" = EXCLUDED.token,
4649
"descr" = EXCLUDED.descr,
4750
"expires" = EXCLUDED.expires,
4851
"limit" = EXCLUDED.limit,
49-
"disabled" = EXCLUDED.disabled`,
52+
"disabled" = EXCLUDED.disabled,
53+
"ephemeral" = EXCLUDED.ephemeral,
54+
"customize" = EXCLUDED.customize`,
5055
params: [
5156
token,
5257
descr ? descr : null,
5358
expires ? expires : null,
5459
limit == null ? null : limit, // if undefined make it null
5560
disabled != null ? disabled : false,
61+
ephemeral == null ? null : ephemeral,
62+
customize == null ? null : customize,
5663
],
5764
});
5865
return rows;

src/packages/file-server/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
"scripts": {
1010
"preinstall": "npx only-allow pnpm",
1111
"build": "pnpm exec tsc --build",
12-
"tsc": "pnpm exec tsc --watch --pretty --preserveWatchOutput",
1312
"test": "pnpm exec jest",
1413
"depcheck": "pnpx depcheck",
1514
"clean": "rm -rf node_modules dist"

src/packages/frontend/account/table.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export class AccountTable extends Table {
4646
last_active: null,
4747
ssh_keys: null,
4848
created: null,
49+
ephemeral: null,
50+
customize: null,
4951
unlisted: null,
5052
tags: null,
5153
tours: null,

src/packages/frontend/account/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ export interface AccountState {
3636
name?: string;
3737
unlisted?: boolean;
3838
profile: TypedMap<{ color: string }>;
39+
customize?: {
40+
disableCollaborators?: boolean;
41+
disableAI?: boolean;
42+
[key: string]: any;
43+
};
3944
email_address?: string;
4045
editor_settings: TypedMap<{
4146
jupyter_classic?: boolean;
@@ -90,6 +95,7 @@ export interface AccountState {
9095
is_ready: boolean; // user signed in and account settings have been loaded.
9196
lti_id?: List<string>;
9297
created?: Date;
98+
ephemeral?: number;
9399
strategies?: List<TypedMap<PassportStrategyFrontend>>;
94100
token?: boolean; // whether or not a registration token is required when creating an account
95101
keyboard_variant_options?: List<any>;

src/packages/frontend/admin/registration-token.tsx

Lines changed: 176 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ import {
1515
Input,
1616
InputNumber,
1717
Popconfirm,
18+
Radio,
19+
Space,
1820
Switch,
1921
Table,
2022
} from "antd";
23+
import type { RadioChangeEvent } from "antd";
2124
import dayjs from "dayjs";
2225
import { List } from "immutable";
2326
import { pick, sortBy } from "lodash";
@@ -36,6 +39,7 @@ import {
3639
Saving,
3740
TimeAgo,
3841
} from "@cocalc/frontend/components";
42+
import Copyable from "@cocalc/frontend/components/copy-to-clipboard";
3943
import { query } from "@cocalc/frontend/frame-editors/generic/client";
4044
import { CancelText } from "@cocalc/frontend/i18n/components";
4145
import { RegistrationTokenSetFields } from "@cocalc/util/db-schema/types";
@@ -52,6 +56,43 @@ interface Token {
5256
limit?: number;
5357
counter?: number; // readonly
5458
expires?: dayjs.Dayjs; // DB uses Date objects, watch out!
59+
ephemeral?: number;
60+
customize?: {
61+
disableCollaborators?: boolean;
62+
disableAI?: boolean;
63+
};
64+
}
65+
66+
const HOUR_MS = 60 * 60 * 1000;
67+
const EPHEMERAL_PRESETS = [
68+
{ key: "6h", label: "6 hours", value: 6 * HOUR_MS },
69+
{ key: "1d", label: "1 day", value: 24 * HOUR_MS },
70+
{ key: "1w", label: "1 week", value: 7 * 24 * HOUR_MS },
71+
] as const;
72+
const CUSTOM_PRESET_KEY = "custom";
73+
74+
function msToHours(value?: number): number | undefined {
75+
if (value == null) return undefined;
76+
return value / HOUR_MS;
77+
}
78+
79+
function findPresetKey(value?: number): string | undefined {
80+
if (value == null) return undefined;
81+
return EPHEMERAL_PRESETS.find((preset) => preset.value === value)?.key;
82+
}
83+
84+
function formatEphemeralHours(value?: number): string {
85+
const hours = msToHours(value);
86+
return hours == null ? "" : `${round1(hours)} h`;
87+
}
88+
89+
function ephemeralSignupUrl(token?: string): string {
90+
if (!token) return "";
91+
if (typeof window === "undefined") {
92+
return `/ephemeral?token=${token}`;
93+
}
94+
const { protocol, host } = window.location;
95+
return `${protocol}//${host}/ephemeral?token=${token}`;
5596
}
5697

5798
function use_registration_tokens() {
@@ -84,6 +125,8 @@ function use_registration_tokens() {
84125
expires: null,
85126
limit: null,
86127
disabled: null,
128+
ephemeral: null,
129+
customize: null,
87130
},
88131
},
89132
});
@@ -140,11 +183,19 @@ function use_registration_tokens() {
140183
"expires",
141184
"limit",
142185
"descr",
186+
"ephemeral",
187+
"customize",
143188
] as RegistrationTokenSetFields[]);
144189
// set optional field to undefined (to get rid of it)
145-
["descr", "limit", "expires"].forEach(
190+
["descr", "limit", "expires", "ephemeral"].forEach(
146191
(k: RegistrationTokenSetFields) => (val[k] = val[k] ?? undefined),
147192
);
193+
if (val.customize != null) {
194+
const { disableCollaborators, disableAI } = val.customize;
195+
if (!disableCollaborators && !disableAI) {
196+
val.customize = undefined;
197+
}
198+
}
148199
try {
149200
set_saving(true);
150201
await query({
@@ -278,7 +329,7 @@ export function RegistrationToken() {
278329

279330
const onFinish = (values) => save(values);
280331
const onRandom = () => form.setFieldsValue({ token: new_random_token() });
281-
const limit_min = editing != null ? editing.counter ?? 0 : 0;
332+
const limit_min = editing != null ? (editing.counter ?? 0) : 0;
282333

283334
return (
284335
<Form
@@ -304,6 +355,98 @@ export function RegistrationToken() {
304355
<Form.Item name="limit" label="Limit" rules={[{ required: false }]}>
305356
<InputNumber min={limit_min} step={1} />
306357
</Form.Item>
358+
<Form.Item name="ephemeral" hidden>
359+
<InputNumber />
360+
</Form.Item>
361+
<Form.Item label="Ephemeral lifetime">
362+
<Form.Item
363+
noStyle
364+
shouldUpdate={(prev, curr) => prev.ephemeral !== curr.ephemeral}
365+
>
366+
{(formInstance) => {
367+
const ephemeral = formInstance.getFieldValue("ephemeral");
368+
const presetKey = findPresetKey(ephemeral);
369+
const selection =
370+
presetKey ??
371+
(ephemeral != null ? CUSTOM_PRESET_KEY : undefined);
372+
const customHours = msToHours(ephemeral);
373+
374+
const handleRadioChange = ({
375+
target: { value },
376+
}: RadioChangeEvent) => {
377+
if (value === CUSTOM_PRESET_KEY) {
378+
if (ephemeral == null) {
379+
formInstance.setFieldsValue({ ephemeral: HOUR_MS });
380+
}
381+
return;
382+
}
383+
const preset = EPHEMERAL_PRESETS.find(
384+
(option) => option.key === value,
385+
);
386+
formInstance.setFieldsValue({
387+
ephemeral: preset?.value,
388+
});
389+
};
390+
391+
const handleCustomHoursChange = (
392+
hours: number | string | null,
393+
) => {
394+
const numeric =
395+
typeof hours === "string" ? parseFloat(hours) : hours;
396+
if (typeof numeric === "number" && !isNaN(numeric)) {
397+
formInstance.setFieldsValue({
398+
ephemeral: numeric >= 0 ? numeric * HOUR_MS : undefined,
399+
});
400+
} else {
401+
formInstance.setFieldsValue({ ephemeral: undefined });
402+
}
403+
};
404+
405+
return (
406+
<>
407+
<Radio.Group value={selection} onChange={handleRadioChange}>
408+
{EPHEMERAL_PRESETS.map(({ key, label }) => (
409+
<Radio key={key} value={key}>
410+
{label}
411+
</Radio>
412+
))}
413+
<Radio value={CUSTOM_PRESET_KEY}>Custom</Radio>
414+
</Radio.Group>
415+
{selection === CUSTOM_PRESET_KEY && (
416+
<div style={{ marginTop: "10px" }}>
417+
<InputNumber
418+
min={0}
419+
step={1}
420+
value={customHours ?? undefined}
421+
onChange={handleCustomHoursChange}
422+
placeholder="Enter hours"
423+
/>{" "}
424+
hours
425+
</div>
426+
)}
427+
</>
428+
);
429+
}}
430+
</Form.Item>
431+
</Form.Item>
432+
<Form.Item label="Restrictions">
433+
<Space direction="vertical">
434+
<Form.Item
435+
name={["customize", "disableCollaborators"]}
436+
valuePropName="checked"
437+
noStyle
438+
>
439+
<Checkbox>Disable configuring collaborators</Checkbox>
440+
</Form.Item>
441+
<Form.Item
442+
name={["customize", "disableAI"]}
443+
valuePropName="checked"
444+
noStyle
445+
>
446+
<Checkbox>Disable artificial intelligence</Checkbox>
447+
</Form.Item>
448+
</Space>
449+
</Form.Item>
307450
<Form.Item name="active" label="Active" valuePropName="checked">
308451
<Switch />
309452
</Form.Item>
@@ -400,6 +543,22 @@ export function RegistrationToken() {
400543
defaultSortOrder={"ascend"}
401544
sorter={(a, b) => a.token.localeCompare(b.token)}
402545
/>
546+
<Table.Column<Token>
547+
title="Ephemeral link"
548+
width={240}
549+
render={(_, token) => {
550+
if (!token?.ephemeral) return null;
551+
const url = ephemeralSignupUrl(token.token);
552+
if (!url) return null;
553+
return (
554+
<Copyable
555+
value={url}
556+
inputWidth="14em"
557+
outerStyle={{ width: "100%" }}
558+
/>
559+
);
560+
}}
561+
/>
403562
<Table.Column<Token> title="Description" dataIndex="descr" />
404563
<Table.Column<Token>
405564
title="Uses"
@@ -411,6 +570,21 @@ export function RegistrationToken() {
411570
dataIndex="limit"
412571
render={(text) => (text != null ? text : "∞")}
413572
/>
573+
<Table.Column<Token>
574+
title="Ephemeral (hours)"
575+
dataIndex="ephemeral"
576+
render={(value) => formatEphemeralHours(value)}
577+
/>
578+
<Table.Column<Token>
579+
title="Restrict collaborators"
580+
render={(_, token) =>
581+
token.customize?.disableCollaborators ? "Yes" : ""
582+
}
583+
/>
584+
<Table.Column<Token>
585+
title="Disable AI"
586+
render={(_, token) => (token.customize?.disableAI ? "Yes" : "")}
587+
/>
414588
<Table.Column<Token>
415589
title="% Used"
416590
dataIndex="used"

0 commit comments

Comments
 (0)