Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/course/configuration/upgrades.tsx
1503 views
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): React.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
title: settings.get("title") ?? "",
326
description: settings.get("description") ?? "",
327
}}
328
>
329
<Button>
330
<FormattedMessage
331
id="course.upgrades.site_license.buy-button.label"
332
defaultMessage={"Buy a license..."}
333
/>
334
</Button>
335
</Next>
336
</div>
337
)}
338
{n == 0 && render_remove_all_licenses()}
339
</Space>
340
<div>
341
<ToggleUpgradingHostProject actions={actions} settings={settings} />
342
</div>
343
</div>
344
);
345
}
346
347
function handle_institute_pay_checkbox(e): void {
348
course_actions.configuration.set_pay_choice("institute", e.target.checked);
349
}
350
351
function render_checkbox() {
352
return (
353
<Checkbox
354
checked={!!institute_pay}
355
onChange={handle_institute_pay_checkbox}
356
>
357
<FormattedMessage
358
id="course.upgrades.checkbox-institute-pays"
359
defaultMessage={"You or your institute will pay for this course"}
360
/>
361
</Checkbox>
362
);
363
}
364
365
function render_details() {
366
return (
367
<div style={{ marginTop: "15px" }}>
368
{render_site_license()}
369
<hr />
370
<div style={{ color: COLORS.GRAY_M }}>
371
<p>
372
<FormattedMessage
373
id="course.upgrades.details"
374
defaultMessage={`Add or remove upgrades to student projects associated to this course,
375
adding to what is provided for free and what students may have purchased.
376
<A>Help...</A>`}
377
values={{
378
A: (c) => (
379
<A href="https://doc.cocalc.com/teaching-create-course.html#option-2-teacher-or-institution-pays-for-upgradespay">
380
{c}
381
</A>
382
),
383
}}
384
description={"Students in an university online course."}
385
/>
386
</p>
387
</div>
388
</div>
389
);
390
}
391
392
function render_onprem(): React.JSX.Element {
393
return <div>{render_site_license()}</div>;
394
}
395
396
function render_title() {
397
if (is_onprem) {
398
return (
399
<div>
400
<FormattedMessage
401
id="course.upgrades.onprem.title"
402
defaultMessage={"Upgrade Student Projects"}
403
/>
404
</div>
405
);
406
} else {
407
return (
408
<div>
409
<Icon name="dashboard" />{" "}
410
<FormattedMessage
411
id="course.upgrades.prod.title"
412
defaultMessage={"Upgrade all Student Projects (Institute Pays)"}
413
/>
414
</div>
415
);
416
}
417
}
418
419
function render_body(): React.JSX.Element {
420
if (is_onprem) {
421
return render_onprem();
422
} else {
423
return (
424
<>
425
{render_checkbox()}
426
{institute_pay ? render_details() : undefined}
427
</>
428
);
429
}
430
}
431
432
return (
433
<Card
434
style={{
435
marginTop: "20px",
436
background:
437
is_onprem || student_pay || institute_pay ? undefined : "#fcf8e3",
438
}}
439
title={render_title()}
440
>
441
{render_body()}
442
</Card>
443
);
444
}
445
446
interface ToggleUpgradingHostProjectProps {
447
actions: ConfigurationActions;
448
settings: CourseSettingsRecord;
449
}
450
451
const ToggleUpgradingHostProject = ({
452
actions,
453
settings,
454
}: ToggleUpgradingHostProjectProps) => {
455
const intl = useIntl();
456
const [needSave, setNeedSave] = useState<boolean>(false);
457
const upgradeHostProject = settings.get("license_upgrade_host_project");
458
const upgrade = upgradeHostProject ?? DEFAULT_LICENSE_UPGRADE_HOST_PROJECT;
459
const [nextVal, setNextVal] = useState<boolean>(upgrade);
460
461
useEffect(() => {
462
setNeedSave(nextVal != upgrade);
463
}, [nextVal, upgrade]);
464
465
const label = intl.formatMessage({
466
id: "course.upgrades.toggle-host.label",
467
defaultMessage: "Upgrade instructor project:",
468
});
469
470
function toggle() {
471
return (
472
<Form layout="inline">
473
<Form.Item label={label} style={{ marginBottom: 0 }}>
474
<Switch checked={nextVal} onChange={(val) => setNextVal(val)} />
475
</Form.Item>
476
<Form.Item>
477
<Button
478
disabled={!needSave}
479
type={needSave ? "primary" : undefined}
480
onClick={() => actions.set_license_upgrade_host_project(nextVal)}
481
>
482
{intl.formatMessage(labels.save)}
483
</Button>
484
</Form.Item>
485
</Form>
486
);
487
}
488
489
return (
490
<>
491
<hr />
492
{toggle()}
493
<Typography.Paragraph
494
ellipsis={{ expandable: true, rows: 1, symbol: "more" }}
495
>
496
{intl.formatMessage({
497
id: "course.upgrades.toggle-host.info",
498
defaultMessage: `If enabled, this instructor project is upgraded using all configured course license(s).
499
Otherwise, explictly add your license to the instructor project.
500
Disabling this options does <i>not</i> remove licenses from the instructor project.`,
501
})}
502
</Typography.Paragraph>
503
</>
504
);
505
};
506
507