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/action.tsx
Views: 687
import { Alert, Button, Modal, Popconfirm, Popover, Spin } from "antd";1import { useEffect, useState } from "react";23import { redux, useStore } from "@cocalc/frontend/app-framework";4import { A, CopyToClipBoard, Icon } from "@cocalc/frontend/components";5import ShowError from "@cocalc/frontend/components/error";6import { appBasePath } from "@cocalc/frontend/customize/app-base-path";7import { CancelText } from "@cocalc/frontend/i18n/components";8import MoneyStatistic from "@cocalc/frontend/purchases/money-statistic";9import confirmStartComputeServer from "@cocalc/frontend/purchases/pay-as-you-go/confirm-start-compute-server";10import { webapp_client } from "@cocalc/frontend/webapp-client";11import {12ACTION_INFO,13STATE_INFO,14getTargetState,15} from "@cocalc/util/db-schema/compute-servers";16import { computeServerAction, getApiKey } from "./api";17import costPerHour from "./cost";1819export default function getActions({20id,21state,22editable,23setError,24configuration,25editModal,26type,27project_id,28}): JSX.Element[] {29if (!editable && !configuration?.allowCollaboratorControl) {30return [];31}32const s = STATE_INFO[state ?? "off"];33if (s == null) {34return [];35}36if ((s.actions ?? []).length == 0) {37return [];38}39const v: JSX.Element[] = [];40for (const action of s.actions) {41if (42!editable &&43action != "stop" &&44action != "deprovision" &&45action != "start" &&46action != "suspend" &&47action != "resume" &&48action != "reboot"49) {50// non-owner can only do start/stop/suspend/resume -- NOT delete or deprovision.51continue;52}53if (!editable && action == "deprovision" && !configuration.ephemeral) {54// also do not allow NON ephemeral deprovision by collaborator.55// For ephemeral, collab is encouraged to delete server.56continue;57}58const a = ACTION_INFO[action];59if (!a) continue;60if (action == "suspend") {61if (configuration.cloud != "google-cloud") {62continue;63}64if (configuration.machineType.startsWith("t2a-")) {65// TODO: suspend/resume breaks the clock badly on ARM64, and I haven't66// figured out a workaround, so don't support it for now. I guess this67// is a GCP bug.68continue;69}70// must have no gpu and <= 208GB of RAM -- https://cloud.google.com/compute/docs/instances/suspend-resume-instance71if (configuration.acceleratorType) {72continue;73}74// [ ] TODO: we don't have an easy way to check the RAM requirement right now.75}76if (!editModal && configuration.ephemeral && action == "stop") {77continue;78}79const {80label,81icon,82tip,83description,84confirm,85danger,86confirmMessage,87clouds,88} = a;89if (danger && !configuration.ephemeral && !editModal) {90continue;91}92if (clouds && !clouds.includes(configuration.cloud)) {93continue;94}95v.push(96<ActionButton97style={v.length > 0 ? { marginLeft: "5px" } : undefined}98key={action}99id={id}100action={action}101label={label}102icon={icon}103tip={tip}104editable={editable}105description={description}106setError={setError}107confirm={confirm}108configuration={configuration}109danger={danger}110confirmMessage={confirmMessage}111type={type}112state={state ?? "off"}113project_id={project_id}114/>,115);116}117return v;118}119120function ActionButton({121id,122action,123icon,124label,125editable,126description,127tip,128setError,129confirm,130confirmMessage,131configuration,132danger,133type,134style,135state,136project_id,137}) {138const [showOnPremStart, setShowOnPremStart] = useState<boolean>(false);139const [showOnPremStop, setShowOnPremStop] = useState<boolean>(false);140const [showOnPremDeprovision, setShowOnPremDeprovision] =141useState<boolean>(false);142const [cost_per_hour, setCostPerHour] = useState<number | null>(null);143const [popConfirm, setPopConfirm] = useState<boolean>(false);144const updateCost = async () => {145try {146const c = await costPerHour({147configuration,148state: getTargetState(action),149});150setCostPerHour(c);151return c;152} catch (err) {153setError(`Unable to compute cost: ${err}`);154setCostPerHour(null);155return null;156}157};158useEffect(() => {159if (configuration == null) return;160updateCost();161}, [configuration, action]);162const customize = useStore("customize");163const [understand, setUnderstand] = useState<boolean>(false);164const [doing, setDoing] = useState<boolean>(!STATE_INFO[state]?.stable);165166const doAction = async () => {167if (action == "start") {168// check version169const required =170customize?.get("version_compute_server_min_project") ?? 0;171if (required > 0) {172if (redux.getStore("projects").get_state(project_id) == "running") {173// only check if running -- if not running, the project will obviously174// not need a restart, since it isn't even running175const api = await webapp_client.project_client.api(project_id);176const version = await api.version();177if (version < required) {178setError(179"You must restart your project to upgrade it to the latest version.",180);181return;182}183}184}185}186187if (configuration.cloud == "onprem") {188if (action == "start") {189setShowOnPremStart(true);190} else if (action == "stop") {191setShowOnPremStop(true);192} else if (action == "deprovision") {193setShowOnPremDeprovision(true);194}195196// right now user has to copy paste197return;198}199try {200setError("");201setDoing(true);202if (editable && (action == "start" || action == "resume")) {203let c = cost_per_hour;204if (c == null) {205c = await updateCost();206if (c == null) {207// error would be displayed above.208return;209}210}211await confirmStartComputeServer({ id, cost_per_hour: c });212}213await computeServerAction({ id, action });214} catch (err) {215setError(`${err}`);216} finally {217setDoing(false);218}219};220useEffect(() => {221setDoing(!STATE_INFO[state]?.stable);222}, [action, state]);223224if (configuration == null) {225return null;226}227228let button = (229<Button230style={style}231disabled={doing}232type={type}233onClick={!confirm ? doAction : undefined}234danger={danger}235>236<Icon name={icon} /> {label}{" "}237{doing && (238<>239<div style={{ display: "inline-block", width: "10px" }} />240<Spin />241</>242)}243</Button>244);245if (confirm) {246button = (247<Popconfirm248onOpenChange={setPopConfirm}249placement="right"250okButtonProps={{251disabled: !configuration.ephemeral && danger && !understand,252}}253title={254<div>255{label} - Are you sure?256{action == "deprovision" && (257<Alert258showIcon259style={{ margin: "15px 0", maxWidth: "400px" }}260type="warning"261message={262"This will delete the boot disk! This does not touch the files in your project's home directory."263}264/>265)}266{action == "stop" && (267<Alert268showIcon269style={{ margin: "15px 0" }}270type="info"271message={`This will safely turn off the VM${272editable ? ", and allow you to edit its configuration." : "."273}`}274/>275)}276{!configuration.ephemeral && danger && (277<div>278{/* ATTN: Not using a checkbox here to WORKAROUND A BUG IN CHROME that I see after a day or so! */}279<Button onClick={() => setUnderstand(!understand)} type="text">280<Icon281name={understand ? "check-square" : "square"}282style={{ marginRight: "5px" }}283/>284{confirmMessage ??285"I understand that this may result in data loss."}286</Button>287</div>288)}289</div>290}291onConfirm={doAction}292okText={`Yes, ${label} VM`}293cancelText={<CancelText />}294>295{button}296</Popconfirm>297);298}299300const content = (301<>302{button}303{showOnPremStart && action == "start" && (304<OnPremGuide305action={action}306setShow={setShowOnPremStart}307configuration={configuration}308id={id}309title={310<>311<Icon name="server" /> Connect Your Virtual Machine to CoCalc312</>313}314/>315)}316{showOnPremStop && action == "stop" && (317<OnPremGuide318action={action}319setShow={setShowOnPremStop}320configuration={configuration}321id={id}322title={323<>324<Icon name="stop" /> Disconnect Your Virtual Machine from CoCalc325</>326}327/>328)}329{showOnPremDeprovision && action == "deprovision" && (330<OnPremGuide331action={action}332setShow={setShowOnPremDeprovision}333configuration={configuration}334id={id}335title={336<div style={{ color: "darkred" }}>337<Icon name="trash" /> Disconnect Your Virtual Machine and Remove338Files339</div>340}341/>342)}343</>344);345346// Do NOT use popover in case we're doing a popconfirm.347// Two popovers at once is just unprofessional and hard to use.348// That's why the "open={popConfirm ? false : undefined}" below349350return (351<Popover352open={popConfirm ? false : undefined}353placement="left"354key={action}355mouseEnterDelay={1}356title={357<div>358<Icon name={icon} /> {tip}359</div>360}361content={362<div style={{ width: "400px" }}>363{description} {editable && <>You will be charged:</>}364{!editable && <>The owner of this compute server will be charged:</>}365{cost_per_hour != null && (366<div style={{ textAlign: "center" }}>367<MoneyStatistic368value={cost_per_hour}369title="Cost per hour"370costPerMonth={730 * cost_per_hour}371/>372</div>373)}374</div>375}376>377{content}378</Popover>379);380}381382function OnPremGuide({ setShow, configuration, id, title, action }) {383const [apiKey, setApiKey] = useState<string | null>(null);384const [error, setError] = useState<string>("");385useEffect(() => {386(async () => {387try {388setError("");389setApiKey(await getApiKey({ id }));390} catch (err) {391setError(`${err}`);392}393})();394}, []);395return (396<Modal397width={800}398title={title}399open={true}400onCancel={() => {401setShow(false);402}}403onOk={() => {404setShow(false);405}}406>407{action == "start" && (408<div>409You can connect any{" "}410<b>Ubuntu 22.04 or 24.04 Linux Virtual Machine (VM)</b> with root411access to this project. This VM can be anywhere (your laptop or a412cloud hosting providing). Your VM needs to be able to create outgoing413network connections, but does NOT need to have a public ip address.414<Alert415style={{ margin: "15px 0" }}416type="warning"417showIcon418message={<b>USE AN UBUNTU 22.04 or 24.04 VIRTUAL MACHINE</b>}419description={420<div>421You can use any{" "}422<u>423<b>424<A href="https://multipass.run/">UBUNTU VIRTUAL MACHINE</A>425</b>426</u>{" "}427that you have a root acount on.{" "}428<A href="https://multipass.run/">429Multipass is a very easy and free way to install one or more430minimal Ubuntu VM's on Windows, Mac, and Linux.431</A>{" "}432After you install Multipass, create a VM by pasting this in a433terminal on your computer (you can increase the cpu, memory and434disk):435<CopyToClipBoard436inputWidth="600px"437style={{ marginTop: "10px" }}438value={`multipass launch --name compute-server-${id} --cpus 1 --memory 4G --disk 25G`}439/>440<br />441Then launch a terminal shell running in the VM:442<CopyToClipBoard443inputWidth="600px"444style={{ marginTop: "10px" }}445value={`multipass shell compute-server-${id}`}446/>447</div>448}449/>450{configuration.gpu && (451<span>452Since you clicked GPU, you must also have an NVIDIA GPU and the453Cuda drivers installed and working.{" "}454</span>455)}456</div>457)}458<div style={{ marginTop: "15px" }}>459{apiKey && (460<div>461<div style={{ marginBottom: "10px" }}>462Copy and paste the following into a terminal shell on your{" "}463<b>Ubuntu Virtual Machine</b>:464</div>465<CopyToClipBoard466inputWidth={"700px"}467value={`curl -fsS https://${window.location.host}${468appBasePath.length > 1 ? appBasePath : ""469}/compute/${id}/onprem/${action}/${apiKey} | sudo bash`}470/>471</div>472)}473{!apiKey && !error && <Spin />}474{error && <ShowError error={error} setError={setError} />}475</div>476{action == "stop" && (477<div>478This will disconnect your VM from CoCalc and stop it from syncing479files, running terminals and Jupyter notebooks. Files and software you480installed will not be deleted and you can start the compute server481later.482<Alert483style={{ margin: "15px 0" }}484type="warning"485showIcon486message={487<b>488If you're using{" "}489<A href="https://multipass.run/">Multipass...</A>490</b>491}492description={493<div>494<CopyToClipBoard495value={`multipass stop compute-server-${id}`}496/>497<br />498HINT: If you ever need to enlarge the disk, do this:499<CopyToClipBoard500inputWidth="600px"501value={`multipass stop compute-server-${id} && multipass set local.compute-server-${id}.disk=30G`}502/>503</div>504}505/>506</div>507)}508{action == "deprovision" && (509<div>510This will disconnect your VM from CoCalc, and permanently delete any511local files and software you installed into your compute server.512<Alert513style={{ margin: "15px 0" }}514type="warning"515showIcon516message={517<b>518If you're using{" "}519<A href="https://multipass.run/">Multipass...</A>520</b>521}522description={523<CopyToClipBoard524value={`multipass delete compute-server-${id}`}525/>526}527/>528</div>529)}530{action == "deprovision" && (531<div style={{ marginTop: "15px" }}>532NOTE: This does not delete Docker or any Docker images. Run this to533delete all unused Docker images:534<br />535<CopyToClipBoard value="docker image prune -a" />536</div>537)}538</Modal>539);540}541542543