Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/compute/create-compute-server.tsx
Views: 687
import { Button, Modal, Spin } from "antd";1import { delay } from "awaiting";2import { cloneDeep } from "lodash";3import { useEffect, useState } from "react";45import { redux, useRedux, useTypedRedux } from "@cocalc/frontend/app-framework";6import ShowError from "@cocalc/frontend/components/error";7import { Icon } from "@cocalc/frontend/components/icon";8import PublicTemplates from "@cocalc/frontend/compute/public-templates";9import { CancelText } from "@cocalc/frontend/i18n/components";10import { randomPetName } from "@cocalc/frontend/project/utils";11import confirmStartComputeServer from "@cocalc/frontend/purchases/pay-as-you-go/confirm-start-compute-server";12import {13CLOUDS_BY_NAME,14Cloud as CloudType,15Configuration,16} from "@cocalc/util/db-schema/compute-servers";17import { replace_all } from "@cocalc/util/misc";18import {19computeServerAction,20createServer,21getTemplate,22setServerConfiguration,23} from "./api";24import { randomColor } from "./color";25import ComputeServer from "./compute-server";26import { Docs } from "./compute-servers";27import { availableClouds } from "./config";28import costPerHour from "./cost";2930export const DEFAULT_FAST_LOCAL = "scratch";3132function defaultTitle() {33return `Untitled ${new Date().toISOString().split("T")[0]}`;34}3536// NOTE that availableClouds() will be empty the moment the page37// loads, but give correct results once customize is loaded right38// after user has loaded page. By the time they are creating a NEW39// compute server, this should all be working fine.4041function defaultCloud() {42return availableClouds()[0];43}4445function defaultConfiguration() {46return genericDefaults(47CLOUDS_BY_NAME[availableClouds()[0]]?.defaultConfiguration ?? {},48);49}5051function genericDefaults(conf) {52return { ...conf, excludeFromSync: [DEFAULT_FAST_LOCAL] };53}5455export default function CreateComputeServer({ project_id, onCreate }) {56const account_id = useTypedRedux("account", "account_id");57const create_compute_server = useRedux(["create_compute_server"], project_id);58const create_compute_server_template_id = useRedux(59["create_compute_server_template_id"],60project_id,61);62const [editing, setEditing] = useState<boolean>(create_compute_server);63const [templateId, setTemplateId] = useState<number | undefined>(64create_compute_server_template_id,65);6667useEffect(() => {68if (create_compute_server_template_id) {69setConfigToTemplate(create_compute_server_template_id);70}71return () => {72if (create_compute_server) {73redux74.getProjectActions(project_id)75.setState({ create_compute_server: false });76}77};78}, []);7980// we have to do this stupid hack because of the animation when showing81// a modal and how select works. It's just working around limitations82// of antd, I think.83const [showTemplates, setShowTemplates] = useState<boolean>(false);84useEffect(() => {85setTimeout(() => setShowTemplates(true), 1000);86}, []);8788const [creating, setCreating] = useState<boolean>(false);89const [error, setError] = useState<string>("");9091const [title, setTitle] = useState<string>(defaultTitle());92const [color, setColor] = useState<string>(randomColor());93const [cloud, setCloud] = useState<CloudType>(defaultCloud());94const [configuration, setConfiguration] = useState<Configuration>(95defaultConfiguration(),96);97const resetConfig = async () => {98try {99setLoadingTemplate(true);100await delay(1);101setTitle(defaultTitle());102setColor(randomColor());103setCloud(defaultCloud());104setConfiguration(defaultConfiguration());105setTemplateId(undefined);106} finally {107setLoadingTemplate(false);108}109};110111const [notes, setNotes] = useState<string>("");112const [loadingTemplate, setLoadingTemplate] = useState<boolean>(false);113const setConfigToTemplate = async (id) => {114setTemplateId(id);115setNotes(`Starting with template ${id}.\n`);116const currentConfiguration = cloneDeep(configuration);117let template;118try {119setLoadingTemplate(true);120template = await getTemplate(id);121setTitle(template.title);122setColor(template.color);123setCloud(template.cloud);124const { configuration } = template;125if (currentConfiguration.dns) {126// keep current config127configuration.dns = currentConfiguration.dns;128} else if (configuration.dns) {129// TODO: should automatically ensure this randomly isn't taken. Can implement130// that later.131configuration.dns += `-${randomPetName().toLowerCase()}`;132}133configuration.excludeFromSync = currentConfiguration.excludeFromSync;134setConfiguration(configuration);135} catch (err) {136setError(`${err}`);137return;138} finally {139setLoadingTemplate(false);140}141};142143useEffect(() => {144if (configuration != null && configuration.cloud != cloud) {145setConfiguration(146genericDefaults(CLOUDS_BY_NAME[cloud]?.defaultConfiguration),147);148}149}, [cloud]);150151const handleCreate = async (start: boolean) => {152try {153setError("");154onCreate();155try {156setCreating(true);157const id = await createServer({158project_id,159cloud,160title,161color,162configuration,163notes,164});165await updateFastDataDirectoryId(id, configuration);166setEditing(false);167resetConfig();168setCreating(false);169if (start && cloud != "onprem") {170(async () => {171try {172await confirmStartComputeServer({173id,174cost_per_hour: await costPerHour({175configuration,176state: "running",177}),178});179await computeServerAction({ id, action: "start" });180} catch (_) {}181})();182}183} catch (err) {184setError(`${err}`);185}186} finally {187setCreating(false);188}189};190191const footer = [192<div style={{ textAlign: "center" }} key="footer">193<Button194key="cancel"195size="large"196onClick={() => setEditing(false)}197style={{ marginRight: "5px" }}198>199<CancelText />200</Button>201{cloud != "onprem" && (202<Button203style={{ marginRight: "5px" }}204key="start"205size="large"206type="primary"207onClick={() => {208handleCreate(true);209}}210disabled={!!error || !title.trim()}211>212<Icon name="run" /> Start Server213{!!error && "(clear error) "}214{!title.trim() && "(set title) "}215</Button>216)}217<Button218key="create"219size="large"220onClick={() => {221handleCreate(false);222}}223disabled={!!error || !title.trim()}224>225<Icon name="run" /> Create Server226{cloud != "onprem" ? " (don't start)" : ""}227{!!error && "(clear error) "}228{!title.trim() && "(set title) "}229</Button>230</div>,231];232233return (234<div style={{ marginTop: "15px" }}>235<Button236size="large"237disabled={creating || editing}238onClick={() => {239resetConfig();240setEditing(true);241}}242style={{243marginRight: "5px",244width: "80%",245height: "auto",246whiteSpace: "normal",247padding: "10px",248...(creating249? {250borderColor: "rgb(22, 119, 255)",251backgroundColor: "rgb(230, 244, 255)",252}253: undefined),254}}255>256<Icon257name="server"258style={{259color: "rgb(66, 139, 202)",260fontSize: "200%",261}}262/>263<br />264Create Compute Server... {creating ? <Spin /> : null}265</Button>266<Modal267width={"900px"}268onCancel={() => {269setEditing(false);270setTemplateId(undefined);271resetConfig();272}}273open={editing}274destroyOnClose275title={276<div>277<div style={{ display: "flex" }}>Create Compute Server</div>278<div style={{ textAlign: "center", color: "#666" }}>279{showTemplates && (280<PublicTemplates281disabled={loadingTemplate}282defaultId={templateId}283setId={async (id) => {284setTemplateId(id);285if (id) {286await setConfigToTemplate(id);287}288}}289/>290)}291</div>292</div>293}294footer={295<div style={{ display: "flex" }}>296{footer}297<Docs key="docs" style={{ flex: 1, marginTop: "10px" }} />298</div>299}300>301<div style={{ marginTop: "15px" }}>302<ShowError303error={error}304setError={setError}305style={{ margin: "15px 0" }}306/>307{cloud != "onprem" && (308<div309style={{310marginBottom: "5px",311color: "#666",312textAlign: "center",313}}314>315Customize your compute server below, then{" "}316<Button317onClick={() => handleCreate(true)}318disabled={!!error || !title.trim()}319type={"primary"}320>321<Icon name="run" /> Start Server322</Button>323</div>324)}325{cloud == "onprem" && (326<div327style={{328marginBottom: "5px",329color: "#666",330textAlign: "center",331}}332>333Customize your compute server below, then{" "}334<Button335onClick={() => handleCreate(false)}336disabled={!!error || !title.trim()}337type={"primary"}338>339<Icon name="run" /> Create Server340</Button>341</div>342)}343{loadingTemplate && <Spin />}344{!loadingTemplate && (345<ComputeServer346server={{347project_id,348account_id,349title,350color,351cloud,352configuration,353}}354editable={!creating}355controls={{356onColorChange: setColor,357onTitleChange: setTitle,358onCloudChange: setCloud,359onConfigurationChange: setConfiguration,360}}361/>362)}363</div>364</Modal>365</div>366);367}368369async function updateFastDataDirectoryId(id: number, configuration) {370const { excludeFromSync } = configuration;371if (excludeFromSync == null || excludeFromSync.length == 0) {372return;373}374const changes = {375excludeFromSync: excludeFromSync.map((x) =>376replace_all(x, "[id]", `${id}`),377),378};379await setServerConfiguration({ id, configuration: changes });380}381382383