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/site-license-cost.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Icon } from "@cocalc/frontend/components/icon";6import { untangleUptime } from "@cocalc/util/consts/site-license";7import {8describeQuotaOnLine,9describe_quota,10} from "@cocalc/util/licenses/describe-quota";11import type {12CostInput,13CostInputPeriod,14PurchaseInfo,15Subscription,16} from "@cocalc/util/licenses/purchase/types";17import { money } from "@cocalc/util/licenses/purchase/utils";18import { plural, round2, round4, round2up } from "@cocalc/util/misc";19import { appendAfterNowToDate, getDays } from "@cocalc/util/stripe/timecalcs";20import {21dedicatedDiskDisplay,22dedicatedVmDisplay,23} from "@cocalc/util/upgrades/utils";24import Timestamp, { processTimestamp } from "components/misc/timestamp";25import { ReactNode } from "react";26import { useTimeFixer } from "./util";27import { Tooltip, Typography } from "antd";28import { currency } from "@cocalc/util/misc";29const { Text } = Typography;30import { periodicCost } from "@cocalc/util/licenses/purchase/compute-cost";3132interface Props {33cost: CostInputPeriod;34simple?: boolean;35oneLine?: boolean;36simpleShowPeriod?: boolean;37discountTooltip?: boolean;38noDiscount?: boolean;39}4041export function DisplayCost({42cost,43simple = false,44oneLine = false,45simpleShowPeriod = true,46}: Props) {47if (cost == null || isNaN(cost.cost)) {48return <>–</>;49}5051if (simple) {52return (53<>54{cost.cost_sub_first_period != null &&55cost.cost_sub_first_period != cost.cost && (56<>57{" "}58{money(round2up(cost.cost_sub_first_period))} due today, then59{oneLine ? <>, </> : <br />}60</>61)}62{money(round2up(periodicCost(cost)))}63{cost.period != "range" ? (64<>65{oneLine ? " " : <br />}66{simpleShowPeriod && cost.period}67</>68) : (69""70)}71{oneLine ? null : <br />}{" "}72</>73);74}75const desc = `${money(round2up(periodicCost(cost)))} ${76cost.period != "range" ? cost.period : ""77}`;7879return (80<span>81{describeItem({ info: cost.input })}82<hr />83<Icon name="money-check" /> Cost:{" "}84<Tooltip title={`$${round4(periodicCost(cost))}`}>{desc}</Tooltip>85</span>86);87}8889interface DescribeItemProps {90info: CostInput;91variant?: "short" | "long";92voucherPeriod?: boolean;93}9495// TODO: this should be a component. Rename it to DescribeItem and use it96// properly, e.g., <DescribeItem info={cost.input}/> above.9798export function describeItem({99info,100variant = "long",101voucherPeriod,102}: DescribeItemProps): ReactNode {103if (info.type == "cash-voucher") {104return <>{currency(info.amount)} account credit</>;105}106if (info.type === "disk") {107return (108<>109Dedicated Disk ({dedicatedDiskDisplay(info.dedicated_disk, variant)}){" "}110{describePeriod({ quota: info, variant, voucherPeriod })}111</>112);113}114115if (info.type === "vm") {116return (117<>118Dedicated VM ({dedicatedVmDisplay(info.dedicated_vm)}){" "}119{describePeriod({ quota: info, variant, voucherPeriod })}120</>121);122}123124if (info.type !== "quota") {125throw Error("at this point, we only deal with type=quota");126}127128if (info.quantity == null) {129throw new Error("should not happen");130}131132const { always_running, idle_timeout } = untangleUptime(133info.custom_uptime ?? "short",134);135136const quota = {137ram: info.custom_ram,138cpu: info.custom_cpu,139disk: info.custom_disk,140always_running,141idle_timeout,142member: info.custom_member,143user: info.user,144};145146if (variant === "short") {147return (148<>149<Text strong={true}>{describeQuantity({ quota: info, variant })}</Text>{" "}150{describeQuotaOnLine(quota)},{" "}151{describePeriod({ quota: info, variant, voucherPeriod })}152</>153);154} else {155return (156<>157{describe_quota(quota, false)}{" "}158{describeQuantity({ quota: info, variant })} (159{describePeriod({ quota: info, variant, voucherPeriod })})160</>161);162}163}164165interface DescribeQuantityProps {166quota: Partial<PurchaseInfo>;167variant?: "short" | "long";168}169170function describeQuantity(props: DescribeQuantityProps): ReactNode {171const { quota: info, variant = "long" } = props;172const { quantity = 1 } = info;173174if (variant === "short") {175return `${quantity}x`;176} else {177return `for ${quantity} running ${plural(quantity, "project")}`;178}179}180181interface PeriodProps {182quota: {183subscription?: Omit<Subscription, "no">;184start?: Date | string | null;185end?: Date | string | null;186};187variant?: "short" | "long";188// voucherPeriod: description used for a voucher -- just give number of days, since the exact dates themselves are discarded.189voucherPeriod?: boolean;190}191192/**193* ATTN: this is not a general purpose period description generator. It's very specific194* to the purchases in the store!195*/196export function describePeriod({197quota,198variant = "long",199voucherPeriod,200}: PeriodProps): ReactNode {201const { subscription, start: startRaw, end: endRaw } = quota;202203const { fromServerTime, serverTimeDate } = useTimeFixer();204205if (subscription == "no") {206if (startRaw == null || endRaw == null)207throw new Error(`start date not set!`);208const start = fromServerTime(startRaw);209const end = fromServerTime(endRaw);210211if (start == null || end == null) {212throw new Error(`this should never happen`);213}214215// days are calculated based on the actual selection216const days = round2(getDays({ start, end }));217218if (voucherPeriod) {219return (220<>221license lasts {days} {plural(days, "day")}222</>223);224}225226// but the displayed end mimics what will happen later on the backend227// i.e. if the day already started, we append the already elapsed period to the end228const endDisplay = appendAfterNowToDate({229now: serverTimeDate,230start,231end,232});233234if (variant === "short") {235const tsStart = processTimestamp({ datetime: start, absolute: true });236const tsEnd = processTimestamp({ datetime: endDisplay, absolute: true });237if (tsStart === "-" || tsEnd === "-") {238return "-";239}240const timespanStr = `${tsStart.absoluteTimeFull} - ${tsEnd.absoluteTimeFull}`;241return (242<Tooltip243trigger={["hover", "click"]}244title={timespanStr}245placement="bottom"246>247{`${days} ${plural(days, "day")}`}248</Tooltip>249);250} else {251return (252<>253<Timestamp datetime={start} absolute /> to{" "}254<Timestamp datetime={endDisplay} absolute />, {days}{" "}255{plural(days, "day")}256</>257);258}259} else {260if (variant === "short") {261return `${subscription}`;262} else {263return `${subscription} subscription`;264}265}266}267268269