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/custom-software/selector.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Col, Row, Alert, Button, Divider, List, Radio } from "antd";
7
import { join } from "path";
8
import {
9
CSS,
10
React,
11
redux,
12
useMemo,
13
useState,
14
useTypedRedux,
15
} from "@cocalc/frontend/app-framework";
16
import {
17
A,
18
Gap,
19
Icon,
20
Markdown,
21
Paragraph,
22
SearchInput,
23
} from "@cocalc/frontend/components";
24
import {
25
CompanyName,
26
HelpEmailLink,
27
SiteName,
28
} from "@cocalc/frontend/customize";
29
import { appBasePath } from "@cocalc/frontend/customize/app-base-path";
30
import { ComputeImageSelector } from "@cocalc/frontend/project/settings/compute-image-selector";
31
import { KUCALC_COCALC_COM } from "@cocalc/util/db-schema/site-defaults";
32
import { unreachable } from "@cocalc/util/misc";
33
import { COLORS } from "@cocalc/util/theme";
34
import { ComputeImage, ComputeImageTypes, ComputeImages } from "./init";
35
import {
36
CUSTOM_SOFTWARE_HELP_URL,
37
compute_image2basename,
38
custom_image_name,
39
is_custom_image,
40
} from "./util";
41
42
const BINDER_URL = "https://mybinder.readthedocs.io/en/latest/";
43
44
const CS_LIST_STYLE: CSS = {
45
height: "250px",
46
overflowX: "hidden" as "hidden",
47
overflowY: "scroll" as "scroll",
48
border: `1px solid ${COLORS.GRAY_LL}`,
49
borderRadius: "5px",
50
marginBottom: "0px",
51
} as const;
52
53
const ENTRIES_ITEM_STYLE: CSS = {
54
width: "100%",
55
margin: "2px 0px",
56
padding: "5px",
57
border: "none",
58
textAlign: "left" as "left",
59
} as const;
60
61
export interface SoftwareEnvironmentState {
62
image_selected?: string;
63
title_text?: string;
64
image_type?: ComputeImageTypes;
65
}
66
67
// this is used in create-project and course/configuration/actions
68
// this derives the proper image name from the image type & image selection of SoftwareEnvironmentState
69
export async function derive_project_img_name(
70
custom_software: SoftwareEnvironmentState,
71
): Promise<string> {
72
const { image_type, image_selected } = custom_software;
73
const dflt_software_img = await redux
74
.getStore("customize")
75
.getDefaultComputeImage();
76
if (image_selected == null || image_type == null) {
77
return dflt_software_img;
78
}
79
switch (image_type) {
80
case "custom":
81
return custom_image_name(image_selected);
82
case "default":
83
case "standard":
84
return image_selected;
85
default:
86
unreachable(image_type);
87
return dflt_software_img; // make TS happy
88
}
89
}
90
91
interface Props {
92
onChange: (obj: SoftwareEnvironmentState) => void;
93
default_image?: string; // which one to initialize state to
94
showTitle?: boolean; // default true
95
}
96
97
// this is a selector for the software environment of a project
98
export const SoftwareEnvironment: React.FC<Props> = (props: Props) => {
99
const { onChange, default_image, showTitle = true } = props;
100
const images: ComputeImages | undefined = useTypedRedux(
101
"compute_images",
102
"images",
103
);
104
105
const customize_kucalc = useTypedRedux("customize", "kucalc");
106
const onCoCalcCom = customize_kucalc === KUCALC_COCALC_COM;
107
const customize_software = useTypedRedux("customize", "software");
108
const [dflt_software_img, software_images] = useMemo(
109
() => [
110
customize_software.get("default"),
111
customize_software.get("environments"),
112
],
113
[customize_software],
114
);
115
116
const haveSoftwareImages: boolean = useMemo(
117
() => (customize_software.get("environments")?.size ?? 0) > 0,
118
[customize_software],
119
);
120
121
const [search_img, set_search_img] = useState<string>("");
122
const [image_selected, set_image_selected] = useState<string | undefined>(
123
undefined,
124
);
125
const set_title_text = useState<string | undefined>(undefined)[1];
126
const [image_type, set_image_type] = useState<ComputeImageTypes>("default");
127
128
function set_state(
129
image_selected: string | undefined,
130
title_text: string | undefined,
131
image_type: ComputeImageTypes,
132
): void {
133
set_image_selected(image_selected);
134
set_title_text(title_text);
135
set_image_type(image_type);
136
onChange({ image_selected, title_text, image_type });
137
}
138
139
// initialize selection, if there is a default image set
140
React.useEffect(() => {
141
if (default_image == null || default_image === dflt_software_img) {
142
// do nothing, that's the initial state already!
143
} else if (is_custom_image(default_image)) {
144
if (images == null) return;
145
const id = compute_image2basename(default_image);
146
const img: ComputeImage | undefined = images.get(id);
147
if (img == null) {
148
// ignore, user has to select from scratch
149
} else {
150
set_state(id, img.get("display", ""), "custom");
151
}
152
} else {
153
// must be standard image
154
const img = software_images.get(default_image);
155
const display = img != null ? (img.get("title") ?? "") : "";
156
set_state(default_image, display, "standard");
157
}
158
}, []);
159
160
function render_custom_image_entries() {
161
if (images == null) return;
162
163
const search_hit = (() => {
164
if (search_img.length > 0) {
165
return (img: ComputeImage) =>
166
img.get("search_str", "").indexOf(search_img.toLowerCase()) >= 0;
167
} else {
168
return (_img: ComputeImage) => true;
169
}
170
})();
171
172
const entries: JSX.Element[] = images
173
.filter((img) => img.get("type", "") === "custom")
174
.filter(search_hit)
175
.sortBy((img) => img.get("display", "").toLowerCase())
176
.entrySeq()
177
.map((e) => {
178
const [id, img] = e;
179
const display = img.get("display", "");
180
return (
181
<List.Item
182
key={id}
183
onClick={() => set_state(id, display, image_type)}
184
style={{
185
...ENTRIES_ITEM_STYLE,
186
...(image_selected === id
187
? { background: "#337ab7", color: "white" }
188
: undefined),
189
}}
190
>
191
{display}
192
</List.Item>
193
);
194
})
195
.toArray();
196
197
if (entries.length > 0) {
198
return <List style={CS_LIST_STYLE}>{entries}</List>;
199
} else {
200
if (search_img.length > 0) {
201
return <div>No search hits.</div>;
202
} else {
203
return <div>No custom software available</div>;
204
}
205
}
206
}
207
208
function search(val: string): void {
209
set_search_img(val);
210
set_state(undefined, undefined, image_type);
211
}
212
213
function render_custom_images() {
214
if (image_type !== "custom") return;
215
216
return (
217
<>
218
<div style={{ display: "flex" }}>
219
<SearchInput
220
placeholder={"Search…"}
221
autoFocus={false}
222
value={search_img}
223
on_escape={() => set_search_img("")}
224
on_change={search}
225
style={{ flex: "1" }}
226
/>
227
</div>
228
{render_custom_image_entries()}
229
</>
230
);
231
}
232
233
function render_custom_images_info() {
234
if (image_type !== "custom") return;
235
236
return (
237
<>
238
<div style={{ color: COLORS.GRAY, margin: "15px 0" }}>
239
Contact us to add more or give feedback:{" "}
240
<HelpEmailLink color={COLORS.GRAY} />.
241
</div>
242
<Alert
243
type="info"
244
banner
245
message={
246
<>
247
The selected <em>custom</em> software environment stays with the
248
project. Create a new project to work in a different software
249
environment. You can always{" "}
250
<A
251
href={
252
"https://doc.cocalc.com/project-files.html#file-actions-on-one-file"
253
}
254
>
255
copy files between projects
256
</A>{" "}
257
as well.
258
</>
259
}
260
/>
261
</>
262
);
263
}
264
265
function render_selected_custom_image_info() {
266
if (image_type !== "custom" || image_selected == null || images == null) {
267
return;
268
}
269
270
const id: string = image_selected;
271
const data = images.get(id);
272
if (data == null) {
273
// we have a serious problem
274
console.warn(`compute_image data missing for '${id}'`);
275
return;
276
}
277
// some fields are derived in the "Table" when the data comes in
278
const img: ComputeImage = data;
279
const disp = img.get("display");
280
const desc = img.get("desc", "");
281
const url = img.get("url");
282
const src = img.get("src");
283
const disp_tag = img.get("display_tag");
284
285
const render_source = () => {
286
if (src == null || src.length == 0) return;
287
return (
288
<div style={{ marginTop: "5px" }}>
289
Source: <code>{src}</code>
290
</div>
291
);
292
};
293
294
const render_url = () => {
295
if (url == null || url.length == 0) return;
296
return (
297
<div style={{ marginTop: "5px" }}>
298
<a href={url} target={"_blank"} rel={"noopener"}>
299
<Icon name="external-link" /> Website
300
</a>
301
</div>
302
);
303
};
304
305
return (
306
<>
307
<h3 style={{ marginTop: "5px" }}>{disp}</h3>
308
<div style={{ marginTop: "5px" }}>
309
Image ID: <code>{disp_tag}</code>
310
</div>
311
<div
312
style={{ marginTop: "10px", overflowY: "auto", maxHeight: "200px" }}
313
>
314
<Markdown value={desc} className={"cc-custom-image-desc"} />
315
</div>
316
{render_source()}
317
{render_url()}
318
</>
319
);
320
}
321
322
function render_onprem() {
323
const selected = image_selected ?? dflt_software_img;
324
return (
325
<>
326
<Paragraph>
327
Select the software enviornment. Either go with the default
328
environment, or select one of the more specialized ones. Whatever you
329
pick, you can change it later in Project Settings → Control → Software
330
Environment at any time.
331
</Paragraph>
332
<Paragraph>
333
<ComputeImageSelector
334
size={"middle"}
335
selected_image={selected}
336
layout={"horizontal"}
337
onSelect={(img) => {
338
const display = software_images.get(img)?.get("title");
339
set_state(img, display, "standard");
340
}}
341
/>
342
</Paragraph>
343
<Paragraph>
344
{selected !== dflt_software_img ? (
345
<Alert
346
type="info"
347
banner
348
message={
349
<>
350
You've selected a non-standard image:{" "}
351
<Button
352
size="small"
353
type="link"
354
onClick={() => {
355
set_state(dflt_software_img, undefined, "standard");
356
}}
357
>
358
Reset
359
</Button>
360
</>
361
}
362
/>
363
) : undefined}
364
</Paragraph>
365
</>
366
);
367
}
368
369
function render_default_explanation(): JSX.Element {
370
if (onCoCalcCom) {
371
return (
372
<>
373
<b>Default</b>: large repository of software, well tested – maintained
374
by <CompanyName />, running <SiteName />.{" "}
375
<a
376
href={join(appBasePath, "doc/software.html")}
377
target={"_blank"}
378
rel={"noopener"}
379
>
380
More info...
381
</a>
382
</>
383
);
384
} else {
385
const dflt_img = software_images.get(dflt_software_img);
386
const descr = dflt_img?.get("descr") ?? "large repository of software";
387
const t = dflt_img?.get("title");
388
const title = t ? `${t}: ${descr}` : descr;
389
return (
390
<>
391
<b>Standard</b>: {title}
392
</>
393
);
394
}
395
}
396
397
function render_default() {
398
return (
399
<Radio
400
checked={image_type === "default"}
401
id={"default-compute-image"}
402
onChange={() => {
403
set_state(undefined, undefined, "default");
404
}}
405
>
406
{render_default_explanation()}
407
</Radio>
408
);
409
}
410
411
function render_standard_explanation(): JSX.Element {
412
if (onCoCalcCom) {
413
return (
414
<>
415
<b>Standard</b>: upcoming and archived versions of the "Default"
416
software environment.
417
</>
418
);
419
} else {
420
return (
421
<>
422
<b>Specialized</b>: alternative software environments for specific
423
purposes.
424
</>
425
);
426
}
427
}
428
429
function render_standard_modify_later(): JSX.Element {
430
return (
431
<Alert
432
type="info"
433
banner
434
message={
435
<>
436
The selected <em>standard</em> software environment can be changed
437
in Project Settings → Control at any time.
438
</>
439
}
440
/>
441
);
442
}
443
444
function render_standard() {
445
if (!haveSoftwareImages) {
446
return;
447
}
448
return (
449
<Radio
450
checked={image_type === "standard"}
451
id={"default-compute-image"}
452
onChange={() => {
453
set_state(undefined, undefined, "standard");
454
}}
455
>
456
{render_standard_explanation()}
457
</Radio>
458
);
459
}
460
461
function render_custom() {
462
if (customize_kucalc !== KUCALC_COCALC_COM) {
463
return null;
464
}
465
466
if (images == null || images.size == 0) {
467
return "There are no customized software environments available.";
468
} else {
469
return (
470
<Radio
471
checked={image_type === "custom"}
472
id={"custom-compute-image"}
473
onChange={() => {
474
set_state(undefined, undefined, "custom");
475
}}
476
>
477
<b>Custom</b>
478
<sup>
479
<em>beta</em>
480
</sup>
481
: 3rd party software environments, e.g.{" "}
482
<a href={BINDER_URL} target={"_blank"} rel={"noopener"}>
483
Binder
484
</a>
485
.{" "}
486
<a href={CUSTOM_SOFTWARE_HELP_URL} target={"_blank"}>
487
More info...
488
</a>
489
</Radio>
490
);
491
}
492
}
493
494
function render_standard_image_selector() {
495
if (image_type !== "standard") return;
496
497
return (
498
<Col sm={24}>
499
<ComputeImageSelector
500
selected_image={image_selected ?? dflt_software_img}
501
layout={"horizontal"}
502
onSelect={(img) => {
503
const display = software_images.get(img)?.get("title");
504
set_state(img, display, "standard");
505
}}
506
/>
507
<Gap />
508
{render_standard_modify_later()}
509
</Col>
510
);
511
}
512
513
function render_type_selection() {
514
return (
515
<>
516
{showTitle ? <div>Software environment</div> : undefined}
517
518
{onCoCalcCom ? (
519
<div>
520
<div style={{ marginBottom: "5px" }}>{render_default()}</div>
521
<div style={{ marginBottom: "5px" }}>{render_standard()}</div>
522
<div style={{ marginBottom: "5px" }}>{render_custom()}</div>
523
</div>
524
) : (
525
render_onprem()
526
)}
527
</>
528
);
529
}
530
531
function render_divider() {
532
if (image_type === "default") return;
533
return (
534
<Divider orientation="left" plain>
535
Configuration
536
</Divider>
537
);
538
}
539
540
return (
541
<Row>
542
<Col sm={24} style={{ marginTop: "10px" }}>
543
{render_type_selection()}
544
</Col>
545
546
{onCoCalcCom ? (
547
<>
548
{render_divider()}
549
{render_standard_image_selector()}
550
<Col sm={12}>{render_custom_images()}</Col>
551
<Col sm={12}>{render_selected_custom_image_info()}</Col>
552
<Col sm={24}>{render_custom_images_info()}</Col>
553
</>
554
) : undefined}
555
</Row>
556
);
557
};
558
559