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.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Create a new site license.
8
*/
9
import { Form, Input } from "antd";
10
import { isEmpty } from "lodash";
11
import { useEffect, useRef, useState } from "react";
12
13
import { Icon } from "@cocalc/frontend/components/icon";
14
import { get_local_storage } from "@cocalc/frontend/misc/local-storage";
15
import { CostInputPeriod } from "@cocalc/util/licenses/purchase/types";
16
import { computeCost } from "@cocalc/util/licenses/store/compute-cost";
17
import { Paragraph, Title } from "components/misc";
18
import A from "components/misc/A";
19
import Loading from "components/share/loading";
20
import SiteName from "components/share/site-name";
21
import apiPost from "lib/api/post";
22
import { MAX_WIDTH } from "lib/config";
23
import { useScrollY } from "lib/use-scroll-y";
24
import { useRouter } from "next/router";
25
import { AddBox } from "./add-box";
26
import { ApplyLicenseToProject } from "./apply-license-to-project";
27
import { InfoBar } from "./cost-info-bar";
28
import { IdleTimeout } from "./member-idletime";
29
import { QuotaConfig } from "./quota-config";
30
import { PRESETS, PRESET_MATCH_FIELDS, Preset } from "./quota-config-presets";
31
import { decodeFormValues, encodeFormValues } from "./quota-query-params";
32
import { Reset } from "./reset";
33
import { RunLimit } from "./run-limit";
34
import { SignInToPurchase } from "./sign-in-to-purchase";
35
import { TitleDescription } from "./title-description";
36
import { ToggleExplanations } from "./toggle-explanations";
37
import { UsageAndDuration } from "./usage-and-duration";
38
39
const DEFAULT_PRESET: Preset = "standard";
40
41
const STYLE: React.CSSProperties = {
42
marginTop: "15px",
43
maxWidth: MAX_WIDTH,
44
margin: "auto",
45
border: "1px solid #ddd",
46
padding: "15px",
47
} as const;
48
49
interface Props {
50
noAccount: boolean;
51
}
52
53
export default function SiteLicense(props: Props) {
54
const { noAccount } = props;
55
const router = useRouter();
56
const headerRef = useRef<HTMLHeadingElement>(null);
57
58
// most likely, user will go to the cart next
59
useEffect(() => {
60
router.prefetch("/store/cart");
61
}, []);
62
63
const [offsetHeader, setOffsetHeader] = useState(0);
64
const scrollY = useScrollY();
65
66
useEffect(() => {
67
if (headerRef.current) {
68
setOffsetHeader(headerRef.current.offsetTop);
69
}
70
}, []);
71
72
return (
73
<>
74
<Title level={3} ref={headerRef}>
75
<Icon name={"key"} style={{ marginRight: "5px" }} />{" "}
76
{router.query.id != null
77
? "Edit License in Shopping Cart"
78
: "Buy a License"}
79
</Title>
80
{router.query.id == null && (
81
<div>
82
<Paragraph style={{ fontSize: "12pt" }}>
83
<A href="https://doc.cocalc.com/licenses.html">
84
<SiteName /> licenses
85
</A>{" "}
86
allow you to upgrade any number of projects to run more quickly,
87
have network access, more disk space and memory. Licenses cover a
88
wide range of use cases, ranging from a single hobbyist project to
89
thousands of simultaneous users across a large organization.
90
</Paragraph>
91
92
<Paragraph style={{ fontSize: "12pt" }}>
93
Create a license using the form below then add it to your{" "}
94
<A href="/store/cart">shopping cart</A>. If you aren't sure exactly
95
what to buy, you can always edit your licenses later. Subscriptions
96
are also flexible and can be{" "}
97
<A
98
href="https://doc.cocalc.com/account/purchases.html#recent-updates-to-subscriptions"
99
external
100
>
101
edited at any time.{" "}
102
</A>
103
It is also possible to{" "}
104
<A href="https://doc.cocalc.com/vouchers.html">create vouchers</A>{" "}
105
for resale or distribution.
106
</Paragraph>
107
</div>
108
)}
109
<CreateSiteLicense
110
showInfoBar={scrollY > offsetHeader}
111
noAccount={noAccount}
112
/>
113
</>
114
);
115
}
116
117
// Note -- the back and forth between moment and Date below
118
// is a *workaround* because of some sort of bug in moment/antd/react.
119
120
function CreateSiteLicense({ showInfoBar = false, noAccount = false }) {
121
const [cost, setCost] = useState<CostInputPeriod | undefined>(undefined);
122
const [loading, setLoading] = useState<boolean>(false);
123
const [cartError, setCartError] = useState<string>("");
124
const [showExplanations, setShowExplanations] = useState<boolean>(true);
125
const [configMode, setConfigMode] = useState<"preset" | "expert">("preset");
126
const [form] = Form.useForm();
127
const router = useRouter();
128
129
const [preset, setPreset] = useState<Preset | null>(DEFAULT_PRESET);
130
const [presetAdjusted, setPresetAdjusted] = useState<boolean>(false);
131
132
/**
133
* Utility function to match current license configuration to a particular preset. If none is
134
* found, this function returns undefined.
135
*/
136
function findPreset() {
137
const currentConfiguration = form.getFieldsValue(
138
Object.keys(PRESET_MATCH_FIELDS),
139
);
140
141
let foundPreset: Preset | undefined;
142
143
Object.keys(PRESETS).some((p) => {
144
const presetMatches = Object.keys(PRESET_MATCH_FIELDS).every(
145
(formField) =>
146
PRESETS[p][formField] === currentConfiguration[formField],
147
);
148
149
if (presetMatches) {
150
foundPreset = p as Preset;
151
}
152
153
return presetMatches;
154
});
155
156
return foundPreset;
157
}
158
159
function onLicenseChange() {
160
const vals = form.getFieldsValue(true);
161
encodeFormValues(router, vals, "regular");
162
setCost(computeCost(vals));
163
164
const foundPreset = findPreset();
165
166
if (foundPreset) {
167
setPresetAdjusted(false);
168
setPreset(foundPreset);
169
} else {
170
setPresetAdjusted(true);
171
}
172
}
173
174
useEffect(() => {
175
const store_site_license_show_explanations = get_local_storage(
176
"store_site_license_show_explanations",
177
);
178
if (store_site_license_show_explanations != null) {
179
setShowExplanations(!!store_site_license_show_explanations);
180
}
181
182
const { id } = router.query;
183
if (!noAccount && id != null) {
184
// editing something in the shopping cart
185
(async () => {
186
try {
187
setLoading(true);
188
const item = await apiPost("/shopping/cart/get", { id });
189
if (item.product == "site-license") {
190
form.setFieldsValue({ ...item.description, type: "regular" });
191
}
192
} catch (err) {
193
setCartError(err.message);
194
} finally {
195
setLoading(false);
196
}
197
onLicenseChange();
198
})();
199
} else {
200
const vals = decodeFormValues(router, "regular");
201
const dflt = PRESETS[DEFAULT_PRESET];
202
if (isEmpty(vals)) {
203
form.setFieldsValue({
204
...dflt,
205
});
206
} else {
207
// we have to make sure cpu, mem and disk are set, otherwise there is no "cost"
208
form.setFieldsValue({
209
...dflt,
210
...vals,
211
});
212
}
213
}
214
onLicenseChange();
215
}, []);
216
217
if (loading) {
218
return <Loading large center />;
219
}
220
221
const addBox = (
222
<AddBox
223
cost={cost}
224
router={router}
225
form={form}
226
cartError={cartError}
227
setCartError={setCartError}
228
noAccount={noAccount}
229
/>
230
);
231
232
return (
233
<div>
234
<ApplyLicenseToProject router={router} />
235
<SignInToPurchase noAccount={noAccount} />
236
<InfoBar
237
show={showInfoBar}
238
cost={cost}
239
router={router}
240
form={form}
241
cartError={cartError}
242
setCartError={setCartError}
243
noAccount={noAccount}
244
/>
245
<Form
246
form={form}
247
style={STYLE}
248
name="basic"
249
labelCol={{ span: 3 }}
250
wrapperCol={{ span: 21 }}
251
autoComplete="off"
252
onValuesChange={onLicenseChange}
253
>
254
<Form.Item wrapperCol={{ offset: 0, span: 24 }}>{addBox}</Form.Item>
255
<ToggleExplanations
256
showExplanations={showExplanations}
257
setShowExplanations={setShowExplanations}
258
/>
259
{/* Hidden form item, used to disambiguate between boost and regular licenses */}
260
<Form.Item name="type" initialValue={"regular"} noStyle>
261
<Input type="hidden" />
262
</Form.Item>
263
<UsageAndDuration
264
showExplanations={showExplanations}
265
form={form}
266
onChange={onLicenseChange}
267
/>
268
<RunLimit
269
showExplanations={showExplanations}
270
form={form}
271
onChange={onLicenseChange}
272
/>
273
<QuotaConfig
274
boost={false}
275
form={form}
276
onChange={onLicenseChange}
277
showExplanations={showExplanations}
278
configMode={configMode}
279
setConfigMode={setConfigMode}
280
preset={preset}
281
setPreset={setPreset}
282
presetAdjusted={presetAdjusted}
283
/>
284
{configMode === "expert" ? (
285
<IdleTimeout
286
showExplanations={showExplanations}
287
form={form}
288
onChange={onLicenseChange}
289
/>
290
) : undefined}
291
<TitleDescription showExplanations={showExplanations} form={form} />
292
<Reset
293
addBox={addBox}
294
form={form}
295
onChange={onLicenseChange}
296
router={router}
297
/>
298
</Form>
299
</div>
300
);
301
}
302
303