Skip to content

Commit

Permalink
Merge branch 'main' into bootstrap5
Browse files Browse the repository at this point in the history
  • Loading branch information
GeorgianaElena authored Dec 17, 2024
2 parents 038b109 + c828fb8 commit 411ce51
Show file tree
Hide file tree
Showing 15 changed files with 197 additions and 269 deletions.
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ but can be adapted to work with any other local kubernetes setup.
username and password.

```bash
python -m jupyterhub
jupyterhub
```

**Troubleshooting:** On MacOS, if you're seeing the error `Errno 8: Nodename nor servname provided`, try running `jupyterhub --ip=localhost` instead.

8. If you're working on the JS / CSS, you can also run the following command in another
terminal to automatically watch and rebuild the JS / CSS as you edit.

Expand Down
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ for use with [jupyterhub-kubespawner](https://github.com/jupyterhub/kubespawner)

## Limitations

1. While multiple `profile_options` are supported, only a single `profile` is
supported.

2. The forms values don't remember their previous state upon refresh
1. The forms values don't remember their previous state upon refresh

## How to use

Expand Down
5 changes: 5 additions & 0 deletions jupyterhub_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,9 @@
}
},
},
{
"display_name": "Profile without any options",
"description": "Just a profile that doesn't contain any profile options",
"kubespawner_override": {"image": "pangeo/pangeo-notebook:2023.09.11"},
},
]
12 changes: 12 additions & 0 deletions setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ window.profileList = [
slug: "custom",
},
{
slug: "build-custom-environment",
description: "Dynamic Image building + unlisted choice",
display_name: "Build custom environment",
profile_options: {
Expand All @@ -220,4 +221,15 @@ window.profileList = [
},
},
},
{
slug: "empty-options",
description: "Profile with empty options",
display_name: "Empty Options",
profile_options: {},
},
{
slug: "no-options",
description: "Profile with no options",
display_name: "No Options",
},
];
141 changes: 83 additions & 58 deletions src/ImageBuilder.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useEffect, useState, useRef, useContext } from "react";
import Select from "react-select";
import { TextField } from "./components/form/fields";
import { SpawnerFormContext } from "./state";
import useRepositoryField from "./hooks/useRepositoryField";
import useRefField from "./hooks/useRefField";

async function buildImage(repo, ref, term, fitAddon, onImageBuilt) {
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/
Expand All @@ -22,6 +21,7 @@ async function buildImage(repo, ref, term, fitAddon, onImageBuilt) {
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) {
Expand All @@ -36,13 +36,12 @@ async function buildImage(repo, ref, term, fitAddon, onImageBuilt) {
switch (data.phase) {
case "failed": {
image.close();
break;
return Promise.reject();
}
case "ready": {
// Close the EventStream when the image has been built
image.close();
onImageBuilt(data.imageName);
break;
return Promise.resolve(data.imageName);
}
default: {
console.log("Unknown phase in response from server");
Expand All @@ -53,7 +52,8 @@ async function buildImage(repo, ref, term, fitAddon, onImageBuilt) {
}
}

function ImageLogs({ setTerm, setFitAddon }) {
function ImageLogs({ setTerm, setFitAddon, name }) {
const terminalId = `${name}--terminal`;
useEffect(() => {
async function setup() {
const { Terminal } = await import("xterm");
Expand All @@ -65,10 +65,21 @@ function ImageLogs({ setTerm, setFitAddon }) {
// available in our form!
cols: 66,
rows: 1,
// Increase scrollback since image builds can sometimes produce a ton of output
scrollback: 10000,
// colors checked with the contrast checker at https://webaim.org/resources/contrastchecker/
theme: {
red: "\x1b[38;2;248;113;133m",
green: "\x1b[38;2;134;239;172m",
yellow: "\x1b[38;2;253;224;71m",
blue: "\x1b[38;2;147;197;253m",
magenta: "\x1b[38;2;249;168;212m",
cyan: "\x1b[38;2;103;232;249m",
}
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById("terminal"));
term.open(document.getElementById(terminalId));
fitAddon.fit();
setTerm(term);
setFitAddon(fitAddon);
Expand All @@ -78,43 +89,39 @@ function ImageLogs({ setTerm, setFitAddon }) {
}, []);

return (
<div className="profile-option-container">
<div className="profile-option-label-container">
<b>Build Logs</b>
</div>
<div className="profile-option-control-container">
<div className="terminal-container">
<div id="terminal"></div>
</div>
</div>
<div className="terminal-container">
<div id={terminalId}></div>
</div>
);
}

export function ImageBuilder({ name }) {
export function ImageBuilder({ name, isActive }) {
const {
binderRepo,
ref: repoRef,
setCustomOption,
} = useContext(SpawnerFormContext);
const { repo, repoId, repoFieldProps, repoError, repoIsValidating } =
const { repo, repoId, repoFieldProps, repoError } =
useRepositoryField(binderRepo);
const { ref, refError, refFieldProps, refIsLoading } = useRefField(
repoId,
repoRef,
);
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);
branchFieldRef.current.value = repoRef;
}
}, [binderRepo, repoRef, setCustomOption]);

Expand All @@ -131,12 +138,16 @@ export function ImageBuilder({ name }) {
return;
}

await buildImage(repoId, ref, term, fitAddon, (imageName) => {
setCustomImage(imageName);
term.write(
"\nImage has been built! Click the start button to launch your server",
);
});
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
Expand Down Expand Up @@ -165,47 +176,61 @@ export function ImageBuilder({ name }) {
{...repoFieldProps}
aria-invalid={!!repoError}
/>
{repoIsValidating && (
<div className="profile-option-control-info">
Validating repository...
</div>
)}
{repoError && <div className="invalid-feedback">{repoError}</div>}
{repoError && <div className="invalid-feedback">{repoError}</div>}
</div>
</div>

<div className="profile-option-container">
<div className="profile-option-label-container">
<div className="form-label">Git Ref</div>
</div>
<div className="profile-option-control-container">
<Select
aria-label="Git Ref"
ref={branchFieldRef}
{...refFieldProps}
aria-invalid={!!refError}
isDisabled={!refFieldProps.options}
/>
{refIsLoading && !refIsLoading && (
<div className="profile-option-control-info">
Loading Git ref options...
</div>
)}
{refError && <div className="is-invalid">{refError}</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 name={name} type="hidden" value={customImage} />
<ImageLogs setFitAddon={setFitAddon} setTerm={setTerm} />
<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="profile-option-container">
<div className="profile-option-label-container">
<b>Build Logs</b>
</div>
<div className="profile-option-control-container">
<ImageLogs setFitAddon={setFitAddon} setTerm={setTerm} name={name} />
{customImageError && (
<div className="profile-option-control-error">
{customImageError}
</div>
)}
</div>
</div>
</>
);
}
Loading

0 comments on commit 411ce51

Please sign in to comment.