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