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/compute-server.tsx
Views: 687
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";2627interface Server1 extends Omit<ComputeServerUserInfo, "id"> {28id?: number;29}3031interface Controls {32setShowDeleted?: (showDeleted: boolean) => void;33onTitleChange?;34onColorChange?;35onCloudChange?;36onConfigurationChange?;37}3839interface Props {40server: Server1;41editable?: boolean;42style?: CSSProperties;43controls?: Controls;44modalOnly?: boolean;45close?: () => void;46}47export const currentlyEditing = {48id: 0,49};5051export default function ComputeServer({52server,53style,54editable,55controls,56modalOnly,57close,58}: Props) {59const {60id,61project_specific_id,62title,63color = randomColor(),64state,65state_changed,66detailed_state,67cloud,68cost_per_hour,69purchase_id,70configuration,71data,72deleted,73error: backendError,74project_id,75account_id,76} = server;7778const {79setShowDeleted,80onTitleChange,81onColorChange,82onCloudChange,83onConfigurationChange,84} = controls ?? {};8586const [error, setError] = useState<string>("");87const [edit, setEdit0] = useState<boolean>(id == null || !!modalOnly);88const setEdit = (edit) => {89setEdit0(edit);90if (!edit && close != null) {91close();92}93if (edit) {94currentlyEditing.id = id ?? 0;95} else {96currentlyEditing.id = 0;97}98};99100if (id == null && modalOnly) {101return <Spin />;102}103104let actions: JSX.Element[] | undefined = undefined;105if (id != null) {106actions = getActions({107id,108state,109editable,110setError,111configuration,112editModal: false,113type: "text",114project_id,115});116if (editable || configuration?.allowCollaboratorControl) {117actions.push(118<Button119key="edit"120type="text"121onClick={() => {122setEdit(!edit);123}}124>125{editable ? (126<>127<Icon name="settings" /> Settings128</>129) : (130<>131<Icon name="eye" /> Settings132</>133)}134</Button>,135);136}137if (deleted && editable && id) {138actions.push(139<Button140key="undelete"141type="text"142onClick={async () => {143try {144await undeleteServer(id);145} catch (err) {146setError(`${err}`);147return;148}149setShowDeleted?.(false);150}}151>152<Icon name="trash" /> Undelete153</Button>,154);155}156157// TODO: for later158// actions.push(159// <div>160// <Icon name="clone" /> Clone161// </div>,162// );163}164165const table = (166<div>167<Divider>168<Icon169name="cloud-dev"170style={{ fontSize: "16pt", marginRight: "15px" }}171/>{" "}172Title, Color, and Cloud173</Divider>174<div175style={{176marginTop: "15px",177display: "flex",178width: "100%",179justifyContent: "space-between",180}}181>182<Title183title={title}184id={id}185editable={editable}186setError={setError}187onChange={onTitleChange}188/>189<Color190color={color}191id={id}192editable={editable}193setError={setError}194onChange={onColorChange}195style={{196marginLeft: "10px",197}}198/>199<Cloud200cloud={cloud}201state={state}202editable={editable}203setError={setError}204setCloud={onCloudChange}205id={id}206style={{ marginTop: "-2.5px", marginLeft: "10px" }}207/>208</div>209<div style={{ color: "#888", marginTop: "5px" }}>210Change the title and color at any time.211</div>212<Divider>213<Icon name="gears" style={{ fontSize: "16pt", marginRight: "15px" }} />{" "}214Configuration215</Divider>216<Configuration217editable={editable}218state={state}219id={id}220project_id={project_id}221configuration={configuration}222data={data}223onChange={onConfigurationChange}224setCloud={onCloudChange}225template={server.template}226/>227</div>228);229230const buttons = (231<div>232<div style={{ width: "100%", display: "flex" }}>233<Button onClick={() => setEdit(false)} style={{ marginRight: "5px" }}>234<Icon name="save" /> {editable ? "Save" : "Close"}235</Button>236<div style={{ marginRight: "5px" }}>237{getActions({238id,239state,240editable,241setError,242configuration,243editModal: edit,244type: undefined,245project_id,246})}247</div>{" "}248{editable &&249id &&250(deleted || state == "deprovisioned") &&251(deleted ? (252<Button253key="undelete"254onClick={async () => {255try {256await undeleteServer(id);257} catch (err) {258setError(`${err}`);259return;260}261setShowDeleted?.(false);262}}263>264<Icon name="trash" /> Undelete265</Button>266) : (267<Popconfirm268key="delete"269title={"Delete this compute server?"}270description={271<div style={{ width: "400px" }}>272Are you sure you want to delete this compute server?273{state != "deprovisioned" && (274<b>WARNING: Any data on the boot disk will be deleted.</b>275)}276</div>277}278onConfirm={async () => {279setEdit(false);280await deleteServer(id);281}}282okText="Yes"283cancelText={<CancelText />}284>285<Button key="trash" danger>286<Icon name="trash" /> Delete287</Button>288</Popconfirm>289))}290</div>291<BackendError error={backendError} id={id} project_id={project_id} />292</div>293);294295const body =296id == null ? (297table298) : (299<Modal300open={edit}301destroyOnClose302width={"900px"}303onCancel={() => setEdit(false)}304title={305<>306{buttons}307<Divider />308<Icon name="edit" style={{ marginRight: "15px" }} />{" "}309{editable ? "Edit" : ""} Compute Server With Id=310{project_specific_id}311</>312}313footer={314<>315<div style={{ display: "flex" }}>316{buttons}317<Docs key="docs" style={{ flex: 1, marginTop: "5px" }} />318</div>319</>320}321>322<div323style={{ fontSize: "12pt", color: COLORS.GRAY_M, display: "flex" }}324>325<Description326account_id={account_id}327cloud={cloud}328data={data}329configuration={configuration}330state={state}331/>332<div style={{ flex: 1 }} />333<State334style={{ marginRight: "5px" }}335state={state}336data={data}337state_changed={state_changed}338editable={editable}339id={id}340account_id={account_id}341configuration={configuration}342cost_per_hour={cost_per_hour}343purchase_id={purchase_id}344/>345</div>346{table}347</Modal>348);349350if (modalOnly) {351return body;352}353354return (355<Card356style={{357opacity: deleted ? 0.5 : undefined,358width: "100%",359minWidth: "500px",360border: `0.5px solid ${color ?? "#f0f0f0"}`,361borderRight: `10px solid ${color ?? "#aaa"}`,362borderLeft: `10px solid ${color ?? "#aaa"}`,363...style,364}}365actions={actions}366>367<Card.Meta368avatar={369<div style={{ width: "64px", marginBottom: "-20px" }}>370<Icon371name={cloud == "onprem" ? "global" : "server"}372style={{ fontSize: "30px", color: color ?? "#666" }}373/>374{id != null && (375<div style={{ color: "#888" }}>Id: {project_specific_id}</div>376)}377<div style={{ display: "flex", marginLeft: "-20px" }}>378{id != null && <ComputeServerLog id={id} />}379{id != null &&380configuration.cloud == "google-cloud" &&381(state == "starting" ||382state == "stopping" ||383state == "running") && (384<SerialPortOutput385id={id}386title={title}387style={{ marginLeft: "-5px" }}388/>389)}390</div>391{id != null && (392<div style={{ marginLeft: "-15px" }}>393<CurrentCost state={state} cost_per_hour={cost_per_hour} />394</div>395)}396{state == "running" && !!data?.externalIp && (397<Launcher398style={{ marginLeft: "-24px" }}399configuration={configuration}400data={data}401compute_server_id={id}402project_id={project_id}403/>404)}405</div>406}407title={408id == null ? undefined : (409<div410style={{411display: "flex",412width: "100%",413justifyContent: "space-between",414color: "#666",415borderBottom: `1px solid ${color}`,416padding: "0 10px 5px 0",417}}418>419<div420style={{421textOverflow: "ellipsis",422overflow: "hidden",423flex: 1,424}}425>426<State427data={data}428state={state}429state_changed={state_changed}430editable={editable}431id={id}432account_id={account_id}433configuration={configuration}434cost_per_hour={cost_per_hour}435purchase_id={purchase_id}436/>437</div>438<Title439title={title}440editable={false}441style={{442textOverflow: "ellipsis",443overflow: "hidden",444flex: 1,445}}446/>447<div448style={{449textOverflow: "ellipsis",450overflow: "hidden",451flex: 1,452}}453>454<DisplayImage configuration={configuration} />455</div>456<div457style={{458textOverflow: "ellipsis",459overflow: "hidden",460textAlign: "right",461}}462>463<Cloud cloud={cloud} state={state} editable={false} id={id} />464</div>465<div>466<Menu467style={{ float: "right" }}468id={id}469project_id={project_id}470/>471</div>472</div>473)474}475description={476<div style={{ color: "#666" }}>477<BackendError478error={backendError}479id={id}480project_id={project_id}481/>482<Description483account_id={account_id}484cloud={cloud}485configuration={configuration}486data={data}487state={state}488short489/>490{(state == "running" ||491state == "stopping" ||492state == "starting") && (493<DetailedState494id={id}495project_id={project_id}496detailed_state={detailed_state}497color={color}498configuration={configuration}499/>500)}501<ShowError502error={error}503setError={setError}504style={{ margin: "15px 0", width: "100%" }}505/>506</div>507}508/>509{body}510</Card>511);512}513514export function useServer({ id, project_id }) {515const computeServers = useTypedRedux({ project_id }, "compute_servers");516const server = useMemo(() => {517return computeServers?.get(`${id}`)?.toJS();518}, [id, project_id, computeServers]);519520return server;521}522523export function EditModal({ project_id, id, close }) {524const account_id = useTypedRedux("account", "account_id");525const server = useServer({ id, project_id });526if (account_id == null || server == null) {527return null;528}529return (530<ComputeServer531modalOnly532editable={account_id == server.account_id}533server={server}534close={close}535/>536);537}538539function BackendError({ error, id, project_id }) {540if (!error || !id) {541return null;542}543return (544<div style={{ marginTop: "10px", display: "flex", fontWeight: "normal" }}>545<ShowError546error={error}547style={{ margin: "15px 0", width: "100%" }}548setError={async () => {549try {550await webapp_client.async_query({551query: {552compute_servers: {553id,554project_id,555error: "",556},557},558});559} catch (err) {560console.warn(err);561}562}}563/>564</div>565);566}567568569