"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const jsx_runtime_1 = require("react/jsx-runtime");
/*
 *  This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.
 *  License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
 */
/*
Create a new site license.
*/
const icon_1 = require("@cocalc/frontend/components/icon");
const local_storage_1 = require("@cocalc/frontend/misc/local-storage");
const dedicated_1 = require("@cocalc/util/consts/dedicated");
const project_1 = require("@cocalc/util/consts/project");
const check_disk_name_basics_1 = require("@cocalc/util/licenses/check-disk-name-basics");
const utils_1 = require("@cocalc/util/licenses/purchase/utils");
const dedicated_2 = require("@cocalc/util/types/dedicated");
const dedicated_3 = require("@cocalc/util/upgrades/dedicated");
const antd_1 = require("antd");
const A_1 = __importDefault(require("components/misc/A"));
const integer_slider_1 = __importDefault(require("components/misc/integer-slider"));
const loading_1 = __importDefault(require("components/share/loading"));
const site_name_1 = __importDefault(require("components/share/site-name"));
const post_1 = __importDefault(require("lib/api/post"));
const use_scroll_y_1 = require("lib/use-scroll-y");
const lodash_1 = require("lodash");
const router_1 = require("next/router");
const react_1 = require("react");
const add_box_1 = require("./add-box");
const apply_license_to_project_1 = require("./apply-license-to-project");
const compute_cost_1 = require("./compute-cost");
const cost_info_bar_1 = require("./cost-info-bar");
const sign_in_to_purchase_1 = require("./sign-in-to-purchase");
const title_description_1 = require("./title-description");
const toggle_explanations_1 = require("./toggle-explanations");
const usage_and_duration_1 = require("./usage-and-duration");
const util_1 = require("./util");
const { Text } = antd_1.Typography;
const GCP_DISK_URL = "https://cloud.google.com/compute/docs/disks/performance#performance_by_disk_size";
function DedicatedResource(props) {
    const { noAccount } = props;
    const router = (0, router_1.useRouter)();
    const headerRef = (0, react_1.useRef)(null);
    // most likely, user will go to the cart next
    (0, react_1.useEffect)(() => {
        router.prefetch("/store/cart");
    }, []);
    const [offsetHeader, setOffsetHeader] = (0, react_1.useState)(0);
    const scrollY = (0, use_scroll_y_1.useScrollY)();
    (0, react_1.useEffect)(() => {
        if (headerRef.current) {
            setOffsetHeader(headerRef.current.offsetTop);
        }
    }, []);
    return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("h3", { ref: headerRef, children: [(0, jsx_runtime_1.jsx)(icon_1.Icon, { name: "dedicated", style: { marginRight: "5px" } }), " ", router.query.id != null
                        ? "Edit Dedicated Resources License in Shopping Cart"
                        : "Buy a Dedicated Resources License"] }), router.query.id == null && ((0, jsx_runtime_1.jsxs)("p", { children: ["A", " ", (0, jsx_runtime_1.jsxs)(A_1.default, { href: "https://doc.cocalc.com/licenses.html", children: [(0, jsx_runtime_1.jsx)(site_name_1.default, {}), " dedicated resource license"] }), " ", "can be used to outfit your project either with additional disk storage or moves your project to a much more powerful virtual machine. Create a dedicated resources license below then add it to your", " ", (0, jsx_runtime_1.jsx)(A_1.default, { href: "/store/cart", children: "shopping cart" }), "."] })), (0, jsx_runtime_1.jsx)(CreateDedicatedResource, { showInfoBar: scrollY > offsetHeader, noAccount: noAccount })] }));
}
exports.default = DedicatedResource;
function CreateDedicatedResource({ showInfoBar = false, noAccount = false }) {
    // somehow this state is necessary to render the form properly
    const [formType, setFormType] = (0, react_1.useState)(null);
    const [cost, setCost] = (0, react_1.useState)(undefined);
    const [loading, setLoading] = (0, react_1.useState)(false);
    const [cartError, setCartError] = (0, react_1.useState)("");
    const [showExplanations, setShowExplanations] = (0, react_1.useState)(true);
    const [durationTypes, setDdurationTypes] = (0, react_1.useState)("monthly");
    const [vmMachine, setVmMachine] = (0, react_1.useState)(null);
    const [diskNameValid, setDiskNameValid] = (0, react_1.useState)(false);
    const [showInfo, setShowInfo] = (0, react_1.useState)(false);
    const [form] = antd_1.Form.useForm();
    const router = (0, router_1.useRouter)();
    // most likely, user will go to the cart next
    (0, react_1.useEffect)(() => {
        router.prefetch("/store/cart");
    }, []);
    function fixupDuration() {
        switch (form.getFieldValue("type")) {
            case "disk":
                setDdurationTypes("monthly");
                if (form.getFieldValue("period") === "range") {
                    form.setFieldsValue({ period: "monthly" });
                }
                break;
            case "vm":
                setDdurationTypes("range");
                if (form.getFieldValue("period") !== "range") {
                    form.setFieldsValue({ period: "range" });
                }
                break;
        }
    }
    function calcCost() {
        const data = form.getFieldsValue(true);
        try {
            switch (data.type) {
                case "disk":
                    const size_gb = data["disk-size_gb"];
                    const speed = data["disk-speed"];
                    if (size_gb == null || speed == null) {
                        return; // no data to compute price
                    }
                    setCost((0, compute_cost_1.computeCost)({
                        type: "disk",
                        period: "monthly",
                        dedicated_disk: {
                            speed,
                            size_gb,
                            name: data["disk-name"],
                        },
                    }));
                    break;
                case "vm":
                    setCost((0, compute_cost_1.computeCost)({
                        type: "vm",
                        period: "range",
                        range: data.range,
                        dedicated_vm: {
                            machine: data["vm-machine"],
                        },
                    }));
                    break;
            }
        }
        catch (err) {
            setCost(undefined);
        }
    }
    function onChange() {
        fixupDuration();
        calcCost();
    }
    async function loadItem(item) {
        if (item.product !== "site-license") {
            throw new Error("not a site license");
        }
        const type = (0, util_1.getType)(item);
        if (type !== "disk" && type !== "vm") {
            throw new Error(`cannot deal with type ${type}`);
        }
        const conf = item.description;
        // restoring name/description
        form.setFieldsValue({
            title: conf.title,
            description: conf.description,
        });
        switch (type) {
            case "disk":
                const d = conf.dedicated_disk;
                form.setFieldsValue({
                    type,
                    "disk-size_gb": d.size_gb,
                    "disk-speed": d.type,
                    "disk-name": d.name,
                });
                // we have to re-validate the disk name, b/c name could be taken in the meantime
                // just calling the form to revalidate does not work.
                try {
                    await testDedicatedDiskName(d.name);
                    setDiskNameValid(true);
                }
                catch (err) {
                    setDiskNameValid(false);
                }
                break;
            case "vm":
                const vm = conf.dedicated_vm?.machine;
                if (dedicated_3.PRICES.vms[vm] == null) {
                    console.warn(`VM type ${vm} not found`);
                }
                else {
                    form.setFieldsValue({
                        "vm-machine": vm,
                    });
                }
                form.setFieldsValue({
                    type,
                    range: (0, util_1.loadDateRange)(conf.range),
                });
                break;
        }
        // unpacking and configuring the form worked, now we do the type selection to show it
        setFormType(type);
    }
    (0, react_1.useEffect)(() => {
        const store_site_license_show_explanations = (0, local_storage_1.get_local_storage)("store_site_license_show_explanations");
        if (store_site_license_show_explanations != null) {
            setShowExplanations(!!store_site_license_show_explanations);
        }
        const { id } = router.query;
        if (!noAccount && id != null) {
            // editing something in the shopping cart
            (async () => {
                try {
                    setLoading(true);
                    const item = await (0, post_1.default)("/shopping/cart/get", { id });
                    await loadItem(item);
                }
                catch (err) {
                    setCartError(err.message);
                }
                finally {
                    setLoading(false);
                }
                onChange();
            })();
        }
        onChange();
    }, []);
    (0, react_1.useEffect)(() => {
        const { type } = router.query;
        if (typeof type === "string") {
            setType(type);
        }
    }, []);
    (0, react_1.useEffect)(() => {
        form.validateFields();
    }, [form.getFieldValue("type")]);
    if (loading) {
        return (0, jsx_runtime_1.jsx)(loading_1.default, { large: true, center: true });
    }
    function setType(type) {
        if (type === "vm" || type === "disk") {
            form.resetFields();
            form.setFieldsValue({ type });
            setFormType(type);
            setCost(undefined);
            setCartError("");
            onChange();
        }
        else {
            console.log(`unable to setType to ${type}`);
        }
    }
    function renderTypeSelection() {
        return ((0, jsx_runtime_1.jsx)(antd_1.Form.Item, { name: "type", label: "Dedicated", rules: [{ required: true, message: "Please select a type" }], extra: showExplanations && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: "Select if you want to get a Dedicate Disk or a Virtual Machine." })), children: (0, jsx_runtime_1.jsxs)(antd_1.Radio.Group, { onChange: (e) => {
                    // Clear error whenever changing this selection to something.
                    // See comment in validateDedicatedDiskName about how this
                    // isn't great.
                    setCartError("");
                    setType(e.target.value);
                }, children: [(0, jsx_runtime_1.jsx)(antd_1.Radio.Button, { value: "disk", children: "Disk" }, "disk"), (0, jsx_runtime_1.jsx)(antd_1.Radio.Button, { value: "vm", children: "Virtual Machine" }, "vm")] }) }));
    }
    function renderAdditionalInfoContent() {
        switch (formType) {
            case "disk":
                return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(antd_1.Typography.Paragraph, { ellipsis: {
                                expandable: true,
                                rows: 2,
                                symbol: "more",
                                onExpand: () => setShowInfo(true),
                            }, children: ["This license attaches a disk to your project. When the license is valid and activated by adding to a project, a disk will be created on the fly. It will be formatted and mounted into your project. You'll be able to access it via a symlink in your project's home directory \u2013 i.e. ", (0, jsx_runtime_1.jsxs)("code", { children: ["~/", dedicated_1.HOME_PREFIX, "/<name>"] }), " will be pointing to ", (0, jsx_runtime_1.jsxs)("code", { children: [dedicated_1.ROOT, "/<name>"] }), "."] }), (0, jsx_runtime_1.jsxs)(antd_1.Typography.Paragraph, { style: { display: showInfo ? "" : "none" }, children: ["Once you cancel the subscription, the subscription will end at the end of the billing period. Then, the disk and all the data it contains ", (0, jsx_runtime_1.jsx)("strong", { children: "will be deleted" }), "!"] }), (0, jsx_runtime_1.jsx)(antd_1.Typography.Paragraph, { style: { display: showInfo ? "" : "none" }, children: "It's also possible to move a disk from one project to another one. First, remove the license from the project, restart the project to unmount the disk. Then, add the license to another project and restart that project as well." }), (0, jsx_runtime_1.jsxs)(antd_1.Typography.Paragraph, { style: { display: showInfo ? "" : "none" }, children: ["Note: it is also possible to mount external data storage to a project:", " ", (0, jsx_runtime_1.jsx)(A_1.default, { href: project_1.DOC_CLOUD_STORAGE_URL, children: "cloud storage & remote file systems" }), ". This could help transferring data in and out of ", (0, jsx_runtime_1.jsx)(site_name_1.default, {}), "."] })] }));
            case "vm":
                return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(antd_1.Typography.Paragraph, { ellipsis: {
                                expandable: true,
                                rows: 2,
                                symbol: "more",
                                onExpand: () => setShowInfo(true),
                            }, children: ["For the specified period of time, a virtual machine is provisioned and started inside of ", (0, jsx_runtime_1.jsx)(site_name_1.default, {}), "'s cluster. You have to add the license to one of your projects in order to tell it to move to this virtual machine. This happens when the project is started or restarted."] }), (0, jsx_runtime_1.jsx)(antd_1.Typography.Paragraph, { style: { display: showInfo ? "" : "none" }, children: "Once your project has moved over, the usual quota upgrades will be ineffective \u2013 instead, your project runs with the quota limits implied by the performance of the underlying virtual machine. The files/data in your project will be exactly the same as before." }), (0, jsx_runtime_1.jsx)(antd_1.Typography.Paragraph, { style: { display: showInfo ? "" : "none" }, children: "Once the license period is over, the virtual machine will be shut down. At that point your project will be stopped as well. The next time it starts, it will run under the usual quota regime on a shared node in the cluster." })] }));
        }
    }
    function renderAdditionalInfo() {
        return ((0, jsx_runtime_1.jsx)(antd_1.Form.Item, { label: "How does it work?", children: (0, jsx_runtime_1.jsx)("div", { style: { paddingTop: "5px" }, children: renderAdditionalInfoContent() }) }));
    }
    function renderDurationExplanation() {
        if (!showExplanations)
            return;
        switch (durationTypes) {
            case "monthly":
                return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: "Currently, disk can be only be rented on a monthly basis only. Note: you can cancel the subscription any time and at the end of the billing period the disk \u2013 and the data it holds \u2013 will be destroyed." }));
            case "range":
                return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: "Dedicated VMs can only be rented for a specific period of time. At its end, the node will be stopped and removed, and your project moves back to the usual upgrade schema." }));
        }
    }
    function renderUsageAndDuration() {
        return ((0, jsx_runtime_1.jsx)(usage_and_duration_1.UsageAndDuration, { extraDuration: renderDurationExplanation(), form: form, onChange: onChange, showUsage: false, duration: durationTypes, discount: false }));
    }
    async function testDedicatedDiskName(name) {
        (0, check_disk_name_basics_1.testDedicatedDiskNameBasic)(name);
        // if the above passes, then we can check if the name is available.
        const serverCheck = await (0, post_1.default)("licenses/check-disk-name", { name }, 60);
        if (serverCheck?.available === true) {
            return;
        }
        else {
            throw new Error("Please choose a different disk name.");
        }
    }
    /**
     * The disk name will get a prefix like "kucalc-[cluster id]-pd-[namespace]-dedicated-..."
     * It's impossible to know the prefix, since the properties of the cluster can change.
     * The maximum total length of the disk name is 63, according to the GCE documentation.
     * https://cloud.google.com/compute/docs/naming-resources#resource-name-format
     * I hope a max length of 20 is sufficiently restrictive.
     */
    function validateDedicatedDiskName() {
        return {
            validator: async (_, name) => {
                try {
                    await testDedicatedDiskName(name);
                    setDiskNameValid(true);
                    // WARNING! This is obviously not good code in general, since we're clearing all
                    // errors if the disk name happens to be valid.
                    // It's OK for now since this is the only field we do validation with, and
                    // any other error would  be, e.g., in submission of the form to the backend.
                    setCartError("");
                }
                catch (err) {
                    setCartError(err.message);
                    setDiskNameValid(false);
                    throw err;
                }
            },
        };
    }
    function renderDedicatedDiskInfo() {
        if (!showExplanations)
            return;
        return ((0, jsx_runtime_1.jsxs)("p", { children: ["More information about Dedicated Disks can be found at", " ", (0, jsx_runtime_1.jsx)(A_1.default, { href: GCP_DISK_URL, children: "GCP: Performance by disk size" }), "."] }));
    }
    function renderDiskPerformance() {
        const size_gb = form.getFieldValue("disk-size_gb");
        const speed = form.getFieldValue("disk-speed");
        if (size_gb == null || speed == null)
            return;
        const diskID = (0, dedicated_3.getDedicatedDiskKey)({ size_gb, speed });
        const di = dedicated_3.PRICES.disks[diskID];
        if (di == null) {
            return ((0, jsx_runtime_1.jsxs)("p", { style: { marginTop: "5px" }, children: ["Unknown disk with ID ", (0, jsx_runtime_1.jsx)("code", { children: diskID }), "."] }));
        }
        return ((0, jsx_runtime_1.jsxs)("p", { style: { marginTop: "5px" }, children: ["Estimated speed: ", di.mbps, " MB/s sustained throughput and ", di.iops, " IOPS read/write. For more detailed information:", " ", (0, jsx_runtime_1.jsx)(A_1.default, { href: GCP_DISK_URL, children: "GCP disk performance" }), " information."] }));
    }
    function renderDiskExtra() {
        if (!showExplanations)
            return;
        const formName = form.getFieldValue("disk-name");
        const name = formName ? formName : (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: "<name>" });
        return ((0, jsx_runtime_1.jsxs)("p", { children: ["Give your disk a name. It must be unique and will be used as part of the directory name. The mount point will be", " ", (0, jsx_runtime_1.jsxs)("code", { children: [dedicated_1.ROOT, "/", name] }), " ", "and if the name isn't already taken. For your convenience, if possible there will be a symlink named", " ", (0, jsx_runtime_1.jsxs)("code", { children: ["~/", dedicated_1.HOME_PREFIX, "/", name] }), " ", "pointing from your home directory to your disk for your convenience."] }));
    }
    // ATTN: the IntegerSlider must be kept in sync with DEDICATED_DISK_SIZES in
    // src/packages/util/upgrades/dedicated.ts
    function renderDedicatedDisk() {
        return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(antd_1.Form.Item, { name: "disk-name", label: "Name", hasFeedback: true, extra: renderDiskExtra(), rules: [validateDedicatedDiskName], children: (0, jsx_runtime_1.jsx)(antd_1.Input, { style: { width: "15em" } }) }), (0, jsx_runtime_1.jsx)(antd_1.Form.Item, { label: "Size", name: "disk-size_gb", initialValue: dedicated_3.DEFAULT_DEDICATED_DISK_SIZE, extra: showExplanations && (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: "Select the size of the dedicated disk." }), children: (0, jsx_runtime_1.jsx)(integer_slider_1.default, { min: dedicated_3.MIN_DEDICATED_DISK_SIZE, max: dedicated_3.MAX_DEDICATED_DISK_SIZE, step: dedicated_3.DEDICATED_DISK_SIZE_INCREMENT, onChange: (val) => {
                            form.setFieldsValue({ "disk-size_gb": val });
                            onChange();
                        }, units: "G", presets: [32, 64, 128, 256, 512, 1024] }) }), (0, jsx_runtime_1.jsx)(antd_1.Form.Item, { name: "disk-speed", label: "Speed", initialValue: dedicated_3.DEFAULT_DEDICATED_DISK_SPEED, extra: renderDedicatedDiskInfo(), children: (0, jsx_runtime_1.jsx)(antd_1.Radio.Group, { onChange: (e) => {
                            form.setFieldsValue({ "disk-speed": e.target.value });
                            onChange();
                        }, children: dedicated_2.DedicatedDiskSpeedNames.map((type) => ((0, jsx_runtime_1.jsx)(antd_1.Radio.Button, { value: type, children: dedicated_2.DISK_NAMES[type] }, type))) }) }), (0, jsx_runtime_1.jsx)(antd_1.Form.Item, { label: "Performance", children: renderDiskPerformance() })] }));
    }
    function renderDedicatedVmInfo() {
        if (!showExplanations)
            return;
        return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["More information about VM types can be found at", " ", (0, jsx_runtime_1.jsx)(A_1.default, { href: "https://cloud.google.com/compute/docs/machine-types", children: "GCP: machine families" }), "."] }));
    }
    function renderVmPerformance() {
        if (vmMachine == null)
            return;
        const { spec } = dedicated_3.PRICES.vms?.[vmMachine] ?? {};
        if (spec == null) {
            return ((0, jsx_runtime_1.jsxs)("p", { children: ["Problem: the specifications of ", (0, jsx_runtime_1.jsx)("code", { children: vmMachine }), " are not known"] }));
        }
        return ((0, jsx_runtime_1.jsxs)("p", { children: ["Restarting your project while this license is active, will move your project on a virtual machine in ", (0, jsx_runtime_1.jsx)(site_name_1.default, {}), "'s cluster. This machine will allow you to use up to ", spec.cpu, " CPU cores and ", spec.mem, " G memory."] }));
    }
    function dedicatedVmOptions() {
        return (0, lodash_1.sortBy)(Object.entries(dedicated_3.PRICES.vms), ([_, vm]) => `${1000 + vm.spec.cpu}:${1000 + vm.spec.mem}`).map(([id, vm]) => {
            return ((0, jsx_runtime_1.jsxs)(antd_1.Select.Option, { value: id, children: [(0, jsx_runtime_1.jsx)(Text, { children: vm.title ?? vm.spec }), (0, jsx_runtime_1.jsxs)(Text, { style: { paddingLeft: "1em" }, type: "secondary", children: ["(", (0, utils_1.money)(vm.price_day), " per day)"] })] }, id));
        });
    }
    function renderDedicatedVM() {
        return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(antd_1.Form.Item, { label: "Type", name: "vm-machine", initialValue: null, extra: renderDedicatedVmInfo(), rules: [{ required: true, message: "Please select a VM type." }], children: (0, jsx_runtime_1.jsx)(antd_1.Select, { onChange: (val) => {
                            form.setFieldsValue({ "vm-machine": val });
                            setVmMachine(val);
                            onChange();
                        }, children: dedicatedVmOptions() }) }), (0, jsx_runtime_1.jsx)(antd_1.Form.Item, { label: "Performance", children: (0, jsx_runtime_1.jsx)("div", { style: { paddingTop: "5px" }, children: renderVmPerformance() }) })] }));
    }
    function renderConfiguration() {
        switch (formType) {
            case "disk":
                return renderDedicatedDisk();
            case "vm":
                return renderDedicatedVM();
        }
    }
    function renderCost() {
        const input = cost?.input;
        const disabled = cost == null ||
            input == null ||
            (input.type === "vm" && (input.start == null || input.end == null)) ||
            (input.type === "disk" && !diskNameValid);
        return ((0, jsx_runtime_1.jsx)(antd_1.Form.Item, { wrapperCol: { offset: 0, span: 24 }, children: (0, jsx_runtime_1.jsx)(add_box_1.AddBox, { cost: cost, form: form, cartError: cartError, setCartError: setCartError, router: router, dedicatedItem: true, disabled: disabled, noAccount: noAccount }) }));
    }
    return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(cost_info_bar_1.InfoBar, { show: showInfoBar, cost: cost, router: router, form: form, cartError: cartError, setCartError: setCartError, noAccount: noAccount }), (0, jsx_runtime_1.jsx)(apply_license_to_project_1.ApplyLicenseToProject, { router: router }), (0, jsx_runtime_1.jsx)(sign_in_to_purchase_1.SignInToPurchase, { noAccount: noAccount }), (0, jsx_runtime_1.jsxs)(antd_1.Form, { form: form, style: {
                    marginTop: "15px",
                    margin: "auto",
                    border: "1px solid #ddd",
                    padding: "15px",
                }, name: "basic", labelCol: { span: 6 }, wrapperCol: { span: 18 }, autoComplete: "off", onValuesChange: onChange, children: [(0, jsx_runtime_1.jsx)(toggle_explanations_1.ToggleExplanations, { showExplanations: showExplanations, setShowExplanations: setShowExplanations }), renderTypeSelection(), formType != null && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [renderAdditionalInfo(), renderUsageAndDuration(), (0, jsx_runtime_1.jsx)(antd_1.Divider, { plain: true, children: "Confguration" }), renderConfiguration(), (0, jsx_runtime_1.jsx)(title_description_1.TitleDescription, { showExplanations: showExplanations }), renderCost()] }))] })] }));
}
//# sourceMappingURL=dedicated.js.map