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/next/components/store/quota-config.tsx
Views: 687
/*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 } from "react";1920import { Icon } from "@cocalc/frontend/components/icon";21import { displaySiteLicense } from "@cocalc/util/consts/site-license";22import { plural } from "@cocalc/util/misc";23import { BOOST, DISK_DEFAULT_GB, REGULAR } from "@cocalc/util/upgrades/consts";24import PricingItem, { Line } from "components/landing/pricing-item";25import { CSS, Paragraph } from "components/misc";26import A from "components/misc/A";27import IntegerSlider from "components/misc/integer-slider";28import {29PRESETS,30PRESET_MATCH_FIELDS,31Preset,32PresetConfig,33} from "./quota-config-presets";3435const { Text } = Typography;3637const EXPERT_CONFIG = "Expert Configuration";38const listFormat = new Intl.ListFormat("en");3940const RAM_HIGH_WARN_THRESHOLD = 10;41const RAM_LOW_WARN_THRESHOLD = 1;42const MEM_MIN_RECOMMEND = 2;43const CPU_HIGH_WARN_THRESHOLD = 3;4445const WARNING_BOX: CSS = { marginTop: "10px", marginBottom: "10px" };4647interface Props {48showExplanations: boolean;49form: any;50disabled?: boolean;51onChange: () => void;52boost?: boolean;53// boost doesn't define any of the below, that's only for site-license54configMode?: "preset" | "expert";55setConfigMode?: (mode: "preset" | "expert") => void;56preset?: Preset | null;57setPreset?: (preset: Preset | null) => void;58presetAdjusted?: boolean;59setPresetAdjusted?: (adjusted: boolean) => void;60}6162export const QuotaConfig: React.FC<Props> = (props: Props) => {63const {64showExplanations,65form,66disabled = false,67onChange,68boost = false,69configMode,70setConfigMode,71preset,72setPreset,73presetAdjusted,74setPresetAdjusted,75} = props;7677const presetsRef = useRef<HTMLDivElement>(null);78const [isClient, setIsClient] = useState(false);79const [narrow, setNarrow] = useState<boolean>(false);8081useEffect(() => {82setIsClient(true);83}, []);8485useEffect(() => {86const observer = new ResizeObserver((entries) => {87if (isClient && entries[0].contentRect.width < 600) {88setNarrow(true);89} else {90setNarrow(false);91}92});9394if (presetsRef.current) {95observer.observe(presetsRef.current);96}9798return () => {99observer.disconnect();100};101}, [presetsRef.current]);102103const ramVal = Form.useWatch("ram", form);104const cpuVal = Form.useWatch("cpu", form);105106function title() {107if (boost) {108return "Booster";109} else {110return "Quota Upgrades";111}112}113114const PARAMS = boost ? BOOST : REGULAR;115116function explainRam() {117return (118<>119{renderRamInfo()}120{showExplanations ? (121<>122This quota limits the total amount of memory a project can use. Note123that RAM may be limited, if many other users are using the same host124– though member hosting significantly reduces competition for RAM.125We recommend at least {MEM_MIN_RECOMMEND}G!126</>127) : undefined}128</>129);130}131132/**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 in135* the details could make no sense, though – that's why this is added.136*/137function presetWasAdjusted() {138setPresetAdjusted?.(true);139}140141function renderRamInfo() {142if (ramVal >= RAM_HIGH_WARN_THRESHOLD) {143return (144<Alert145style={WARNING_BOX}146type="warning"147message="Consider using a compute server?"148description={149<>150You selected a RAM quota of {ramVal}G. If your use-case involves a151lot of RAM, consider using a{" "}152<A href="https://doc.cocalc.com/compute_server.html">153compute server154</A>{" "}155or{" "}156<A href={"/store/dedicated?type=vm"}>157dedicated virtual machines158</A>159. This will not only give you much more RAM, but also a far160superior experience!161</>162}163/>164);165} else if (!boost && ramVal <= RAM_LOW_WARN_THRESHOLD) {166return (167<Alert168style={WARNING_BOX}169type="warning"170message="Low memory"171description={172<>173Your choice of {ramVal}G of RAM is beyond our recommendation of at174least {MEM_MIN_RECOMMEND}G. You will not be able to run several175notebooks at once, use SageMath or Julia effectively, etc.176</>177}178/>179);180}181}182183function ram() {184return (185<Form.Item186label="Shared RAM"187name="ram"188initialValue={PARAMS.ram.dflt}189extra={explainRam()}190>191<IntegerSlider192disabled={disabled}193min={PARAMS.ram.min}194max={PARAMS.ram.max}195onChange={(ram) => {196form.setFieldsValue({ ram });197presetWasAdjusted();198onChange();199}}200units={"GB RAM"}201presets={boost ? [0, 2, 4, 8, 10] : [1, 2, 4, 8, 16]}202/>203</Form.Item>204);205}206207function renderCpuInfo() {208if (cpuVal >= CPU_HIGH_WARN_THRESHOLD) {209return (210<Alert211style={WARNING_BOX}212type="warning"213message="Consider using a compute server?"214description={215<>216You selected a CPU quota of {cpuVal} vCPU cores is high. If your217use-case involves harnessing a lot of CPU power, consider using a{" "}218<A href="https://doc.cocalc.com/compute_server.html">219compute server220</A>{" "}221or{" "}222<A href={"/store/dedicated?type=vm"}>223dedicated virtual machines224</A>225. This will not only give you many more CPU cores, but also a far226superior experience!227</>228}229/>230);231}232}233234function renderCpuExtra() {235return (236<>237{renderCpuInfo()}238{showExplanations ? (239<>240<A href="https://cloud.google.com/compute/docs/faq#virtualcpu">241Google Cloud vCPUs.242</A>{" "}243To keep prices low, these vCPUs may be shared with other projects,244though member hosting very significantly reduces competition for245CPUs.246</>247) : undefined}248</>249);250}251252function cpu() {253return (254<Form.Item255label="Shared CPUs"256name="cpu"257initialValue={PARAMS.cpu.dflt}258extra={renderCpuExtra()}259>260<IntegerSlider261disabled={disabled}262min={PARAMS.cpu.min}263max={PARAMS.cpu.max}264onChange={(cpu) => {265form.setFieldsValue({ cpu });266presetWasAdjusted();267onChange();268}}269units={"vCPU"}270presets={boost ? [0, 1, 2] : [1, 2, 3]}271/>272</Form.Item>273);274}275276function disk() {277// 2022-06: price increase "version 2": minimum disk we sell (also the free quota) is 3gb, not 1gb278return (279<Form.Item280label="Disk space"281name="disk"282initialValue={PARAMS.disk.dflt}283extra={284showExplanations ? (285<>286Extra disk space lets you store a larger number of files.287Snapshots and file edit history is included at no additional288charge. Each project receives at least {DISK_DEFAULT_GB}G of289storage space. We also offer MUCH larger disks (and CPU and290memory) via{" "}291<A href="https://doc.cocalc.com/compute_server.html">292compute server293</A>294.295</>296) : undefined297}298>299<IntegerSlider300disabled={disabled}301min={PARAMS.disk.min}302max={PARAMS.disk.max}303onChange={(disk) => {304form.setFieldsValue({ disk });305presetWasAdjusted();306onChange();307}}308units={"G Disk"}309presets={310boost ? [0, 3, 6, PARAMS.disk.max] : [3, 5, 10, PARAMS.disk.max]311}312/>313</Form.Item>314);315}316317function presetIsAdjusted() {318if (preset == null) return;319const presetData: PresetConfig = PRESETS[preset];320if (presetData == null) {321return (322<div>323Error: preset <code>{preset}</code> is not known.324</div>325);326}327328const quotaConfig: Record<string, string> = form.getFieldsValue(329Object.keys(PRESET_MATCH_FIELDS),330);331const invalidConfigValues = Object.keys(quotaConfig).filter(332(field) => quotaConfig[field] == null,333);334if (invalidConfigValues.length) {335return;336}337338const presetDiff = Object.keys(PRESET_MATCH_FIELDS).reduce(339(diff, presetField) => {340if (presetData[presetField] !== quotaConfig[presetField]) {341diff.push(PRESET_MATCH_FIELDS[presetField]);342}343344return diff;345},346[] as string[],347);348349if (!presetAdjusted || !presetDiff.length) return;350return (351<Alert352type="warning"353style={{ marginBottom: "20px" }}354message={355<>356The currently configured license differs from the selected preset in{" "}357<strong>{listFormat.format(presetDiff)}</strong>. By clicking any of358the presets below, you reconfigure your license configuration to359match the original preset.360</>361}362/>363);364}365366function presetsCommon() {367if (!showExplanations) return null;368return (369<Text type="secondary">370{preset == null ? (371<>After selecting a preset, feel free to</>372) : (373<>374Selected preset <strong>"{PRESETS[preset]?.name}"</strong>. You can375</>376)}{" "}377fine tune the selection in the "{EXPERT_CONFIG}" tab. Subsequent preset378selections will reset your adjustments.379</Text>380);381}382383function renderNoPresetWarning() {384if (preset != null) return;385return (386<Text type="danger">387Currently, no preset selection is active. Select a preset above to reset388your recent changes.389</Text>390);391}392393function renderPresetsNarrow() {394const p = preset != null ? PRESETS[preset] : undefined;395let presetInfo: JSX.Element | undefined = undefined;396if (p != null) {397const { name, cpu, disk, ram, uptime, note } = p;398const basic = (399<>400provides up to{" "}401<Text strong>402{cpu} {plural(cpu, "vCPU")}403</Text>404, <Text strong>{ram} GB memory</Text>, and{" "}405<Text strong>{disk} GB disk space</Text> for each project.406</>407);408const ut = (409<>410the project's{" "}411<Text strong>idle timeout is {displaySiteLicense(uptime)}</Text>412</>413);414presetInfo = (415<Paragraph>416<strong>{name}</strong> {basic} Additionally, {ut}. {note}417</Paragraph>418);419}420421return (422<>423<Form.Item label="Preset">424<Radio.Group425size="large"426value={preset}427onChange={(e) => onPresetChange(e.target.value)}428>429<Space direction="vertical">430{(Object.keys(PRESETS) as Array<Preset>).map((p) => {431const { name, icon, descr } = PRESETS[p];432return (433<Radio key={p} value={p}>434<span>435<Icon name={icon ?? "arrow-up"} />{" "}436<strong>{name}:</strong> {descr}437</span>438</Radio>439);440})}441</Space>442</Radio.Group>443</Form.Item>444{presetInfo}445</>446);447}448449function renderPresetPanels() {450if (narrow) return renderPresetsNarrow();451452const panels = (Object.keys(PRESETS) as Array<Preset>).map((p, idx) => {453const { name, icon, cpu, ram, disk, uptime, expect, descr, note } =454PRESETS[p];455const active = preset === p;456return (457<PricingItem458key={idx}459title={name}460icon={icon}461style={{ flex: 1 }}462active={active}463onClick={() => onPresetChange(p)}464>465<Paragraph>466<strong>{name}</strong> {descr}.467</Paragraph>468<Divider />469<Line amount={cpu} desc={"CPU"} indent={false} />470<Line amount={ram} desc={"RAM"} indent={false} />471<Line amount={disk} desc={"Disk space"} indent={false} />472<Line473amount={displaySiteLicense(uptime)}474desc={"Idle timeout"}475indent={false}476/>477<Divider />478<Paragraph>479<Text type="secondary">In each project, you will be able to:</Text>480<ul>481{expect.map((what, idx) => (482<li key={idx}>{what}</li>483))}484</ul>485</Paragraph>486{active && note != null ? (487<>488<Divider />489<Paragraph type="secondary">{note}</Paragraph>490</>491) : undefined}492<Paragraph style={{ marginTop: "20px", textAlign: "center" }}>493<Button494onClick={() => onPresetChange(p)}495size="large"496type={active ? "primary" : undefined}497>498{name}499</Button>500</Paragraph>501</PricingItem>502);503});504return (505<Flex506style={{ width: "100%" }}507justify={"space-between"}508align={"flex-start"}509gap="10px"510>511{panels}512</Flex>513);514}515516function presetExtra() {517return (518<Space ref={presetsRef} direction="vertical">519<div>520{presetIsAdjusted()}521{renderPresetPanels()}522{renderNoPresetWarning()}523</div>524{presetsCommon()}525</Space>526);527}528529function onPresetChange(val: Preset) {530if (val == null || setPreset == null) return;531setPreset(val);532setPresetAdjusted?.(false);533const presetData = PRESETS[val];534if (presetData != null) {535const { cpu, ram, disk, uptime = "short", member = true } = presetData;536form.setFieldsValue({ uptime, member, cpu, ram, disk });537}538onChange();539}540541function detailed() {542return (543<>544{ram()}545{cpu()}546{disk()}547</>548);549}550551function main() {552if (boost) {553return (554<>555<Row>556<Col xs={16} offset={6} style={{ marginBottom: "20px" }}>557<Text type="secondary">558Configure the quotas you want to add on top of your existing559license. E.g. if your license provides a limit of 2 GB of RAM and560you add a matching boost license with 3 GB of RAM, you'll end up561with a total quota limit of 5 GB of RAM.562</Text>563</Col>564</Row>565{detailed()}566</>567);568} else {569return (570<Tabs571activeKey={configMode}572onChange={setConfigMode}573type="card"574tabPosition="top"575size="middle"576centered={true}577items={[578{579key: "preset",580label: (581<span>582<Icon name="gears" style={{ marginRight: "5px" }} />583Presets584</span>585),586children: presetExtra(),587},588{589key: "expert",590label: (591<span>592<Icon name="wrench" style={{ marginRight: "5px" }} />593{EXPERT_CONFIG}594</span>595),596children: detailed(),597},598]}599/>600);601}602}603604return (605<>606<Divider plain>{title()}</Divider>607{main()}608</>609);610};611612613