Path: blob/master/src/packages/next/components/store/site-license.tsx
1450 views
/*1* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Create a new site license.7*/8import { Form, Input } from "antd";9import { isEmpty } from "lodash";10import { useEffect, useRef, useState } from "react";11import { Icon } from "@cocalc/frontend/components/icon";12import { get_local_storage } from "@cocalc/frontend/misc/local-storage";13import { CostInputPeriod } from "@cocalc/util/licenses/purchase/types";14import { computeCost } from "@cocalc/util/licenses/store/compute-cost";15import { Paragraph, Title } from "components/misc";16import A from "components/misc/A";17import Loading from "components/share/loading";18import SiteName from "components/share/site-name";19import apiPost from "lib/api/post";20import { MAX_WIDTH } from "lib/config";21import { useScrollY } from "lib/use-scroll-y";22import { useRouter } from "next/router";23import { AddBox } from "./add-box";24import { ApplyLicenseToProject } from "./apply-license-to-project";25import { InfoBar } from "./cost-info-bar";26import { IdleTimeout } from "./member-idletime";27import { QuotaConfig } from "./quota-config";28import { PRESETS, PRESET_MATCH_FIELDS, Preset } from "./quota-config-presets";29import { decodeFormValues, encodeFormValues } from "./quota-query-params";30import { RunLimit } from "./run-limit";31import { SignInToPurchase } from "./sign-in-to-purchase";32import { TitleDescription } from "./title-description";33import { ToggleExplanations } from "./toggle-explanations";34import { UsageAndDuration } from "./usage-and-duration";3536const DEFAULT_PRESET: Preset = "standard";3738const STYLE: React.CSSProperties = {39marginTop: "15px",40maxWidth: MAX_WIDTH,41margin: "auto",42border: "1px solid #ddd",43padding: "15px",44} as const;4546interface Props {47noAccount: boolean;48}4950export default function SiteLicense({ noAccount }: Props) {51const router = useRouter();52const headerRef = useRef<HTMLHeadingElement>(null);5354// most likely, user will go to the cart next55useEffect(() => {56router.prefetch("/store/cart");57}, []);5859const [offsetHeader, setOffsetHeader] = useState(0);60const scrollY = useScrollY();6162useEffect(() => {63if (headerRef.current) {64setOffsetHeader(headerRef.current.offsetTop);65}66}, []);6768return (69<>70<Title level={3} ref={headerRef}>71<Icon name={"key"} style={{ marginRight: "5px" }} />{" "}72{router.query.id != null73? "Edit License in Shopping Cart"74: "Configure a License"}75</Title>76{router.query.id == null && (77<div>78<Paragraph style={{ fontSize: "12pt" }}>79<A href="https://doc.cocalc.com/licenses.html">80<SiteName /> licenses81</A>{" "}82allow you to upgrade projects to run more quickly, have network83access, more disk space and memory. Licenses cover a wide range of84use cases, ranging from a single hobbyist project to thousands of85simultaneous users across a large organization.86</Paragraph>8788<Paragraph style={{ fontSize: "12pt" }}>89Create a license using the form below then add it to your{" "}90<A href="/store/cart">shopping cart</A>. If you aren't sure exactly91what to buy, you can always edit your licenses later. Subscriptions92are also flexible and can be{" "}93<A94href="https://doc.cocalc.com/account/purchases.html#recent-updates-to-subscriptions"95external96>97edited at any time.{" "}98</A>99</Paragraph>100</div>101)}102<CreateSiteLicense103showInfoBar={scrollY > offsetHeader}104noAccount={noAccount}105/>106</>107);108}109110// Note -- the back and forth between moment and Date below111// is a *workaround* because of some sort of bug in moment/antd/react.112113function CreateSiteLicense({ showInfoBar = false, noAccount = false }) {114const [cost, setCost] = useState<CostInputPeriod | undefined>(undefined);115const [loading, setLoading] = useState<boolean>(false);116const [cartError, setCartError] = useState<string>("");117const [showExplanations, setShowExplanations] = useState<boolean>(false);118const [configMode, setConfigMode] = useState<"preset" | "expert">("preset");119const [form] = Form.useForm();120const router = useRouter();121122const [preset, setPreset] = useState<Preset | null>(DEFAULT_PRESET);123const [presetAdjusted, setPresetAdjusted] = useState<boolean>(false);124125/**126* Utility function to match current license configuration to a particular preset. If none is127* found, this function returns undefined.128*/129function findPreset() {130const currentConfiguration = form.getFieldsValue(131Object.keys(PRESET_MATCH_FIELDS),132);133134let foundPreset: Preset | undefined;135136Object.keys(PRESETS).some((p) => {137const presetMatches = Object.keys(PRESET_MATCH_FIELDS).every(138(formField) =>139PRESETS[p][formField] === currentConfiguration[formField],140);141142if (presetMatches) {143foundPreset = p as Preset;144}145146return presetMatches;147});148149return foundPreset;150}151152function onLicenseChange() {153const vals = form.getFieldsValue(true);154encodeFormValues(router, vals, "regular");155setCost(computeCost(vals));156157const foundPreset = findPreset();158159if (foundPreset) {160setPresetAdjusted(false);161setPreset(foundPreset);162} else {163setPresetAdjusted(true);164}165}166167useEffect(() => {168const store_site_license_show_explanations = get_local_storage(169"store_site_license_show_explanations",170);171if (store_site_license_show_explanations != null) {172setShowExplanations(!!store_site_license_show_explanations);173}174175const { id } = router.query;176if (!noAccount && id != null) {177// editing something in the shopping cart178(async () => {179try {180setLoading(true);181const item = await apiPost("/shopping/cart/get", { id });182if (item.product == "site-license") {183form.setFieldsValue({ ...item.description, type: "regular" });184}185} catch (err) {186setCartError(err.message);187} finally {188setLoading(false);189}190onLicenseChange();191})();192} else {193const vals = decodeFormValues(router, "regular");194const dflt = PRESETS[DEFAULT_PRESET];195if (isEmpty(vals)) {196form.setFieldsValue({197...dflt,198});199} else {200// we have to make sure cpu, mem and disk are set, otherwise there is no "cost"201form.setFieldsValue({202...dflt,203...vals,204});205}206}207onLicenseChange();208}, []);209210if (loading) {211return <Loading large center />;212}213214const addBox = (215<AddBox216cost={cost}217router={router}218form={form}219cartError={cartError}220setCartError={setCartError}221noAccount={noAccount}222/>223);224225return (226<div>227<ApplyLicenseToProject router={router} />228<SignInToPurchase noAccount={noAccount} />229<InfoBar230show={showInfoBar}231cost={cost}232router={router}233form={form}234cartError={cartError}235setCartError={setCartError}236noAccount={noAccount}237/>238<Form239form={form}240style={STYLE}241name="basic"242labelCol={{ span: 3 }}243wrapperCol={{ span: 21 }}244autoComplete="off"245onValuesChange={onLicenseChange}246>247<Form.Item wrapperCol={{ offset: 0, span: 24 }}>{addBox}</Form.Item>248<ToggleExplanations249showExplanations={showExplanations}250setShowExplanations={setShowExplanations}251/>252{/* Hidden form item, used to disambiguate between boost and regular licenses */}253<Form.Item name="type" initialValue={"regular"} noStyle>254<Input type="hidden" />255</Form.Item>256<UsageAndDuration257showExplanations={showExplanations}258form={form}259onChange={onLicenseChange}260/>261<RunLimit262showExplanations={showExplanations}263form={form}264onChange={onLicenseChange}265/>266<QuotaConfig267boost={false}268form={form}269onChange={onLicenseChange}270showExplanations={showExplanations}271configMode={configMode}272setConfigMode={setConfigMode}273preset={preset}274setPreset={setPreset}275presetAdjusted={presetAdjusted}276/>277{configMode === "expert" ? (278<IdleTimeout279showExplanations={showExplanations}280form={form}281onChange={onLicenseChange}282/>283) : undefined}284<TitleDescription showExplanations={showExplanations} form={form} />285</Form>286</div>287);288}289290291