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/select-image.tsx
Views: 687
1
import type {
2
Architecture,
3
State,
4
Configuration,
5
Images,
6
GoogleCloudImages,
7
} from "@cocalc/util/db-schema/compute-servers";
8
import { makeValidGoogleName } from "@cocalc/util/db-schema/compute-servers";
9
import { Alert, Checkbox, Select, Spin } from "antd";
10
import { CSSProperties, useEffect, useMemo, useState } from "react";
11
import { Icon, Markdown } from "@cocalc/frontend/components";
12
import { A } from "@cocalc/frontend/components/A";
13
import { field_cmp, trunc } from "@cocalc/util/misc";
14
import { useImages } from "./images-hook";
15
import SelectVersion from "./select-version";
16
import Advanced from "./advanced";
17
import { RenderImage } from "./images";
18
19
interface Props {
20
setConfig;
21
configuration: Configuration;
22
disabled?: boolean;
23
state?: State;
24
style?: CSSProperties;
25
// if explicitly set, only gpu images shown when
26
// gpu true, and only non-gpu when false.
27
gpu: boolean;
28
// if googleImages is set, use this to restrict list of images to only
29
// what is actually available in non-advanced view, and to enhance the
30
// view otherwise (explicitly saying images aren't actually available)
31
googleImages?: GoogleCloudImages;
32
arch: Architecture;
33
// if specified, only show images with dockerSizeGb set and <= maxDockerSizeGb
34
// Ignored if advanced is selected
35
maxDockerSizeGb?: number;
36
// show a warning if dockerSizeGb is bigger than this:
37
warnBigGb?: number;
38
}
39
40
export default function SelectImage({
41
setConfig,
42
configuration,
43
disabled,
44
state = "deprovisioned",
45
style,
46
gpu,
47
googleImages,
48
arch,
49
maxDockerSizeGb,
50
warnBigGb,
51
}: Props) {
52
const [advanced, setAdvanced] = useState<boolean>(false);
53
const [IMAGES, ImagesError] = useImages();
54
const [dockerSizeGb, setDockerSizeGb] = useState<number | undefined>(
55
undefined,
56
);
57
const [value, setValue] = useState<string | undefined>(configuration.image);
58
useEffect(() => {
59
setValue(configuration.image);
60
}, [configuration.image]);
61
62
const options = useMemo(() => {
63
if (IMAGES == null || typeof IMAGES == "string") {
64
return [];
65
}
66
return getOptions({
67
IMAGES,
68
googleImages,
69
gpu,
70
advanced,
71
value,
72
selectedTag: configuration.tag,
73
arch,
74
maxDockerSizeGb,
75
});
76
}, [IMAGES, gpu, advanced, value, configuration.tag]);
77
78
if (IMAGES == null) {
79
return <Spin />;
80
}
81
if (ImagesError != null) {
82
return ImagesError;
83
}
84
const filterOption = (input: string, option?: { search: string }) =>
85
(option?.search ?? "").includes(input.toLowerCase());
86
87
return (
88
<div>
89
<Advanced
90
advanced={advanced}
91
setAdvanced={setAdvanced}
92
style={{ float: "right", marginTop: "10px" }}
93
title={
94
"Show possibly untested, old, missing, or broken images and versions."
95
}
96
/>
97
<Select
98
size="large"
99
disabled={disabled || state != "deprovisioned"}
100
placeholder="Select compute server image..."
101
defaultOpen={!value && state == "deprovisioned"}
102
value={value}
103
style={{ width: "500px", ...style }}
104
options={options}
105
onChange={(val) => {
106
setValue(val);
107
const x = {
108
image: val,
109
tag: null,
110
};
111
for (const option of options) {
112
if (option.value == val) {
113
setDockerSizeGb(option.dockerSizeGb);
114
break;
115
}
116
}
117
setConfig(x);
118
}}
119
showSearch
120
filterOption={filterOption}
121
/>
122
{advanced && IMAGES != null && typeof IMAGES != "string" && value && (
123
<SelectVersion
124
style={{ margin: "10px 0" }}
125
disabled={disabled || state != "deprovisioned"}
126
image={value}
127
IMAGES={IMAGES}
128
setConfig={setConfig}
129
configuration={configuration}
130
/>
131
)}
132
{warnBigGb && (dockerSizeGb ?? 1) > warnBigGb && (
133
<Alert
134
style={{ margin: "15px 0" }}
135
type="warning"
136
message={<h4>Large Image Warning</h4>}
137
description={
138
<>
139
The compute server will take{" "}
140
<b>up to {Math.ceil((dockerSizeGb ?? 1) / 3)} extra minutes</b> to
141
start the first time, because a {dockerSizeGb} GB Docker image
142
must be pulled and decompressed. Please be patient!
143
<br />
144
<br />
145
<Checkbox>
146
I understand that initial startup will take at least{" "}
147
{Math.ceil((dockerSizeGb ?? 1) / 3)} extra minutes
148
</Checkbox>
149
</>
150
}
151
/>
152
)}
153
</div>
154
);
155
}
156
157
function getOptions({
158
IMAGES,
159
advanced,
160
googleImages,
161
gpu,
162
value,
163
selectedTag,
164
arch,
165
maxDockerSizeGb,
166
}: {
167
IMAGES: Images;
168
advanced?: boolean;
169
gpu?: boolean;
170
value?: string;
171
selectedTag?: string;
172
googleImages?: GoogleCloudImages;
173
arch: Architecture;
174
maxDockerSizeGb?: number;
175
}) {
176
const options: {
177
key: string;
178
tag: string;
179
priority: number;
180
value: string;
181
search: string;
182
label: JSX.Element;
183
dockerSizeGb?: number;
184
}[] = [];
185
for (const name in IMAGES) {
186
const image = IMAGES[name];
187
let { label, icon, versions, priority = 0, dockerSizeGb } = image;
188
if (image.system) {
189
continue;
190
}
191
if (image.disabled && !advanced) {
192
continue;
193
}
194
if (gpu != null && gpu != image.gpu) {
195
continue;
196
}
197
if (!advanced && maxDockerSizeGb != null) {
198
if (dockerSizeGb == null || dockerSizeGb > maxDockerSizeGb) {
199
continue;
200
}
201
}
202
if (!advanced) {
203
// restrict to only tested versions.
204
versions = versions.filter((x) => x.tested);
205
206
if (googleImages != null) {
207
const x = googleImages[name];
208
// on google cloud, so make sure image is built and tested
209
versions = versions.filter(
210
(y) =>
211
x?.[`${makeValidGoogleName(y.tag)}-${makeValidGoogleName(arch)}`]
212
?.tested,
213
);
214
}
215
}
216
if (versions.length == 0) {
217
// no available versions, so no point in showing this option
218
continue;
219
}
220
let tag;
221
let versionLabel: string | undefined = undefined;
222
if (selectedTag && name == value) {
223
tag = selectedTag;
224
for (const x of versions) {
225
if (x.tag == tag) {
226
versionLabel = x.label ?? tag;
227
break;
228
}
229
}
230
} else {
231
tag = versions[versions.length - 1]?.tag;
232
versionLabel = versions[versions.length - 1]?.label ?? tag;
233
}
234
235
let extra = "";
236
if (advanced && googleImages != null) {
237
const img =
238
googleImages[name]?.[
239
`${makeValidGoogleName(tag)}-${makeValidGoogleName(arch)}`
240
];
241
if (!img) {
242
extra = " (no image)";
243
} else {
244
const tested = img?.tested;
245
if (!tested) {
246
extra = " (not tested)";
247
}
248
}
249
}
250
if (dockerSizeGb) {
251
extra += ` - ${dockerSizeGb} GB`;
252
}
253
254
options.push({
255
key: name,
256
value: name,
257
priority,
258
search: label?.toLowerCase() ?? "",
259
tag,
260
dockerSizeGb,
261
label: (
262
<div style={{ fontSize: "12pt" }}>
263
<div style={{ float: "right" }}>{versionLabel}</div>
264
<Icon name={icon} style={{ marginRight: "5px" }} /> {label}
265
{image.disabled && <> (disabled)</>}
266
{extra}
267
</div>
268
),
269
});
270
}
271
options.sort(field_cmp("priority")).reverse();
272
return options;
273
}
274
275
export function ImageLinks({ image, style }: { image; style? }) {
276
const [IMAGES, ImagesError] = useImages();
277
if (IMAGES == null) {
278
return <Spin />;
279
}
280
if (typeof IMAGES == "string") {
281
return ImagesError;
282
}
283
const data = IMAGES[image];
284
if (data == null) {
285
return null;
286
}
287
return (
288
<div
289
style={{
290
display: "flex",
291
flexDirection: "column",
292
marginTop: "10px",
293
...style,
294
}}
295
>
296
{data.videos != null && data.videos.length > 0 && (
297
<A style={{ flex: 1 }} href={data.videos[0]}>
298
<Icon name="youtube" style={{ color: "red" }} /> YouTube
299
</A>
300
)}
301
{data.tutorials != null && data.tutorials.length > 0 && (
302
<A style={{ flex: 1 }} href={data.tutorials[0]}>
303
<Icon name="graduation-cap" /> Tutorial
304
</A>
305
)}
306
<A style={{ flex: 1 }} href={data.source}>
307
<Icon name="github" /> GitHub
308
</A>
309
<A style={{ flex: 1 }} href={data.url}>
310
<Icon name="external-link" /> {trunc(data.label, 10)}
311
</A>
312
<A style={{ flex: 1 }} href={packageNameToUrl(data.package)}>
313
<Icon name="docker" /> DockerHub
314
</A>
315
</div>
316
);
317
}
318
319
// this is a heuristic but is probably right in many cases, and
320
// right now the only case is n<=1, where it is right.
321
function packageNameToUrl(name: string): string {
322
const n = name.split("/").length - 1;
323
if (n <= 1) {
324
return `https://hub.docker.com/r/${name}`;
325
} else {
326
// e.g., us-docker.pkg.dev/colab-images/public/runtime
327
return `https://${name}`;
328
}
329
}
330
331
export function DisplayImage({
332
configuration,
333
style,
334
}: {
335
configuration: { image: string };
336
style?;
337
}) {
338
const [IMAGES, ImagesError] = useImages();
339
if (ImagesError != null) {
340
return ImagesError;
341
}
342
return (
343
<RenderImage configuration={configuration} style={style} IMAGES={IMAGES} />
344
);
345
}
346
347
export function ImageDescription({
348
configuration,
349
}: {
350
configuration: { image: string };
351
}) {
352
const [IMAGES, ImagesError] = useImages();
353
if (IMAGES == null) {
354
return <Spin />;
355
}
356
if (typeof IMAGES == "string") {
357
return ImagesError;
358
}
359
return (
360
<Alert
361
style={{ padding: "7.5px 15px", marginTop: "10px" }}
362
type="info"
363
description={
364
<Markdown
365
value={IMAGES[configuration?.image ?? ""]?.description ?? ""}
366
/>
367
}
368
/>
369
);
370
}
371
372