-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathImageBuilder.jsx
More file actions
248 lines (227 loc) · 7.23 KB
/
Copy pathImageBuilder.jsx
File metadata and controls
248 lines (227 loc) · 7.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
import { useEffect, useState, useRef, useContext } from "react";
import { TextField } from "./components/form/fields";
import { SpawnerFormContext } from "./state";
import useRepositoryField from "./hooks/useRepositoryField";
async function buildImage(repo, ref, term, fitAddon) {
const { BinderRepository } = await import("@jupyterhub/binderhub-client");
const providerSpec = "gh/" + repo + "/" + ref;
// FIXME: Assume the binder api is available in the same hostname, under /services/binder/
const buildEndPointURL = new URL(
"/services/binder/build/",
window.location.origin,
);
const image = new BinderRepository(
providerSpec,
buildEndPointURL,
null,
true,
);
// Clear the last line written, so we start from scratch
term.write("\x1b[2K\r");
term.resize(66, 16);
fitAddon.fit();
for await (const data of image.fetch()) {
// Write message to the log terminal if there is a message
if (data.message !== undefined) {
// Write out all messages to the terminal!
term.write(data.message);
// Resize our terminal to make sure it fits messages appropriately
fitAddon.fit();
} else {
console.log(data);
}
switch (data.phase) {
case "failed": {
image.close();
return Promise.reject();
}
case "ready": {
// Close the EventStream when the image has been built
image.close();
return Promise.resolve(data.imageName);
}
default: {
console.log("Unknown phase in response from server");
console.log(data);
break;
}
}
}
}
function ImageLogs({ setTerm, setFitAddon, name }) {
const terminalId = `${name}--terminal`;
const observedElementRef = useRef(null);
useEffect(() => {
let observer;
async function setup() {
const { Terminal } = await import("xterm");
const { FitAddon } = await import("xterm-addon-fit");
const term = new Terminal({
convertEol: true,
disableStdin: true,
// 60 cols is pretty small, but unfortunately we have very limited width
// available in our form!
cols: 66,
rows: 1,
// Increase scrollback since image builds can sometimes produce a ton of output
scrollback: 10000,
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById(terminalId));
fitAddon.fit();
setTerm(term);
setFitAddon(fitAddon);
term.write("Logs will appear here when image is being built");
if (observedElementRef.current) {
observer = new ResizeObserver(() => {
console.log("resize");
fitAddon.fit();
});
observer.observe(observedElementRef.current);
}
}
setup();
return () => {
if (observer) observer.disconnect();
};
}, []);
return (
<div className="terminal-container" ref={observedElementRef}>
<div id={terminalId}></div>
</div>
);
}
export function ImageBuilder({ name, isActive }) {
const {
binderRepo,
ref: repoRef,
setCustomOption,
} = useContext(SpawnerFormContext);
const { repo, repoId, repoFieldProps, repoError } =
useRepositoryField(binderRepo);
const [ref, setRef] = useState(repoRef || "HEAD");
const repoFieldRef = useRef();
const branchFieldRef = useRef();
const [customImage, setCustomImage] = useState("");
const [customImageError, setCustomImageError] = useState(null);
const [term, setTerm] = useState(null);
const [fitAddon, setFitAddon] = useState(null);
const [isBuildingImage, setIsBuildingImage] = useState(false);
useEffect(() => {
if (!isActive) setCustomImageError("");
}, [isActive]);
useEffect(() => {
if (setCustomOption) {
repoFieldRef.current.setAttribute("value", binderRepo);
}
}, [binderRepo, repoRef, setCustomOption]);
const handleBuildStart = async () => {
if (!repo) {
repoFieldRef.current.focus();
repoFieldRef.current.blur();
return;
}
if (!ref) {
branchFieldRef.current.focus();
branchFieldRef.current.blur();
return;
}
setIsBuildingImage(true);
buildImage(repoId, ref, term, fitAddon)
.then((imageName) => {
setCustomImage(imageName);
term.write(
"\nImage has been built! Click the start button to launch your server",
);
})
.catch(() => console.log(`Error building image.`))
.finally(() => setIsBuildingImage(false));
};
// We render everything, but only toggle visibility based on wether we are being
// shown or hidden. This provides for more DOM stability, and also allows the image
// to continue being built evn if the user moves away elsewhere. When hidden, we just
// don't generate the hidden input that posts the built image out.
return (
<>
<div className="row mb-4 align-items-center ">
<div className="col-sm-3">
<div className="form-label">Provider</div>
</div>
<div className="col-sm-9">GitHub</div>
</div>
<div
className={`row mb-4 align-items-center ${repoError ? "has-error" : ""}`}
>
<div className="col-sm-12 col-xl-3">
<label htmlFor="repo" className="form-label">
Repository
</label>
</div>
<div className="col-sm-12 col-xl-9">
<input
className="form-control"
id="repo"
type="text"
ref={repoFieldRef}
{...repoFieldProps}
aria-invalid={!!repoError}
/>
{repoError && (
<div className="profile-option-control-error">{repoError}</div>
)}
</div>
</div>
<TextField
ref={branchFieldRef}
id={`${name}--ref`}
label="Git Ref"
hint="Branch, Tag or Commit to use. HEAD will use the default branch"
value={ref}
validate={
isActive && {
required: "Enter a git ref.",
}
}
onChange={(e) => setRef(e.target.value)}
tabIndex={isActive ? "0" : "-1"}
/>
<div className="right-button">
<button
type="button"
className="btn btn-jupyter"
onClick={handleBuildStart}
disabled={isBuildingImage}
>
Build image
</button>
</div>
<input
type="text"
name={name}
value={customImage}
aria-invalid={isActive && !customImage}
required={isActive}
aria-hidden="true"
style={{ display: "none" }}
onInvalid={() =>
setCustomImageError("Wait for the image build to complete.")
}
onChange={() => {}} // Hack to prevent a console error, while at the same time allowing for this field to be validatable, ie. not making it read-only
/>
<div className="row mb-4 align-items-center">
<div className="col-sm-12 col-xl-3">
<div className="form-label">Build Logs</div>
</div>
<div className="col-sm-12 col-xl-9">
<ImageLogs setFitAddon={setFitAddon} setTerm={setTerm} name={name} />
{customImageError && (
<div className="profile-option-control-error">
{customImageError}
</div>
)}
</div>
</div>
</>
);
}