Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/store/quota-query-params.ts
5911 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
import dayjs from "dayjs";
7
import { clamp, isDate } from "lodash";
8
import { NextRouter } from "next/router";
9
10
import { testDedicatedDiskNameBasic } from "@cocalc/util/licenses/check-disk-name-basics";
11
import { BOOST, REGULAR } from "@cocalc/util/upgrades/consts";
12
import {
13
DEDICATED_DISK_SIZES,
14
DEDICATED_DISK_SPEEDS,
15
DEFAULT_DEDICATED_DISK_SIZE,
16
DEFAULT_DEDICATED_DISK_SPEED,
17
DEFAULT_DEDICATED_VM_MACHINE,
18
PRICES,
19
} from "@cocalc/util/upgrades/dedicated";
20
import type { DateRange } from "@cocalc/util/upgrades/shopping";
21
import { MAX_ALLOWED_RUN_LIMIT } from "./run-limit";
22
// Various support functions for storing quota parameters as a query parameter in the browser URL
23
24
export function encodeRange(
25
vals: [Date | string | undefined, Date | string | undefined],
26
): string {
27
const [start, end] = vals;
28
if (start == null || end == null) {
29
return "";
30
}
31
try {
32
return `${new Date(start).toISOString()}_${new Date(end).toISOString()}`;
33
} catch {
34
// there are a LOT of values for start/end that would throw an error above, e.g., "undefined".
35
return "";
36
}
37
}
38
39
// the inverse of encodeRange
40
function decodeRange(val: string): DateRange {
41
if (!val) return [undefined, undefined];
42
const vals = val.split("_");
43
if (vals.length != 2) return [undefined, undefined];
44
const w: Date[] = [];
45
for (const x of vals) {
46
const d = dayjs(x);
47
if (d.isValid()) {
48
w.push(d.toDate());
49
} else {
50
return [undefined, undefined];
51
}
52
}
53
return w as DateRange;
54
}
55
56
const COMMON_FIELDS = [
57
"user",
58
"period",
59
"range",
60
"title",
61
"description",
62
] as const;
63
64
const REGULAR_FIELDS = [
65
...COMMON_FIELDS,
66
"run_limit",
67
"member",
68
"uptime",
69
"cpu",
70
"ram",
71
"disk",
72
] as const;
73
74
const DEDICATED_FIELDS = [
75
...COMMON_FIELDS,
76
"disk-size_gb",
77
"disk-name",
78
"disk-speed",
79
"vm-machine",
80
] as const;
81
82
function getFormFields(
83
type: "regular" | "boost" | "dedicated",
84
): readonly string[] {
85
switch (type) {
86
case "regular":
87
case "boost":
88
return REGULAR_FIELDS;
89
case "dedicated":
90
return DEDICATED_FIELDS;
91
}
92
}
93
94
export const ALL_FIELDS: Set<string> = new Set(
95
REGULAR_FIELDS.concat(DEDICATED_FIELDS as any).concat([
96
"type",
97
"source",
98
] as any),
99
);
100
101
// Global flag to prevent URL encoding during initial page load
102
let allowUrlEncoding = false;
103
104
export function setAllowUrlEncoding(allow: boolean) {
105
allowUrlEncoding = allow;
106
}
107
108
export function encodeFormValues(
109
router: NextRouter,
110
vals: any,
111
type: "regular" | "boost" | "dedicated",
112
): void {
113
if (!allowUrlEncoding) {
114
return;
115
}
116
const { query } = router;
117
for (const key in vals) {
118
if (!getFormFields(type).includes(key)) continue;
119
const val = vals[key];
120
if (val == null) {
121
delete query[key];
122
} else if (key === "range") {
123
query[key] = encodeRange(val);
124
} else {
125
query[key] = val;
126
}
127
}
128
router.replace({ query }, undefined, { shallow: true, scroll: false });
129
}
130
131
function decodeValue(val): boolean | number | string | DateRange {
132
if (val === "true") return true;
133
if (val === "false") return false;
134
const num = Number(val);
135
if (!isNaN(num)) return num;
136
return val;
137
}
138
139
function fixNumVal(
140
val: any,
141
param: { min: number; max: number; dflt: number },
142
): number {
143
if (typeof val !== "number") {
144
return param.dflt;
145
} else {
146
return clamp(val, param.min, param.max);
147
}
148
}
149
150
/** a query looks like this:
151
* user=academic&period=monthly&run_limit=1&member=true&uptime=short&cpu=1&ram=2&disk=3
152
*
153
* NOTE: the support for dedicated disk & vm does not work. the form is too complicated, not no need to support this yet.
154
*/
155
export function decodeFormValues(
156
router: NextRouter,
157
type: "regular" | "boost" | "dedicated",
158
): {
159
[key: string]: string | number | boolean;
160
} {
161
const P = type === "boost" ? BOOST : REGULAR;
162
const fields: readonly string[] = getFormFields(type);
163
164
const data = {};
165
for (const key in router.query) {
166
const val = router.query[key];
167
if (!fields.includes(key)) {
168
continue;
169
}
170
if (typeof val !== "string") {
171
// Handle non-string values by converting them to string first
172
const stringVal = String(val);
173
const decoded =
174
key === "range" ? decodeRange(stringVal) : decodeValue(stringVal);
175
data[key] = decoded;
176
continue;
177
}
178
const decoded = key === "range" ? decodeRange(val) : decodeValue(val);
179
data[key] = decoded;
180
}
181
182
// we also have to sanitize the values
183
for (const key in data) {
184
const val = data[key];
185
switch (key) {
186
case "user":
187
if (!["academic", "business"].includes(val)) {
188
data[key] = "academic";
189
}
190
break;
191
192
case "period":
193
if (!["monthly", "yearly", "range"].includes(val)) {
194
data[key] = "monthly";
195
}
196
break;
197
198
case "range":
199
// check that val is an array of length 2 and both entries are Date objects
200
if (!Array.isArray(val) || val.length !== 2 || !val.every(isDate)) {
201
data[key] = [undefined, undefined];
202
}
203
break;
204
205
case "run_limit":
206
// check that val is a number and in the range of 1 to 1000
207
if (typeof val !== "number" || val < 1 || val > MAX_ALLOWED_RUN_LIMIT) {
208
data[key] = 1;
209
}
210
break;
211
212
case "member":
213
if (typeof val !== "boolean") {
214
data[key] = true;
215
}
216
break;
217
218
case "uptime":
219
if (!["short", "medium", "day", "always_running"].includes(val)) {
220
data[key] = "short";
221
}
222
break;
223
224
case "cpu":
225
data[key] = fixNumVal(val, P.cpu);
226
break;
227
228
case "ram":
229
data[key] = fixNumVal(val, P.ram);
230
break;
231
232
case "disk":
233
data[key] = fixNumVal(val, P.disk);
234
break;
235
236
case "disk-size_gb":
237
if (typeof val !== "number" || !DEDICATED_DISK_SIZES.includes(val)) {
238
data[key] = DEFAULT_DEDICATED_DISK_SIZE;
239
}
240
break;
241
242
case "disk-name":
243
try {
244
testDedicatedDiskNameBasic(val);
245
} catch {
246
data[key] = "";
247
}
248
break;
249
250
case "disk-speed":
251
if (!DEDICATED_DISK_SPEEDS.includes(val)) {
252
data[key] = DEFAULT_DEDICATED_DISK_SPEED;
253
}
254
break;
255
256
case "vm-machine":
257
if (PRICES.vms[val] == null) {
258
data[key] = DEFAULT_DEDICATED_VM_MACHINE;
259
}
260
break;
261
262
case "title":
263
case "description":
264
data[key] = val;
265
break;
266
267
default:
268
console.log(`decodingFormValues: unknown key '${key}'`);
269
delete data[key];
270
}
271
}
272
273
// hosting quality vs. uptime restriction:
274
if (["always_running", "day"].includes(data["uptime"])) {
275
data["member"] = true;
276
}
277
278
if (type === "dedicated") {
279
data["type"] = data["vm-machine"] != null ? "vm" : null;
280
281
// if any key in data starts with "disk-" then set data["type"] to "disk"
282
if (data["type"] == null) {
283
for (const key in data) {
284
if (key.startsWith("disk-")) {
285
data["type"] = "disk";
286
break;
287
}
288
}
289
}
290
291
if (data["type"] === "disk") {
292
data["period"] = "monthly";
293
}
294
if (data["type"] === "vm") {
295
data["period"] = "range";
296
}
297
}
298
299
return data;
300
}
301
302