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/public-templates.tsx
Views: 687
1
import { Select, Spin, Tag, Tooltip } from "antd";
2
import { useEffect, useState } from "react";
3
import { getTemplates } from "@cocalc/frontend/compute/api";
4
import type { ConfigurationTemplate } from "@cocalc/util/compute/templates";
5
import type { HyperstackConfiguration } from "@cocalc/util/db-schema/compute-servers";
6
import { CLOUDS_BY_NAME } from "@cocalc/util/compute/cloud/clouds";
7
import { avatar_fontcolor } from "@cocalc/frontend/account/avatar/font-color";
8
import { cmp, currency, search_match } from "@cocalc/util/misc";
9
import HyperstackSpecs from "@cocalc/frontend/compute/cloud/hyperstack/specs";
10
import GoogleCloudSpecs from "@cocalc/frontend/compute/cloud/google-cloud/specs";
11
import { RenderImage } from "@cocalc/frontend/compute/images";
12
import { filterOption } from "@cocalc/frontend/compute/util";
13
import DisplayCloud from "./display-cloud";
14
import { Icon } from "@cocalc/frontend/components/icon";
15
16
const { CheckableTag } = Tag;
17
18
const TAGS = {
19
GPU: {
20
label: (
21
<>
22
<Icon name="gpu" /> GPU
23
</>
24
),
25
search: hasGPU,
26
desc: "that have a GPU",
27
group: 0,
28
},
29
H100: {
30
label: (
31
<>
32
<Icon name="nvidia" /> H100
33
</>
34
),
35
search: (template) =>
36
template.configuration.flavor_name?.toLowerCase().includes("h100"),
37
desc: "that have a high end NVIDIA H100 GPU",
38
group: 0,
39
},
40
A100: {
41
label: (
42
<>
43
<Icon name="nvidia" /> A100
44
</>
45
),
46
search: (template) =>
47
template.configuration.flavor_name?.toLowerCase().includes("a100") ||
48
template.configuration.acceleratorType?.toLowerCase().includes("a100"),
49
desc: "that have a high end NVIDIA A100 GPU",
50
group: 0,
51
},
52
L40: {
53
label: (
54
<>
55
<Icon name="nvidia" /> L40
56
</>
57
),
58
search: (template) =>
59
template.configuration.flavor_name?.toLowerCase().includes("l40"),
60
desc: "that have a midrange NVIDIA L40 GPU",
61
group: 0,
62
},
63
RTX: {
64
label: (
65
<>
66
<Icon name="nvidia" /> RTX
67
</>
68
),
69
search: (template) =>
70
template.configuration.flavor_name?.toLowerCase().includes("rtx"),
71
desc: "that have a midrange NVIDIA RTX-4000/5000/6000 GPU",
72
group: 0,
73
},
74
75
L4: {
76
label: (
77
<>
78
<Icon name="nvidia" /> L4
79
</>
80
),
81
search: (template) =>
82
template.configuration.acceleratorType
83
?.toLowerCase()
84
.includes("nvidia-l4"),
85
desc: "that have a midrange NVIDIA L4 GPU",
86
group: 0,
87
},
88
T4: {
89
label: (
90
<>
91
<Icon name="nvidia" /> T4
92
</>
93
),
94
search: (template) =>
95
template.configuration.acceleratorType
96
?.toLowerCase()
97
.includes("tesla-t4"),
98
desc: "that have a budget NVIDIA T4 GPU",
99
group: 0,
100
},
101
CPU: {
102
label: (
103
<>
104
<Icon name="microchip" /> CPU
105
</>
106
),
107
search: (template) => !hasGPU(template),
108
desc: "that have no GPU's",
109
group: 0,
110
},
111
Python: {
112
label: (
113
<>
114
<Icon name="python" /> Python
115
</>
116
),
117
search: ({ configuration }) => {
118
const im = configuration.image.toLowerCase();
119
return (
120
im.includes("python") || im.includes("anaconda") || im.includes("colab")
121
);
122
},
123
desc: "with a Python oriented image",
124
group: 1,
125
},
126
SageMath: {
127
label: (
128
<>
129
<Icon name="sagemath" /> Sage
130
</>
131
),
132
search: ({ configuration }) => {
133
const im = configuration.image.toLowerCase();
134
return im.includes("sage") || im.includes("anaconda");
135
},
136
desc: "with a Julia oriented image",
137
group: 1,
138
},
139
Julia: {
140
label: (
141
<>
142
<Icon name="julia" /> Julia
143
</>
144
),
145
search: ({ configuration }) => {
146
const im = configuration.image.toLowerCase();
147
return im.includes("julia") || im.includes("anaconda");
148
},
149
desc: "with a Julia oriented image",
150
group: 1,
151
},
152
R: {
153
label: (
154
<>
155
<Icon name="r" /> R
156
</>
157
),
158
search: ({ configuration }) => {
159
const im = configuration.image.toLowerCase();
160
return im.includes("rstat") || im.includes("colab");
161
},
162
desc: "with an R Statistics oriented image",
163
group: 1,
164
},
165
PyTorch: {
166
label: (
167
<>
168
<Icon name="pytorch" /> PyTorch
169
</>
170
),
171
search: ({ configuration }) => {
172
const im = configuration.image.toLowerCase();
173
return (
174
im.includes("torch") || im.includes("colab") || im.includes("conda")
175
);
176
},
177
desc: "with a PyTorch capable image",
178
group: 1,
179
},
180
Tensorflow: {
181
label: (
182
<>
183
<Icon name="tensorflow" /> Tensorflow
184
</>
185
),
186
search: ({ configuration }) => {
187
const im = configuration.image.toLowerCase();
188
return (
189
im.includes("tensorflow") ||
190
im.includes("colab") ||
191
im.includes("conda")
192
);
193
},
194
desc: "with a Tensorflow oriented image",
195
group: 1,
196
},
197
HPC: {
198
label: (
199
<>
200
<Icon name="cube" /> HPC/Fortran
201
</>
202
),
203
search: ({ configuration }) => {
204
const im = configuration.image.toLowerCase();
205
return im == "hpc";
206
},
207
desc: "with an HPC/Fortran oriented image",
208
group: 1,
209
},
210
Ollama: {
211
label: (
212
<>
213
<Icon name="magic" /> Ollama
214
</>
215
),
216
search: ({ configuration }) => {
217
const im = configuration.image.toLowerCase();
218
return im.includes("openwebui");
219
},
220
desc: "with an Open WebUI / Ollama AI oriented image",
221
group: 1,
222
},
223
Google: {
224
label: <DisplayCloud cloud="google-cloud" height={18} />,
225
search: ({ configuration }) => configuration.cloud == "google-cloud",
226
group: 2,
227
desc: "in Google Cloud",
228
},
229
Hyperstack: {
230
label: <DisplayCloud cloud="hyperstack" height={18} />,
231
search: ({ configuration }) => configuration.cloud == "hyperstack",
232
group: 2,
233
desc: "in Hyperstack Cloud",
234
},
235
} as const;
236
237
export default function PublicTemplates({
238
style,
239
setId,
240
defaultId,
241
disabled,
242
defaultOpen,
243
placement,
244
getPopupContainer,
245
}: {
246
style?;
247
setId: (number) => void;
248
defaultId?: number;
249
disabled?: boolean;
250
defaultOpen?: boolean;
251
placement?;
252
getPopupContainer?;
253
}) {
254
const [loading, setLoading] = useState<boolean>(false);
255
const [templates, setTemplates] = useState<
256
(ConfigurationTemplate | { search: string })[] | null
257
>(null);
258
const [data, setData] = useState<any>(null);
259
const [options, setOptions] = useState<any[]>([]);
260
const [visibleTags, setVisibleTags] = useState<Set<string>>(new Set());
261
const [filterTags, setFilterTags] = useState<Set<string>>(new Set());
262
const [selectOpen, setSelectOpen] = useState<boolean>(!!defaultOpen);
263
const [value, setValue0] = useState<number | undefined>(defaultId);
264
const setValue = (n: number) => {
265
setValue0(n);
266
setId(n);
267
};
268
269
useEffect(() => {
270
(async () => {
271
try {
272
setLoading(true);
273
const { templates, data } = await getTemplates();
274
if (templates == null || templates.length == 0) {
275
setTemplates(null);
276
setData(null);
277
setOptions([]);
278
return;
279
}
280
setTemplates(templates);
281
setData(data);
282
const options = getOptions(templates, data);
283
const tags = new Set<string>();
284
for (const tag in TAGS) {
285
if (matchingOptions(options, tag).length > 0) {
286
tags.add(tag);
287
}
288
}
289
setVisibleTags(tags);
290
} finally {
291
setLoading(false);
292
}
293
})();
294
}, []);
295
296
useEffect(() => {
297
if (templates == null) {
298
return;
299
}
300
let options = getOptions(templates, data);
301
if (filterTags.size > 0) {
302
for (const tag of filterTags) {
303
options = matchingOptions(options, tag);
304
}
305
// we also sort by price when there is a filter (otherwise not)
306
options.sort((a, b) =>
307
cmp(a.template.cost_per_hour.running, b.template.cost_per_hour.running),
308
);
309
}
310
setOptions(options);
311
}, [filterTags, templates, data]);
312
313
if (loading) {
314
return (
315
<div style={{ maxWidth: "1200px", margin: "15px auto", ...style }}>
316
Loading Templates... <Spin />
317
</div>
318
);
319
}
320
321
if (templates == null || templates?.length == 0) {
322
// not loaded or no configured templates right now.
323
return null;
324
}
325
326
let group = 0;
327
328
return (
329
<div style={{ maxWidth: "1200px", margin: "15px auto", ...style }}>
330
<div style={{ display: "flex" }}>
331
<div
332
style={{
333
fontWeight: "bold",
334
fontSize: "13pt",
335
flex: 0.1,
336
color: "#666",
337
display: "flex",
338
justifyContent: "center",
339
flexDirection: "column",
340
whiteSpace: "nowrap",
341
paddingLeft: "15px",
342
}}
343
>
344
Templates:
345
</div>
346
<div
347
style={{
348
flex: 1,
349
textAlign: "center",
350
marginBottom: "5px",
351
fontWeight: "normal",
352
border: "1px solid lightgrey",
353
borderRadius: "5px",
354
marginLeft: "15px",
355
background: "#fffeee",
356
padding: "10px",
357
}}
358
>
359
{Object.keys(TAGS)
360
.filter((tag) => visibleTags.has(tag))
361
.map((name) => {
362
const t = (
363
<Tooltip
364
mouseEnterDelay={1}
365
key={name}
366
title={
367
TAGS[name].tip ?? (
368
<>Only show templates {TAGS[name].desc}.</>
369
)
370
}
371
>
372
{TAGS[name].group != group && <br />}
373
<CheckableTag
374
key={name}
375
style={{ cursor: "pointer", fontSize: "12pt" }}
376
checked={filterTags.has(name)}
377
onChange={(checked) => {
378
let v = Array.from(filterTags);
379
if (checked) {
380
v.push(name);
381
v = v.filter(
382
(x) => x == name || TAGS[x].group != TAGS[name].group,
383
);
384
} else {
385
v = v.filter((x) => x != name);
386
}
387
setFilterTags(new Set(v));
388
setSelectOpen(v.length > 0);
389
}}
390
>
391
{TAGS[name].label ?? name}
392
</CheckableTag>
393
</Tooltip>
394
);
395
group = TAGS[name].group;
396
return t;
397
})}
398
</div>
399
<div style={{ flex: 0.1 }}></div>
400
</div>
401
<Select
402
allowClear
403
open={selectOpen}
404
defaultOpen={defaultOpen}
405
placement={placement}
406
getPopupContainer={getPopupContainer}
407
disabled={disabled}
408
value={value}
409
onChange={setValue}
410
options={options}
411
style={{
412
width: "100%",
413
height: "auto",
414
}}
415
placeholder={
416
<div style={{ color: "#666" }}>
417
Use filters above or type here to find a template, then modify it...
418
</div>
419
}
420
showSearch
421
optionFilterProp="children"
422
filterOption={filterOption}
423
onDropdownVisibleChange={setSelectOpen}
424
/>
425
</div>
426
);
427
}
428
429
function TemplateLabel({ template, data }) {
430
const { title, color, cloud, cost_per_hour } = template;
431
const cost = (
432
<div style={{ fontSize: "13pt" }}>
433
{currency(cost_per_hour.running)}/hour
434
</div>
435
);
436
let specs;
437
if (template.cloud == "hyperstack") {
438
specs = (
439
<HyperstackSpecs
440
{...(template.configuration as HyperstackConfiguration)}
441
priceData={data.hyperstackPriceData}
442
/>
443
);
444
} else if (template.cloud == "google-cloud") {
445
specs = (
446
<GoogleCloudSpecs
447
configuration={template.configuration}
448
priceData={data.googleCloudPriceData}
449
IMAGES={data.images}
450
/>
451
);
452
} else {
453
specs = null;
454
}
455
return (
456
<div
457
style={{
458
lineHeight: "normal",
459
borderWidth: "0.5px 10px",
460
borderStyle: "solid",
461
borderColor: color,
462
borderRadius: "5px",
463
padding: "10px",
464
overflow: "auto",
465
margin: "5px 10px",
466
}}
467
>
468
<div style={{ display: "flex", margin: "0 15px" }}>
469
<div style={{ flex: 1, textAlign: "center" }}>{cost}</div>
470
<div
471
style={{
472
flex: 1,
473
background: color ?? "#fff",
474
color: avatar_fontcolor(color ?? "#fff"),
475
padding: "2.5px 5px",
476
overflow: "auto",
477
}}
478
>
479
{title}
480
</div>
481
<div style={{ flex: 1 }}>
482
<div style={{ float: "right" }}>
483
<RenderImage
484
configuration={template.configuration}
485
IMAGES={data.images}
486
/>
487
</div>
488
</div>
489
<div style={{ flex: 1 }}>
490
<div style={{ width: "120px", float: "right" }}>
491
<img src={CLOUDS_BY_NAME[cloud]?.image} alt={cloud} />
492
</div>
493
</div>
494
</div>
495
<div
496
style={{
497
whiteSpace: "nowrap",
498
lineHeight: "normal",
499
marginTop: "5px",
500
textAlign: "center",
501
maxHeight: "1.2em",
502
textOverflow: "ellipsis",
503
color: "#666",
504
}}
505
>
506
{specs}
507
</div>
508
</div>
509
);
510
}
511
512
function hasGPU(template) {
513
if (template.configuration.cloud == "hyperstack") {
514
return !template.configuration.flavor_name.includes("cpu");
515
} else if (template.configuration.cloud == "google-cloud") {
516
return !!template.configuration.acceleratorCount;
517
} else {
518
return JSON.stringify(template).includes("gpu");
519
}
520
}
521
522
function getOptions(templates, data) {
523
return templates.map((template) => {
524
return {
525
template,
526
value: template.id,
527
label: <TemplateLabel template={template} data={data} />,
528
search: JSON.stringify(template).toLowerCase(),
529
};
530
});
531
}
532
533
function matchingOptions(options, tag) {
534
const f = TAGS[tag]?.search;
535
if (!f) {
536
return options;
537
}
538
if (typeof f == "function") {
539
return options.filter(({ template }) => f(template));
540
} else {
541
return options.filter(({ search }) => search_match(search, f));
542
}
543
}
544
545