Skip to content

Commit b918aa2

Browse files
authored
Merge pull request #25 from developmentseed/state-refactor
Refactor state management
2 parents f78d620 + eb4e719 commit b918aa2

File tree

14 files changed

+314
-224
lines changed

14 files changed

+314
-224
lines changed

src/ImageBuilder.jsx renamed to src/ImageSelect/ImageBuilder.jsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { useEffect, useState, useContext } from "react";
12
import { Terminal } from "xterm";
23
import { FitAddon } from "xterm-addon-fit";
3-
import { useEffect, useState } from "react";
44
import { BinderRepository } from "@jupyterhub/binderhub-client";
5+
import { SpawnerFormContext } from "../state";
56

67
async function buildImage(repo, ref, term, fitAddon, onImageBuilt) {
78
const providerSpec = "gh/" + repo + "/" + ref;
@@ -89,9 +90,9 @@ function ImageLogs({ visible, setTerm, setFitAddon }) {
8990
</>
9091
);
9192
}
92-
export function ImageBuilder({ visible, unlistedInputName }) {
93+
export function ImageBuilder({ visible, name }) {
9394
const [repo, setRepo] = useState("");
94-
const [builtImage, setBuiltImage] = useState(null);
95+
const { customImage, setCustomImage } = useContext(SpawnerFormContext);
9596

9697
// FIXME: Allow users to actually configure this
9798
const [ref, _] = useState("HEAD"); // eslint-disable-line no-unused-vars
@@ -132,15 +133,15 @@ export function ImageBuilder({ visible, unlistedInputName }) {
132133
value="Build image"
133134
onClick={async () => {
134135
await buildImage(repo, ref, term, fitAddon, (imageName) => {
135-
setBuiltImage(imageName);
136+
setCustomImage(imageName);
136137
term.write(
137138
"\nImage has been built! Click the start button to launch your server",
138139
);
139140
});
140141
}}
141142
/>
142-
{visible && builtImage && (
143-
<input name={unlistedInputName} type="hidden" value={builtImage} />
143+
{visible && customImage && (
144+
<input name={name} type="hidden" value={customImage} />
144145
)}
145146
</div>
146147

src/ImageSelect/index.jsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useContext } from "react";
2+
import { SelectField, TextField } from "../components/form/fields";
3+
import { ImageBuilder } from "./ImageBuilder";
4+
import useSelectOptions from "../hooks/useSelectOptions";
5+
import { SpawnerFormContext } from "../state";
6+
7+
const extraChoices = [
8+
{
9+
value: "dockerImage",
10+
label: "Specify an existing docker image",
11+
description:
12+
"Use a pre-existing docker image from a public docker registry",
13+
},
14+
{
15+
value: "buildImage",
16+
label: "Build your own image",
17+
description:
18+
"Use a mybinder.org compatible GitHub repo to build your own image",
19+
},
20+
];
21+
22+
function ImageSelect({ config }) {
23+
const { profile } = useContext(SpawnerFormContext);
24+
const FIELD_ID = `profile-option-${profile.slug}--image`;
25+
const FIELD_ID_UNLISTED = `${FIELD_ID}--unlisted-choice`;
26+
const { display_name, choices } = config;
27+
28+
const { options, defaultOption } = useSelectOptions(choices, extraChoices);
29+
30+
const {
31+
image,
32+
setImage,
33+
customImage,
34+
setCustomImage,
35+
errors,
36+
touched,
37+
setTouched,
38+
} = useContext(SpawnerFormContext);
39+
40+
return (
41+
<>
42+
<SelectField
43+
id={FIELD_ID}
44+
label={display_name}
45+
options={options}
46+
defaultOption={defaultOption}
47+
error={touched[FIELD_ID] && errors[FIELD_ID]}
48+
onChange={(e) => setImage(e.value)}
49+
onBlur={() => setTouched(FIELD_ID, true)}
50+
/>
51+
{image === "dockerImage" && (
52+
<TextField
53+
id={FIELD_ID_UNLISTED}
54+
label="Custom image"
55+
value={customImage}
56+
required
57+
pattern="^.+:.+$"
58+
error={touched[FIELD_ID_UNLISTED] && errors[FIELD_ID_UNLISTED]}
59+
onChange={(e) => setCustomImage(e.target.value)}
60+
onBlur={() => setTouched(FIELD_ID_UNLISTED, true)}
61+
/>
62+
)}
63+
{image === "buildImage" && (
64+
<ImageBuilder name={FIELD_ID_UNLISTED} visible="true" />
65+
)}
66+
</>
67+
);
68+
}
69+
70+
export default ImageSelect;
Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { createRoot } from "react-dom/client";
2-
1+
import { useContext } from "react";
32
import "../node_modules/xterm/css/xterm.css";
43

54
import "./form.css";
6-
import { ResourceSelector } from "./ResourceSelector";
7-
import { SpawnerFormContext, SpawnerFormProvider } from "./state";
8-
import { useContext } from "react";
5+
import ImageSelect from "./ImageSelect";
6+
import ResourceSelect from "./ResourceSelect";
7+
import { SpawnerFormContext } from "./state";
98

109
/**
1110
* Generates the *contents* of the form shown in the profile selection page
@@ -14,11 +13,11 @@ import { useContext } from "react";
1413
* be generated here.
1514
*/
1615
function Form() {
17-
const profileList = window.profileList;
18-
1916
// Currently, we only support a single profile, with many options.
20-
const profile = profileList[0];
21-
const { canSubmit } = useContext(SpawnerFormContext);
17+
const { profile, errors } = useContext(SpawnerFormContext);
18+
const { image, resources } = profile.profile_options;
19+
20+
const canSubmit = Object.keys(errors).length === 0;
2221

2322
return (
2423
<div className="form-grid">
@@ -30,21 +29,18 @@ function Form() {
3029
checked
3130
readOnly
3231
/>
33-
<ResourceSelector profile={profile} />
34-
<input
35-
id="submit-button"
32+
<ImageSelect config={image} />
33+
<ResourceSelect config={resources} />
34+
<div />
35+
<button
36+
className="btn btn-jupyter form-control"
3637
type="submit"
37-
value="Start"
3838
disabled={!canSubmit}
39-
className="btn btn-jupyter form-control"
40-
/>
39+
>
40+
Start
41+
</button>
4142
</div>
4243
);
4344
}
4445

45-
const root = createRoot(document.getElementById("form"));
46-
root.render(
47-
<SpawnerFormProvider>
48-
<Form />
49-
</SpawnerFormProvider>,
50-
);
46+
export default Form;

src/ProfileOption.jsx

Lines changed: 0 additions & 107 deletions
This file was deleted.

src/ResourceSelect.jsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useContext } from "react";
2+
import useSelectOptions from "./hooks/useSelectOptions";
3+
import { SpawnerFormContext } from "./state";
4+
import { SelectField } from "./components/form/fields";
5+
6+
function ResourceSelect({ config }) {
7+
const { display_name, choices } = config;
8+
9+
const { options, defaultOption } = useSelectOptions(choices);
10+
const { setResource, profile, touched, setTouched, errors } =
11+
useContext(SpawnerFormContext);
12+
const FIELD_ID = `profile-option-${profile.slug}--resource`;
13+
14+
return (
15+
<SelectField
16+
id={FIELD_ID}
17+
label={display_name}
18+
options={options}
19+
defaultOption={defaultOption}
20+
error={touched[FIELD_ID] && errors[FIELD_ID]}
21+
onChange={(e) => setResource(e.value)}
22+
onBlur={() => setTouched(FIELD_ID, true)}
23+
/>
24+
);
25+
}
26+
27+
export default ResourceSelect;

src/ResourceSelector.jsx

Lines changed: 0 additions & 27 deletions
This file was deleted.

src/CustomSelect.jsx renamed to src/components/form/CustomSelect.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import Select from "react-select";
2323
* @param {Props} props
2424
* @returns
2525
*/
26-
export function CustomizedSelect({ options, ...props }) {
26+
export function CustomizedSelect({ options, hasError, ...props }) {
2727
const [lastSelectedChoice, setLastSelectedChoice] = useState(null);
2828
return (
2929
<Select
@@ -67,6 +67,12 @@ export function CustomizedSelect({ options, ...props }) {
6767
}
6868
setLastSelectedChoice(option);
6969
}}
70+
styles={{
71+
control: (baseStyles) => ({
72+
...baseStyles,
73+
borderColor: hasError ? "red" : "grey",
74+
}),
75+
}}
7076
{...props}
7177
/>
7278
);

0 commit comments

Comments
 (0)