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