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/next/components/store/quota-config.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import {
7
Alert,
8
Button,
9
Col,
10
Divider,
11
Flex,
12
Form,
13
Radio,
14
Row,
15
Space,
16
Tabs,
17
Typography,
18
} from "antd";
19
import { useEffect, useRef, useState } from "react";
20
21
import { Icon } from "@cocalc/frontend/components/icon";
22
import { displaySiteLicense } from "@cocalc/util/consts/site-license";
23
import { plural } from "@cocalc/util/misc";
24
import { BOOST, DISK_DEFAULT_GB, REGULAR } from "@cocalc/util/upgrades/consts";
25
import PricingItem, { Line } from "components/landing/pricing-item";
26
import { CSS, Paragraph } from "components/misc";
27
import A from "components/misc/A";
28
import IntegerSlider from "components/misc/integer-slider";
29
import {
30
PRESETS,
31
PRESET_MATCH_FIELDS,
32
Preset,
33
PresetConfig,
34
} from "./quota-config-presets";
35
36
const { Text } = Typography;
37
38
const EXPERT_CONFIG = "Expert Configuration";
39
const listFormat = new Intl.ListFormat("en");
40
41
const RAM_HIGH_WARN_THRESHOLD = 10;
42
const RAM_LOW_WARN_THRESHOLD = 1;
43
const MEM_MIN_RECOMMEND = 2;
44
const CPU_HIGH_WARN_THRESHOLD = 3;
45
46
const WARNING_BOX: CSS = { marginTop: "10px", marginBottom: "10px" };
47
48
interface Props {
49
showExplanations: boolean;
50
form: any;
51
disabled?: boolean;
52
onChange: () => void;
53
boost?: boolean;
54
// boost doesn't define any of the below, that's only for site-license
55
configMode?: "preset" | "expert";
56
setConfigMode?: (mode: "preset" | "expert") => void;
57
preset?: Preset | null;
58
setPreset?: (preset: Preset | null) => void;
59
presetAdjusted?: boolean;
60
setPresetAdjusted?: (adjusted: boolean) => void;
61
}
62
63
export const QuotaConfig: React.FC<Props> = (props: Props) => {
64
const {
65
showExplanations,
66
form,
67
disabled = false,
68
onChange,
69
boost = false,
70
configMode,
71
setConfigMode,
72
preset,
73
setPreset,
74
presetAdjusted,
75
setPresetAdjusted,
76
} = props;
77
78
const presetsRef = useRef<HTMLDivElement>(null);
79
const [isClient, setIsClient] = useState(false);
80
const [narrow, setNarrow] = useState<boolean>(false);
81
82
useEffect(() => {
83
setIsClient(true);
84
}, []);
85
86
useEffect(() => {
87
const observer = new ResizeObserver((entries) => {
88
if (isClient && entries[0].contentRect.width < 600) {
89
setNarrow(true);
90
} else {
91
setNarrow(false);
92
}
93
});
94
95
if (presetsRef.current) {
96
observer.observe(presetsRef.current);
97
}
98
99
return () => {
100
observer.disconnect();
101
};
102
}, [presetsRef.current]);
103
104
const ramVal = Form.useWatch("ram", form);
105
const cpuVal = Form.useWatch("cpu", form);
106
107
function title() {
108
if (boost) {
109
return "Booster";
110
} else {
111
return "Quota Upgrades";
112
}
113
}
114
115
const PARAMS = boost ? BOOST : REGULAR;
116
117
function explainRam() {
118
return (
119
<>
120
{renderRamInfo()}
121
{showExplanations ? (
122
<>
123
This quota limits the total amount of memory a project can use. Note
124
that RAM may be limited, if many other users are using the same host
125
– though member hosting significantly reduces competition for RAM.
126
We recommend at least {MEM_MIN_RECOMMEND}G!
127
</>
128
) : undefined}
129
</>
130
);
131
}
132
133
/**
134
* When a quota is changed, we warn the user that the preset was adjusted.
135
* (the text updates, though, since it rerenders every time). Explanation in
136
* the details could make no sense, though – that's why this is added.
137
*/
138
function presetWasAdjusted() {
139
setPresetAdjusted?.(true);
140
}
141
142
function renderRamInfo() {
143
if (ramVal >= RAM_HIGH_WARN_THRESHOLD) {
144
return (
145
<Alert
146
style={WARNING_BOX}
147
type="warning"
148
message="Consider using a compute server?"
149
description={
150
<>
151
You selected a RAM quota of {ramVal}G. If your use-case involves a
152
lot of RAM, consider using a{" "}
153
<A href="https://doc.cocalc.com/compute_server.html">
154
compute server
155
</A>{" "}
156
or{" "}
157
<A href={"/store/dedicated?type=vm"}>
158
dedicated virtual machines
159
</A>
160
. This will not only give you much more RAM, but also a far
161
superior experience!
162
</>
163
}
164
/>
165
);
166
} else if (!boost && ramVal <= RAM_LOW_WARN_THRESHOLD) {
167
return (
168
<Alert
169
style={WARNING_BOX}
170
type="warning"
171
message="Low memory"
172
description={
173
<>
174
Your choice of {ramVal}G of RAM is beyond our recommendation of at
175
least {MEM_MIN_RECOMMEND}G. You will not be able to run several
176
notebooks at once, use SageMath or Julia effectively, etc.
177
</>
178
}
179
/>
180
);
181
}
182
}
183
184
function ram() {
185
return (
186
<Form.Item
187
label="Shared RAM"
188
name="ram"
189
initialValue={PARAMS.ram.dflt}
190
extra={explainRam()}
191
>
192
<IntegerSlider
193
disabled={disabled}
194
min={PARAMS.ram.min}
195
max={PARAMS.ram.max}
196
onChange={(ram) => {
197
form.setFieldsValue({ ram });
198
presetWasAdjusted();
199
onChange();
200
}}
201
units={"GB RAM"}
202
presets={boost ? [0, 2, 4, 8, 10] : [1, 2, 4, 8, 16]}
203
/>
204
</Form.Item>
205
);
206
}
207
208
function renderCpuInfo() {
209
if (cpuVal >= CPU_HIGH_WARN_THRESHOLD) {
210
return (
211
<Alert
212
style={WARNING_BOX}
213
type="warning"
214
message="Consider using a compute server?"
215
description={
216
<>
217
You selected a CPU quota of {cpuVal} vCPU cores is high. If your
218
use-case involves harnessing a lot of CPU power, consider using a{" "}
219
<A href="https://doc.cocalc.com/compute_server.html">
220
compute server
221
</A>{" "}
222
or{" "}
223
<A href={"/store/dedicated?type=vm"}>
224
dedicated virtual machines
225
</A>
226
. This will not only give you many more CPU cores, but also a far
227
superior experience!
228
</>
229
}
230
/>
231
);
232
}
233
}
234
235
function renderCpuExtra() {
236
return (
237
<>
238
{renderCpuInfo()}
239
{showExplanations ? (
240
<>
241
<A href="https://cloud.google.com/compute/docs/faq#virtualcpu">
242
Google Cloud vCPUs.
243
</A>{" "}
244
To keep prices low, these vCPUs may be shared with other projects,
245
though member hosting very significantly reduces competition for
246
CPUs.
247
</>
248
) : undefined}
249
</>
250
);
251
}
252
253
function cpu() {
254
return (
255
<Form.Item
256
label="Shared CPUs"
257
name="cpu"
258
initialValue={PARAMS.cpu.dflt}
259
extra={renderCpuExtra()}
260
>
261
<IntegerSlider
262
disabled={disabled}
263
min={PARAMS.cpu.min}
264
max={PARAMS.cpu.max}
265
onChange={(cpu) => {
266
form.setFieldsValue({ cpu });
267
presetWasAdjusted();
268
onChange();
269
}}
270
units={"vCPU"}
271
presets={boost ? [0, 1, 2] : [1, 2, 3]}
272
/>
273
</Form.Item>
274
);
275
}
276
277
function disk() {
278
// 2022-06: price increase "version 2": minimum disk we sell (also the free quota) is 3gb, not 1gb
279
return (
280
<Form.Item
281
label="Disk space"
282
name="disk"
283
initialValue={PARAMS.disk.dflt}
284
extra={
285
showExplanations ? (
286
<>
287
Extra disk space lets you store a larger number of files.
288
Snapshots and file edit history is included at no additional
289
charge. Each project receives at least {DISK_DEFAULT_GB}G of
290
storage space. We also offer MUCH larger disks (and CPU and
291
memory) via{" "}
292
<A href="https://doc.cocalc.com/compute_server.html">
293
compute server
294
</A>
295
.
296
</>
297
) : undefined
298
}
299
>
300
<IntegerSlider
301
disabled={disabled}
302
min={PARAMS.disk.min}
303
max={PARAMS.disk.max}
304
onChange={(disk) => {
305
form.setFieldsValue({ disk });
306
presetWasAdjusted();
307
onChange();
308
}}
309
units={"G Disk"}
310
presets={
311
boost ? [0, 3, 6, PARAMS.disk.max] : [3, 5, 10, PARAMS.disk.max]
312
}
313
/>
314
</Form.Item>
315
);
316
}
317
318
function presetIsAdjusted() {
319
if (preset == null) return;
320
const presetData: PresetConfig = PRESETS[preset];
321
if (presetData == null) {
322
return (
323
<div>
324
Error: preset <code>{preset}</code> is not known.
325
</div>
326
);
327
}
328
329
const quotaConfig: Record<string, string> = form.getFieldsValue(
330
Object.keys(PRESET_MATCH_FIELDS),
331
);
332
const invalidConfigValues = Object.keys(quotaConfig).filter(
333
(field) => quotaConfig[field] == null,
334
);
335
if (invalidConfigValues.length) {
336
return;
337
}
338
339
const presetDiff = Object.keys(PRESET_MATCH_FIELDS).reduce(
340
(diff, presetField) => {
341
if (presetData[presetField] !== quotaConfig[presetField]) {
342
diff.push(PRESET_MATCH_FIELDS[presetField]);
343
}
344
345
return diff;
346
},
347
[] as string[],
348
);
349
350
if (!presetAdjusted || !presetDiff.length) return;
351
return (
352
<Alert
353
type="warning"
354
style={{ marginBottom: "20px" }}
355
message={
356
<>
357
The currently configured license differs from the selected preset in{" "}
358
<strong>{listFormat.format(presetDiff)}</strong>. By clicking any of
359
the presets below, you reconfigure your license configuration to
360
match the original preset.
361
</>
362
}
363
/>
364
);
365
}
366
367
function presetsCommon() {
368
if (!showExplanations) return null;
369
return (
370
<Text type="secondary">
371
{preset == null ? (
372
<>After selecting a preset, feel free to</>
373
) : (
374
<>
375
Selected preset <strong>"{PRESETS[preset]?.name}"</strong>. You can
376
</>
377
)}{" "}
378
fine tune the selection in the "{EXPERT_CONFIG}" tab. Subsequent preset
379
selections will reset your adjustments.
380
</Text>
381
);
382
}
383
384
function renderNoPresetWarning() {
385
if (preset != null) return;
386
return (
387
<Text type="danger">
388
Currently, no preset selection is active. Select a preset above to reset
389
your recent changes.
390
</Text>
391
);
392
}
393
394
function renderPresetsNarrow() {
395
const p = preset != null ? PRESETS[preset] : undefined;
396
let presetInfo: JSX.Element | undefined = undefined;
397
if (p != null) {
398
const { name, cpu, disk, ram, uptime, note } = p;
399
const basic = (
400
<>
401
provides up to{" "}
402
<Text strong>
403
{cpu} {plural(cpu, "vCPU")}
404
</Text>
405
, <Text strong>{ram} GB memory</Text>, and{" "}
406
<Text strong>{disk} GB disk space</Text> for each project.
407
</>
408
);
409
const ut = (
410
<>
411
the project's{" "}
412
<Text strong>idle timeout is {displaySiteLicense(uptime)}</Text>
413
</>
414
);
415
presetInfo = (
416
<Paragraph>
417
<strong>{name}</strong> {basic} Additionally, {ut}. {note}
418
</Paragraph>
419
);
420
}
421
422
return (
423
<>
424
<Form.Item label="Preset">
425
<Radio.Group
426
size="large"
427
value={preset}
428
onChange={(e) => onPresetChange(e.target.value)}
429
>
430
<Space direction="vertical">
431
{(Object.keys(PRESETS) as Array<Preset>).map((p) => {
432
const { name, icon, descr } = PRESETS[p];
433
return (
434
<Radio key={p} value={p}>
435
<span>
436
<Icon name={icon ?? "arrow-up"} />{" "}
437
<strong>{name}:</strong> {descr}
438
</span>
439
</Radio>
440
);
441
})}
442
</Space>
443
</Radio.Group>
444
</Form.Item>
445
{presetInfo}
446
</>
447
);
448
}
449
450
function renderPresetPanels() {
451
if (narrow) return renderPresetsNarrow();
452
453
const panels = (Object.keys(PRESETS) as Array<Preset>).map((p, idx) => {
454
const { name, icon, cpu, ram, disk, uptime, expect, descr, note } =
455
PRESETS[p];
456
const active = preset === p;
457
return (
458
<PricingItem
459
key={idx}
460
title={name}
461
icon={icon}
462
style={{ flex: 1 }}
463
active={active}
464
onClick={() => onPresetChange(p)}
465
>
466
<Paragraph>
467
<strong>{name}</strong> {descr}.
468
</Paragraph>
469
<Divider />
470
<Line amount={cpu} desc={"CPU"} indent={false} />
471
<Line amount={ram} desc={"RAM"} indent={false} />
472
<Line amount={disk} desc={"Disk space"} indent={false} />
473
<Line
474
amount={displaySiteLicense(uptime)}
475
desc={"Idle timeout"}
476
indent={false}
477
/>
478
<Divider />
479
<Paragraph>
480
<Text type="secondary">In each project, you will be able to:</Text>
481
<ul>
482
{expect.map((what, idx) => (
483
<li key={idx}>{what}</li>
484
))}
485
</ul>
486
</Paragraph>
487
{active && note != null ? (
488
<>
489
<Divider />
490
<Paragraph type="secondary">{note}</Paragraph>
491
</>
492
) : undefined}
493
<Paragraph style={{ marginTop: "20px", textAlign: "center" }}>
494
<Button
495
onClick={() => onPresetChange(p)}
496
size="large"
497
type={active ? "primary" : undefined}
498
>
499
{name}
500
</Button>
501
</Paragraph>
502
</PricingItem>
503
);
504
});
505
return (
506
<Flex
507
style={{ width: "100%" }}
508
justify={"space-between"}
509
align={"flex-start"}
510
gap="10px"
511
>
512
{panels}
513
</Flex>
514
);
515
}
516
517
function presetExtra() {
518
return (
519
<Space ref={presetsRef} direction="vertical">
520
<div>
521
{presetIsAdjusted()}
522
{renderPresetPanels()}
523
{renderNoPresetWarning()}
524
</div>
525
{presetsCommon()}
526
</Space>
527
);
528
}
529
530
function onPresetChange(val: Preset) {
531
if (val == null || setPreset == null) return;
532
setPreset(val);
533
setPresetAdjusted?.(false);
534
const presetData = PRESETS[val];
535
if (presetData != null) {
536
const { cpu, ram, disk, uptime = "short", member = true } = presetData;
537
form.setFieldsValue({ uptime, member, cpu, ram, disk });
538
}
539
onChange();
540
}
541
542
function detailed() {
543
return (
544
<>
545
{ram()}
546
{cpu()}
547
{disk()}
548
</>
549
);
550
}
551
552
function main() {
553
if (boost) {
554
return (
555
<>
556
<Row>
557
<Col xs={16} offset={6} style={{ marginBottom: "20px" }}>
558
<Text type="secondary">
559
Configure the quotas you want to add on top of your existing
560
license. E.g. if your license provides a limit of 2 GB of RAM and
561
you add a matching boost license with 3 GB of RAM, you'll end up
562
with a total quota limit of 5 GB of RAM.
563
</Text>
564
</Col>
565
</Row>
566
{detailed()}
567
</>
568
);
569
} else {
570
return (
571
<Tabs
572
activeKey={configMode}
573
onChange={setConfigMode}
574
type="card"
575
tabPosition="top"
576
size="middle"
577
centered={true}
578
items={[
579
{
580
key: "preset",
581
label: (
582
<span>
583
<Icon name="gears" style={{ marginRight: "5px" }} />
584
Presets
585
</span>
586
),
587
children: presetExtra(),
588
},
589
{
590
key: "expert",
591
label: (
592
<span>
593
<Icon name="wrench" style={{ marginRight: "5px" }} />
594
{EXPERT_CONFIG}
595
</span>
596
),
597
children: detailed(),
598
},
599
]}
600
/>
601
);
602
}
603
}
604
605
return (
606
<>
607
<Divider plain>{title()}</Divider>
608
{main()}
609
</>
610
);
611
};
612
613