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/compute/cloud-filesystem/create.tsx
Views: 687
1
import {
2
Alert,
3
Button,
4
Card,
5
Divider,
6
Input,
7
InputNumber,
8
Modal,
9
Radio,
10
Spin,
11
} from "antd";
12
import { useEffect, useState } from "react";
13
14
import { A, Icon } from "@cocalc/frontend/components";
15
import ShowError from "@cocalc/frontend/components/error";
16
import { checkInAll } from "@cocalc/frontend/compute/check-in";
17
import { CancelText } from "@cocalc/frontend/i18n/components";
18
import confirmCreateCloudFilesystem from "@cocalc/frontend/purchases/pay-as-you-go/confirm-create-cloud-filesystem";
19
import type {
20
Compression,
21
CreateCloudFilesystem,
22
} from "@cocalc/util/db-schema/cloud-filesystems";
23
import {
24
DEFAULT_CONFIGURATION,
25
MAX_BLOCK_SIZE,
26
MAX_CLOUD_FILESYSTEMS_PER_PROJECT,
27
MIN_BLOCK_SIZE,
28
RECOMMENDED_BLOCK_SIZE,
29
} from "@cocalc/util/db-schema/cloud-filesystems";
30
import Color, { randomColor } from "../color";
31
import { ProgressBarTimer } from "../state";
32
import Title from "../title";
33
import { createCloudFilesystem } from "./api";
34
import { BucketLocation, BucketStorageClass } from "./bucket";
35
import type { CloudFilesystems } from "./cloud-filesystems";
36
37
interface Props {
38
project_id: string;
39
cloudFilesystems: CloudFilesystems | null;
40
refresh: Function;
41
}
42
43
export default function CreateCloudFilesystem({
44
project_id,
45
cloudFilesystems,
46
refresh,
47
}: Props) {
48
const [taken, setTaken] = useState<{
49
ports: Set<number>;
50
mountpoints: Set<string>;
51
}>({ ports: new Set(), mountpoints: new Set() });
52
useEffect(() => {
53
if (cloudFilesystems == null) {
54
return;
55
}
56
const v = Object.values(cloudFilesystems);
57
setTaken({
58
ports: new Set(v.map((x) => x.port)),
59
mountpoints: new Set(v.map((x) => x.mountpoint)),
60
});
61
}, [cloudFilesystems]);
62
const [creating, setCreating] = useState<boolean>(false);
63
const [createStarted, setCreateStarted] = useState<Date>(new Date());
64
const [editing, setEditing] = useState<boolean>(false);
65
const [error, setError] = useState<string>("");
66
const [advanced, setAdvanced] = useState<boolean>(false);
67
const [configuration, setConfiguration] =
68
useState<CreateCloudFilesystem | null>(null);
69
70
const reset = () => {
71
setConfiguration({
72
project_id,
73
...DEFAULT_CONFIGURATION,
74
mountpoint: generateMountpoint(
75
taken.mountpoints,
76
DEFAULT_CONFIGURATION.mountpoint,
77
),
78
color: randomColor(),
79
// start mounted by default -- way less confusing
80
mount: true,
81
// bucket_location gets filled in by BucketLocation component on init
82
bucket_location: "",
83
});
84
};
85
86
const create = async () => {
87
if (creating || configuration == null) {
88
return;
89
}
90
try {
91
setCreating(true);
92
setCreateStarted(new Date());
93
await confirmCreateCloudFilesystem();
94
setCreateStarted(new Date());
95
await createCloudFilesystem({
96
...configuration,
97
position: getPosition(cloudFilesystems),
98
});
99
checkInAll(project_id); // cause filesystem to be noticed (and mounted) asap
100
setEditing(false);
101
reset();
102
refresh();
103
} catch (err) {
104
setError(`${err}`);
105
} finally {
106
refresh();
107
setCreating(false);
108
}
109
};
110
111
return (
112
<div style={{ textAlign: "center", margin: "15px 0" }}>
113
<Button
114
size="large"
115
disabled={creating || editing}
116
onClick={() => {
117
reset();
118
setEditing(true);
119
}}
120
style={{
121
marginRight: "5px",
122
width: "80%",
123
height: "auto",
124
whiteSpace: "normal",
125
padding: "10px",
126
...(creating
127
? {
128
borderColor: "rgb(22, 119, 255)",
129
backgroundColor: "rgb(230, 244, 255)",
130
}
131
: undefined),
132
}}
133
>
134
<Icon
135
name="server"
136
style={{
137
color: "rgb(66, 139, 202)",
138
fontSize: "200%",
139
}}
140
/>
141
<br />
142
Create Cloud File System... {creating ? <Spin /> : null}
143
</Button>
144
<Modal
145
width={"900px"}
146
onCancel={() => {
147
setEditing(false);
148
reset();
149
}}
150
open={editing && configuration != null}
151
title={
152
<div style={{ display: "flex", fontSize: "15pt" }}>
153
<Icon name="disk-round" style={{ marginRight: "15px" }} /> Create a
154
CoCalc Cloud File System
155
</div>
156
}
157
footer={[
158
<Button
159
key="cancel"
160
disabled={creating}
161
onClick={() => {
162
setEditing(false);
163
reset();
164
}}
165
>
166
<CancelText />
167
</Button>,
168
<Button key="ok" type="primary" disabled={creating} onClick={create}>
169
<>
170
Create Cloud File System{" "}
171
{creating ? <Spin style={{ marginLeft: "15px" }} /> : undefined}
172
</>
173
</Button>,
174
]}
175
>
176
<ShowError
177
error={error}
178
setError={setError}
179
style={{ margin: "15px 0" }}
180
/>{" "}
181
<Card
182
style={{
183
margin: "15px 0",
184
border: `0.5px solid ${configuration?.color ?? "#f0f0f0"}`,
185
borderRight: `10px solid ${configuration?.color ?? "#aaa"}`,
186
borderLeft: `10px solid ${configuration?.color ?? "#aaa"}`,
187
...(creating ? { opacity: 0.4 } : undefined),
188
}}
189
>
190
<Divider>
191
<Icon
192
name="cloud-dev"
193
style={{ fontSize: "16pt", marginRight: "15px" }}
194
/>{" "}
195
Title and Color
196
</Divider>
197
Select a meaningful title and color for your Cloud File System. You
198
can change these at any time, and they do not impact anything else.
199
<br />
200
<div style={{ display: "flex" }}>
201
<div style={{ flex: 1 }} />
202
<EditTitle
203
configuration={configuration}
204
setConfiguration={setConfiguration}
205
/>
206
<div style={{ flex: 1 }} />
207
208
<SelectColor
209
configuration={configuration}
210
setConfiguration={setConfiguration}
211
/>
212
<div style={{ flex: 1 }} />
213
</div>
214
<Divider>
215
<Icon
216
name="folder-open"
217
style={{ fontSize: "16pt", marginRight: "15px" }}
218
/>{" "}
219
Mountpoint
220
</Divider>
221
<Mountpoint
222
configuration={configuration}
223
setConfiguration={setConfiguration}
224
mountpoints={taken.mountpoints}
225
/>
226
<Divider>
227
<Icon
228
name="disk-snapshot"
229
style={{ fontSize: "16pt", marginRight: "15px" }}
230
/>{" "}
231
Bucket
232
</Divider>
233
Your data is stored in a Google Cloud Storage bucket in a single
234
region or multiregion bucket, and recently used data is cached locally
235
on each compute server's disk.
236
<BucketLocation
237
configuration={configuration}
238
setConfiguration={setConfiguration}
239
/>
240
<Divider>
241
{advanced ? (
242
<Button
243
onClick={() => setAdvanced(false)}
244
type="link"
245
style={{ fontSize: "12pt" }}
246
>
247
<Icon name="eye-slash" /> Hide Advanced Settings
248
</Button>
249
) : (
250
<Button
251
onClick={() => setAdvanced(true)}
252
type="link"
253
style={{ fontSize: "12pt" }}
254
>
255
<Icon name="eye" /> Show Advanced Settings...
256
</Button>
257
)}
258
</Divider>
259
{advanced && (
260
<>
261
<p>
262
<b>What is it?:</b> The CoCalc Cloud File System is a fully
263
POSIX compliant distributed file system built using{" "}
264
<A href="https://juicefs.com/">JuiceFS</A>,{" "}
265
<A href="https://docs.keydb.dev/">KeyDB</A> and{" "}
266
<A href="https://cloud.google.com/storage">
267
Google Cloud Storage
268
</A>
269
.
270
</p>
271
<p>
272
<b>Scope:</b> You can make up to{" "}
273
{MAX_CLOUD_FILESYSTEMS_PER_PROJECT} cloud file systems per
274
project. Cloud file systems can be instantly moved between
275
projects.
276
</p>
277
<p>
278
<b>Cost:</b> The cost is a slightly marked up version of{" "}
279
<A href="https://cloud.google.com/storage/pricing">
280
Google Cloud Storage Pricing, which is highly competitive.
281
</A>{" "}
282
You can see how much your file system costs and why in realtime
283
by clicking "Show Metrics" in the cloud file system menu. If
284
your compute server and filesystem are in the same region, then
285
data transfer fees at completely free, and you mainly pay for
286
storage and operations (i.e., there is a fee per block of data
287
that is uploaded).
288
</p>
289
<Divider>
290
<Icon
291
name="lock"
292
style={{ fontSize: "16pt", marginRight: "15px" }}
293
/>{" "}
294
Safety
295
</Divider>
296
<Lock
297
configuration={configuration}
298
setConfiguration={setConfiguration}
299
/>
300
{false && (
301
<TrashDays
302
configuration={configuration}
303
setConfiguration={setConfiguration}
304
/>
305
)}
306
<Divider>
307
<Icon
308
name="database"
309
style={{ fontSize: "16pt", marginRight: "15px" }}
310
/>{" "}
311
Data Storage
312
</Divider>
313
<div>
314
<BucketStorageClass
315
configuration={configuration}
316
setConfiguration={setConfiguration}
317
/>
318
<Compression
319
configuration={configuration}
320
setConfiguration={setConfiguration}
321
/>
322
<BlockSize
323
configuration={configuration}
324
setConfiguration={setConfiguration}
325
/>
326
</div>
327
<MountAndKeyDBOptions
328
showHeader
329
configuration={configuration}
330
setConfiguration={setConfiguration}
331
/>
332
</>
333
)}
334
{creating && (
335
<div style={{ textAlign: "center", fontSize: "14pt" }}>
336
Creating Cloud File System...{" "}
337
<ProgressBarTimer
338
startTime={createStarted}
339
style={{ marginLeft: "10px" }}
340
/>
341
</div>
342
)}
343
<ShowError
344
error={error}
345
setError={setError}
346
style={{ margin: "15px 0" }}
347
/>
348
</Card>
349
</Modal>
350
</div>
351
);
352
}
353
354
function EditTitle({ configuration, setConfiguration }) {
355
return (
356
<Title
357
editable
358
title={configuration.title}
359
onChange={(title) => setConfiguration({ ...configuration, title })}
360
/>
361
);
362
}
363
364
function SelectColor({ configuration, setConfiguration }) {
365
return (
366
<Color
367
editable
368
color={configuration.color}
369
onChange={(color) => setConfiguration({ ...configuration, color })}
370
/>
371
);
372
}
373
374
function Mountpoint({ configuration, setConfiguration, mountpoints }) {
375
const taken = mountpoints.has(configuration.mountpoint);
376
return (
377
<div>
378
Mount at <code>~/{configuration.mountpoint}</code> on all compute servers.
379
You can change this when the file system is not mounted.
380
<br />
381
<Input
382
status={taken ? "error" : undefined}
383
style={{ marginTop: "10px" }}
384
value={configuration.mountpoint}
385
onChange={(e) => {
386
setConfiguration({ ...configuration, mountpoint: e.target.value });
387
}}
388
/>
389
{taken && (
390
<Alert
391
style={{ margin: "10px 0" }}
392
showIcon
393
type="error"
394
message="This mountpoint is already being used by another Cloud File System in this project. Please change the mountpoint."
395
/>
396
)}
397
</div>
398
);
399
}
400
401
function Compression({ configuration, setConfiguration }) {
402
return (
403
<div style={{ marginTop: "10px" }}>
404
<b style={{ fontSize: "13pt", color: "#666" }}>
405
<A href="https://juicefs.com/docs/community/internals/#data-compression">
406
{EXTERNAL}
407
Compression
408
</A>
409
</b>
410
{NO_CHANGE}
411
You can compress your data automatically.
412
<Alert
413
style={{ margin: "10px" }}
414
showIcon
415
type="info"
416
message={`Recommendation: LZ4`}
417
description={
418
<>
419
Do not enable compression if most of your data is already
420
compressed. Otherwise, <A href="https://lz4.github.io/lz4">LZ4</A>{" "}
421
is a good choice; it uses less CPU, and can save significant space.
422
Use <A href="https://facebook.github.io/zstd">ZSTD</A> if a lot of
423
your data is compressible and more CPU usage is OK.
424
</>
425
}
426
/>
427
<div style={{ textAlign: "center", marginTop: "10px" }}>
428
<Radio.Group
429
onChange={(e) =>
430
setConfiguration({ ...configuration, compression: e.target.value })
431
}
432
value={configuration.compression}
433
>
434
<Radio value={"lz4"}>LZ4 - faster performance</Radio>
435
<Radio value={"zstd"}>ZSTD - better compression ratio</Radio>
436
<Radio value={"none"}>None</Radio>
437
</Radio.Group>
438
</div>
439
</div>
440
);
441
}
442
443
function BlockSize({ configuration, setConfiguration }) {
444
return (
445
<div style={{ marginTop: "10px" }}>
446
<b style={{ fontSize: "13pt", color: "#666" }}>Block Size</b>
447
{NO_CHANGE}
448
The block size, which is between {MIN_BLOCK_SIZE} MB and {MAX_BLOCK_SIZE}{" "}
449
MB, is an upper bound on the size of the objects that are stored in the
450
cloud storage bucket.
451
<Alert
452
style={{ margin: "10px" }}
453
showIcon
454
type="info"
455
message={`Recommendation: ${RECOMMENDED_BLOCK_SIZE} MB`}
456
description={
457
<>
458
Larger block size reduces the number of PUT and GET operations, and
459
they each cost money. Also, if you use an autoclass storage class,
460
there is a monthly per-object cost.
461
</>
462
}
463
/>
464
<div style={{ textAlign: "center" }}>
465
<InputNumber
466
size="large"
467
style={{ width: "110px" }}
468
addonAfter={"MB"}
469
min={MIN_BLOCK_SIZE}
470
max={MAX_BLOCK_SIZE}
471
value={configuration.block_size}
472
onChange={(block_size) =>
473
setConfiguration({ ...configuration, block_size })
474
}
475
/>
476
</div>
477
</div>
478
);
479
}
480
481
// The Juicefs Trash is REALLY WEIRD to use, and I also
482
// think it might cause corruption or problems, especially
483
// with keydb. So do NOT enable this.
484
function TrashDays({ configuration, setConfiguration }) {
485
return (
486
<div style={{ marginTop: "10px" }}>
487
<A href="https://juicefs.com/docs/community/security/trash">
488
<b style={{ fontSize: "13pt" }}>{EXTERNAL} Trash</b>
489
</A>
490
<br />
491
Optionally store deleted files in{" "}
492
<code>~/{configuration.mountpoint}/.trash</code> for a certain number of
493
days. Set to 0 to disable. You <b>can</b> change this later, but it only
494
impacts newly written data.
495
<div style={{ textAlign: "center", marginTop: "5px" }}>
496
<InputNumber
497
size="large"
498
style={{ width: "200px" }}
499
addonAfter={"days"}
500
min={0}
501
value={configuration.trash_days}
502
onChange={(trash_days) =>
503
setConfiguration({
504
...configuration,
505
trash_days: Math.round(trash_days ?? 0),
506
})
507
}
508
/>
509
</div>
510
</div>
511
);
512
}
513
514
function Lock({ configuration, setConfiguration }) {
515
return (
516
<div>
517
If you delete this filesystem, you will be asked to type this phrase to
518
avoid mistakes. You can change this at any time.
519
<br />
520
<Input
521
style={{ marginTop: "5px", color: "red" }}
522
value={configuration.lock}
523
onChange={(e) => {
524
setConfiguration({ ...configuration, lock: e.target.value });
525
}}
526
/>
527
</div>
528
);
529
}
530
531
export function MountAndKeyDBOptions({
532
configuration,
533
setConfiguration,
534
showHeader,
535
disabled,
536
}: {
537
configuration;
538
setConfiguration;
539
showHeader;
540
disabled?;
541
}) {
542
const [details, setDetails] = useState<boolean>(false);
543
return (
544
<>
545
{showHeader && (
546
<Divider>
547
<Icon
548
name="database"
549
style={{ fontSize: "16pt", marginRight: "15px" }}
550
/>
551
Mount Options
552
</Divider>
553
)}
554
<p>
555
Changing the mount parameters can lead to filesystem corruption.
556
<Button
557
onClick={() => setDetails(!details)}
558
style={{ marginLeft: "15px" }}
559
>
560
{details ? "Hide" : "Show"} Details...
561
</Button>
562
</p>
563
{details && (
564
<>
565
<p>
566
Mount options impact cache speed and other aspects of your
567
filesystem, and{" "}
568
<i>can only be changed when the file system is not mounted</i>. You
569
can set any possible JuiceFS or KeyDB configuration, which will be
570
used when mounting your file system. Be careful: changes here can
571
make it so the file system will not mount (if that happens, unmount
572
and undo your change); also, some options may cause corruption.
573
</p>
574
<MountOptions
575
configuration={configuration}
576
setConfiguration={setConfiguration}
577
disabled={disabled}
578
/>
579
<br />
580
<KeyDBOptions
581
configuration={configuration}
582
setConfiguration={setConfiguration}
583
disabled={disabled}
584
/>
585
</>
586
)}
587
</>
588
);
589
}
590
591
function MountOptions({
592
configuration,
593
setConfiguration,
594
disabled,
595
}: {
596
configuration;
597
setConfiguration;
598
disabled?;
599
}) {
600
return (
601
<div>
602
<Button
603
style={{ float: "right" }}
604
type="text"
605
disabled={disabled}
606
onClick={() => {
607
setConfiguration({
608
...configuration,
609
mount_options: DEFAULT_CONFIGURATION.mount_options,
610
});
611
}}
612
>
613
Reset
614
</Button>
615
<A href="https://juicefs.com/docs/community/command_reference#mount">
616
{EXTERNAL} JuiceFS Mount Options
617
</A>
618
<br />
619
<Input
620
disabled={disabled}
621
value={configuration.mount_options}
622
onChange={(e) => {
623
setConfiguration({ ...configuration, mount_options: e.target.value });
624
}}
625
/>
626
</div>
627
);
628
}
629
630
function KeyDBOptions({
631
configuration,
632
setConfiguration,
633
disabled,
634
}: {
635
configuration;
636
setConfiguration;
637
disabled?;
638
}) {
639
return (
640
<div>
641
<Button
642
style={{ float: "right" }}
643
type="text"
644
disabled={disabled}
645
onClick={() => {
646
setConfiguration({
647
...configuration,
648
keydb_options: DEFAULT_CONFIGURATION.keydb_options,
649
});
650
}}
651
>
652
Reset
653
</Button>
654
<A href="https://docs.keydb.dev/docs/config-file/">
655
{EXTERNAL} KeyDB Config File Options
656
</A>
657
<br />
658
The text below is placed at the end of keydb.conf and can be used to
659
override or add to the keydb configuration used on each client.
660
<Input.TextArea
661
disabled={disabled}
662
style={{ marginTop: "5px" }}
663
rows={2}
664
value={configuration.keydb_options}
665
onChange={(e) => {
666
setConfiguration({ ...configuration, keydb_options: e.target.value });
667
}}
668
/>
669
</div>
670
);
671
}
672
673
function generateMountpoint(mountpoints, base): string {
674
if (!mountpoints.has(base)) {
675
return base;
676
}
677
let i = 1;
678
while (true) {
679
const mountpoint = `${base}-${i}`;
680
if (!mountpoints.has(mountpoint)) {
681
return mountpoint;
682
}
683
i += 1;
684
}
685
}
686
687
export const NO_CHANGE = (
688
<div style={{ color: "#666" }}>
689
<b>Cannot be changed later.</b>
690
<br />
691
</div>
692
);
693
694
export const EXTERNAL = (
695
<Icon name="external-link" style={{ marginRight: "5px" }} />
696
);
697
698
// at least 1 bigger than any current one, so it is at the top
699
function getPosition(cloudFilesystems: CloudFilesystems | null): number {
700
let position = 0;
701
if (cloudFilesystems == null) return position;
702
for (const cloudFilesystem of Object.values(cloudFilesystems)) {
703
const pos = cloudFilesystem.position ?? cloudFilesystem.id;
704
if (pos > position) {
705
position = pos + 1;
706
}
707
}
708
return position;
709
}
710
711