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