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-servers.tsx
Views: 687
1
import { A } from "@cocalc/frontend/components/A";
2
import ComputeServer, { currentlyEditing } from "./compute-server";
3
import CreateComputeServer from "./create-compute-server";
4
import { useTypedRedux } from "@cocalc/frontend/app-framework";
5
import { cmp, plural } from "@cocalc/util/misc";
6
import { availableClouds } from "./config";
7
import {
8
Alert,
9
Button,
10
Input,
11
Card,
12
Checkbox,
13
Radio,
14
Switch,
15
Tooltip,
16
} from "antd";
17
import { useEffect, useState } from "react";
18
const { Search } = Input;
19
import { search_match, search_split } from "@cocalc/util/misc";
20
import {
21
SortableList,
22
SortableItem,
23
DragHandle,
24
} from "@cocalc/frontend/components/sortable-list";
25
import { webapp_client } from "@cocalc/frontend/webapp-client";
26
import { Icon } from "@cocalc/frontend/components";
27
import { STATE_TO_NUMBER } from "@cocalc/util/db-schema/compute-servers";
28
import {
29
get_local_storage,
30
set_local_storage,
31
} from "@cocalc/frontend/misc/local-storage";
32
33
export function Docs({ style }: { style? }) {
34
return (
35
<A style={style} href="https://doc.cocalc.com/compute_server.html">
36
<Icon name="external-link" /> Docs
37
</A>
38
);
39
}
40
41
export default function ComputeServers({ project_id }: { project_id: string }) {
42
const computeServers = useTypedRedux({ project_id }, "compute_servers");
43
const account_id = useTypedRedux("account", "account_id");
44
const [help, setHelp] = useState<boolean>(false);
45
const supported = availableClouds().length > 0;
46
47
return (
48
<div style={{ paddingRight: "15px" }}>
49
{supported && (
50
<>
51
<Switch
52
checkedChildren={"Help"}
53
unCheckedChildren={"Help"}
54
style={{ float: "right" }}
55
checked={help}
56
onChange={setHelp}
57
/>
58
{help && (
59
<div style={{ fontSize: "12pt" }}>
60
<A href="https://doc.cocalc.com/compute_server.html">
61
Compute Servers
62
</A>{" "}
63
provide <strong>affordable GPUs</strong>,{" "}
64
<strong>high end VM's</strong>, <strong>root access</strong>,{" "}
65
<strong>Docker</strong> and <strong>Kubernetes</strong> on CoCalc.
66
Compute servers are virtual machines where you and your
67
collaborators can run Jupyter notebooks, terminals and web servers
68
collaboratively, with full access to your project.
69
<ul>
70
<li>
71
<Icon name="ubuntu" /> Full root and internet access on an
72
Ubuntu Linux server,
73
</li>
74
<li>
75
<Icon name="server" /> Dedicated GPUs, hundreds of very fast
76
vCPUs, and thousands of GB of RAM
77
</li>
78
<li>
79
{" "}
80
<Icon name="dns" /> Public ip address and (optional) domain
81
name
82
</li>
83
<li>
84
{" "}
85
<Icon name="sync" /> Files sync'd with the project
86
</li>
87
</ul>
88
<h3>Getting Started</h3>
89
<ul>
90
<li>Create a compute server below and start it.</li>
91
<li>
92
Once your compute server is running, select it in the upper
93
left of any terminal or Jupyter notebook in this project.{" "}
94
</li>
95
<li>
96
Compute servers stay running independently of your project, so
97
if you need to restart your project for any reason, that
98
doesn't impact running notebooks and terminals on your compute
99
servers.
100
</li>
101
<li>
102
A compute server belongs to the user who created it, and they
103
will be billed by the second for usage. Select "Allow
104
Collaborator Control" to allow project collaborators to start
105
and stop a compute server. Project collaborators can always
106
connect to running compute servers.
107
</li>
108
<li>
109
You can ssh to user@ at the ip address of your compute server
110
using any{" "}
111
<A href="https://doc.cocalc.com/project-settings.html#ssh-keys">
112
project
113
</A>{" "}
114
or{" "}
115
<A href="https://doc.cocalc.com/account/ssh.html">
116
account public ssh keys
117
</A>{" "}
118
that has access to this project (wait about 30 seconds after
119
you add keys). If you start a web service on any port P on
120
your compute server, type{" "}
121
<code>ssh -L P:localhost:P user@ip_address</code>
122
on your laptop, and you can connect to that web service on
123
localhost on your laptop. Also ports 80 and 443 are always
124
publicly visible (so no port forwarding is required). If you
125
connect to root@ip_address, you are root on the underlying
126
virtual machine outside of any Docker container; if you
127
connect to user@ip_address, you are the user inside the main
128
compute container, with full access to your chosen image --
129
this is the same as opening a terminal and selecting the
130
compute server.
131
</li>
132
</ul>
133
<h3>Click this Button ↓</h3>
134
</div>
135
)}
136
</>
137
)}
138
{supported ? (
139
<ComputeServerTable
140
computeServers={computeServers}
141
project_id={project_id}
142
account_id={account_id}
143
/>
144
) : (
145
<b>No Compute Server Clouds are currently enabled.</b>
146
)}
147
</div>
148
);
149
}
150
151
function computeServerToSearch(computeServers, id) {
152
return JSON.stringify(computeServers.get(id)).toLowerCase();
153
}
154
155
function ComputeServerTable({
156
computeServers: computeServers0,
157
project_id,
158
account_id,
159
}) {
160
const [computeServers, setComputeServers] = useState<any>(computeServers0);
161
useEffect(() => {
162
setComputeServers(computeServers0);
163
}, [computeServers0]);
164
165
const [search, setSearch0] = useState<string>(
166
(get_local_storage(`${project_id}-compute-server-search`) ?? "") as string,
167
);
168
const setSearch = (value) => {
169
setSearch0(value);
170
set_local_storage(`${project_id}-compute-server-search`, value);
171
};
172
const [showDeleted, setShowDeleted] = useState<boolean>(false);
173
const [sortBy, setSortBy] = useState<
174
"id" | "title" | "custom" | "edited" | "state"
175
>(
176
(get_local_storage(`${project_id}-compute-server-sort`) ?? "custom") as any,
177
);
178
179
if (!computeServers || computeServers.size == 0) {
180
return (
181
<div style={{ textAlign: "center" }}>
182
<CreateComputeServer
183
project_id={project_id}
184
onCreate={() => setSearch("")}
185
/>
186
</div>
187
);
188
}
189
const search_words = search_split(search.toLowerCase());
190
const ids: number[] = [];
191
let numDeleted = 0;
192
let numSkipped = 0;
193
for (const [id] of computeServers) {
194
if (currentlyEditing.id == id) {
195
// always include the one that is currently being edited. We wouldn't want,
196
// e.g., changing the title shouldn't make the editing modal vanish!
197
ids.push(id);
198
continue;
199
}
200
const isDeleted = !!computeServers.getIn([id, "deleted"]);
201
if (isDeleted) {
202
numDeleted += 1;
203
}
204
if (showDeleted != isDeleted) {
205
continue;
206
}
207
if (search_words.length > 0) {
208
if (
209
!search_match(computeServerToSearch(computeServers, id), search_words)
210
) {
211
numSkipped += 1;
212
continue;
213
}
214
}
215
ids.push(id);
216
}
217
ids.sort((a, b) => {
218
if (a == b) {
219
return 0;
220
}
221
const cs_a = computeServers.get(a);
222
const cs_b = computeServers.get(b);
223
if (sortBy == "custom") {
224
return -cmp(
225
cs_a.get("position") ?? cs_a.get("id"),
226
cs_b.get("position") ?? cs_b.get("id"),
227
);
228
} else if (sortBy == "title") {
229
return cmp(
230
cs_a.get("title")?.toLowerCase(),
231
cs_b.get("title")?.toLowerCase(),
232
);
233
} else if (sortBy == "id") {
234
// sort by id
235
return -cmp(cs_a.get("id"), cs_b.get("id"));
236
} else if (sortBy == "edited") {
237
return -cmp(cs_a.get("last_edited") ?? 0, cs_b.get("last_edited") ?? 0);
238
} else if (sortBy == "state") {
239
const a = cmp(
240
STATE_TO_NUMBER[cs_a.get("state")] ?? 100,
241
STATE_TO_NUMBER[cs_b.get("state")] ?? 100,
242
);
243
if (a == 0) {
244
return -cmp(
245
cs_a.get("position") ?? cs_a.get("id"),
246
cs_b.get("position") ?? cs_b.get("id"),
247
);
248
}
249
return a;
250
} else {
251
return -cmp(cs_a.get("id"), cs_b.get("id"));
252
}
253
});
254
255
const renderItem = (id) => {
256
const server = computeServers.get(id).toJS();
257
258
return (
259
<div style={{ display: "flex" }}>
260
{sortBy == "custom" && (
261
<div
262
style={{
263
fontSize: "20px",
264
color: "#888",
265
display: "flex",
266
justifyContent: "center",
267
flexDirection: "column",
268
marginRight: "5px",
269
}}
270
>
271
<DragHandle id={id} />
272
</div>
273
)}
274
<ComputeServer
275
server={server}
276
style={{ marginBottom: "10px" }}
277
key={`${id}`}
278
editable={account_id == server.account_id}
279
controls={{ setShowDeleted }}
280
/>
281
</div>
282
);
283
};
284
285
const v: JSX.Element[] = [];
286
for (const id of ids) {
287
v.push(
288
<SortableItem key={`${id}`} id={id}>
289
{renderItem(id)}
290
</SortableItem>,
291
);
292
}
293
294
return (
295
<div style={{ margin: "5px" }}>
296
<div style={{ margin: "15px 0", textAlign: "center" }} key="create">
297
<CreateComputeServer
298
project_id={project_id}
299
onCreate={() => setSearch("")}
300
/>
301
</div>
302
<Card>
303
<div style={{ marginBottom: "15px" }}>
304
{computeServers.size > 1 && (
305
<Search
306
allowClear
307
placeholder={`Filter ${computeServers.size} Compute ${plural(
308
computeServers.size,
309
"Server",
310
)}...`}
311
value={search}
312
onChange={(e) => setSearch(e.target.value)}
313
style={{ width: 300, maxWidth: "100%" }}
314
/>
315
)}
316
{computeServers.size > 1 && (
317
<span
318
style={{
319
marginLeft: "15px",
320
display: "inline-block",
321
marginTop: "5px",
322
float: "right",
323
}}
324
>
325
Sort:{" "}
326
<Radio.Group
327
buttonStyle="solid"
328
value={sortBy}
329
size="small"
330
onChange={(e) => {
331
setSortBy(e.target.value);
332
try {
333
set_local_storage(
334
`${project_id}-compute-server-sort`,
335
e.target.value,
336
);
337
} catch (_) {}
338
}}
339
>
340
<Tooltip title="Custom sort order with drag and drop via handle on the left">
341
<Radio.Button value="custom">Custom</Radio.Button>
342
</Tooltip>
343
<Tooltip title="Sort by state with most alive (e.g., 'running') being first">
344
<Radio.Button value="state">State</Radio.Button>
345
</Tooltip>
346
<Tooltip title="Sort by when something about compute server last changed">
347
<Radio.Button value="edited">Changed</Radio.Button>
348
</Tooltip>
349
<Tooltip title="Sort in alphabetical order by the title">
350
<Radio.Button value="title">Title</Radio.Button>
351
</Tooltip>
352
<Tooltip title="Sort by the numerical id from highest (newest) to lowest (oldest)">
353
<Radio.Button value="id">Id</Radio.Button>
354
</Tooltip>
355
</Radio.Group>
356
</span>
357
)}
358
{numDeleted > 0 && (
359
<Checkbox
360
style={{ marginLeft: "10px", marginTop: "5px" }}
361
checked={showDeleted}
362
onChange={() => setShowDeleted(!showDeleted)}
363
>
364
Deleted ({numDeleted})
365
</Checkbox>
366
)}
367
</div>
368
{numSkipped > 0 && (
369
<Alert
370
showIcon
371
style={{ margin: "15px auto", maxWidth: "600px" }}
372
type="warning"
373
message={
374
<div style={{ marginTop: "5px" }}>
375
Not showing {numSkipped} compute servers due to current filter.
376
<Button
377
type="text"
378
style={{ float: "right", marginTop: "-5px" }}
379
onClick={() => setSearch("")}
380
>
381
Clear
382
</Button>
383
</div>
384
}
385
/>
386
)}
387
<div
388
style={{ /* maxHeight: "60vh", overflow: "auto", */ width: "100%" }}
389
>
390
<SortableList
391
disabled={sortBy != "custom"}
392
items={ids}
393
Item={({ id }) => renderItem(id)}
394
onDragStop={(oldIndex, newIndex) => {
395
let position;
396
if (newIndex == ids.length - 1) {
397
const last = computeServers.get(ids[ids.length - 1]);
398
// putting it at the bottom, so subtract 1 from very bottom position
399
position = (last.get("position") ?? last.get("id")) - 1;
400
} else {
401
// putting it above what was at position newIndex.
402
if (newIndex == 0) {
403
// very top
404
const first = computeServers.get(ids[0]);
405
// putting it at the bottom, so subtract 1 from very bottom position
406
position = (first.get("position") ?? first.get("id")) + 1;
407
} else {
408
// not at the very top: between two
409
let x, y;
410
if (newIndex > oldIndex) {
411
x = computeServers.get(ids[newIndex]);
412
y = computeServers.get(ids[newIndex + 1]);
413
} else {
414
x = computeServers.get(ids[newIndex - 1]);
415
y = computeServers.get(ids[newIndex]);
416
}
417
418
const x0 = x.get("position") ?? x.get("id");
419
const y0 = y.get("position") ?? y.get("id");
420
// TODO: yes, positions could get too close and this doesn't work, and then
421
// we have to globally reset them all. This is done for jupyter etc.
422
// not implemented here *yet*.
423
position = (x0 + y0) / 2;
424
}
425
}
426
const id = ids[oldIndex];
427
let cur = computeServers.get(ids[oldIndex]);
428
cur = cur.set("position", position);
429
setComputeServers(computeServers.set(ids[oldIndex], cur));
430
(async () => {
431
try {
432
await webapp_client.async_query({
433
query: {
434
compute_servers: {
435
id,
436
project_id,
437
position,
438
},
439
},
440
});
441
} catch (err) {
442
console.warn(err);
443
}
444
})();
445
}}
446
>
447
{v}
448
</SortableList>
449
</div>
450
</Card>
451
</div>
452
);
453
}
454
455