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