Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/compute/cloud-filesystem/create.tsx
Views: 687
import {1Alert,2Button,3Card,4Divider,5Input,6InputNumber,7Modal,8Radio,9Spin,10} from "antd";11import { useEffect, useState } from "react";1213import { A, Icon } from "@cocalc/frontend/components";14import ShowError from "@cocalc/frontend/components/error";15import { checkInAll } from "@cocalc/frontend/compute/check-in";16import { CancelText } from "@cocalc/frontend/i18n/components";17import confirmCreateCloudFilesystem from "@cocalc/frontend/purchases/pay-as-you-go/confirm-create-cloud-filesystem";18import type {19Compression,20CreateCloudFilesystem,21} from "@cocalc/util/db-schema/cloud-filesystems";22import {23DEFAULT_CONFIGURATION,24MAX_BLOCK_SIZE,25MAX_CLOUD_FILESYSTEMS_PER_PROJECT,26MIN_BLOCK_SIZE,27RECOMMENDED_BLOCK_SIZE,28} from "@cocalc/util/db-schema/cloud-filesystems";29import Color, { randomColor } from "../color";30import { ProgressBarTimer } from "../state";31import Title from "../title";32import { createCloudFilesystem } from "./api";33import { BucketLocation, BucketStorageClass } from "./bucket";34import type { CloudFilesystems } from "./cloud-filesystems";3536interface Props {37project_id: string;38cloudFilesystems: CloudFilesystems | null;39refresh: Function;40}4142export default function CreateCloudFilesystem({43project_id,44cloudFilesystems,45refresh,46}: Props) {47const [taken, setTaken] = useState<{48ports: Set<number>;49mountpoints: Set<string>;50}>({ ports: new Set(), mountpoints: new Set() });51useEffect(() => {52if (cloudFilesystems == null) {53return;54}55const v = Object.values(cloudFilesystems);56setTaken({57ports: new Set(v.map((x) => x.port)),58mountpoints: new Set(v.map((x) => x.mountpoint)),59});60}, [cloudFilesystems]);61const [creating, setCreating] = useState<boolean>(false);62const [createStarted, setCreateStarted] = useState<Date>(new Date());63const [editing, setEditing] = useState<boolean>(false);64const [error, setError] = useState<string>("");65const [advanced, setAdvanced] = useState<boolean>(false);66const [configuration, setConfiguration] =67useState<CreateCloudFilesystem | null>(null);6869const reset = () => {70setConfiguration({71project_id,72...DEFAULT_CONFIGURATION,73mountpoint: generateMountpoint(74taken.mountpoints,75DEFAULT_CONFIGURATION.mountpoint,76),77color: randomColor(),78// start mounted by default -- way less confusing79mount: true,80// bucket_location gets filled in by BucketLocation component on init81bucket_location: "",82});83};8485const create = async () => {86if (creating || configuration == null) {87return;88}89try {90setCreating(true);91setCreateStarted(new Date());92await confirmCreateCloudFilesystem();93setCreateStarted(new Date());94await createCloudFilesystem({95...configuration,96position: getPosition(cloudFilesystems),97});98checkInAll(project_id); // cause filesystem to be noticed (and mounted) asap99setEditing(false);100reset();101refresh();102} catch (err) {103setError(`${err}`);104} finally {105refresh();106setCreating(false);107}108};109110return (111<div style={{ textAlign: "center", margin: "15px 0" }}>112<Button113size="large"114disabled={creating || editing}115onClick={() => {116reset();117setEditing(true);118}}119style={{120marginRight: "5px",121width: "80%",122height: "auto",123whiteSpace: "normal",124padding: "10px",125...(creating126? {127borderColor: "rgb(22, 119, 255)",128backgroundColor: "rgb(230, 244, 255)",129}130: undefined),131}}132>133<Icon134name="server"135style={{136color: "rgb(66, 139, 202)",137fontSize: "200%",138}}139/>140<br />141Create Cloud File System... {creating ? <Spin /> : null}142</Button>143<Modal144width={"900px"}145onCancel={() => {146setEditing(false);147reset();148}}149open={editing && configuration != null}150title={151<div style={{ display: "flex", fontSize: "15pt" }}>152<Icon name="disk-round" style={{ marginRight: "15px" }} /> Create a153CoCalc Cloud File System154</div>155}156footer={[157<Button158key="cancel"159disabled={creating}160onClick={() => {161setEditing(false);162reset();163}}164>165<CancelText />166</Button>,167<Button key="ok" type="primary" disabled={creating} onClick={create}>168<>169Create Cloud File System{" "}170{creating ? <Spin style={{ marginLeft: "15px" }} /> : undefined}171</>172</Button>,173]}174>175<ShowError176error={error}177setError={setError}178style={{ margin: "15px 0" }}179/>{" "}180<Card181style={{182margin: "15px 0",183border: `0.5px solid ${configuration?.color ?? "#f0f0f0"}`,184borderRight: `10px solid ${configuration?.color ?? "#aaa"}`,185borderLeft: `10px solid ${configuration?.color ?? "#aaa"}`,186...(creating ? { opacity: 0.4 } : undefined),187}}188>189<Divider>190<Icon191name="cloud-dev"192style={{ fontSize: "16pt", marginRight: "15px" }}193/>{" "}194Title and Color195</Divider>196Select a meaningful title and color for your Cloud File System. You197can change these at any time, and they do not impact anything else.198<br />199<div style={{ display: "flex" }}>200<div style={{ flex: 1 }} />201<EditTitle202configuration={configuration}203setConfiguration={setConfiguration}204/>205<div style={{ flex: 1 }} />206207<SelectColor208configuration={configuration}209setConfiguration={setConfiguration}210/>211<div style={{ flex: 1 }} />212</div>213<Divider>214<Icon215name="folder-open"216style={{ fontSize: "16pt", marginRight: "15px" }}217/>{" "}218Mountpoint219</Divider>220<Mountpoint221configuration={configuration}222setConfiguration={setConfiguration}223mountpoints={taken.mountpoints}224/>225<Divider>226<Icon227name="disk-snapshot"228style={{ fontSize: "16pt", marginRight: "15px" }}229/>{" "}230Bucket231</Divider>232Your data is stored in a Google Cloud Storage bucket in a single233region or multiregion bucket, and recently used data is cached locally234on each compute server's disk.235<BucketLocation236configuration={configuration}237setConfiguration={setConfiguration}238/>239<Divider>240{advanced ? (241<Button242onClick={() => setAdvanced(false)}243type="link"244style={{ fontSize: "12pt" }}245>246<Icon name="eye-slash" /> Hide Advanced Settings247</Button>248) : (249<Button250onClick={() => setAdvanced(true)}251type="link"252style={{ fontSize: "12pt" }}253>254<Icon name="eye" /> Show Advanced Settings...255</Button>256)}257</Divider>258{advanced && (259<>260<p>261<b>What is it?:</b> The CoCalc Cloud File System is a fully262POSIX compliant distributed file system built using{" "}263<A href="https://juicefs.com/">JuiceFS</A>,{" "}264<A href="https://docs.keydb.dev/">KeyDB</A> and{" "}265<A href="https://cloud.google.com/storage">266Google Cloud Storage267</A>268.269</p>270<p>271<b>Scope:</b> You can make up to{" "}272{MAX_CLOUD_FILESYSTEMS_PER_PROJECT} cloud file systems per273project. Cloud file systems can be instantly moved between274projects.275</p>276<p>277<b>Cost:</b> The cost is a slightly marked up version of{" "}278<A href="https://cloud.google.com/storage/pricing">279Google Cloud Storage Pricing, which is highly competitive.280</A>{" "}281You can see how much your file system costs and why in realtime282by clicking "Show Metrics" in the cloud file system menu. If283your compute server and filesystem are in the same region, then284data transfer fees at completely free, and you mainly pay for285storage and operations (i.e., there is a fee per block of data286that is uploaded).287</p>288<Divider>289<Icon290name="lock"291style={{ fontSize: "16pt", marginRight: "15px" }}292/>{" "}293Safety294</Divider>295<Lock296configuration={configuration}297setConfiguration={setConfiguration}298/>299{false && (300<TrashDays301configuration={configuration}302setConfiguration={setConfiguration}303/>304)}305<Divider>306<Icon307name="database"308style={{ fontSize: "16pt", marginRight: "15px" }}309/>{" "}310Data Storage311</Divider>312<div>313<BucketStorageClass314configuration={configuration}315setConfiguration={setConfiguration}316/>317<Compression318configuration={configuration}319setConfiguration={setConfiguration}320/>321<BlockSize322configuration={configuration}323setConfiguration={setConfiguration}324/>325</div>326<MountAndKeyDBOptions327showHeader328configuration={configuration}329setConfiguration={setConfiguration}330/>331</>332)}333{creating && (334<div style={{ textAlign: "center", fontSize: "14pt" }}>335Creating Cloud File System...{" "}336<ProgressBarTimer337startTime={createStarted}338style={{ marginLeft: "10px" }}339/>340</div>341)}342<ShowError343error={error}344setError={setError}345style={{ margin: "15px 0" }}346/>347</Card>348</Modal>349</div>350);351}352353function EditTitle({ configuration, setConfiguration }) {354return (355<Title356editable357title={configuration.title}358onChange={(title) => setConfiguration({ ...configuration, title })}359/>360);361}362363function SelectColor({ configuration, setConfiguration }) {364return (365<Color366editable367color={configuration.color}368onChange={(color) => setConfiguration({ ...configuration, color })}369/>370);371}372373function Mountpoint({ configuration, setConfiguration, mountpoints }) {374const taken = mountpoints.has(configuration.mountpoint);375return (376<div>377Mount at <code>~/{configuration.mountpoint}</code> on all compute servers.378You can change this when the file system is not mounted.379<br />380<Input381status={taken ? "error" : undefined}382style={{ marginTop: "10px" }}383value={configuration.mountpoint}384onChange={(e) => {385setConfiguration({ ...configuration, mountpoint: e.target.value });386}}387/>388{taken && (389<Alert390style={{ margin: "10px 0" }}391showIcon392type="error"393message="This mountpoint is already being used by another Cloud File System in this project. Please change the mountpoint."394/>395)}396</div>397);398}399400function Compression({ configuration, setConfiguration }) {401return (402<div style={{ marginTop: "10px" }}>403<b style={{ fontSize: "13pt", color: "#666" }}>404<A href="https://juicefs.com/docs/community/internals/#data-compression">405{EXTERNAL}406Compression407</A>408</b>409{NO_CHANGE}410You can compress your data automatically.411<Alert412style={{ margin: "10px" }}413showIcon414type="info"415message={`Recommendation: LZ4`}416description={417<>418Do not enable compression if most of your data is already419compressed. Otherwise, <A href="https://lz4.github.io/lz4">LZ4</A>{" "}420is a good choice; it uses less CPU, and can save significant space.421Use <A href="https://facebook.github.io/zstd">ZSTD</A> if a lot of422your data is compressible and more CPU usage is OK.423</>424}425/>426<div style={{ textAlign: "center", marginTop: "10px" }}>427<Radio.Group428onChange={(e) =>429setConfiguration({ ...configuration, compression: e.target.value })430}431value={configuration.compression}432>433<Radio value={"lz4"}>LZ4 - faster performance</Radio>434<Radio value={"zstd"}>ZSTD - better compression ratio</Radio>435<Radio value={"none"}>None</Radio>436</Radio.Group>437</div>438</div>439);440}441442function BlockSize({ configuration, setConfiguration }) {443return (444<div style={{ marginTop: "10px" }}>445<b style={{ fontSize: "13pt", color: "#666" }}>Block Size</b>446{NO_CHANGE}447The block size, which is between {MIN_BLOCK_SIZE} MB and {MAX_BLOCK_SIZE}{" "}448MB, is an upper bound on the size of the objects that are stored in the449cloud storage bucket.450<Alert451style={{ margin: "10px" }}452showIcon453type="info"454message={`Recommendation: ${RECOMMENDED_BLOCK_SIZE} MB`}455description={456<>457Larger block size reduces the number of PUT and GET operations, and458they each cost money. Also, if you use an autoclass storage class,459there is a monthly per-object cost.460</>461}462/>463<div style={{ textAlign: "center" }}>464<InputNumber465size="large"466style={{ width: "110px" }}467addonAfter={"MB"}468min={MIN_BLOCK_SIZE}469max={MAX_BLOCK_SIZE}470value={configuration.block_size}471onChange={(block_size) =>472setConfiguration({ ...configuration, block_size })473}474/>475</div>476</div>477);478}479480// The Juicefs Trash is REALLY WEIRD to use, and I also481// think it might cause corruption or problems, especially482// with keydb. So do NOT enable this.483function TrashDays({ configuration, setConfiguration }) {484return (485<div style={{ marginTop: "10px" }}>486<A href="https://juicefs.com/docs/community/security/trash">487<b style={{ fontSize: "13pt" }}>{EXTERNAL} Trash</b>488</A>489<br />490Optionally store deleted files in{" "}491<code>~/{configuration.mountpoint}/.trash</code> for a certain number of492days. Set to 0 to disable. You <b>can</b> change this later, but it only493impacts newly written data.494<div style={{ textAlign: "center", marginTop: "5px" }}>495<InputNumber496size="large"497style={{ width: "200px" }}498addonAfter={"days"}499min={0}500value={configuration.trash_days}501onChange={(trash_days) =>502setConfiguration({503...configuration,504trash_days: Math.round(trash_days ?? 0),505})506}507/>508</div>509</div>510);511}512513function Lock({ configuration, setConfiguration }) {514return (515<div>516If you delete this filesystem, you will be asked to type this phrase to517avoid mistakes. You can change this at any time.518<br />519<Input520style={{ marginTop: "5px", color: "red" }}521value={configuration.lock}522onChange={(e) => {523setConfiguration({ ...configuration, lock: e.target.value });524}}525/>526</div>527);528}529530export function MountAndKeyDBOptions({531configuration,532setConfiguration,533showHeader,534disabled,535}: {536configuration;537setConfiguration;538showHeader;539disabled?;540}) {541const [details, setDetails] = useState<boolean>(false);542return (543<>544{showHeader && (545<Divider>546<Icon547name="database"548style={{ fontSize: "16pt", marginRight: "15px" }}549/>550Mount Options551</Divider>552)}553<p>554Changing the mount parameters can lead to filesystem corruption.555<Button556onClick={() => setDetails(!details)}557style={{ marginLeft: "15px" }}558>559{details ? "Hide" : "Show"} Details...560</Button>561</p>562{details && (563<>564<p>565Mount options impact cache speed and other aspects of your566filesystem, and{" "}567<i>can only be changed when the file system is not mounted</i>. You568can set any possible JuiceFS or KeyDB configuration, which will be569used when mounting your file system. Be careful: changes here can570make it so the file system will not mount (if that happens, unmount571and undo your change); also, some options may cause corruption.572</p>573<MountOptions574configuration={configuration}575setConfiguration={setConfiguration}576disabled={disabled}577/>578<br />579<KeyDBOptions580configuration={configuration}581setConfiguration={setConfiguration}582disabled={disabled}583/>584</>585)}586</>587);588}589590function MountOptions({591configuration,592setConfiguration,593disabled,594}: {595configuration;596setConfiguration;597disabled?;598}) {599return (600<div>601<Button602style={{ float: "right" }}603type="text"604disabled={disabled}605onClick={() => {606setConfiguration({607...configuration,608mount_options: DEFAULT_CONFIGURATION.mount_options,609});610}}611>612Reset613</Button>614<A href="https://juicefs.com/docs/community/command_reference#mount">615{EXTERNAL} JuiceFS Mount Options616</A>617<br />618<Input619disabled={disabled}620value={configuration.mount_options}621onChange={(e) => {622setConfiguration({ ...configuration, mount_options: e.target.value });623}}624/>625</div>626);627}628629function KeyDBOptions({630configuration,631setConfiguration,632disabled,633}: {634configuration;635setConfiguration;636disabled?;637}) {638return (639<div>640<Button641style={{ float: "right" }}642type="text"643disabled={disabled}644onClick={() => {645setConfiguration({646...configuration,647keydb_options: DEFAULT_CONFIGURATION.keydb_options,648});649}}650>651Reset652</Button>653<A href="https://docs.keydb.dev/docs/config-file/">654{EXTERNAL} KeyDB Config File Options655</A>656<br />657The text below is placed at the end of keydb.conf and can be used to658override or add to the keydb configuration used on each client.659<Input.TextArea660disabled={disabled}661style={{ marginTop: "5px" }}662rows={2}663value={configuration.keydb_options}664onChange={(e) => {665setConfiguration({ ...configuration, keydb_options: e.target.value });666}}667/>668</div>669);670}671672function generateMountpoint(mountpoints, base): string {673if (!mountpoints.has(base)) {674return base;675}676let i = 1;677while (true) {678const mountpoint = `${base}-${i}`;679if (!mountpoints.has(mountpoint)) {680return mountpoint;681}682i += 1;683}684}685686export const NO_CHANGE = (687<div style={{ color: "#666" }}>688<b>Cannot be changed later.</b>689<br />690</div>691);692693export const EXTERNAL = (694<Icon name="external-link" style={{ marginRight: "5px" }} />695);696697// at least 1 bigger than any current one, so it is at the top698function getPosition(cloudFilesystems: CloudFilesystems | null): number {699let position = 0;700if (cloudFilesystems == null) return position;701for (const cloudFilesystem of Object.values(cloudFilesystems)) {702const pos = cloudFilesystem.position ?? cloudFilesystem.id;703if (pos > position) {704position = pos + 1;705}706}707return position;708}709710711