This guide is for contributors who want to understand the codebase, make changes, or help maintain the project.
Architecture overview¶
How it works¶
Templates: Jinja2 templates render the initial HTML structure for the profile selection form
React App: JavaScript/React code handles the interactive UI, form state, and dynamic features
Webpack: Bundles the frontend code and outputs it to
static/HTTP Handlers: JupyterHub serves the static assets when the profile page loads
KubeSpawner Integration: The
setup_ui()function configures KubeSpawner to use these templates and handlers
Design philosophy¶
Keep this tool a fairly simple React app focused on profile selection. This won’t become a super-heavy, complex application.
Why react?¶
If this file gets over 200 lines of code long (not counting docs / comments), start using a framework
From the BinderHub JS Source Code
The file did get more than 200 lines long, and BinderHub learned this lesson the hard way. For this project:
Lightweight: Plain React without TypeScript keeps it approachable
Mainstream: Attracts frontend developers and contributors
Just Right: Complex enough for multiple interactive features, not so heavy that it’s hard to maintain
Single Page: Perfect scope for React—one complex page with state management
Development setup¶
Setting up a local Kubernetes cluster¶
We will run a local Kubernetes cluster and point our local JupyterHub instance at it. This allows us to spawn pods in the local cluster and test the user experience end-to-end without needing a remote cluster.
Download, set up, and start minikube or colima.
For Mac OS users: The minikube docker driver does not allow the host machine to communicate with pods inside the cluster, which is required for our development workflow. To work around this, either use colima (
colima start --kubernetes --network-address), or configure minikube with theqemudriver andsocket_vmnetnetworking as described in the minikube docs and start minikube with those options(minikube start --driver qemu --network socket_vmnet).Get the kubernetes pod subnet range – for minikube, run
export POD_SUBNET=$(kubectl get node minikube -o jsonpath="{.spec.podCIDR}")or for colima, run
export POD_SUBNET=$(kubectl get node colima -o jsonpath="{.spec.podCIDR}")Get the gateway IP address of the kubernetes cluster – for minikube, run
export GATEWAY_IP=$(minikube ip)or for colima, get the virtual machine IP address with
export GATEWAY_IP=$(colima ssh -- hostname -I | awk '{print $2}')Add a route for your local host to reach the pod subnet via the gateway IP address
# Linux sudo ip route add $POD_SUBNET via $GATEWAY_IP # later on you can undo this with sudo ip route del $POD_SUBNET # macOS sudo route -n add -net $POD_SUBNET $GATEWAY_IP # later on you can undo this with sudo route delete -net $POD_SUBNET
Troubleshooting¶
Local JupyterHub can’t reach the Kubernetes cluster or user pods¶
If your locally running JupyterHub can’t reach the Kubernetes cluster or the user pods running within it, work through these checks. The steps below use $POD_SUBNET and $GATEWAY_IP — set these first if you haven’t already:
# minikube
export POD_SUBNET=$(kubectl get node minikube -o jsonpath="{.spec.podCIDR}")
export GATEWAY_IP=$(minikube ip)
# colima
export POD_SUBNET=$(kubectl get node colima -o jsonpath="{.spec.podCIDR}")
export GATEWAY_IP=$(colima ssh -- hostname -I | awk '{print $2}')Confirm kubectl is pointing at the right cluster:
kubectl config current-contextExpected output:
minikubeorcolima. If not, runkubectl config use-context minikubeorkubectl config use-context colima.Confirm the node is ready:
# minikube kubectl get node minikube # colima kubectl get node colimaThe
STATUScolumn should showReady. If not, tryminikube startorcolima start --kubernetes --network-address.Confirm the VM is reachable from your host:
ping -c 3 $GATEWAY_IPIf this fails, the VM itself is unreachable—restart minikube or colima.
Confirm the pod CIDR route was added:
# Linux ip route show $POD_SUBNET # macOS netstat -rn | grep $GATEWAY_IPIf no route is shown, re-run the
ip route add/route -n addcommand from the previous step.Confirm a running pod is reachable:
# Find a pod IP kubectl get pods -A -o wide # Ping it ping -c 3 <pod-ip>If steps 3 and 4 pass but this fails, the route is misconfigured—delete it and re-add it.
“Build your own image” fails with a Docker connection error¶
If you see an error like:
docker.errors.DockerException: Error while fetching server API version: ('Connection aborted.', FileNotFoundError(2, 'No such file or directory'))Make sure DOCKER_HOST points to the correct socket. For colima users:
export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock"Setting up the development environment¶
Clone the repository:
git clone https://github.com/2i2c-org/jupyterhub-fancy-profiles.git cd jupyterhub-fancy-profilesSet up a virtual environment (using
venv,conda, etc.)Install Python dependencies:
pip install -r dev-requirements.txt pip install -e .This also builds the JS and CSS assets.
Install configurable
-http -proxy (required for JupyterHub): npm install configurable-http-proxyAdd
configurable-http-proxyto your$PATH:export PATH="$(pwd)/node_modules/.bin:${PATH}"Start JupyterHub and navigate to
localhost:8000:jupyterhubYou can login with any username and password.
If working on JS/CSS, run this in another terminal to automatically watch and rebuild:
npm run webpack:watch
Development workflow¶
React app changes¶
npm run webpack:watch automatically rebuilds the JS/CSS when you save a file. However, the browser caches the old bundle, so you might need to reload or force-reload the page to pick up the new assets.
JupyterHub config and Jinja template changes¶
Changes to jupyterhub_config.py or the Jinja2 templates require a JupyterHub restart to take effect. Stop the running jupyterhub process and start it again.
Testing¶
Tests for the frontend use Jest and React Testing Library for rendering components and asserting DOM state.
Run all tests¶
npm testRun specific test suite¶
To run tests in a specific file (e.g., ProfileForm.test.tsx):
npm test ProfileFormMaking a release¶
We use automation to publish releases to PyPI. Release early and often!
Creating the release¶
Update your local checkout:
git checkout main git stash # if needed git pull upstream main # or origin, as neededCreate a new git tag:
git tag -a v<version-number>In the tag message, at minimum write:
Version <version-number>Ideally, include a brief changelog of notable changes.
Push your tag to GitHub:
git push origin --tagsDone! A new release will automatically be published to PyPI.
Generating release notes¶
After making the release:
Install
github-activity:pip install github-activityGenerate release notes using the previous and current release tags:
github-activity 2i2c-org/jupyterhub-fancy-profiles -s <last-release-tag> -u <this-release-tag>For example, for v0.5.0:
github-activity 2i2c-org/jupyterhub-fancy-profiles -s v0.4.0 -u v0.5.0Copy the output and rearrange/categorize as needed.
github-activitywill automatically group PRs based on tags (e.g.,enhancement,bug) or prefixes (e.g.,[ENH],[BUG]).Create a GitHub release, use the tag as the title, and paste in the generated release notes.
Click Publish Release.