Path: blob/master/src/packages/frontend/compute/compute-server.tsx
5884 views
import { Button, Card, Divider, Modal, Popconfirm, Spin } from "antd";1import { CSSProperties, useEffect, useMemo, useState } from "react";2import { redux, useTypedRedux } from "@cocalc/frontend/app-framework";3import { Icon } from "@cocalc/frontend/components";4import ShowError from "@cocalc/frontend/components/error";5import { CancelText } from "@cocalc/frontend/i18n/components";6import { webapp_client } from "@cocalc/frontend/webapp-client";7import type { ComputeServerUserInfo } from "@cocalc/util/db-schema/compute-servers";8import { COLORS } from "@cocalc/util/theme";9import getActions from "./action";10import { deleteServer, undeleteServer } from "./api";11import Cloud from "./cloud";12import Color, { randomColor } from "./color";13import ComputeServerLog from "./compute-server-log";14import { Docs } from "./compute-servers";15import Configuration from "./configuration";16import CurrentCost from "./current-cost";17import Description from "./description";18import DetailedState from "./detailed-state";19import Launcher from "./launcher";20import Menu from "./menu";21import { DisplayImage } from "./select-image";22import SerialPortOutput from "./serial-port-output";23import State from "./state";24import Title from "./title";25import { IdleTimeoutMessage } from "./idle-timeout";26import { ShutdownTimeMessage } from "./shutdown-time";27import { RunningProgress } from "@cocalc/frontend/compute/doc-status";28import { SpendLimitStatus } from "./spend-limit";2930interface Server1 extends Omit<ComputeServerUserInfo, "id"> {31id?: number;32}3334interface Controls {35setShowDeleted?: (showDeleted: boolean) => void;36onTitleChange?;37onColorChange?;38onCloudChange?;39onConfigurationChange?;40}4142interface Props {43server: Server1;44editable?: boolean;45style?: CSSProperties;46controls?: Controls;47modalOnly?: boolean;48close?: () => void;49}50export const currentlyEditing = {51id: 0,52};5354export default function ComputeServer({55server,56style,57editable,58controls,59modalOnly,60close,61}: Props) {62const {63id,64project_specific_id,65title,66color = randomColor(),67state,68state_changed,69detailed_state,70cloud,71cost_per_hour,72purchase_id,73configuration,74data,75deleted,76error: backendError,77project_id,78account_id,79} = server;8081const {82setShowDeleted,83onTitleChange,84onColorChange,85onCloudChange,86onConfigurationChange,87} = controls ?? {};8889const [error, setError] = useState<string>("");90const [edit, setEdit0] = useState<boolean>(id == null || !!modalOnly);91const setEdit = (edit) => {92setEdit0(edit);93if (!edit && close != null) {94close();95}96if (edit) {97currentlyEditing.id = id ?? 0;98} else {99currentlyEditing.id = 0;100}101};102103if (id == null && modalOnly) {104return <Spin />;105}106107let actions: React.JSX.Element[] | undefined = undefined;108if (id != null) {109actions = getActions({110id,111state,112editable,113setError,114configuration,115editModal: false,116type: "text",117project_id,118});119if (editable || configuration?.allowCollaboratorControl) {120actions.push(121<Button122key="edit"123type="text"124onClick={() => {125setEdit(!edit);126}}127>128{editable ? (129<>130<Icon name="settings" /> Settings131</>132) : (133<>134<Icon name="eye" /> Settings135</>136)}137</Button>,138);139}140if (deleted && editable && id) {141actions.push(142<Button143key="undelete"144type="text"145onClick={async () => {146try {147await undeleteServer(id);148} catch (err) {149setError(`${err}`);150return;151}152setShowDeleted?.(false);153}}154>155<Icon name="trash" /> Undelete156</Button>,157);158}159160// TODO: for later161// actions.push(162// <div>163// <Icon name="clone" /> Clone164// </div>,165// );166}167168const table = (169<div>170<Divider>171<Icon172name="cloud-dev"173style={{ fontSize: "16pt", marginRight: "15px" }}174/>{" "}175Title, Color, and Cloud176</Divider>177<div178style={{179marginTop: "15px",180display: "flex",181width: "100%",182justifyContent: "space-between",183}}184>185<Title186title={title}187id={id}188editable={editable}189setError={setError}190onChange={onTitleChange}191/>192<Color193color={color}194id={id}195editable={editable}196setError={setError}197onChange={onColorChange}198style={{199marginLeft: "10px",200}}201/>202<Cloud203cloud={cloud}204state={state}205editable={editable}206setError={setError}207setCloud={onCloudChange}208id={id}209style={{ marginTop: "-2.5px", marginLeft: "10px" }}210/>211</div>212<div style={{ color: "#888", marginTop: "5px" }}>213Change the title and color at any time.214</div>215<Divider>216<Icon name="gears" style={{ fontSize: "16pt", marginRight: "15px" }} />{" "}217Configuration218</Divider>219<Configuration220editable={editable}221state={state}222id={id}223project_id={project_id}224configuration={configuration}225data={data}226onChange={onConfigurationChange}227setCloud={onCloudChange}228template={server.template}229/>230</div>231);232233const buttons = (234<div>235<div style={{ width: "100%", display: "flex" }}>236<Button onClick={() => setEdit(false)} style={{ marginRight: "5px" }}>237<Icon name="save" /> {editable ? "Save" : "Close"}238</Button>239<div style={{ marginRight: "5px" }}>240{getActions({241id,242state,243editable,244setError,245configuration,246editModal: edit,247type: undefined,248project_id,249})}250</div>{" "}251{editable &&252id &&253(deleted || state == "deprovisioned") &&254(deleted ? (255<Button256key="undelete"257onClick={async () => {258try {259await undeleteServer(id);260} catch (err) {261setError(`${err}`);262return;263}264setShowDeleted?.(false);265}}266>267<Icon name="trash" /> Undelete268</Button>269) : (270<Popconfirm271key="delete"272title={"Delete this compute server?"}273description={274<div style={{ width: "400px" }}>275Are you sure you want to delete this compute server?276{state != "deprovisioned" && (277<b>WARNING: Any data on the boot disk will be deleted.</b>278)}279</div>280}281onConfirm={async () => {282setEdit(false);283await deleteServer(id);284}}285okText="Yes"286cancelText={<CancelText />}287>288<Button key="trash" danger>289<Icon name="trash" /> Delete290</Button>291</Popconfirm>292))}293</div>294<BackendError error={backendError} id={id} project_id={project_id} />295</div>296);297298const body =299id == null ? (300table301) : (302<Modal303open={edit}304destroyOnHidden305width={"900px"}306onCancel={() => setEdit(false)}307title={308<>309{buttons}310<Divider />311<Icon name="edit" style={{ marginRight: "15px" }} />{" "}312{editable ? "Edit" : ""} Compute Server With Id=313{project_specific_id}314</>315}316footer={317<>318<div style={{ display: "flex" }}>319{buttons}320<Docs key="docs" style={{ flex: 1, marginTop: "5px" }} />321</div>322</>323}324>325<div326style={{ fontSize: "12pt", color: COLORS.GRAY_M, display: "flex" }}327>328<Description329account_id={account_id}330cloud={cloud}331data={data}332configuration={configuration}333state={state}334/>335<div style={{ flex: 1 }} />336<State337style={{ marginRight: "5px" }}338state={state}339data={data}340state_changed={state_changed}341editable={editable}342id={id}343account_id={account_id}344configuration={configuration}345cost_per_hour={cost_per_hour}346purchase_id={purchase_id}347/>348</div>349{table}350</Modal>351);352353if (modalOnly) {354return body;355}356357return (358<Card359style={{360opacity: deleted ? 0.5 : undefined,361width: "100%",362minWidth: "500px",363border: `0.5px solid ${color ?? "#f0f0f0"}`,364borderRight: `10px solid ${color ?? "#aaa"}`,365borderLeft: `10px solid ${color ?? "#aaa"}`,366...style,367}}368actions={actions}369>370<Card.Meta371avatar={372<div style={{ width: "64px", marginBottom: "-20px" }}>373<Icon374name={cloud == "onprem" ? "global" : "server"}375style={{ fontSize: "30px", color: color ?? "#666" }}376/>377{id != null && (378<div style={{ color: "#888" }}>Id: {project_specific_id}</div>379)}380<div style={{ display: "flex", marginLeft: "-20px" }}>381{id != null && <ComputeServerLog id={id} />}382{id != null &&383configuration?.cloud == "google-cloud" &&384(state == "starting" ||385state == "stopping" ||386state == "running") && (387<SerialPortOutput388id={id}389title={title}390style={{ marginLeft: "-5px" }}391/>392)}393</div>394{cloud != "onprem" && state == "running" && id && (395<>396{!!server.configuration?.idleTimeoutMinutes && (397<div398style={{399display: "flex",400marginLeft: "-10px",401color: "#666",402}}403>404<IdleTimeoutMessage405id={id}406project_id={project_id}407minimal408/>409</div>410)}411{!!server.configuration?.shutdownTime?.enabled && (412<div413style={{414display: "flex",415marginLeft: "-15px",416color: "#666",417}}418>419<ShutdownTimeMessage420id={id}421project_id={project_id}422minimal423/>424</div>425)}426</>427)}428{id != null && (429<div style={{ marginLeft: "-15px" }}>430<CurrentCost state={state} cost_per_hour={cost_per_hour} />431</div>432)}433{state == "running" && !!data?.externalIp && (434<Launcher435style={{ marginLeft: "-24px" }}436configuration={configuration}437data={data}438compute_server_id={id}439project_id={project_id}440/>441)}442{server?.id != null && <SpendLimitStatus server={server} />}443</div>444}445title={446id == null ? undefined : (447<div448style={{449display: "flex",450width: "100%",451justifyContent: "space-between",452color: "#666",453borderBottom: `1px solid ${color}`,454padding: "0 10px 5px 0",455}}456>457<div458style={{459textOverflow: "ellipsis",460overflow: "hidden",461flex: 1,462display: "flex",463}}464>465<State466data={data}467state={state}468state_changed={state_changed}469editable={editable}470id={id}471account_id={account_id}472configuration={configuration}473cost_per_hour={cost_per_hour}474purchase_id={purchase_id}475/>476{state == "running" && id && (477<div478style={{479width: "75px",480marginTop: "2.5px",481marginLeft: "10px",482}}483>484<RunningProgress server={{ ...server, id }} />485</div>486)}487</div>488<Title489title={title}490editable={false}491style={{492textOverflow: "ellipsis",493overflow: "hidden",494flex: 1,495}}496/>497<div498style={{499textOverflow: "ellipsis",500overflow: "hidden",501flex: 1,502}}503>504<DisplayImage configuration={configuration} />505</div>506<div507style={{508textOverflow: "ellipsis",509overflow: "hidden",510textAlign: "right",511}}512>513<Cloud cloud={cloud} state={state} editable={false} id={id} />514</div>515<div>516<Menu517style={{ float: "right" }}518id={id}519project_id={project_id}520/>521</div>522</div>523)524}525description={526<div style={{ color: "#666" }}>527<BackendError528error={backendError}529id={id}530project_id={project_id}531/>532<Description533account_id={account_id}534cloud={cloud}535configuration={configuration}536data={data}537state={state}538short539/>540{(state == "running" ||541state == "stopping" ||542state == "starting") && (543<DetailedState544id={id}545project_id={project_id}546detailed_state={detailed_state}547color={color}548configuration={configuration}549/>550)}551<ShowError552error={error}553setError={setError}554style={{ margin: "15px 0", width: "100%" }}555/>556</div>557}558/>559{body}560</Card>561);562}563564export function useServer({ id, project_id }) {565useEffect(() => {566const actions = redux.getProjectActions(project_id);567actions.incrementReferenceCount();568return () => {569actions.decrementReferenceCount();570};571}, [project_id]);572const computeServers = useTypedRedux({ project_id }, "compute_servers");573const server = useMemo(() => {574return computeServers?.get(`${id}`)?.toJS();575}, [id, project_id, computeServers]);576577return server;578}579580export function EditModal({ project_id, id, close }) {581const account_id = useTypedRedux("account", "account_id");582const server = useServer({ id, project_id });583if (account_id == null || server == null) {584return null;585}586return (587<ComputeServer588modalOnly589editable={account_id == server.account_id}590server={server}591close={close}592/>593);594}595596function BackendError({ error, id, project_id }) {597if (!error || !id) {598return null;599}600return (601<div style={{ marginTop: "10px", display: "flex", fontWeight: "normal" }}>602<ShowError603error={error}604style={{ margin: "15px 0", width: "100%" }}605setError={async () => {606try {607await webapp_client.async_query({608query: {609compute_servers: {610id,611project_id,612error: "",613},614},615});616} catch (err) {617console.warn(err);618}619}}620/>621</div>622);623}624625626