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