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