Skip to content

Commit 2429b15

Browse files
committed
Merge remote-tracking branch 'origin/master' into delete-project-data
2 parents 39d0065 + 72b6ecf commit 2429b15

File tree

7 files changed

+106
-50
lines changed

7 files changed

+106
-50
lines changed

src/packages/frontend/compute/onprem-config.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export default function OnPremCloudConfiguration({
7777
if (!editable || !project_id) {
7878
return (
7979
<div>
80-
On Prem {configuration.arch == "arm64" ? "ARM64" : "x86_64"} Linux VM
80+
OnPrem {configuration.arch == "arm64" ? "ARM64" : "x86_64"} Linux VM
8181
{configuration.gpu ? " that has an NVIDIA GPU" : ""}.
8282
</div>
8383
);
@@ -91,7 +91,7 @@ export default function OnPremCloudConfiguration({
9191
<div style={{ marginBottom: "30px" }}>
9292
<div style={{ color: "#666", marginBottom: "15px" }}>
9393
<div style={{ color: "#666", marginBottom: "5px" }}>
94-
<b>On Prem Compute Server</b>
94+
<b>OnPrem Compute Server</b>
9595
</div>
9696
You can connect your own <b>Ubuntu 22.04 Virtual Machine</b> to this
9797
CoCalc project and seamlessly run Jupyter notebooks and terminals on it.

src/packages/frontend/customize.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export interface CustomizeState {
127127
max_upgrades: TypedMap<Partial<Upgrades>>;
128128
nonfree_countries?: List<string>;
129129
limit_free_project_uptime: number; // minutes
130+
require_license_to_create_project?: boolean;
130131
onprem_quota_heading: string;
131132
organization_email: string;
132133
organization_name: string;

src/packages/frontend/projects/actions.ts

+3
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ export class ProjectsActions extends Actions<ProjectsState> {
347347
image?: string; // if given, sets the compute image (the ID string)
348348
start?: boolean; // immediately start on create
349349
noPool?: boolean; // never use the pool
350+
license?: string;
350351
}): Promise<string> {
351352
const image = await redux.getStore("customize").getDefaultComputeImage();
352353

@@ -356,12 +357,14 @@ export class ProjectsActions extends Actions<ProjectsState> {
356357
image?: string;
357358
start: boolean;
358359
noPool?: boolean;
360+
license?: string;
359361
} = defaults(opts, {
360362
title: "No Title",
361363
description: "No Description",
362364
image,
363365
start: false,
364366
noPool: undefined,
367+
license: undefined,
365368
});
366369
if (!opts2.image) {
367370
// make falseish same as not specified.

src/packages/frontend/projects/create-project.tsx

+64-32
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Create a new project
99

1010
import { Button, Card, Col, Form, Input, Row } from "antd";
1111
import { delay } from "awaiting";
12-
12+
import { BuyLicenseForProject } from "@cocalc/frontend/site-licenses/purchase/buy-license-for-project";
1313
import { Alert, Well } from "@cocalc/frontend/antd-bootstrap";
1414
import {
1515
CSS,
@@ -48,9 +48,10 @@ interface Props {
4848

4949
type EditState = "edit" | "view" | "saving";
5050

51-
export const NewProjectCreator: React.FC<Props> = (props: Props) => {
52-
const { start_in_edit_mode, default_value } = props;
53-
51+
export const NewProjectCreator: React.FC<Props> = ({
52+
start_in_edit_mode,
53+
default_value,
54+
}: Props) => {
5455
const managed_licenses = useTypedRedux("billing", "managed_licenses");
5556

5657
// view --> edit --> saving --> view
@@ -60,7 +61,6 @@ export const NewProjectCreator: React.FC<Props> = (props: Props) => {
6061
const [title_text, set_title_text] = useState<string>(default_value ?? "");
6162
const [error, set_error] = useState<string>("");
6263
const [show_advanced, set_show_advanced] = useState<boolean>(false);
63-
const [show_add_license, set_show_add_license] = useState<boolean>(false);
6464
const [title_prefill, set_title_prefill] = useState<boolean>(false);
6565
const [license_id, set_license_id] = useState<string>("");
6666
const [warnBoost, setWarnBoost] = useState<boolean>(false);
@@ -72,6 +72,12 @@ export const NewProjectCreator: React.FC<Props> = (props: Props) => {
7272

7373
const is_anonymous = useTypedRedux("account", "is_anonymous");
7474
const customize_kucalc = useTypedRedux("customize", "kucalc");
75+
const requireLicense = !!useTypedRedux(
76+
"customize",
77+
"require_license_to_create_project",
78+
);
79+
const [show_add_license, set_show_add_license] =
80+
useState<boolean>(requireLicense);
7581

7682
// onprem and cocalc.com use licenses to adjust quota configs – but only cocalc.com has custom software images
7783
const show = useMemo(
@@ -110,7 +116,7 @@ export const NewProjectCreator: React.FC<Props> = (props: Props) => {
110116
set_error("");
111117
set_custom_software({});
112118
set_show_advanced(false);
113-
set_show_add_license(false);
119+
set_show_add_license(requireLicense);
114120
set_title_prefill(true);
115121
set_license_id("");
116122
}
@@ -131,6 +137,7 @@ export const NewProjectCreator: React.FC<Props> = (props: Props) => {
131137
title: title_text,
132138
image: await derive_project_img_name(custom_software),
133139
start: true, // used to not start, due to apply_default_upgrades, but upgrades are deprecated
140+
license: license_id,
134141
};
135142
try {
136143
project_id = await actions.create_project(opts);
@@ -140,9 +147,6 @@ export const NewProjectCreator: React.FC<Props> = (props: Props) => {
140147
set_error(`Error creating project -- ${err}`);
141148
return;
142149
}
143-
if (isValidUUID(license_id)) {
144-
await actions.add_site_license_to_project(project_id, license_id);
145-
}
146150
track("create-project", {
147151
how: "projects-page",
148152
project_id,
@@ -224,10 +228,13 @@ export const NewProjectCreator: React.FC<Props> = (props: Props) => {
224228
);
225229
}
226230

227-
function create_disabled() {
231+
function isDisabled() {
232+
if (requireLicense && !license_id) {
233+
return true;
234+
}
228235
return (
229236
// no name of new project
230-
title_text === "" ||
237+
!title_text?.trim() ||
231238
// currently saving (?)
232239
state === "saving" ||
233240
// user wants a non-default image, but hasn't selected one yet
@@ -283,8 +290,20 @@ export const NewProjectCreator: React.FC<Props> = (props: Props) => {
283290
function render_add_license() {
284291
if (!show_add_license) return;
285292
return (
286-
<Card size="small" title="Select license" style={CARD_STYLE}>
293+
<Card
294+
size="small"
295+
title={
296+
<>
297+
<div style={{ float: "right" }}>
298+
<BuyLicenseForProject size="small" />
299+
</div>
300+
<Icon name="key" /> Select License
301+
</>
302+
}
303+
style={CARD_STYLE}
304+
>
287305
<SiteLicenseInput
306+
requireValid
288307
confirmLabel={"Add this license"}
289308
onChange={addSiteLicense}
290309
/>
@@ -322,6 +341,7 @@ export const NewProjectCreator: React.FC<Props> = (props: Props) => {
322341
return (
323342
<div style={TOGGLE_STYLE}>
324343
<Button
344+
disabled={requireLicense}
325345
onClick={() => set_show_add_license(true)}
326346
type="link"
327347
style={TOGGLE_BUTTON_STYLE}
@@ -345,7 +365,7 @@ export const NewProjectCreator: React.FC<Props> = (props: Props) => {
345365
>
346366
add/remove licenses
347367
</A>{" "}
348-
in the project settings later on.
368+
in project settings later.
349369
</div>
350370
);
351371
}
@@ -381,36 +401,48 @@ export const NewProjectCreator: React.FC<Props> = (props: Props) => {
381401
/>
382402
</Form.Item>
383403
</Form>
404+
<div style={{ color: COLORS.GRAY, float: "right" }}>
405+
You can change the title at any time.
406+
</div>
384407
</Col>
385408
<Col sm={12}>
386409
<div style={{ color: COLORS.GRAY, marginLeft: "30px" }}>
387-
A <A href="https://doc.cocalc.com/project.html">project</A> is an
388-
isolated private computational workspace that you can share with
389-
others. You can easily change the project's title at any time in
390-
project settings.
410+
A <A href="https://doc.cocalc.com/project.html">project</A> is a
411+
private computational workspace that you can use with
412+
collaborators that you explicitly invite. You can attach powerful{" "}
413+
<A href="https://doc.cocalc.com/compute_server.html">
414+
GPUs, CPUs
415+
</A>{" "}
416+
and{" "}
417+
<A href="https://doc.cocalc.com/cloud_file_system.html">
418+
storage
419+
</A>{" "}
420+
to a project.
391421
</div>
392422
</Col>
393423
</Row>
394-
{render_advanced_toggle()}
395-
{render_advanced()}
396424
{render_add_license_toggle()}
397425
{render_add_license()}
398426
{render_license()}
427+
{render_advanced_toggle()}
428+
{render_advanced()}
399429
<Row>
400430
<Col sm={24} style={{ marginTop: "10px" }}>
401-
<Button.Group>
402-
<Button disabled={state === "saving"} onClick={cancel_editing}>
403-
Cancel
404-
</Button>
405-
<Button
406-
disabled={create_disabled()}
407-
onClick={() => create_project()}
408-
type="primary"
409-
>
410-
Create Project
411-
{create_disabled() ? " (enter a title above!)" : ""}
412-
</Button>
413-
</Button.Group>
431+
<Button
432+
disabled={state === "saving"}
433+
onClick={cancel_editing}
434+
style={{ marginRight: "8px" }}
435+
>
436+
Cancel
437+
</Button>
438+
<Button
439+
disabled={isDisabled()}
440+
onClick={() => create_project()}
441+
type="primary"
442+
>
443+
Create Project
444+
{requireLicense && !license_id && <> (select license above)</>}
445+
</Button>
414446
</Col>
415447
</Row>
416448
<Row>

src/packages/frontend/site-licenses/input.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ interface Props {
3434
style?: CSS;
3535
extra?: React.ReactNode;
3636
extraButtons?: React.ReactNode;
37+
requireValid?: boolean;
3738
}
3839

3940
export const SiteLicenseInput: React.FC<Props> = (props: Props) => {
@@ -46,6 +47,7 @@ export const SiteLicenseInput: React.FC<Props> = (props: Props) => {
4647
confirmLabel = "Apply License",
4748
extra,
4849
extraButtons,
50+
requireValid,
4951
} = props;
5052

5153
const managedLicenses = useManagedLicenses();
@@ -63,6 +65,7 @@ export const SiteLicenseInput: React.FC<Props> = (props: Props) => {
6365
style={style}
6466
extra={extra}
6567
extraButtons={extraButtons}
68+
requireValid={requireValid}
6669
/>
6770
);
6871
};

src/packages/frontend/site-licenses/select-license.tsx

+24-16
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface Props {
3535
style?: CSS;
3636
extra?: ReactNode; // plain-text node is ok
3737
extraButtons?: ReactNode;
38+
requireValid?: boolean;
3839
}
3940

4041
export default function SelectLicense(props: Props) {
@@ -49,6 +50,7 @@ export default function SelectLicense(props: Props) {
4950
style,
5051
extra,
5152
extraButtons,
53+
requireValid,
5254
} = props;
5355
const isBlurredRef = useRef<boolean>(true);
5456
const [licenseId, setLicenseId] = useState<string>(defaultLicenseId ?? "");
@@ -154,24 +156,30 @@ export default function SelectLicense(props: Props) {
154156
style={{ width: "100%", ...style }}
155157
>
156158
<div>
157-
{(showAll || licenseIds.length < len(managedLicenses)) && (
158-
<Checkbox
159-
style={{
160-
flex: "1 0 0",
161-
margin: "5px 0",
162-
color: COLORS.GRAY_M,
163-
whiteSpace: "nowrap",
164-
}}
165-
checked={showAll}
166-
onChange={() => setShowAll(!showAll)}
167-
>
168-
Show expired
169-
</Checkbox>
170-
)}{" "}
159+
{!requireValid &&
160+
(showAll || licenseIds.length < len(managedLicenses)) && (
161+
<Checkbox
162+
style={{
163+
flex: "1 0 0",
164+
margin: "5px 0",
165+
color: COLORS.GRAY_M,
166+
whiteSpace: "nowrap",
167+
}}
168+
checked={showAll}
169+
onChange={() => setShowAll(!showAll)}
170+
>
171+
Show expired
172+
</Checkbox>
173+
)}{" "}
171174
<Select
172-
style={{ width: "100%", flex: "1 1 0", marginRight: "10px" }}
175+
style={{
176+
width: "100%",
177+
height: licenseId ? "100px" : undefined,
178+
flex: "1 1 0",
179+
marginRight: "10px",
180+
}}
173181
placeholder={
174-
"Enter license code " +
182+
`Enter${requireValid ? " valid " : " "}license code ` +
175183
(options.length > 0
176184
? `or select from the ${options.length} licenses you manage`
177185
: "")

src/packages/util/db-schema/site-defaults.ts

+9
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export type SiteSettingsKeys =
7777
| "max_trial_projects"
7878
| "nonfree_countries"
7979
| "limit_free_project_uptime"
80+
| "require_license_to_create_project"
8081
| "google_analytics"
8182
| "kucalc"
8283
| "dns"
@@ -576,6 +577,14 @@ export const site_settings_conf: SiteSettings = {
576577
show: only_cocalc_com,
577578
to_display: (val) => `${val} minutes`,
578579
},
580+
require_license_to_create_project: {
581+
name: "Require License to Create Project",
582+
desc: "If yes the 'New Project' creation form on the projects page requires the user to enter a valid license. This has no other impact and only impacts the frontend UI. Users can circumvent this via the API or a course.",
583+
default: "no",
584+
valid: only_booleans,
585+
show: only_cocalc_com,
586+
to_val: to_bool,
587+
},
579588
datastore: {
580589
name: "Datastore",
581590
desc: `Show the '${DATASTORE_TITLE}' panel in the project settings`,

0 commit comments

Comments
 (0)