Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/compute/select-image.tsx
5844 views
1
import { Icon, isIconName, Markdown } from "@cocalc/frontend/components";
2
import { A } from "@cocalc/frontend/components/A";
3
import type {
4
Architecture,
5
Configuration,
6
GoogleCloudImages,
7
Images,
8
State,
9
} from "@cocalc/util/db-schema/compute-servers";
10
import { makeValidGoogleName } from "@cocalc/util/db-schema/compute-servers";
11
import { field_cmp, trunc } from "@cocalc/util/misc";
12
import { Alert, Checkbox, Select, Spin } from "antd";
13
import { CSSProperties, useEffect, useMemo, useState } from "react";
14
import Advanced from "./advanced";
15
import { RenderImage } from "./images";
16
import { useImages } from "./images-hook";
17
import SelectVersion from "./select-version";
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: React.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
{isIconName(icon) && (
265
<Icon name={icon} style={{ marginRight: "5px" }} />
266
)}{" "}
267
{label}
268
{image.disabled && <> (disabled)</>}
269
{extra}
270
</div>
271
),
272
});
273
}
274
options.sort(field_cmp("priority")).reverse();
275
return options;
276
}
277
278
export function ImageLinks({ image, style }: { image; style? }) {
279
const [IMAGES, ImagesError] = useImages();
280
if (IMAGES == null) {
281
return <Spin />;
282
}
283
if (typeof IMAGES == "string") {
284
return ImagesError;
285
}
286
const data = IMAGES[image];
287
if (data == null) {
288
return null;
289
}
290
return (
291
<div
292
style={{
293
display: "flex",
294
flexDirection: "column",
295
marginTop: "10px",
296
...style,
297
}}
298
>
299
{data.videos != null && data.videos.length > 0 && (
300
<A style={{ flex: 1 }} href={data.videos[0]}>
301
<Icon name="youtube" style={{ color: "red" }} /> YouTube
302
</A>
303
)}
304
{data.tutorials != null && data.tutorials.length > 0 && (
305
<A style={{ flex: 1 }} href={data.tutorials[0]}>
306
<Icon name="graduation-cap" /> Tutorial
307
</A>
308
)}
309
<A style={{ flex: 1 }} href={data.source}>
310
<Icon name="github" /> GitHub
311
</A>
312
{!!data.url && (
313
<A style={{ flex: 1 }} href={data.url}>
314
<Icon name="external-link" /> {trunc(data.label, 10)}
315
</A>
316
)}
317
{!!data.package && (
318
<A style={{ flex: 1 }} href={packageNameToUrl(data.package)}>
319
<Icon name="docker" /> DockerHub
320
</A>
321
)}
322
</div>
323
);
324
}
325
326
// this is a heuristic but is probably right in many cases, and
327
// right now the only case is n<=1, where it is right.
328
function packageNameToUrl(name: string): string {
329
const n = name.split("/").length - 1;
330
if (n <= 1) {
331
return `https://hub.docker.com/r/${name}`;
332
} else {
333
// e.g., us-docker.pkg.dev/colab-images/public/runtime
334
return `https://${name}`;
335
}
336
}
337
338
export function DisplayImage({
339
configuration,
340
style,
341
}: {
342
configuration: { image: string };
343
style?;
344
}) {
345
const [IMAGES, ImagesError] = useImages();
346
if (ImagesError != null) {
347
return ImagesError;
348
}
349
return (
350
<RenderImage configuration={configuration} style={style} IMAGES={IMAGES} />
351
);
352
}
353
354
export function ImageDescription({
355
configuration,
356
}: {
357
configuration: { image: string };
358
}) {
359
const [IMAGES, ImagesError] = useImages();
360
if (IMAGES == null) {
361
return <Spin />;
362
}
363
if (typeof IMAGES == "string") {
364
return ImagesError;
365
}
366
return (
367
<Alert
368
style={{ padding: "7.5px 15px", marginTop: "10px" }}
369
type="info"
370
description={
371
<Markdown
372
value={IMAGES[configuration?.image ?? ""]?.description ?? ""}
373
/>
374
}
375
/>
376
);
377
}
378
379