Path: blob/master/src/packages/next/components/store/quota-query-params.ts
5911 views
/*1* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import dayjs from "dayjs";6import { clamp, isDate } from "lodash";7import { NextRouter } from "next/router";89import { testDedicatedDiskNameBasic } from "@cocalc/util/licenses/check-disk-name-basics";10import { BOOST, REGULAR } from "@cocalc/util/upgrades/consts";11import {12DEDICATED_DISK_SIZES,13DEDICATED_DISK_SPEEDS,14DEFAULT_DEDICATED_DISK_SIZE,15DEFAULT_DEDICATED_DISK_SPEED,16DEFAULT_DEDICATED_VM_MACHINE,17PRICES,18} from "@cocalc/util/upgrades/dedicated";19import type { DateRange } from "@cocalc/util/upgrades/shopping";20import { MAX_ALLOWED_RUN_LIMIT } from "./run-limit";21// Various support functions for storing quota parameters as a query parameter in the browser URL2223export function encodeRange(24vals: [Date | string | undefined, Date | string | undefined],25): string {26const [start, end] = vals;27if (start == null || end == null) {28return "";29}30try {31return `${new Date(start).toISOString()}_${new Date(end).toISOString()}`;32} catch {33// there are a LOT of values for start/end that would throw an error above, e.g., "undefined".34return "";35}36}3738// the inverse of encodeRange39function decodeRange(val: string): DateRange {40if (!val) return [undefined, undefined];41const vals = val.split("_");42if (vals.length != 2) return [undefined, undefined];43const w: Date[] = [];44for (const x of vals) {45const d = dayjs(x);46if (d.isValid()) {47w.push(d.toDate());48} else {49return [undefined, undefined];50}51}52return w as DateRange;53}5455const COMMON_FIELDS = [56"user",57"period",58"range",59"title",60"description",61] as const;6263const REGULAR_FIELDS = [64...COMMON_FIELDS,65"run_limit",66"member",67"uptime",68"cpu",69"ram",70"disk",71] as const;7273const DEDICATED_FIELDS = [74...COMMON_FIELDS,75"disk-size_gb",76"disk-name",77"disk-speed",78"vm-machine",79] as const;8081function getFormFields(82type: "regular" | "boost" | "dedicated",83): readonly string[] {84switch (type) {85case "regular":86case "boost":87return REGULAR_FIELDS;88case "dedicated":89return DEDICATED_FIELDS;90}91}9293export const ALL_FIELDS: Set<string> = new Set(94REGULAR_FIELDS.concat(DEDICATED_FIELDS as any).concat([95"type",96"source",97] as any),98);99100// Global flag to prevent URL encoding during initial page load101let allowUrlEncoding = false;102103export function setAllowUrlEncoding(allow: boolean) {104allowUrlEncoding = allow;105}106107export function encodeFormValues(108router: NextRouter,109vals: any,110type: "regular" | "boost" | "dedicated",111): void {112if (!allowUrlEncoding) {113return;114}115const { query } = router;116for (const key in vals) {117if (!getFormFields(type).includes(key)) continue;118const val = vals[key];119if (val == null) {120delete query[key];121} else if (key === "range") {122query[key] = encodeRange(val);123} else {124query[key] = val;125}126}127router.replace({ query }, undefined, { shallow: true, scroll: false });128}129130function decodeValue(val): boolean | number | string | DateRange {131if (val === "true") return true;132if (val === "false") return false;133const num = Number(val);134if (!isNaN(num)) return num;135return val;136}137138function fixNumVal(139val: any,140param: { min: number; max: number; dflt: number },141): number {142if (typeof val !== "number") {143return param.dflt;144} else {145return clamp(val, param.min, param.max);146}147}148149/** a query looks like this:150* user=academic&period=monthly&run_limit=1&member=true&uptime=short&cpu=1&ram=2&disk=3151*152* NOTE: the support for dedicated disk & vm does not work. the form is too complicated, not no need to support this yet.153*/154export function decodeFormValues(155router: NextRouter,156type: "regular" | "boost" | "dedicated",157): {158[key: string]: string | number | boolean;159} {160const P = type === "boost" ? BOOST : REGULAR;161const fields: readonly string[] = getFormFields(type);162163const data = {};164for (const key in router.query) {165const val = router.query[key];166if (!fields.includes(key)) {167continue;168}169if (typeof val !== "string") {170// Handle non-string values by converting them to string first171const stringVal = String(val);172const decoded =173key === "range" ? decodeRange(stringVal) : decodeValue(stringVal);174data[key] = decoded;175continue;176}177const decoded = key === "range" ? decodeRange(val) : decodeValue(val);178data[key] = decoded;179}180181// we also have to sanitize the values182for (const key in data) {183const val = data[key];184switch (key) {185case "user":186if (!["academic", "business"].includes(val)) {187data[key] = "academic";188}189break;190191case "period":192if (!["monthly", "yearly", "range"].includes(val)) {193data[key] = "monthly";194}195break;196197case "range":198// check that val is an array of length 2 and both entries are Date objects199if (!Array.isArray(val) || val.length !== 2 || !val.every(isDate)) {200data[key] = [undefined, undefined];201}202break;203204case "run_limit":205// check that val is a number and in the range of 1 to 1000206if (typeof val !== "number" || val < 1 || val > MAX_ALLOWED_RUN_LIMIT) {207data[key] = 1;208}209break;210211case "member":212if (typeof val !== "boolean") {213data[key] = true;214}215break;216217case "uptime":218if (!["short", "medium", "day", "always_running"].includes(val)) {219data[key] = "short";220}221break;222223case "cpu":224data[key] = fixNumVal(val, P.cpu);225break;226227case "ram":228data[key] = fixNumVal(val, P.ram);229break;230231case "disk":232data[key] = fixNumVal(val, P.disk);233break;234235case "disk-size_gb":236if (typeof val !== "number" || !DEDICATED_DISK_SIZES.includes(val)) {237data[key] = DEFAULT_DEDICATED_DISK_SIZE;238}239break;240241case "disk-name":242try {243testDedicatedDiskNameBasic(val);244} catch {245data[key] = "";246}247break;248249case "disk-speed":250if (!DEDICATED_DISK_SPEEDS.includes(val)) {251data[key] = DEFAULT_DEDICATED_DISK_SPEED;252}253break;254255case "vm-machine":256if (PRICES.vms[val] == null) {257data[key] = DEFAULT_DEDICATED_VM_MACHINE;258}259break;260261case "title":262case "description":263data[key] = val;264break;265266default:267console.log(`decodingFormValues: unknown key '${key}'`);268delete data[key];269}270}271272// hosting quality vs. uptime restriction:273if (["always_running", "day"].includes(data["uptime"])) {274data["member"] = true;275}276277if (type === "dedicated") {278data["type"] = data["vm-machine"] != null ? "vm" : null;279280// if any key in data starts with "disk-" then set data["type"] to "disk"281if (data["type"] == null) {282for (const key in data) {283if (key.startsWith("disk-")) {284data["type"] = "disk";285break;286}287}288}289290if (data["type"] === "disk") {291data["period"] = "monthly";292}293if (data["type"] === "vm") {294data["period"] = "range";295}296}297298return data;299}300301302