Path: blob/master/src/packages/frontend/compute/compute-server.tsx
1503 views
import { Button, Card, Divider, Modal, Popconfirm, Spin } from "antd";1import { CSSProperties, useMemo, useState } from "react";23import { useTypedRedux } from "@cocalc/frontend/app-framework";4import { Icon } from "@cocalc/frontend/components";5import ShowError from "@cocalc/frontend/components/error";6import { CancelText } from "@cocalc/frontend/i18n/components";7import { webapp_client } from "@cocalc/frontend/webapp-client";8import type { ComputeServerUserInfo } from "@cocalc/util/db-schema/compute-servers";9import { COLORS } from "@cocalc/util/theme";10import getActions from "./action";11import { deleteServer, undeleteServer } from "./api";12import Cloud from "./cloud";13import Color, { randomColor } from "./color";14import ComputeServerLog from "./compute-server-log";15import { Docs } from "./compute-servers";16import Configuration from "./configuration";17import CurrentCost from "./current-cost";18import Description from "./description";19import DetailedState from "./detailed-state";20import Launcher from "./launcher";21import Menu from "./menu";22import { DisplayImage } from "./select-image";23import SerialPortOutput from "./serial-port-output";24import State from "./state";25import Title from "./title";26import { IdleTimeoutMessage } from "./idle-timeout";27import { ShutdownTimeMessage } from "./shutdown-time";28import { RunningProgress } from "@cocalc/frontend/compute/doc-status";29import { SpendLimitStatus } from "./spend-limit";3031interface Server1 extends Omit<ComputeServerUserInfo, "id"> {32id?: number;33}3435interface Controls {36setShowDeleted?: (showDeleted: boolean) => void;37onTitleChange?;38onColorChange?;39onCloudChange?;40onConfigurationChange?;41}4243interface Props {44server: Server1;45editable?: boolean;46style?: CSSProperties;47controls?: Controls;48modalOnly?: boolean;49close?: () => void;50}51export const currentlyEditing = {52id: 0,53};5455export default function ComputeServer({56server,57style,58editable,59controls,60modalOnly,61close,62}: Props) {63const {64id,65project_specific_id,66title,67color = randomColor(),68state,69state_changed,70detailed_state,71cloud,72cost_per_hour,73purchase_id,74configuration,75data,76deleted,77error: backendError,78project_id,79account_id,80} = server;8182const {83setShowDeleted,84onTitleChange,85onColorChange,86onCloudChange,87onConfigurationChange,88} = controls ?? {};8990const [error, setError] = useState<string>("");91const [edit, setEdit0] = useState<boolean>(id == null || !!modalOnly);92const setEdit = (edit) => {93setEdit0(edit);94if (!edit && close != null) {95close();96}97if (edit) {98currentlyEditing.id = id ?? 0;99} else {100currentlyEditing.id = 0;101}102};103104if (id == null && modalOnly) {105return <Spin />;106}107108let actions: React.JSX.Element[] | undefined = undefined;109if (id != null) {110actions = getActions({111id,112state,113editable,114setError,115configuration,116editModal: false,117type: "text",118project_id,119});120if (editable || configuration?.allowCollaboratorControl) {121actions.push(122<Button123key="edit"124type="text"125onClick={() => {126setEdit(!edit);127}}128>129{editable ? (130<>131<Icon name="settings" /> Settings132</>133) : (134<>135<Icon name="eye" /> Settings136</>137)}138</Button>,139);140}141if (deleted && editable && id) {142actions.push(143<Button144key="undelete"145type="text"146onClick={async () => {147try {148await undeleteServer(id);149} catch (err) {150setError(`${err}`);151return;152}153setShowDeleted?.(false);154}}155>156<Icon name="trash" /> Undelete157</Button>,158);159}160161// TODO: for later162// actions.push(163// <div>164// <Icon name="clone" /> Clone165// </div>,166// );167}168169const table = (170<div>171<Divider>172<Icon173name="cloud-dev"174style={{ fontSize: "16pt", marginRight: "15px" }}175/>{" "}176Title, Color, and Cloud177</Divider>178<div179style={{180marginTop: "15px",181display: "flex",182width: "100%",183justifyContent: "space-between",184}}185>186<Title187title={title}188id={id}189editable={editable}190setError={setError}191onChange={onTitleChange}192/>193<Color194color={color}195id={id}196editable={editable}197setError={setError}198onChange={onColorChange}199style={{200marginLeft: "10px",201}}202/>203<Cloud204cloud={cloud}205state={state}206editable={editable}207setError={setError}208setCloud={onCloudChange}209id={id}210style={{ marginTop: "-2.5px", marginLeft: "10px" }}211/>212</div>213<div style={{ color: "#888", marginTop: "5px" }}>214Change the title and color at any time.215</div>216<Divider>217<Icon name="gears" style={{ fontSize: "16pt", marginRight: "15px" }} />{" "}218Configuration219</Divider>220<Configuration221editable={editable}222state={state}223id={id}224project_id={project_id}225configuration={configuration}226data={data}227onChange={onConfigurationChange}228setCloud={onCloudChange}229template={server.template}230/>231</div>232);233234const buttons = (235<div>236<div style={{ width: "100%", display: "flex" }}>237<Button onClick={() => setEdit(false)} style={{ marginRight: "5px" }}>238<Icon name="save" /> {editable ? "Save" : "Close"}239</Button>240<div style={{ marginRight: "5px" }}>241{getActions({242id,243state,244editable,245setError,246configuration,247editModal: edit,248type: undefined,249project_id,250})}251</div>{" "}252{editable &&253id &&254(deleted || state == "deprovisioned") &&255(deleted ? (256<Button257key="undelete"258onClick={async () => {259try {260await undeleteServer(id);261} catch (err) {262setError(`${err}`);263return;264}265setShowDeleted?.(false);266}}267>268<Icon name="trash" /> Undelete269</Button>270) : (271<Popconfirm272key="delete"273title={"Delete this compute server?"}274description={275<div style={{ width: "400px" }}>276Are you sure you want to delete this compute server?277{state != "deprovisioned" && (278<b>WARNING: Any data on the boot disk will be deleted.</b>279)}280</div>281}282onConfirm={async () => {283setEdit(false);284await deleteServer(id);285}}286okText="Yes"287cancelText={<CancelText />}288>289<Button key="trash" danger>290<Icon name="trash" /> Delete291</Button>292</Popconfirm>293))}294</div>295<BackendError error={backendError} id={id} project_id={project_id} />296</div>297);298299const body =300id == null ? (301table302) : (303<Modal304open={edit}305destroyOnHidden306width={"900px"}307onCancel={() => setEdit(false)}308title={309<>310{buttons}311<Divider />312<Icon name="edit" style={{ marginRight: "15px" }} />{" "}313{editable ? "Edit" : ""} Compute Server With Id=314{project_specific_id}315</>316}317footer={318<>319<div style={{ display: "flex" }}>320{buttons}321<Docs key="docs" style={{ flex: 1, marginTop: "5px" }} />322</div>323</>324}325>326<div327style={{ fontSize: "12pt", color: COLORS.GRAY_M, display: "flex" }}328>329<Description330account_id={account_id}331cloud={cloud}332data={data}333configuration={configuration}334state={state}335/>336<div style={{ flex: 1 }} />337<State338style={{ marginRight: "5px" }}339state={state}340data={data}341state_changed={state_changed}342editable={editable}343id={id}344account_id={account_id}345configuration={configuration}346cost_per_hour={cost_per_hour}347purchase_id={purchase_id}348/>349</div>350{table}351</Modal>352);353354if (modalOnly) {355return body;356}357358return (359<Card360style={{361opacity: deleted ? 0.5 : undefined,362width: "100%",363minWidth: "500px",364border: `0.5px solid ${color ?? "#f0f0f0"}`,365borderRight: `10px solid ${color ?? "#aaa"}`,366borderLeft: `10px solid ${color ?? "#aaa"}`,367...style,368}}369actions={actions}370>371<Card.Meta372avatar={373<div style={{ width: "64px", marginBottom: "-20px" }}>374<Icon375name={cloud == "onprem" ? "global" : "server"}376style={{ fontSize: "30px", color: color ?? "#666" }}377/>378{id != null && (379<div style={{ color: "#888" }}>Id: {project_specific_id}</div>380)}381<div style={{ display: "flex", marginLeft: "-20px" }}>382{id != null && <ComputeServerLog id={id} />}383{id != null &&384configuration?.cloud == "google-cloud" &&385(state == "starting" ||386state == "stopping" ||387state == "running") && (388<SerialPortOutput389id={id}390title={title}391style={{ marginLeft: "-5px" }}392/>393)}394</div>395{cloud != "onprem" && state == "running" && id && (396<>397{!!server.configuration?.idleTimeoutMinutes && (398<div399style={{400display: "flex",401marginLeft: "-10px",402color: "#666",403}}404>405<IdleTimeoutMessage406id={id}407project_id={project_id}408minimal409/>410</div>411)}412{!!server.configuration?.shutdownTime?.enabled && (413<div414style={{415display: "flex",416marginLeft: "-15px",417color: "#666",418}}419>420<ShutdownTimeMessage421id={id}422project_id={project_id}423minimal424/>425</div>426)}427</>428)}429{id != null && (430<div style={{ marginLeft: "-15px" }}>431<CurrentCost state={state} cost_per_hour={cost_per_hour} />432</div>433)}434{state == "running" && !!data?.externalIp && (435<Launcher436style={{ marginLeft: "-24px" }}437configuration={configuration}438data={data}439compute_server_id={id}440project_id={project_id}441/>442)}443{server?.id != null && <SpendLimitStatus server={server} />}444</div>445}446title={447id == null ? undefined : (448<div449style={{450display: "flex",451width: "100%",452justifyContent: "space-between",453color: "#666",454borderBottom: `1px solid ${color}`,455padding: "0 10px 5px 0",456}}457>458<div459style={{460textOverflow: "ellipsis",461overflow: "hidden",462flex: 1,463display: "flex",464}}465>466<State467data={data}468state={state}469state_changed={state_changed}470editable={editable}471id={id}472account_id={account_id}473configuration={configuration}474cost_per_hour={cost_per_hour}475purchase_id={purchase_id}476/>477{state == "running" && id && (478<div479style={{480width: "75px",481marginTop: "2.5px",482marginLeft: "10px",483}}484>485<RunningProgress server={{ ...server, id }} />486</div>487)}488</div>489<Title490title={title}491editable={false}492style={{493textOverflow: "ellipsis",494overflow: "hidden",495flex: 1,496}}497/>498<div499style={{500textOverflow: "ellipsis",501overflow: "hidden",502flex: 1,503}}504>505<DisplayImage configuration={configuration} />506</div>507<div508style={{509textOverflow: "ellipsis",510overflow: "hidden",511textAlign: "right",512}}513>514<Cloud cloud={cloud} state={state} editable={false} id={id} />515</div>516<div>517<Menu518style={{ float: "right" }}519id={id}520project_id={project_id}521/>522</div>523</div>524)525}526description={527<div style={{ color: "#666" }}>528<BackendError529error={backendError}530id={id}531project_id={project_id}532/>533<Description534account_id={account_id}535cloud={cloud}536configuration={configuration}537data={data}538state={state}539short540/>541{(state == "running" ||542state == "stopping" ||543state == "starting") && (544<DetailedState545id={id}546project_id={project_id}547detailed_state={detailed_state}548color={color}549configuration={configuration}550/>551)}552<ShowError553error={error}554setError={setError}555style={{ margin: "15px 0", width: "100%" }}556/>557</div>558}559/>560{body}561</Card>562);563}564565export function useServer({ id, project_id }) {566const computeServers = useTypedRedux({ project_id }, "compute_servers");567const server = useMemo(() => {568return computeServers?.get(`${id}`)?.toJS();569}, [id, project_id, computeServers]);570571return server;572}573574export function EditModal({ project_id, id, close }) {575const account_id = useTypedRedux("account", "account_id");576const server = useServer({ id, project_id });577if (account_id == null || server == null) {578return null;579}580return (581<ComputeServer582modalOnly583editable={account_id == server.account_id}584server={server}585close={close}586/>587);588}589590function BackendError({ error, id, project_id }) {591if (!error || !id) {592return null;593}594return (595<div style={{ marginTop: "10px", display: "flex", fontWeight: "normal" }}>596<ShowError597error={error}598style={{ margin: "15px 0", width: "100%" }}599setError={async () => {600try {601await webapp_client.async_query({602query: {603compute_servers: {604id,605project_id,606error: "",607},608},609});610} catch (err) {611console.warn(err);612}613}}614/>615</div>616);617}618619620