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/create-compute-server.tsx
Views: 687
1
import { Button, Modal, Spin } from "antd";
2
import { delay } from "awaiting";
3
import { cloneDeep } from "lodash";
4
import { useEffect, useState } from "react";
5
6
import { redux, useRedux, useTypedRedux } from "@cocalc/frontend/app-framework";
7
import ShowError from "@cocalc/frontend/components/error";
8
import { Icon } from "@cocalc/frontend/components/icon";
9
import PublicTemplates from "@cocalc/frontend/compute/public-templates";
10
import { CancelText } from "@cocalc/frontend/i18n/components";
11
import { randomPetName } from "@cocalc/frontend/project/utils";
12
import confirmStartComputeServer from "@cocalc/frontend/purchases/pay-as-you-go/confirm-start-compute-server";
13
import {
14
CLOUDS_BY_NAME,
15
Cloud as CloudType,
16
Configuration,
17
} from "@cocalc/util/db-schema/compute-servers";
18
import { replace_all } from "@cocalc/util/misc";
19
import {
20
computeServerAction,
21
createServer,
22
getTemplate,
23
setServerConfiguration,
24
} from "./api";
25
import { randomColor } from "./color";
26
import ComputeServer from "./compute-server";
27
import { Docs } from "./compute-servers";
28
import { availableClouds } from "./config";
29
import costPerHour from "./cost";
30
31
export const DEFAULT_FAST_LOCAL = "scratch";
32
33
function defaultTitle() {
34
return `Untitled ${new Date().toISOString().split("T")[0]}`;
35
}
36
37
// NOTE that availableClouds() will be empty the moment the page
38
// loads, but give correct results once customize is loaded right
39
// after user has loaded page. By the time they are creating a NEW
40
// compute server, this should all be working fine.
41
42
function defaultCloud() {
43
return availableClouds()[0];
44
}
45
46
function defaultConfiguration() {
47
return genericDefaults(
48
CLOUDS_BY_NAME[availableClouds()[0]]?.defaultConfiguration ?? {},
49
);
50
}
51
52
function genericDefaults(conf) {
53
return { ...conf, excludeFromSync: [DEFAULT_FAST_LOCAL] };
54
}
55
56
export default function CreateComputeServer({ project_id, onCreate }) {
57
const account_id = useTypedRedux("account", "account_id");
58
const create_compute_server = useRedux(["create_compute_server"], project_id);
59
const create_compute_server_template_id = useRedux(
60
["create_compute_server_template_id"],
61
project_id,
62
);
63
const [editing, setEditing] = useState<boolean>(create_compute_server);
64
const [templateId, setTemplateId] = useState<number | undefined>(
65
create_compute_server_template_id,
66
);
67
68
useEffect(() => {
69
if (create_compute_server_template_id) {
70
setConfigToTemplate(create_compute_server_template_id);
71
}
72
return () => {
73
if (create_compute_server) {
74
redux
75
.getProjectActions(project_id)
76
.setState({ create_compute_server: false });
77
}
78
};
79
}, []);
80
81
// we have to do this stupid hack because of the animation when showing
82
// a modal and how select works. It's just working around limitations
83
// of antd, I think.
84
const [showTemplates, setShowTemplates] = useState<boolean>(false);
85
useEffect(() => {
86
setTimeout(() => setShowTemplates(true), 1000);
87
}, []);
88
89
const [creating, setCreating] = useState<boolean>(false);
90
const [error, setError] = useState<string>("");
91
92
const [title, setTitle] = useState<string>(defaultTitle());
93
const [color, setColor] = useState<string>(randomColor());
94
const [cloud, setCloud] = useState<CloudType>(defaultCloud());
95
const [configuration, setConfiguration] = useState<Configuration>(
96
defaultConfiguration(),
97
);
98
const resetConfig = async () => {
99
try {
100
setLoadingTemplate(true);
101
await delay(1);
102
setTitle(defaultTitle());
103
setColor(randomColor());
104
setCloud(defaultCloud());
105
setConfiguration(defaultConfiguration());
106
setTemplateId(undefined);
107
} finally {
108
setLoadingTemplate(false);
109
}
110
};
111
112
const [notes, setNotes] = useState<string>("");
113
const [loadingTemplate, setLoadingTemplate] = useState<boolean>(false);
114
const setConfigToTemplate = async (id) => {
115
setTemplateId(id);
116
setNotes(`Starting with template ${id}.\n`);
117
const currentConfiguration = cloneDeep(configuration);
118
let template;
119
try {
120
setLoadingTemplate(true);
121
template = await getTemplate(id);
122
setTitle(template.title);
123
setColor(template.color);
124
setCloud(template.cloud);
125
const { configuration } = template;
126
if (currentConfiguration.dns) {
127
// keep current config
128
configuration.dns = currentConfiguration.dns;
129
} else if (configuration.dns) {
130
// TODO: should automatically ensure this randomly isn't taken. Can implement
131
// that later.
132
configuration.dns += `-${randomPetName().toLowerCase()}`;
133
}
134
configuration.excludeFromSync = currentConfiguration.excludeFromSync;
135
setConfiguration(configuration);
136
} catch (err) {
137
setError(`${err}`);
138
return;
139
} finally {
140
setLoadingTemplate(false);
141
}
142
};
143
144
useEffect(() => {
145
if (configuration != null && configuration.cloud != cloud) {
146
setConfiguration(
147
genericDefaults(CLOUDS_BY_NAME[cloud]?.defaultConfiguration),
148
);
149
}
150
}, [cloud]);
151
152
const handleCreate = async (start: boolean) => {
153
try {
154
setError("");
155
onCreate();
156
try {
157
setCreating(true);
158
const id = await createServer({
159
project_id,
160
cloud,
161
title,
162
color,
163
configuration,
164
notes,
165
});
166
await updateFastDataDirectoryId(id, configuration);
167
setEditing(false);
168
resetConfig();
169
setCreating(false);
170
if (start && cloud != "onprem") {
171
(async () => {
172
try {
173
await confirmStartComputeServer({
174
id,
175
cost_per_hour: await costPerHour({
176
configuration,
177
state: "running",
178
}),
179
});
180
await computeServerAction({ id, action: "start" });
181
} catch (_) {}
182
})();
183
}
184
} catch (err) {
185
setError(`${err}`);
186
}
187
} finally {
188
setCreating(false);
189
}
190
};
191
192
const footer = [
193
<div style={{ textAlign: "center" }} key="footer">
194
<Button
195
key="cancel"
196
size="large"
197
onClick={() => setEditing(false)}
198
style={{ marginRight: "5px" }}
199
>
200
<CancelText />
201
</Button>
202
{cloud != "onprem" && (
203
<Button
204
style={{ marginRight: "5px" }}
205
key="start"
206
size="large"
207
type="primary"
208
onClick={() => {
209
handleCreate(true);
210
}}
211
disabled={!!error || !title.trim()}
212
>
213
<Icon name="run" /> Start Server
214
{!!error && "(clear error) "}
215
{!title.trim() && "(set title) "}
216
</Button>
217
)}
218
<Button
219
key="create"
220
size="large"
221
onClick={() => {
222
handleCreate(false);
223
}}
224
disabled={!!error || !title.trim()}
225
>
226
<Icon name="run" /> Create Server
227
{cloud != "onprem" ? " (don't start)" : ""}
228
{!!error && "(clear error) "}
229
{!title.trim() && "(set title) "}
230
</Button>
231
</div>,
232
];
233
234
return (
235
<div style={{ marginTop: "15px" }}>
236
<Button
237
size="large"
238
disabled={creating || editing}
239
onClick={() => {
240
resetConfig();
241
setEditing(true);
242
}}
243
style={{
244
marginRight: "5px",
245
width: "80%",
246
height: "auto",
247
whiteSpace: "normal",
248
padding: "10px",
249
...(creating
250
? {
251
borderColor: "rgb(22, 119, 255)",
252
backgroundColor: "rgb(230, 244, 255)",
253
}
254
: undefined),
255
}}
256
>
257
<Icon
258
name="server"
259
style={{
260
color: "rgb(66, 139, 202)",
261
fontSize: "200%",
262
}}
263
/>
264
<br />
265
Create Compute Server... {creating ? <Spin /> : null}
266
</Button>
267
<Modal
268
width={"900px"}
269
onCancel={() => {
270
setEditing(false);
271
setTemplateId(undefined);
272
resetConfig();
273
}}
274
open={editing}
275
destroyOnClose
276
title={
277
<div>
278
<div style={{ display: "flex" }}>Create Compute Server</div>
279
<div style={{ textAlign: "center", color: "#666" }}>
280
{showTemplates && (
281
<PublicTemplates
282
disabled={loadingTemplate}
283
defaultId={templateId}
284
setId={async (id) => {
285
setTemplateId(id);
286
if (id) {
287
await setConfigToTemplate(id);
288
}
289
}}
290
/>
291
)}
292
</div>
293
</div>
294
}
295
footer={
296
<div style={{ display: "flex" }}>
297
{footer}
298
<Docs key="docs" style={{ flex: 1, marginTop: "10px" }} />
299
</div>
300
}
301
>
302
<div style={{ marginTop: "15px" }}>
303
<ShowError
304
error={error}
305
setError={setError}
306
style={{ margin: "15px 0" }}
307
/>
308
{cloud != "onprem" && (
309
<div
310
style={{
311
marginBottom: "5px",
312
color: "#666",
313
textAlign: "center",
314
}}
315
>
316
Customize your compute server below, then{" "}
317
<Button
318
onClick={() => handleCreate(true)}
319
disabled={!!error || !title.trim()}
320
type={"primary"}
321
>
322
<Icon name="run" /> Start Server
323
</Button>
324
</div>
325
)}
326
{cloud == "onprem" && (
327
<div
328
style={{
329
marginBottom: "5px",
330
color: "#666",
331
textAlign: "center",
332
}}
333
>
334
Customize your compute server below, then{" "}
335
<Button
336
onClick={() => handleCreate(false)}
337
disabled={!!error || !title.trim()}
338
type={"primary"}
339
>
340
<Icon name="run" /> Create Server
341
</Button>
342
</div>
343
)}
344
{loadingTemplate && <Spin />}
345
{!loadingTemplate && (
346
<ComputeServer
347
server={{
348
project_id,
349
account_id,
350
title,
351
color,
352
cloud,
353
configuration,
354
}}
355
editable={!creating}
356
controls={{
357
onColorChange: setColor,
358
onTitleChange: setTitle,
359
onCloudChange: setCloud,
360
onConfigurationChange: setConfiguration,
361
}}
362
/>
363
)}
364
</div>
365
</Modal>
366
</div>
367
);
368
}
369
370
async function updateFastDataDirectoryId(id: number, configuration) {
371
const { excludeFromSync } = configuration;
372
if (excludeFromSync == null || excludeFromSync.length == 0) {
373
return;
374
}
375
const changes = {
376
excludeFromSync: excludeFromSync.map((x) =>
377
replace_all(x, "[id]", `${id}`),
378
),
379
};
380
await setServerConfiguration({ id, configuration: changes });
381
}
382
383