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/init.ts
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
// Manage DB <-> UI integration of available *custom* compute images
7
// TODO: also get rid of hardcoded official software images
8
9
import { redux, Store, Actions, Table } from "@cocalc/frontend/app-framework";
10
import { Map as iMap } from "immutable";
11
import { NAME } from "./util";
12
import { capitalize } from "@cocalc/util/misc";
13
14
// this must match db-schema.compute_images → field type → allowed values
15
// "standard" image names are "default", "exp", "ubuntu2020", or a timestamp-string
16
// custom iamges are "custom/<image-id>/<tag, usually latest>"
17
// the "custom/" string is supposed to be CUSTOM_IMG_PREFIX!
18
export type ComputeImageTypes = "default" | "standard" | "custom";
19
20
// this must be compatible with db-schema.compute_images → field keys
21
export type ComputeImageKeys =
22
| "id"
23
| "src"
24
| "type"
25
| "display"
26
| "url"
27
| "desc"
28
| "path"
29
| "search_str"
30
| "display_tag"
31
| "disabled";
32
33
export type ComputeImage = iMap<ComputeImageKeys, string>;
34
export type ComputeImages = iMap<string, ComputeImage>;
35
36
export interface ComputeImagesState {
37
images?: ComputeImages;
38
}
39
40
export class ComputeImagesStore extends Store<ComputeImagesState> {}
41
42
export function launchcode2display(
43
images: ComputeImages,
44
launch: string
45
): string | undefined {
46
// launch expected to be "csi/some-id/..."
47
const id = launch.split("/")[1];
48
if (!id) return undefined;
49
const img = images.get(id);
50
if (img == null) return undefined;
51
return img.get("display") || id2name(id);
52
}
53
54
export class ComputeImagesActions<
55
ComputeImagesState
56
> extends Actions<ComputeImagesState> {}
57
58
function id2name(id: string): string {
59
return id.split("-").map(capitalize).join(" ");
60
}
61
62
function fallback(
63
img: ComputeImage,
64
key: ComputeImageKeys,
65
replace: (img: ComputeImage) => string | undefined
66
): string {
67
const ret = img.get(key);
68
if (ret == null || ret.length == 0) {
69
return replace(img) || "";
70
}
71
return ret;
72
}
73
74
function display_fallback(img: ComputeImage, id: string) {
75
return fallback(img, "display", (_) => id2name(id));
76
}
77
78
function desc_fallback(img: ComputeImage) {
79
return fallback(img, "desc", (_) => "*No description available.*");
80
}
81
82
/* if there is no URL set, derive it from the git source URL
83
* this supports github, gitlab and bitbucket. https URLs look like this:
84
* https://github.com/sagemathinc/cocalc.git
85
* https://gitlab.com/orgname/projectname.git
86
* https://[email protected]/orgname/projectname.git
87
*/
88
function url_fallback(img: ComputeImage) {
89
const cloudgit = ["github.com", "gitlab.com", "bitbucket.org"];
90
const derive_url = (img: ComputeImage) => {
91
const src = img.get("src", undefined);
92
if (src == null || src.length == 0) return;
93
if (!src.startsWith("http")) return;
94
for (const srv of cloudgit) {
95
if (src.indexOf(srv) < 0) continue;
96
if (src.endsWith(".git")) {
97
return src.slice(0, -".git".length);
98
} else {
99
return src;
100
}
101
}
102
};
103
return fallback(img, "url", derive_url);
104
}
105
106
class ComputeImagesTable extends Table {
107
constructor(NAME, redux) {
108
super(NAME, redux);
109
this._change = this._change.bind(this);
110
}
111
112
query() {
113
return NAME;
114
}
115
116
options(): any[] {
117
return [];
118
}
119
120
prepare(data: ComputeImages): ComputeImages {
121
// console.log("ComputeImagesTable data:", data);
122
// deriving disp, desc, etc. must be robust against null and empty strings
123
return (
124
data
125
// filter disabled ones. we still want to have the data available, though.
126
.filter((img) => !img.get("disabled", false))
127
.map((img, id) => {
128
const display = display_fallback(img, id);
129
const desc = desc_fallback(img);
130
const url = url_fallback(img);
131
const search_str = `${id} ${display} ${desc} ${url}`
132
.split(" ")
133
.filter((x) => x.length > 0)
134
.join(" ")
135
.toLowerCase();
136
// derive the displayed tag, docker-like
137
const tag = id.indexOf(":") >= 0 ? "" : ":latest";
138
const disp_tag = `${id}${tag}`;
139
140
return img.withMutations((img) =>
141
img
142
.set("display", display)
143
.set("desc", desc)
144
.set("search_str", search_str)
145
.set("url", url)
146
.set("display_tag", disp_tag)
147
);
148
})
149
);
150
}
151
152
_change(table, _keys): void {
153
const store: ComputeImagesStore | undefined = this.redux.getStore(NAME);
154
if (store == null) throw Error("store must be defined");
155
const actions = this.redux.getActions(NAME);
156
if (actions == null) throw Error("actions must be defined");
157
const data = table.get();
158
actions.setState({ images: this.prepare(data) });
159
}
160
}
161
162
export function init() {
163
if (!redux.hasStore(NAME)) {
164
redux.createStore(NAME, ComputeImagesStore, {});
165
redux.createActions(NAME, ComputeImagesActions);
166
redux.createTable(NAME, ComputeImagesTable);
167
}
168
}
169
170