Path: blob/master/src/packages/next/components/store/quota-config.tsx
1450 views
/*1* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import {6Alert,7Button,8Col,9Divider,10Flex,11Form,12Radio,13Row,14Space,15Tabs,16Typography,17} from "antd";18import { useEffect, useRef, useState, type JSX } from "react";19import { Icon } from "@cocalc/frontend/components/icon";20import { displaySiteLicense } from "@cocalc/util/consts/site-license";21import { plural } from "@cocalc/util/misc";22import { BOOST, DISK_DEFAULT_GB, REGULAR } from "@cocalc/util/upgrades/consts";23import PricingItem, { Line } from "components/landing/pricing-item";24import { CSS, Paragraph } from "components/misc";25import A from "components/misc/A";26import IntegerSlider from "components/misc/integer-slider";27import {28PRESETS,29PRESET_MATCH_FIELDS,30Preset,31PresetConfig,32} from "./quota-config-presets";3334const { Text } = Typography;3536const EXPERT_CONFIG = "Expert Configuration";37const listFormat = new Intl.ListFormat("en");3839const RAM_HIGH_WARN_THRESHOLD = 10;40const RAM_LOW_WARN_THRESHOLD = 1;41const MEM_MIN_RECOMMEND = 2;42const CPU_HIGH_WARN_THRESHOLD = 3;4344const WARNING_BOX: CSS = { marginTop: "10px", marginBottom: "10px" };4546interface Props {47showExplanations: boolean;48form: any;49disabled?: boolean;50onChange: () => void;51boost?: boolean;52// boost doesn't define any of the below, that's only for site-license53configMode?: "preset" | "expert";54setConfigMode?: (mode: "preset" | "expert") => void;55preset?: Preset | null;56setPreset?: (preset: Preset | null) => void;57presetAdjusted?: boolean;58setPresetAdjusted?: (adjusted: boolean) => void;59}6061export const QuotaConfig: React.FC<Props> = (props: Props) => {62const {63showExplanations,64form,65disabled = false,66onChange,67boost = false,68configMode,69setConfigMode,70preset,71setPreset,72presetAdjusted,73setPresetAdjusted,74} = props;7576const presetsRef = useRef<HTMLDivElement>(null);77const [isClient, setIsClient] = useState(false);78const [narrow, setNarrow] = useState<boolean>(false);7980useEffect(() => {81setIsClient(true);82}, []);8384useEffect(() => {85const observer = new ResizeObserver((entries) => {86if (isClient && entries[0].contentRect.width < 600) {87setNarrow(true);88} else {89setNarrow(false);90}91});9293if (presetsRef.current) {94observer.observe(presetsRef.current);95}9697return () => {98observer.disconnect();99};100}, [presetsRef.current]);101102const ramVal = Form.useWatch("ram", form);103const cpuVal = Form.useWatch("cpu", form);104105function title() {106if (boost) {107return "Booster";108} else {109return "Quota Upgrades";110}111}112113const PARAMS = boost ? BOOST : REGULAR;114115function explainRam() {116return (117<>118{renderRamInfo()}119{showExplanations ? (120<>121This quota limits the total amount of memory a project can use. Note122that RAM may be limited, if many other users are using the same host123– though member hosting significantly reduces competition for RAM.124We recommend at least {MEM_MIN_RECOMMEND}G!125</>126) : undefined}127</>128);129}130131/**132* When a quota is changed, we warn the user that the preset was adjusted.133* (the text updates, though, since it rerenders every time). Explanation in134* the details could make no sense, though – that's why this is added.135*/136function presetWasAdjusted() {137setPresetAdjusted?.(true);138}139140function renderRamInfo() {141if (ramVal >= RAM_HIGH_WARN_THRESHOLD) {142return (143<Alert144style={WARNING_BOX}145type="warning"146message="Consider using a compute server?"147description={148<>149You selected a RAM quota of {ramVal}G. If your use-case involves a150lot of RAM, consider using a{" "}151<A href="https://doc.cocalc.com/compute_server.html">152compute server.153</A>154</>155}156/>157);158} else if (!boost && ramVal <= RAM_LOW_WARN_THRESHOLD) {159return (160<Alert161style={WARNING_BOX}162type="warning"163message="Low memory"164description={165<>166Your choice of {ramVal}G of RAM is beyond our recommendation of at167least {MEM_MIN_RECOMMEND}G. You will not be able to run several168notebooks at once, use SageMath or Julia effectively, etc.169</>170}171/>172);173}174}175176function ram() {177return (178<Form.Item179label="Shared RAM"180name="ram"181initialValue={PARAMS.ram.dflt}182extra={explainRam()}183>184<IntegerSlider185disabled={disabled}186min={PARAMS.ram.min}187max={PARAMS.ram.max}188onChange={(ram) => {189form.setFieldsValue({ ram });190presetWasAdjusted();191onChange();192}}193units={"GB RAM"}194presets={boost ? [0, 2, 4, 8, 10] : [4, 8, 16]}195/>196</Form.Item>197);198}199200function renderCpuInfo() {201if (cpuVal >= CPU_HIGH_WARN_THRESHOLD) {202return (203<Alert204style={WARNING_BOX}205type="warning"206message="Consider using a compute server?"207description={208<>209You selected a CPU quota of {cpuVal} vCPU cores is high. If your210use-case involves harnessing a lot of CPU power, consider using a{" "}211<A href="https://doc.cocalc.com/compute_server.html">212compute server213</A>{" "}214or{" "}215<A href={"/store/dedicated?type=vm"}>216dedicated virtual machines217</A>218. This will not only give you many more CPU cores, but also a far219superior experience!220</>221}222/>223);224}225}226227function renderCpuExtra() {228return (229<>230{renderCpuInfo()}231{showExplanations ? (232<>233<A href="https://cloud.google.com/compute/docs/faq#virtualcpu">234Google Cloud vCPUs.235</A>{" "}236To keep prices low, these vCPUs may be shared with other projects,237though member hosting very significantly reduces competition for238CPUs.239</>240) : undefined}241</>242);243}244245function cpu() {246return (247<Form.Item248label="Shared CPUs"249name="cpu"250initialValue={PARAMS.cpu.dflt}251extra={renderCpuExtra()}252>253<IntegerSlider254disabled={disabled}255min={PARAMS.cpu.min}256max={PARAMS.cpu.max}257onChange={(cpu) => {258form.setFieldsValue({ cpu });259presetWasAdjusted();260onChange();261}}262units={"vCPU"}263presets={boost ? [0, 1, 2] : [1, 2, 3]}264/>265</Form.Item>266);267}268269function disk() {270// 2022-06: price increase "version 2": minimum disk we sell (also the free quota) is 3gb, not 1gb271return (272<Form.Item273label="Disk space"274name="disk"275initialValue={PARAMS.disk.dflt}276extra={277showExplanations ? (278<>279Extra disk space lets you store a larger number of files.280Snapshots and file edit history is included at no additional281charge. Each project receives at least {DISK_DEFAULT_GB}G of282storage space. We also offer MUCH larger disks (and CPU and283memory) via{" "}284<A href="https://doc.cocalc.com/compute_server.html">285compute server286</A>287.288</>289) : undefined290}291>292<IntegerSlider293disabled={disabled}294min={PARAMS.disk.min}295max={PARAMS.disk.max}296onChange={(disk) => {297form.setFieldsValue({ disk });298presetWasAdjusted();299onChange();300}}301units={"G Disk"}302presets={303boost ? [0, 3, 6, PARAMS.disk.max] : [3, 5, 10, PARAMS.disk.max]304}305/>306</Form.Item>307);308}309310function presetIsAdjusted() {311if (preset == null) return;312const presetData: PresetConfig = PRESETS[preset];313if (presetData == null) {314return (315<div>316Error: preset <code>{preset}</code> is not known.317</div>318);319}320321const quotaConfig: Record<string, string> = form.getFieldsValue(322Object.keys(PRESET_MATCH_FIELDS),323);324const invalidConfigValues = Object.keys(quotaConfig).filter(325(field) => quotaConfig[field] == null,326);327if (invalidConfigValues.length) {328return;329}330331const presetDiff = Object.keys(PRESET_MATCH_FIELDS).reduce(332(diff, presetField) => {333if (presetData[presetField] !== quotaConfig[presetField]) {334diff.push(PRESET_MATCH_FIELDS[presetField]);335}336337return diff;338},339[] as string[],340);341342if (!presetAdjusted || !presetDiff.length) return;343return (344<Alert345type="warning"346style={{ marginBottom: "20px" }}347message={348<>349The currently configured license differs from the selected preset in{" "}350<strong>{listFormat.format(presetDiff)}</strong>. By clicking any of351the presets below, you reconfigure your license configuration to352match the original preset.353</>354}355/>356);357}358359function presetsCommon() {360if (!showExplanations) return null;361return (362<Text type="secondary">363{preset == null ? (364<>After selecting a preset, feel free to</>365) : (366<>367Selected preset <strong>"{PRESETS[preset]?.name}"</strong>. You can368</>369)}{" "}370fine tune the selection in the "{EXPERT_CONFIG}" tab. Subsequent preset371selections will reset your adjustments.372</Text>373);374}375376function renderNoPresetWarning() {377if (preset != null) return;378return (379<Text type="danger">380Currently, no preset selection is active. Select a preset above to reset381your recent changes.382</Text>383);384}385386function renderPresetsNarrow() {387const p = preset != null ? PRESETS[preset] : undefined;388let presetInfo: JSX.Element | undefined = undefined;389if (p != null) {390const { name, cpu, disk, ram, uptime, note } = p;391const basic = (392<>393provides up to{" "}394<Text strong>395{cpu} {plural(cpu, "vCPU")}396</Text>397, <Text strong>{ram} GB memory</Text>, and{" "}398<Text strong>{disk} GB disk space</Text> for each project.399</>400);401const ut = (402<>403the project's{" "}404<Text strong>idle timeout is {displaySiteLicense(uptime)}</Text>405</>406);407presetInfo = (408<Paragraph>409<strong>{name}</strong> {basic} Additionally, {ut}. {note}410</Paragraph>411);412}413414return (415<>416<Form.Item label="Preset">417<Radio.Group418size="large"419value={preset}420onChange={(e) => onPresetChange(e.target.value)}421>422<Space direction="vertical">423{(Object.keys(PRESETS) as Array<Preset>).map((p) => {424const { name, icon, descr } = PRESETS[p];425return (426<Radio key={p} value={p}>427<span>428<Icon name={icon ?? "arrow-up"} />{" "}429<strong>{name}:</strong> {descr}430</span>431</Radio>432);433})}434</Space>435</Radio.Group>436</Form.Item>437{presetInfo}438</>439);440}441442function renderPresetPanels() {443if (narrow) return renderPresetsNarrow();444445const panels = (Object.keys(PRESETS) as Array<Preset>).map((p, idx) => {446const { name, icon, cpu, ram, disk, uptime, expect, descr, note } =447PRESETS[p];448const active = preset === p;449return (450<PricingItem451key={idx}452title={name}453icon={icon}454style={{ flex: 1 }}455active={active}456onClick={() => onPresetChange(p)}457>458<Paragraph>459<strong>{name}</strong> {descr}.460</Paragraph>461<Divider />462<Line amount={cpu} desc={"CPU"} indent={false} />463<Line amount={ram} desc={"RAM"} indent={false} />464<Line amount={disk} desc={"Disk space"} indent={false} />465<Line466amount={displaySiteLicense(uptime)}467desc={"Idle timeout"}468indent={false}469/>470<Divider />471<Paragraph>472<Text type="secondary">In each project, you will be able to:</Text>473<ul>474{expect.map((what, idx) => (475<li key={idx}>{what}</li>476))}477</ul>478</Paragraph>479{active && note != null ? (480<>481<Divider />482<Paragraph type="secondary">{note}</Paragraph>483</>484) : undefined}485<Paragraph style={{ marginTop: "20px", textAlign: "center" }}>486<Button487onClick={() => onPresetChange(p)}488size="large"489type={active ? "primary" : undefined}490>491{name}492</Button>493</Paragraph>494</PricingItem>495);496});497return (498<Flex499style={{ width: "100%" }}500justify={"space-between"}501align={"flex-start"}502gap="10px"503>504{panels}505</Flex>506);507}508509function presetExtra() {510return (511<Space ref={presetsRef} direction="vertical">512<div>513{presetIsAdjusted()}514{renderPresetPanels()}515{renderNoPresetWarning()}516</div>517{presetsCommon()}518</Space>519);520}521522function onPresetChange(val: Preset) {523if (val == null || setPreset == null) return;524setPreset(val);525setPresetAdjusted?.(false);526const presetData = PRESETS[val];527if (presetData != null) {528const { cpu, ram, disk, uptime = "short", member = true } = presetData;529form.setFieldsValue({ uptime, member, cpu, ram, disk });530}531onChange();532}533534function detailed() {535return (536<>537{ram()}538{cpu()}539{disk()}540</>541);542}543544function main() {545if (boost) {546return (547<>548<Row>549<Col xs={16} offset={6} style={{ marginBottom: "20px" }}>550<Text type="secondary">551Configure the quotas you want to add on top of your existing552license. E.g. if your license provides a limit of 2 GB of RAM553and you add a matching boost license with 3 GB of RAM, you'll554end up with a total quota limit of 5 GB of RAM.555</Text>556</Col>557</Row>558{detailed()}559</>560);561} else {562return (563<Tabs564activeKey={configMode}565onChange={setConfigMode}566type="card"567tabPosition="top"568size="middle"569centered={true}570items={[571{572key: "preset",573label: (574<span>575<Icon name="gears" style={{ marginRight: "5px" }} />576Presets577</span>578),579children: presetExtra(),580},581{582key: "expert",583label: (584<span>585<Icon name="wrench" style={{ marginRight: "5px" }} />586{EXPERT_CONFIG}587</span>588),589children: detailed(),590},591]}592/>593);594}595}596597return (598<>599<Divider plain>{title()}</Divider>600{main()}601</>602);603};604605606