CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/course/configuration/upgrades.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
// Upgrading quotas for all student projects
7
8
import {
9
Button,
10
Card,
11
Checkbox,
12
Divider,
13
Form,
14
Popconfirm,
15
Radio,
16
Space,
17
Switch,
18
Typography,
19
} from "antd";
20
import { delay } from "awaiting";
21
import { useEffect, useState } from "react";
22
import { FormattedMessage, useIntl } from "react-intl";
23
24
import { alert_message } from "@cocalc/frontend/alerts";
25
import { CSS, redux, useActions } from "@cocalc/frontend/app-framework";
26
import { A, Icon, Paragraph } from "@cocalc/frontend/components";
27
import Next from "@cocalc/frontend/components/next";
28
import { labels } from "@cocalc/frontend/i18n";
29
import { SiteLicenseInput } from "@cocalc/frontend/site-licenses/input";
30
import { SiteLicensePublicInfoTable } from "@cocalc/frontend/site-licenses/site-license-public-info";
31
import { SiteLicenses } from "@cocalc/frontend/site-licenses/types";
32
import { ShowSupportLink } from "@cocalc/frontend/support";
33
import { COLORS } from "@cocalc/util/theme";
34
import { CourseActions } from "../actions";
35
import {
36
CourseSettingsRecord,
37
CourseStore,
38
DEFAULT_LICENSE_UPGRADE_HOST_PROJECT,
39
} from "../store";
40
import { SiteLicenseStrategy } from "../types";
41
import { ConfigurationActions } from "./actions";
42
43
const radioStyle: CSS = {
44
display: "block",
45
whiteSpace: "normal",
46
fontWeight: "inherit", // this is to undo what react-bootstrap does to the labels.
47
} as const;
48
49
interface Props {
50
name: string;
51
is_onprem: boolean;
52
is_commercial: boolean;
53
institute_pay?: boolean;
54
student_pay?: boolean;
55
site_license_id?: string;
56
site_license_strategy?: SiteLicenseStrategy;
57
shared_project_id?: string;
58
disabled?: boolean;
59
settings: CourseSettingsRecord;
60
actions: ConfigurationActions;
61
}
62
63
export function StudentProjectUpgrades({
64
name,
65
is_onprem,
66
is_commercial,
67
institute_pay,
68
student_pay,
69
site_license_id,
70
site_license_strategy,
71
shared_project_id,
72
disabled,
73
settings,
74
actions,
75
}: Props) {
76
const intl = useIntl();
77
78
const course_actions = useActions<CourseActions>({ name });
79
const [show_site_license, set_show_site_license] = useState<boolean>(false);
80
81
function get_store(): CourseStore {
82
return redux.getStore(name) as any;
83
}
84
85
async function add_site_license_id(license_id: string) {
86
course_actions.configuration.add_site_license_id(license_id);
87
await delay(100);
88
course_actions.configuration.configure_all_projects();
89
}
90
91
async function remove_site_license_id(license_id: string) {
92
course_actions.configuration.remove_site_license_id(license_id);
93
await delay(100);
94
course_actions.configuration.configure_all_projects();
95
}
96
97
function render_site_license_text() {
98
if (!show_site_license) return;
99
return (
100
<div>
101
<br />
102
<FormattedMessage
103
id="course.upgrades.site_license_text.info"
104
defaultMessage={`Enter a license key below to automatically apply upgrades from that
105
license to this course project, all student projects, and the shared
106
project whenever they are running. Clear the field below to stop
107
applying those upgrades. Upgrades from the license are only applied when
108
a project is started.`}
109
/>{" "}
110
{is_commercial ? (
111
<FormattedMessage
112
id="course.upgrades.site_license_text.info-commercial"
113
defaultMessage={`Create a {support} if you need to purchase a license key
114
via a purchase order.`}
115
values={{
116
support: <ShowSupportLink />,
117
}}
118
/>
119
) : undefined}
120
<SiteLicenseInput
121
onSave={(license_id) => {
122
set_show_site_license(false);
123
add_site_license_id(license_id);
124
}}
125
onCancel={() => {
126
set_show_site_license(false);
127
}}
128
/>
129
</div>
130
);
131
}
132
133
function render_licenses(site_licenses: SiteLicenses): JSX.Element {
134
return (
135
<SiteLicensePublicInfoTable
136
site_licenses={site_licenses}
137
onRemove={(license_id) => {
138
remove_site_license_id(license_id);
139
}}
140
warn_if={(info, _) => {
141
const upgradeHostProject = settings.get(
142
"license_upgrade_host_project",
143
);
144
const n =
145
get_store().get_student_ids().length +
146
(upgradeHostProject ? 1 : 0) +
147
(shared_project_id ? 1 : 0);
148
if (info.run_limit < n) {
149
return (
150
<FormattedMessage
151
id="course.upgrades.render_licenses.note"
152
defaultMessage={`NOTE: This license can only upgrade {run_limit} simultaneous running projects,
153
but there are {n} projects associated to this course.`}
154
values={{ n, run_limit: info.run_limit }}
155
/>
156
);
157
}
158
}}
159
/>
160
);
161
}
162
163
function render_site_license_strategy() {
164
return (
165
<Paragraph
166
style={{
167
margin: "0",
168
border: `1px solid ${COLORS.GRAY_L}`,
169
padding: "15px",
170
borderRadius: "5px",
171
}}
172
>
173
<FormattedMessage
174
id="course.upgrades.license_strategy.explanation"
175
defaultMessage={`<b>License strategy:</b>
176
Since you have multiple licenses,
177
there are two different ways they can be used,
178
depending on whether you're trying to maximize the number of covered students
179
or the upgrades per students:`}
180
/>
181
<br />
182
<Radio.Group
183
disabled={disabled}
184
style={{ marginLeft: "15px", marginTop: "15px" }}
185
onChange={(e) => {
186
course_actions.configuration.set_site_license_strategy(
187
e.target.value,
188
);
189
course_actions.configuration.configure_all_projects(true);
190
}}
191
value={site_license_strategy ?? "serial"}
192
>
193
<Radio value={"serial"} key={"serial"} style={radioStyle}>
194
<FormattedMessage
195
id="course.upgrades.license_strategy.radio.coverage"
196
defaultMessage={`<b>Maximize number of covered students:</b>
197
apply one license to each project associated to this course
198
(e.g., you bought a license to handle a few more students who were added your course).
199
If you have more students than license seats,
200
the first students to start their projects will get the upgrades.`}
201
/>
202
</Radio>
203
<Radio value={"parallel"} key={"parallel"} style={radioStyle}>
204
<FormattedMessage
205
id="course.upgrades.license_strategy.radio.upgrades"
206
defaultMessage={` <b>Maximize upgrades to each project:</b>
207
apply all licenses to all projects associated to this course
208
(e.g., you bought a license to increase the RAM or CPU for all students).`}
209
/>
210
</Radio>
211
</Radio.Group>
212
<Divider type="horizontal" />
213
<FormattedMessage
214
id="course.upgrades.license_strategy.redistribute"
215
defaultMessage={`<Button>Redistribute licenses</Button> – e.g. useful if a license expired`}
216
values={{
217
Button: (c) => (
218
<Button
219
onClick={() =>
220
course_actions.configuration.configure_all_projects(true)
221
}
222
size="small"
223
>
224
<Icon name="arrows" /> {c}
225
</Button>
226
),
227
}}
228
/>
229
</Paragraph>
230
);
231
}
232
233
function render_current_licenses() {
234
if (!site_license_id) return;
235
const licenses = site_license_id.split(",");
236
237
const site_licenses: SiteLicenses = licenses.reduce((acc, v) => {
238
acc[v] = null; // we have no info about them yet
239
return acc;
240
}, {});
241
242
return (
243
<div style={{ margin: "15px 0" }}>
244
<FormattedMessage
245
id="course.upgades.current_licenses.info"
246
defaultMessage={`This project and all student projects will be upgraded using the
247
following
248
<b>{n} {n, select, 1 {license} other {licenses}}</b>,
249
unless it is expired or in use by too many projects:`}
250
values={{ n: licenses.length }}
251
/>
252
<br />
253
<div style={{ margin: "15px 0", padding: "0" }}>
254
{render_licenses(site_licenses)}
255
</div>
256
{licenses.length > 1 && render_site_license_strategy()}
257
</div>
258
);
259
}
260
261
function render_remove_all_licenses() {
262
return (
263
<Popconfirm
264
title={intl.formatMessage({
265
id: "course.upgrades.remove_all_licenses.title",
266
defaultMessage: "Remove all licenses from all student projects?",
267
})}
268
onConfirm={async () => {
269
try {
270
await course_actions.student_projects.remove_all_project_licenses();
271
alert_message({
272
type: "info",
273
message: intl.formatMessage({
274
id: "course.upgrades.remove_all_licenses.success",
275
defaultMessage:
276
"Successfully removed all licenses from student projects.",
277
}),
278
});
279
} catch (err) {
280
alert_message({ type: "error", message: `${err}` });
281
}
282
}}
283
>
284
<Button style={{ marginTop: "15px" }}>
285
<FormattedMessage
286
id="course.upgrades.remove_all_licenses"
287
defaultMessage={"Remove licenses from student projects..."}
288
/>
289
</Button>
290
</Popconfirm>
291
);
292
}
293
294
function render_site_license() {
295
const n = !!site_license_id ? site_license_id.split(",").length : 0;
296
return (
297
<div>
298
{render_current_licenses()}
299
<div>
300
<Button
301
onClick={() => set_show_site_license(true)}
302
disabled={show_site_license}
303
>
304
<Icon name="key" />{" "}
305
<FormattedMessage
306
id="course.upgades.site_license.upgrade-button.label"
307
defaultMessage={`{n, select,
308
0 {Upgrade using a license key}
309
other {Add another license key (more students or better upgrades)}}`}
310
values={{ n }}
311
/>
312
...
313
</Button>
314
{render_site_license_text()}
315
</div>
316
<Space>
317
{is_commercial && (
318
<div style={{ marginTop: "15px" }}>
319
<Next
320
href={"store/site-license"}
321
query={{
322
user: "academic",
323
period: "range",
324
run_limit: (get_store()?.num_students() ?? 0) + 2,
325
member: true,
326
uptime: "short",
327
cpu: 1,
328
ram: 2,
329
disk: 3,
330
title: settings.get("title") ?? "",
331
description: settings.get("description") ?? "",
332
}}
333
>
334
<Button>
335
<FormattedMessage
336
id="course.upgrades.site_license.buy-button.label"
337
defaultMessage={"Buy a license..."}
338
/>
339
</Button>
340
</Next>
341
</div>
342
)}
343
{n == 0 && render_remove_all_licenses()}
344
</Space>
345
<div>
346
<ToggleUpgradingHostProject actions={actions} settings={settings} />
347
</div>
348
</div>
349
);
350
}
351
352
function handle_institute_pay_checkbox(e): void {
353
course_actions.configuration.set_pay_choice("institute", e.target.checked);
354
}
355
356
function render_checkbox() {
357
return (
358
<Checkbox
359
checked={!!institute_pay}
360
onChange={handle_institute_pay_checkbox}
361
>
362
<FormattedMessage
363
id="course.upgrades.checkbox-institute-pays"
364
defaultMessage={"You or your institute will pay for this course"}
365
/>
366
</Checkbox>
367
);
368
}
369
370
function render_details() {
371
return (
372
<div style={{ marginTop: "15px" }}>
373
{render_site_license()}
374
<hr />
375
<div style={{ color: COLORS.GRAY_M }}>
376
<p>
377
<FormattedMessage
378
id="course.upgrades.details"
379
defaultMessage={`Add or remove upgrades to student projects associated to this course,
380
adding to what is provided for free and what students may have purchased.
381
<A>Help...</A>`}
382
values={{
383
A: (c) => (
384
<A href="https://doc.cocalc.com/teaching-create-course.html#option-2-teacher-or-institution-pays-for-upgradespay">
385
{c}
386
</A>
387
),
388
}}
389
description={"Students in an university online course."}
390
/>
391
</p>
392
</div>
393
</div>
394
);
395
}
396
397
function render_onprem(): JSX.Element {
398
return <div>{render_site_license()}</div>;
399
}
400
401
function render_title() {
402
if (is_onprem) {
403
return (
404
<div>
405
<FormattedMessage
406
id="course.upgrades.onprem.title"
407
defaultMessage={"Upgrade Student Projects"}
408
/>
409
</div>
410
);
411
} else {
412
return (
413
<div>
414
<Icon name="dashboard" />{" "}
415
<FormattedMessage
416
id="course.upgrades.prod.title"
417
defaultMessage={"Upgrade all Student Projects (Institute Pays)"}
418
/>
419
</div>
420
);
421
}
422
}
423
424
function render_body(): JSX.Element {
425
if (is_onprem) {
426
return render_onprem();
427
} else {
428
return (
429
<>
430
{render_checkbox()}
431
{institute_pay ? render_details() : undefined}
432
</>
433
);
434
}
435
}
436
437
return (
438
<Card
439
style={{
440
marginTop: "20px",
441
background:
442
is_onprem || student_pay || institute_pay ? undefined : "#fcf8e3",
443
}}
444
title={render_title()}
445
>
446
{render_body()}
447
</Card>
448
);
449
}
450
451
interface ToggleUpgradingHostProjectProps {
452
actions: ConfigurationActions;
453
settings: CourseSettingsRecord;
454
}
455
456
const ToggleUpgradingHostProject = ({
457
actions,
458
settings,
459
}: ToggleUpgradingHostProjectProps) => {
460
const intl = useIntl();
461
const [needSave, setNeedSave] = useState<boolean>(false);
462
const upgradeHostProject = settings.get("license_upgrade_host_project");
463
const upgrade = upgradeHostProject ?? DEFAULT_LICENSE_UPGRADE_HOST_PROJECT;
464
const [nextVal, setNextVal] = useState<boolean>(upgrade);
465
466
useEffect(() => {
467
setNeedSave(nextVal != upgrade);
468
}, [nextVal, upgrade]);
469
470
const label = intl.formatMessage({
471
id: "course.upgrades.toggle-host.label",
472
defaultMessage: "Upgrade instructor project:",
473
});
474
475
function toggle() {
476
return (
477
<Form layout="inline">
478
<Form.Item label={label} style={{ marginBottom: 0 }}>
479
<Switch checked={nextVal} onChange={(val) => setNextVal(val)} />
480
</Form.Item>
481
<Form.Item>
482
<Button
483
disabled={!needSave}
484
type={needSave ? "primary" : undefined}
485
onClick={() => actions.set_license_upgrade_host_project(nextVal)}
486
>
487
{intl.formatMessage(labels.save)}
488
</Button>
489
</Form.Item>
490
</Form>
491
);
492
}
493
494
return (
495
<>
496
<hr />
497
{toggle()}
498
<Typography.Paragraph
499
ellipsis={{ expandable: true, rows: 1, symbol: "more" }}
500
>
501
{intl.formatMessage({
502
id: "course.upgrades.toggle-host.info",
503
defaultMessage: `If enabled, this instructor project is upgraded using all configured course license(s).
504
Otherwise, explictly add your license to the instructor project.
505
Disabling this options does <i>not</i> remove licenses from the instructor project.`,
506
})}
507
</Typography.Paragraph>
508
</>
509
);
510
};
511
512