CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/compute/compute-server.tsx
Views: 687
1
import { Button, Card, Divider, Modal, Popconfirm, Spin } from "antd";
2
import { CSSProperties, useMemo, useState } from "react";
3
4
import { useTypedRedux } from "@cocalc/frontend/app-framework";
5
import { Icon } from "@cocalc/frontend/components";
6
import ShowError from "@cocalc/frontend/components/error";
7
import { CancelText } from "@cocalc/frontend/i18n/components";
8
import { webapp_client } from "@cocalc/frontend/webapp-client";
9
import type { ComputeServerUserInfo } from "@cocalc/util/db-schema/compute-servers";
10
import { COLORS } from "@cocalc/util/theme";
11
import getActions from "./action";
12
import { deleteServer, undeleteServer } from "./api";
13
import Cloud from "./cloud";
14
import Color, { randomColor } from "./color";
15
import ComputeServerLog from "./compute-server-log";
16
import { Docs } from "./compute-servers";
17
import Configuration from "./configuration";
18
import CurrentCost from "./current-cost";
19
import Description from "./description";
20
import DetailedState from "./detailed-state";
21
import Launcher from "./launcher";
22
import Menu from "./menu";
23
import { DisplayImage } from "./select-image";
24
import SerialPortOutput from "./serial-port-output";
25
import State from "./state";
26
import Title from "./title";
27
28
interface Server1 extends Omit<ComputeServerUserInfo, "id"> {
29
id?: number;
30
}
31
32
interface Controls {
33
setShowDeleted?: (showDeleted: boolean) => void;
34
onTitleChange?;
35
onColorChange?;
36
onCloudChange?;
37
onConfigurationChange?;
38
}
39
40
interface Props {
41
server: Server1;
42
editable?: boolean;
43
style?: CSSProperties;
44
controls?: Controls;
45
modalOnly?: boolean;
46
close?: () => void;
47
}
48
export const currentlyEditing = {
49
id: 0,
50
};
51
52
export default function ComputeServer({
53
server,
54
style,
55
editable,
56
controls,
57
modalOnly,
58
close,
59
}: Props) {
60
const {
61
id,
62
project_specific_id,
63
title,
64
color = randomColor(),
65
state,
66
state_changed,
67
detailed_state,
68
cloud,
69
cost_per_hour,
70
purchase_id,
71
configuration,
72
data,
73
deleted,
74
error: backendError,
75
project_id,
76
account_id,
77
} = server;
78
79
const {
80
setShowDeleted,
81
onTitleChange,
82
onColorChange,
83
onCloudChange,
84
onConfigurationChange,
85
} = controls ?? {};
86
87
const [error, setError] = useState<string>("");
88
const [edit, setEdit0] = useState<boolean>(id == null || !!modalOnly);
89
const setEdit = (edit) => {
90
setEdit0(edit);
91
if (!edit && close != null) {
92
close();
93
}
94
if (edit) {
95
currentlyEditing.id = id ?? 0;
96
} else {
97
currentlyEditing.id = 0;
98
}
99
};
100
101
if (id == null && modalOnly) {
102
return <Spin />;
103
}
104
105
let actions: JSX.Element[] | undefined = undefined;
106
if (id != null) {
107
actions = getActions({
108
id,
109
state,
110
editable,
111
setError,
112
configuration,
113
editModal: false,
114
type: "text",
115
project_id,
116
});
117
if (editable || configuration?.allowCollaboratorControl) {
118
actions.push(
119
<Button
120
key="edit"
121
type="text"
122
onClick={() => {
123
setEdit(!edit);
124
}}
125
>
126
{editable ? (
127
<>
128
<Icon name="settings" /> Settings
129
</>
130
) : (
131
<>
132
<Icon name="eye" /> Settings
133
</>
134
)}
135
</Button>,
136
);
137
}
138
if (deleted && editable && id) {
139
actions.push(
140
<Button
141
key="undelete"
142
type="text"
143
onClick={async () => {
144
try {
145
await undeleteServer(id);
146
} catch (err) {
147
setError(`${err}`);
148
return;
149
}
150
setShowDeleted?.(false);
151
}}
152
>
153
<Icon name="trash" /> Undelete
154
</Button>,
155
);
156
}
157
158
// TODO: for later
159
// actions.push(
160
// <div>
161
// <Icon name="clone" /> Clone
162
// </div>,
163
// );
164
}
165
166
const table = (
167
<div>
168
<Divider>
169
<Icon
170
name="cloud-dev"
171
style={{ fontSize: "16pt", marginRight: "15px" }}
172
/>{" "}
173
Title, Color, and Cloud
174
</Divider>
175
<div
176
style={{
177
marginTop: "15px",
178
display: "flex",
179
width: "100%",
180
justifyContent: "space-between",
181
}}
182
>
183
<Title
184
title={title}
185
id={id}
186
editable={editable}
187
setError={setError}
188
onChange={onTitleChange}
189
/>
190
<Color
191
color={color}
192
id={id}
193
editable={editable}
194
setError={setError}
195
onChange={onColorChange}
196
style={{
197
marginLeft: "10px",
198
}}
199
/>
200
<Cloud
201
cloud={cloud}
202
state={state}
203
editable={editable}
204
setError={setError}
205
setCloud={onCloudChange}
206
id={id}
207
style={{ marginTop: "-2.5px", marginLeft: "10px" }}
208
/>
209
</div>
210
<div style={{ color: "#888", marginTop: "5px" }}>
211
Change the title and color at any time.
212
</div>
213
<Divider>
214
<Icon name="gears" style={{ fontSize: "16pt", marginRight: "15px" }} />{" "}
215
Configuration
216
</Divider>
217
<Configuration
218
editable={editable}
219
state={state}
220
id={id}
221
project_id={project_id}
222
configuration={configuration}
223
data={data}
224
onChange={onConfigurationChange}
225
setCloud={onCloudChange}
226
template={server.template}
227
/>
228
</div>
229
);
230
231
const buttons = (
232
<div>
233
<div style={{ width: "100%", display: "flex" }}>
234
<Button onClick={() => setEdit(false)} style={{ marginRight: "5px" }}>
235
<Icon name="save" /> {editable ? "Save" : "Close"}
236
</Button>
237
<div style={{ marginRight: "5px" }}>
238
{getActions({
239
id,
240
state,
241
editable,
242
setError,
243
configuration,
244
editModal: edit,
245
type: undefined,
246
project_id,
247
})}
248
</div>{" "}
249
{editable &&
250
id &&
251
(deleted || state == "deprovisioned") &&
252
(deleted ? (
253
<Button
254
key="undelete"
255
onClick={async () => {
256
try {
257
await undeleteServer(id);
258
} catch (err) {
259
setError(`${err}`);
260
return;
261
}
262
setShowDeleted?.(false);
263
}}
264
>
265
<Icon name="trash" /> Undelete
266
</Button>
267
) : (
268
<Popconfirm
269
key="delete"
270
title={"Delete this compute server?"}
271
description={
272
<div style={{ width: "400px" }}>
273
Are you sure you want to delete this compute server?
274
{state != "deprovisioned" && (
275
<b>WARNING: Any data on the boot disk will be deleted.</b>
276
)}
277
</div>
278
}
279
onConfirm={async () => {
280
setEdit(false);
281
await deleteServer(id);
282
}}
283
okText="Yes"
284
cancelText={<CancelText />}
285
>
286
<Button key="trash" danger>
287
<Icon name="trash" /> Delete
288
</Button>
289
</Popconfirm>
290
))}
291
</div>
292
<BackendError error={backendError} id={id} project_id={project_id} />
293
</div>
294
);
295
296
const body =
297
id == null ? (
298
table
299
) : (
300
<Modal
301
open={edit}
302
destroyOnClose
303
width={"900px"}
304
onCancel={() => setEdit(false)}
305
title={
306
<>
307
{buttons}
308
<Divider />
309
<Icon name="edit" style={{ marginRight: "15px" }} />{" "}
310
{editable ? "Edit" : ""} Compute Server With Id=
311
{project_specific_id}
312
</>
313
}
314
footer={
315
<>
316
<div style={{ display: "flex" }}>
317
{buttons}
318
<Docs key="docs" style={{ flex: 1, marginTop: "5px" }} />
319
</div>
320
</>
321
}
322
>
323
<div
324
style={{ fontSize: "12pt", color: COLORS.GRAY_M, display: "flex" }}
325
>
326
<Description
327
account_id={account_id}
328
cloud={cloud}
329
data={data}
330
configuration={configuration}
331
state={state}
332
/>
333
<div style={{ flex: 1 }} />
334
<State
335
style={{ marginRight: "5px" }}
336
state={state}
337
data={data}
338
state_changed={state_changed}
339
editable={editable}
340
id={id}
341
account_id={account_id}
342
configuration={configuration}
343
cost_per_hour={cost_per_hour}
344
purchase_id={purchase_id}
345
/>
346
</div>
347
{table}
348
</Modal>
349
);
350
351
if (modalOnly) {
352
return body;
353
}
354
355
return (
356
<Card
357
style={{
358
opacity: deleted ? 0.5 : undefined,
359
width: "100%",
360
minWidth: "500px",
361
border: `0.5px solid ${color ?? "#f0f0f0"}`,
362
borderRight: `10px solid ${color ?? "#aaa"}`,
363
borderLeft: `10px solid ${color ?? "#aaa"}`,
364
...style,
365
}}
366
actions={actions}
367
>
368
<Card.Meta
369
avatar={
370
<div style={{ width: "64px", marginBottom: "-20px" }}>
371
<Icon
372
name={cloud == "onprem" ? "global" : "server"}
373
style={{ fontSize: "30px", color: color ?? "#666" }}
374
/>
375
{id != null && (
376
<div style={{ color: "#888" }}>Id: {project_specific_id}</div>
377
)}
378
<div style={{ display: "flex", marginLeft: "-20px" }}>
379
{id != null && <ComputeServerLog id={id} />}
380
{id != null &&
381
configuration.cloud == "google-cloud" &&
382
(state == "starting" ||
383
state == "stopping" ||
384
state == "running") && (
385
<SerialPortOutput
386
id={id}
387
title={title}
388
style={{ marginLeft: "-5px" }}
389
/>
390
)}
391
</div>
392
{id != null && (
393
<div style={{ marginLeft: "-15px" }}>
394
<CurrentCost state={state} cost_per_hour={cost_per_hour} />
395
</div>
396
)}
397
{state == "running" && !!data?.externalIp && (
398
<Launcher
399
style={{ marginLeft: "-24px" }}
400
configuration={configuration}
401
data={data}
402
compute_server_id={id}
403
project_id={project_id}
404
/>
405
)}
406
</div>
407
}
408
title={
409
id == null ? undefined : (
410
<div
411
style={{
412
display: "flex",
413
width: "100%",
414
justifyContent: "space-between",
415
color: "#666",
416
borderBottom: `1px solid ${color}`,
417
padding: "0 10px 5px 0",
418
}}
419
>
420
<div
421
style={{
422
textOverflow: "ellipsis",
423
overflow: "hidden",
424
flex: 1,
425
}}
426
>
427
<State
428
data={data}
429
state={state}
430
state_changed={state_changed}
431
editable={editable}
432
id={id}
433
account_id={account_id}
434
configuration={configuration}
435
cost_per_hour={cost_per_hour}
436
purchase_id={purchase_id}
437
/>
438
</div>
439
<Title
440
title={title}
441
editable={false}
442
style={{
443
textOverflow: "ellipsis",
444
overflow: "hidden",
445
flex: 1,
446
}}
447
/>
448
<div
449
style={{
450
textOverflow: "ellipsis",
451
overflow: "hidden",
452
flex: 1,
453
}}
454
>
455
<DisplayImage configuration={configuration} />
456
</div>
457
<div
458
style={{
459
textOverflow: "ellipsis",
460
overflow: "hidden",
461
textAlign: "right",
462
}}
463
>
464
<Cloud cloud={cloud} state={state} editable={false} id={id} />
465
</div>
466
<div>
467
<Menu
468
style={{ float: "right" }}
469
id={id}
470
project_id={project_id}
471
/>
472
</div>
473
</div>
474
)
475
}
476
description={
477
<div style={{ color: "#666" }}>
478
<BackendError
479
error={backendError}
480
id={id}
481
project_id={project_id}
482
/>
483
<Description
484
account_id={account_id}
485
cloud={cloud}
486
configuration={configuration}
487
data={data}
488
state={state}
489
short
490
/>
491
{(state == "running" ||
492
state == "stopping" ||
493
state == "starting") && (
494
<DetailedState
495
id={id}
496
project_id={project_id}
497
detailed_state={detailed_state}
498
color={color}
499
configuration={configuration}
500
/>
501
)}
502
<ShowError
503
error={error}
504
setError={setError}
505
style={{ margin: "15px 0", width: "100%" }}
506
/>
507
</div>
508
}
509
/>
510
{body}
511
</Card>
512
);
513
}
514
515
export function useServer({ id, project_id }) {
516
const computeServers = useTypedRedux({ project_id }, "compute_servers");
517
const server = useMemo(() => {
518
return computeServers?.get(`${id}`)?.toJS();
519
}, [id, project_id, computeServers]);
520
521
return server;
522
}
523
524
export function EditModal({ project_id, id, close }) {
525
const account_id = useTypedRedux("account", "account_id");
526
const server = useServer({ id, project_id });
527
if (account_id == null || server == null) {
528
return null;
529
}
530
return (
531
<ComputeServer
532
modalOnly
533
editable={account_id == server.account_id}
534
server={server}
535
close={close}
536
/>
537
);
538
}
539
540
function BackendError({ error, id, project_id }) {
541
if (!error || !id) {
542
return null;
543
}
544
return (
545
<div style={{ marginTop: "10px", display: "flex", fontWeight: "normal" }}>
546
<ShowError
547
error={error}
548
style={{ margin: "15px 0", width: "100%" }}
549
setError={async () => {
550
try {
551
await webapp_client.async_query({
552
query: {
553
compute_servers: {
554
id,
555
project_id,
556
error: "",
557
},
558
},
559
});
560
} catch (err) {
561
console.warn(err);
562
}
563
}}
564
/>
565
</div>
566
);
567
}
568
569