CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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