Skip to content

Commit

Permalink
Merge pull request #25 from developmentseed/state-refactor
Browse files Browse the repository at this point in the history
Refactor state management
  • Loading branch information
batpad authored Apr 15, 2024
2 parents f78d620 + eb4e719 commit b918aa2
Show file tree
Hide file tree
Showing 14 changed files with 314 additions and 224 deletions.
13 changes: 7 additions & 6 deletions src/ImageBuilder.jsx → src/ImageSelect/ImageBuilder.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect, useState, useContext } from "react";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import { useEffect, useState } from "react";
import { BinderRepository } from "@jupyterhub/binderhub-client";
import { SpawnerFormContext } from "../state";

async function buildImage(repo, ref, term, fitAddon, onImageBuilt) {
const providerSpec = "gh/" + repo + "/" + ref;
Expand Down Expand Up @@ -89,9 +90,9 @@ function ImageLogs({ visible, setTerm, setFitAddon }) {
</>
);
}
export function ImageBuilder({ visible, unlistedInputName }) {
export function ImageBuilder({ visible, name }) {
const [repo, setRepo] = useState("");
const [builtImage, setBuiltImage] = useState(null);
const { customImage, setCustomImage } = useContext(SpawnerFormContext);

// FIXME: Allow users to actually configure this
const [ref, _] = useState("HEAD"); // eslint-disable-line no-unused-vars
Expand Down Expand Up @@ -132,15 +133,15 @@ export function ImageBuilder({ visible, unlistedInputName }) {
value="Build image"
onClick={async () => {
await buildImage(repo, ref, term, fitAddon, (imageName) => {
setBuiltImage(imageName);
setCustomImage(imageName);
term.write(
"\nImage has been built! Click the start button to launch your server",
);
});
}}
/>
{visible && builtImage && (
<input name={unlistedInputName} type="hidden" value={builtImage} />
{visible && customImage && (
<input name={name} type="hidden" value={customImage} />
)}
</div>

Expand Down
70 changes: 70 additions & 0 deletions src/ImageSelect/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useContext } from "react";
import { SelectField, TextField } from "../components/form/fields";
import { ImageBuilder } from "./ImageBuilder";
import useSelectOptions from "../hooks/useSelectOptions";
import { SpawnerFormContext } from "../state";

const extraChoices = [
{
value: "dockerImage",
label: "Specify an existing docker image",
description:
"Use a pre-existing docker image from a public docker registry",
},
{
value: "buildImage",
label: "Build your own image",
description:
"Use a mybinder.org compatible GitHub repo to build your own image",
},
];

function ImageSelect({ config }) {
const { profile } = useContext(SpawnerFormContext);
const FIELD_ID = `profile-option-${profile.slug}--image`;
const FIELD_ID_UNLISTED = `${FIELD_ID}--unlisted-choice`;
const { display_name, choices } = config;

const { options, defaultOption } = useSelectOptions(choices, extraChoices);

const {
image,
setImage,
customImage,
setCustomImage,
errors,
touched,
setTouched,
} = useContext(SpawnerFormContext);

return (
<>
<SelectField
id={FIELD_ID}
label={display_name}
options={options}
defaultOption={defaultOption}
error={touched[FIELD_ID] && errors[FIELD_ID]}
onChange={(e) => setImage(e.value)}
onBlur={() => setTouched(FIELD_ID, true)}
/>
{image === "dockerImage" && (
<TextField
id={FIELD_ID_UNLISTED}
label="Custom image"
value={customImage}
required
pattern="^.+:.+$"
error={touched[FIELD_ID_UNLISTED] && errors[FIELD_ID_UNLISTED]}
onChange={(e) => setCustomImage(e.target.value)}
onBlur={() => setTouched(FIELD_ID_UNLISTED, true)}
/>
)}
{image === "buildImage" && (
<ImageBuilder name={FIELD_ID_UNLISTED} visible="true" />
)}
</>
);
}

export default ImageSelect;
38 changes: 17 additions & 21 deletions src/Form.jsx → src/ProfileForm.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { createRoot } from "react-dom/client";

import { useContext } from "react";
import "../node_modules/xterm/css/xterm.css";

import "./form.css";
import { ResourceSelector } from "./ResourceSelector";
import { SpawnerFormContext, SpawnerFormProvider } from "./state";
import { useContext } from "react";
import ImageSelect from "./ImageSelect";
import ResourceSelect from "./ResourceSelect";
import { SpawnerFormContext } from "./state";

/**
* Generates the *contents* of the form shown in the profile selection page
Expand All @@ -14,11 +13,11 @@ import { useContext } from "react";
* be generated here.
*/
function Form() {
const profileList = window.profileList;

// Currently, we only support a single profile, with many options.
const profile = profileList[0];
const { canSubmit } = useContext(SpawnerFormContext);
const { profile, errors } = useContext(SpawnerFormContext);
const { image, resources } = profile.profile_options;

const canSubmit = Object.keys(errors).length === 0;

return (
<div className="form-grid">
Expand All @@ -30,21 +29,18 @@ function Form() {
checked
readOnly
/>
<ResourceSelector profile={profile} />
<input
id="submit-button"
<ImageSelect config={image} />
<ResourceSelect config={resources} />
<div />
<button
className="btn btn-jupyter form-control"
type="submit"
value="Start"
disabled={!canSubmit}
className="btn btn-jupyter form-control"
/>
>
Start
</button>
</div>
);
}

const root = createRoot(document.getElementById("form"));
root.render(
<SpawnerFormProvider>
<Form />
</SpawnerFormProvider>,
);
export default Form;
107 changes: 0 additions & 107 deletions src/ProfileOption.jsx

This file was deleted.

27 changes: 27 additions & 0 deletions src/ResourceSelect.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useContext } from "react";
import useSelectOptions from "./hooks/useSelectOptions";
import { SpawnerFormContext } from "./state";
import { SelectField } from "./components/form/fields";

function ResourceSelect({ config }) {
const { display_name, choices } = config;

const { options, defaultOption } = useSelectOptions(choices);
const { setResource, profile, touched, setTouched, errors } =
useContext(SpawnerFormContext);
const FIELD_ID = `profile-option-${profile.slug}--resource`;

return (
<SelectField
id={FIELD_ID}
label={display_name}
options={options}
defaultOption={defaultOption}
error={touched[FIELD_ID] && errors[FIELD_ID]}
onChange={(e) => setResource(e.value)}
onBlur={() => setTouched(FIELD_ID, true)}
/>
);
}

export default ResourceSelect;
27 changes: 0 additions & 27 deletions src/ResourceSelector.jsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import Select from "react-select";
* @param {Props} props
* @returns
*/
export function CustomizedSelect({ options, ...props }) {
export function CustomizedSelect({ options, hasError, ...props }) {
const [lastSelectedChoice, setLastSelectedChoice] = useState(null);
return (
<Select
Expand Down Expand Up @@ -67,6 +67,12 @@ export function CustomizedSelect({ options, ...props }) {
}
setLastSelectedChoice(option);
}}
styles={{
control: (baseStyles) => ({
...baseStyles,
borderColor: hasError ? "red" : "grey",
}),
}}
{...props}
/>
);
Expand Down
Loading

0 comments on commit b918aa2

Please sign in to comment.