Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/compute/menu.tsx
1503 views
1
/*
2
Compute server hamburger menu.
3
*/
4
5
import type { MenuProps } from "antd";
6
import { Button, Dropdown, Spin } from "antd";
7
import { useMemo, useState } from "react";
8
import { redux, useTypedRedux } from "@cocalc/frontend/app-framework";
9
import { A, Icon } from "@cocalc/frontend/components";
10
import ShowError from "@cocalc/frontend/components/error";
11
import {
12
setServerConfiguration,
13
setTemplate,
14
} from "@cocalc/frontend/compute/api";
15
import openSupportTab from "@cocalc/frontend/support/open";
16
import CloneModal from "./clone";
17
import { EditModal } from "./compute-server";
18
import { LogModal } from "./compute-server-log";
19
import getTitle from "./get-title";
20
import { AppLauncherModal } from "./launcher";
21
import { SerialLogModal } from "./serial-port-output";
22
import { TitleColorModal } from "./title-color";
23
import { AutomaticShutdownModal } from "./automatic-shutdown";
24
25
function getServer({ id, project_id }) {
26
return redux
27
.getProjectStore(project_id)
28
.getIn(["compute_servers", `${id}`])
29
?.toJS();
30
}
31
32
export function getApps(image) {
33
const IMAGES = redux.getStore("customize").get("compute_servers_images");
34
if (IMAGES == null || typeof IMAGES == "string") {
35
// string when error
36
return {};
37
}
38
let apps =
39
IMAGES.getIn([image, "apps"])?.toJS() ??
40
IMAGES.getIn(["defaults", "apps"])?.toJS() ??
41
{};
42
if (IMAGES.getIn([image, "jupyterKernels"]) === false) {
43
apps = { ...apps, jupyterlab: undefined };
44
}
45
if (apps["xpra"]) {
46
if (!apps["xpra"].tip) {
47
apps["xpra"].tip =
48
"Launch an X11 Linux Graphical Desktop environment running directly on the compute server.";
49
}
50
}
51
return apps;
52
}
53
54
function getItems({
55
id,
56
project_id,
57
account_id,
58
isAdmin,
59
}: {
60
id: number;
61
project_id: string;
62
account_id: string;
63
title?: string;
64
color?: string;
65
isAdmin?: boolean;
66
}): MenuProps["items"] {
67
if (!id) {
68
return [];
69
}
70
const server = getServer({ id, project_id });
71
if (server == null) {
72
return [
73
{
74
key: "loading",
75
label: (
76
<>
77
Loading... <Spin />
78
</>
79
),
80
disabled: true,
81
},
82
];
83
}
84
const apps = getApps(server.configuration?.image ?? "defaults");
85
const is_owner = account_id == server.account_id;
86
87
// will be used for start/stop/etc.
88
// const is_collab = is_owner || server.configuration?.allowCollaboratorControl;
89
90
const titleAndColor = {
91
key: "title-color",
92
icon: <Icon name="colors" />,
93
disabled: !is_owner,
94
label: "Edit Title and Color",
95
};
96
const automaticShutdown = {
97
key: "automatic-shutdown",
98
icon: <Icon name="stopwatch" />,
99
disabled: server.cloud == "onprem",
100
label: "Automatic Shutdown & Health Check",
101
};
102
const jupyterlab = {
103
key: "top-jupyterlab",
104
label: "JupyterLab",
105
icon: <Icon name="jupyter" />,
106
disabled:
107
apps["jupyterlab"] == null ||
108
server.state != "running" ||
109
!server.data?.externalIp,
110
};
111
const vscode = {
112
key: "top-vscode",
113
label: "VS Code",
114
icon: <Icon name="vscode" />,
115
disabled:
116
apps["vscode"] == null ||
117
server.state != "running" ||
118
!server.data?.externalIp,
119
};
120
const xpra = {
121
key: "xpra",
122
label: "X11 Desktop",
123
icon: <Icon name="desktop" />,
124
disabled:
125
apps["xpra"] == null ||
126
server.state != "running" ||
127
!server.data?.externalIp,
128
};
129
130
const optionItems: (
131
| { key: string; label; icon; disabled?: boolean }
132
| { type: "divider" }
133
)[] = [
134
// {
135
// key: "dns",
136
// label: "DNS...",
137
// icon: <Icon name="network" />,
138
// },
139
{
140
key: "allowCollaboratorControl",
141
label: "Collaborator Control",
142
icon: (
143
<Icon
144
style={{ fontSize: "12pt" }}
145
name={
146
server.configuration?.allowCollaboratorControl
147
? "check-square"
148
: "square"
149
}
150
/>
151
),
152
},
153
{
154
key: "ephemeral",
155
label: "Ephemeral",
156
icon: (
157
<Icon
158
style={{ fontSize: "12pt" }}
159
name={server.configuration?.ephemeral ? "check-square" : "square"}
160
/>
161
),
162
},
163
{
164
type: "divider",
165
},
166
];
167
if (server.cloud == "google-cloud") {
168
optionItems.push({
169
key: "autoRestart",
170
label: "Automatically Restart",
171
disabled: server.cloud != "google-cloud",
172
icon: (
173
<Icon
174
style={{ fontSize: "12pt" }}
175
name={server.configuration?.autoRestart ? "check-square" : "square"}
176
/>
177
),
178
});
179
optionItems.push({
180
key: "enableNestedVirtualization",
181
label: "Nested Virtualization",
182
disabled:
183
server.cloud != "google-cloud" || server.state != "deprovisioned",
184
icon: (
185
<Icon
186
style={{ fontSize: "12pt" }}
187
name={
188
server.configuration?.enableNestedVirtualization
189
? "check-square"
190
: "square"
191
}
192
/>
193
),
194
});
195
}
196
if (isAdmin) {
197
if (optionItems[optionItems.length - 1]?.["type"] != "divider") {
198
optionItems.push({
199
type: "divider",
200
});
201
}
202
optionItems.push({
203
key: "template",
204
label: "Use as Template",
205
icon: (
206
<Icon
207
style={{ fontSize: "12pt" }}
208
name={server.template?.enabled ? "check-square" : "square"}
209
/>
210
),
211
});
212
}
213
214
const options = {
215
key: "options",
216
label: "Options",
217
disabled: !is_owner,
218
icon: <Icon name="gears" />,
219
children: [
220
{
221
key: "run-app-on",
222
type: "group",
223
label: "Configure Server",
224
children: optionItems,
225
},
226
],
227
};
228
229
const help = {
230
key: "help",
231
icon: <Icon name="question-circle" />,
232
label: "Help",
233
children: [
234
{
235
key: "documentation",
236
icon: <Icon name="question-circle" />,
237
label: (
238
<A href="https://doc.cocalc.com/compute_server.html">Documentation</A>
239
),
240
},
241
{
242
key: "support",
243
icon: <Icon name="medkit" />,
244
label: "Support",
245
},
246
{
247
key: "videos",
248
icon: <Icon name="youtube" style={{ color: "red" }} />,
249
label: (
250
<A href="https://www.youtube.com/playlist?list=PLOEk1mo1p5tJmEuAlou4JIWZFH7IVE2PZ">
251
Videos
252
</A>
253
),
254
},
255
{
256
type: "divider",
257
},
258
{
259
key: "dedicated",
260
icon: <Icon name="bank" />,
261
label: "Dedicated Always On Server for 6+ Months...",
262
},
263
],
264
};
265
266
const settings = {
267
key: "settings",
268
icon: <Icon name="settings" />,
269
label: is_owner ? "Settings" : "Details...",
270
};
271
272
const clone = {
273
key: "clone",
274
icon: <Icon name="copy" />,
275
label: "Clone Server Configuration",
276
};
277
278
return [
279
titleAndColor,
280
// {
281
// type: "divider",
282
// },
283
// {
284
// key: "new-jupyter",
285
// label: "New Jupyter Notebook",
286
// icon: <Icon name="jupyter" />,
287
// disabled: server.state != "running",
288
// },
289
// {
290
// key: "new-terminal",
291
// label: "New Linux Terminal",
292
// icon: <Icon name="terminal" />,
293
// disabled: server.state != "running",
294
// },
295
{
296
type: "divider",
297
},
298
jupyterlab,
299
vscode,
300
xpra,
301
{
302
type: "divider",
303
},
304
settings,
305
automaticShutdown,
306
//spendLimit,
307
options,
308
{
309
type: "divider",
310
},
311
{
312
key: "control-log",
313
icon: <Icon name="history" />,
314
label: "Compute Server Log",
315
},
316
{
317
key: "serial-console-log",
318
disabled:
319
server.cloud != "google-cloud" ||
320
server.state == "off" ||
321
server.state == "deprovisioned",
322
icon: <Icon name="laptop" />,
323
label: "Serial Console",
324
},
325
{
326
type: "divider",
327
},
328
clone,
329
{
330
type: "divider",
331
},
332
help,
333
// {
334
// key: "control",
335
// icon: <Icon name="wrench" />,
336
// label: "Control",
337
// children: [
338
// {
339
// key: "start",
340
// icon: <Icon name="play" />,
341
// label: "Start",
342
// },
343
// {
344
// key: "suspend",
345
// icon: <Icon name="pause" />,
346
// label: "Suspend",
347
// },
348
// {
349
// key: "stop",
350
// icon: <Icon name="stop" />,
351
// label: "Stop",
352
// },
353
// {
354
// key: "reboot",
355
// icon: <Icon name="redo" />,
356
// label: "Hard Reboot",
357
// danger: true,
358
// },
359
// {
360
// key: "deprovision",
361
// icon: <Icon name="trash" />,
362
// label: "Deprovision",
363
// danger: true,
364
// },
365
// {
366
// key: "delete",
367
// icon: <Icon name="trash" />,
368
// label: "Delete",
369
// danger: true,
370
// },
371
// ],
372
// },
373
// {
374
// key: "files",
375
// label: "Files",
376
// icon: <Icon name="files" />,
377
// children: [
378
// {
379
// key: "explorer",
380
// label: "Explorer",
381
// icon: <Icon name="folder-open" />,
382
// },
383
// {
384
// type: "divider",
385
// },
386
// {
387
// key: "sync",
388
// icon: <Icon name="sync" />,
389
// label: "Sync Files",
390
// },
391
// {
392
// key: "disk",
393
// icon: <Icon name="disk-drive" />,
394
// label: "Disk Space",
395
// },
396
// {
397
// type: "divider",
398
// },
399
// { key: "file1", label: "foo.ipynb", icon: <Icon name="jupyter" /> },
400
// { key: "file2", label: "tmp/a.term", icon: <Icon name="terminal" /> },
401
// {
402
// key: "file3",
403
// label: "compoute-server-38/foo-bar.ipynb",
404
// icon: <Icon name="jupyter" />,
405
// },
406
// {
407
// key: "file4",
408
// label: "compoute-server-38/example.ipynb",
409
// icon: <Icon name="jupyter" />,
410
// },
411
// ],
412
// },
413
];
414
}
415
416
export default function Menu({
417
id,
418
project_id,
419
style,
420
fontSize,
421
size,
422
}: {
423
id: number;
424
project_id: string;
425
style?;
426
fontSize?;
427
size?;
428
}) {
429
const [error, setError] = useState<string>("");
430
const [open, setOpen] = useState<boolean>(false);
431
const account_id = useTypedRedux("account", "account_id");
432
const [modal, setModal] = useState<any>(null);
433
const close = () => setModal(null);
434
const [title, setTitle] = useState<{
435
title: string;
436
color: string;
437
project_specific_id: number;
438
} | null>(null);
439
const isAdmin = useTypedRedux("account", "is_admin");
440
const { items, onClick } = useMemo(() => {
441
if (!open) {
442
return { onClick: () => {}, items: [] };
443
}
444
445
(async () => {
446
setTitle(await getTitle(id));
447
})();
448
return {
449
items: getItems({ id, project_id, account_id, isAdmin }),
450
onClick: async (obj) => {
451
setOpen(false);
452
let cmd = obj.key.startsWith("top-") ? obj.key.slice(4) : obj.key;
453
switch (cmd) {
454
case "control-log":
455
setModal(<LogModal id={id} close={close} />);
456
break;
457
458
case "settings":
459
setModal(
460
<EditModal id={id} project_id={project_id} close={close} />,
461
);
462
break;
463
464
case "clone":
465
setModal(<CloneModal id={id} close={close} />);
466
break;
467
468
case "serial-console-log":
469
setModal(
470
<SerialLogModal
471
id={id}
472
title={title?.title ?? ""}
473
close={close}
474
/>,
475
);
476
break;
477
478
case "vscode":
479
case "jupyterlab":
480
case "xpra":
481
setModal(
482
<AppLauncherModal
483
name={cmd}
484
id={id}
485
project_id={project_id}
486
close={close}
487
/>,
488
);
489
break;
490
491
case "title-color":
492
setModal(
493
<TitleColorModal id={id} project_id={project_id} close={close} />,
494
);
495
break;
496
497
case "automatic-shutdown":
498
setModal(
499
<AutomaticShutdownModal
500
id={id}
501
project_id={project_id}
502
close={close}
503
/>,
504
);
505
break;
506
507
case "ephemeral":
508
case "allowCollaboratorControl":
509
case "autoRestart":
510
case "enableNestedVirtualization":
511
case "template":
512
const server = getServer({ id, project_id });
513
if (server != null) {
514
try {
515
if (obj.key == "template") {
516
await setTemplate({
517
id,
518
template: { enabled: !server.template?.enabled },
519
});
520
} else {
521
await setServerConfiguration({
522
id,
523
configuration: {
524
[cmd]: !server.configuration?.[cmd],
525
},
526
});
527
}
528
} catch (err) {
529
setError(`${err}`);
530
}
531
}
532
break;
533
534
case "documentation":
535
case "videos":
536
// click opens new tab anyways
537
break;
538
539
case "support":
540
openSupportTab({
541
type: "question",
542
subject: `Compute Server (Global Id: ${id}; Project Specific Id: ${title?.project_specific_id})`,
543
body: `I am using a compute server, and have a question...`,
544
});
545
break;
546
547
case "dedicated":
548
openSupportTab({
549
type: "question",
550
subject: `Compute Server (Global Id: ${id}; Project Specific Id: ${title?.project_specific_id})`,
551
body: `I need a dedicated always on compute server for at least 6 months, and am interested in significant discounts.\nI would love to tell you about my problem, and see if CoCalc can help!`,
552
});
553
break;
554
555
default:
556
setError(`not implemented -- '${cmd}'`);
557
}
558
},
559
};
560
}, [id, project_id, open, title]);
561
562
return (
563
<div style={style}>
564
<Dropdown
565
menu={{ items, onClick }}
566
trigger={["click"]}
567
onOpenChange={setOpen}
568
>
569
<Button type="text" size={size}>
570
<Icon
571
name="ellipsis"
572
style={{ fontSize: fontSize ?? "15pt", color: "#000" }}
573
rotate="90"
574
/>
575
</Button>
576
</Dropdown>
577
{modal}
578
<ShowError
579
error={error}
580
setError={setError}
581
style={{
582
fontWeight: "normal",
583
whiteSpace: "normal",
584
position: "absolute",
585
right: 0,
586
maxWidth: "500px",
587
zIndex: 1000,
588
boxShadow: "2px 2px 2px 2px #bbb",
589
}}
590
/>
591
</div>
592
);
593
}
594
595