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