From 841c35a87068ae1502773d40586b1e839613ef92 Mon Sep 17 00:00:00 2001 From: Oren Fromberg Date: Mon, 8 Jun 2026 12:34:47 -0400 Subject: [PATCH 1/3] feat(singleuser): support jupyterlab-gallery for curated notebook exhibits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add first-class support for the jupyterlab-gallery extension (already maintained in the nebari-dev org but not shipped or configurable in this chart today). Deployers wanting to surface curated starter notebooks/tutorials currently have to either build a fork of the singleuser image or use lifecycleHooks.postStart to pip-install at spawn time (~30s per spawn). This change: - Adds jupyterlab-gallery to images/jupyterlab/pixi.toml so the extension is in the singleuser image at build time - Exposes a new singleuser.gallery values block (enabled=false by default; no behavior change for existing deploys) - Renders GalleryManager traitlet config into jupyter_server_config.py when enabled, so exhibits, title, and destination are deploy-time configurable without code changes NOTE: images/jupyterlab/pixi.lock needs regeneration before merge — run `pixi update -m images/jupyterlab/` (or whatever the repo's canonical regen path is). The schema-only changes (values, template, README) are independently reviewable. --- README.md | 25 +++++++++++++++++++++++++ images/jupyterlab/pixi.toml | 1 + templates/singleuser-config.yaml | 9 +++++++++ values.yaml | 27 +++++++++++++++++++++++++++ 4 files changed, 62 insertions(+) diff --git a/README.md b/README.md index 15fa974..9708fb5 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,31 @@ make down See `values.yaml` for all configuration options. The chart wraps the [JupyterHub Helm chart](https://z2jh.jupyter.org/) - all `jupyterhub.*` values are passed through. +## Gallery + +Set `singleuser.gallery.enabled=true` and populate `singleuser.gallery.exhibits` +to surface curated starter notebooks/tutorials in JupyterLab's launcher. Each +exhibit is a Git repo that clones into the user's home on first open: + +```yaml +singleuser: + gallery: + enabled: true + title: "Examples" + destination: "/home/jovyan/gallery" + exhibits: + - title: "Tutorials" + git: "https://github.com/your-org/tutorials" + description: "End-to-end tutorial notebooks" + - title: "Starters" + git: "https://github.com/your-org/starter-notebooks" +``` + +The [jupyterlab-gallery](https://github.com/nebari-dev/jupyterlab-gallery) +extension is baked into the singleuser image; the values block above is the +only deploy-time configuration needed. See the extension README for the full +per-exhibit schema (icons, private-repo auth, etc.). + ## Shared Storage Per-group shared directories (`/shared/` in every user pod) need a diff --git a/images/jupyterlab/pixi.toml b/images/jupyterlab/pixi.toml index 376d36d..ac8cb3c 100644 --- a/images/jupyterlab/pixi.toml +++ b/images/jupyterlab/pixi.toml @@ -36,6 +36,7 @@ jupyterlab-spellchecker = ">=0.7.3" jupyterlab-pioneer = "*" jupyter-ai = "*" jupyterlab-favorites = ">=3.2.1" +jupyterlab-gallery = ">=0.5.0" # jupyter-scheduler is disabled until the service is configured # jupyter-scheduler = ">=2.8.0,<3.0.0" diff --git a/templates/singleuser-config.yaml b/templates/singleuser-config.yaml index 88bccf3..f2f1bd8 100644 --- a/templates/singleuser-config.yaml +++ b/templates/singleuser-config.yaml @@ -15,3 +15,12 @@ data: c.TerminalManager.cull_interval = {{ .Values.singleuserCuller.terminal.cullInterval }} c.ServerApp.shutdown_no_activity_timeout = {{ .Values.singleuserCuller.server.shutdownNoActivityTimeout }} + {{- if .Values.singleuser.gallery.enabled }} + + # jupyterlab-gallery: curated notebook exhibits surfaced in the launcher. + # GalleryManager is the traitlet config namespace from + # https://github.com/nebari-dev/jupyterlab-gallery + c.GalleryManager.title = {{ .Values.singleuser.gallery.title | quote }} + c.GalleryManager.destination = {{ .Values.singleuser.gallery.destination | quote }} + c.GalleryManager.exhibits = {{ .Values.singleuser.gallery.exhibits | toJson }} + {{- end }} diff --git a/values.yaml b/values.yaml index ede006e..e71abd8 100644 --- a/values.yaml +++ b/values.yaml @@ -132,6 +132,33 @@ singleuser: gatewayName: nebari-gateway gatewayPort: 10443 + # --------------------------------------------------------------------------- + # jupyterlab-gallery — curated notebook exhibits surfaced in the JupyterLab + # launcher. The extension is baked into the singleuser image + # (images/jupyterlab/pixi.toml); this block controls whether it is + # configured and which exhibits show up. Disabled by default so existing + # deploys see no behavior change. + # https://github.com/nebari-dev/jupyterlab-gallery + # --------------------------------------------------------------------------- + gallery: + enabled: false + # Launcher card title. + title: "Gallery" + # Where exhibit repos are cloned in the user's home. The extension creates + # one subdirectory per exhibit, named after exhibit.title. + destination: "/home/jovyan/gallery" + # List of exhibits. Each entry produces one launcher card. Minimum + # fields: `title` and `git` (a public repo URL). Optional fields: + # `description` (subtitle), `icon` (URL to a thumbnail), `account` and + # `token` for private repos (typically templated from a Secret via + # singleuser.extraEnv rather than hard-coded here). See the extension + # README for the full per-exhibit schema. Example: + # exhibits: + # - title: "Tutorials" + # git: "https://github.com/your-org/tutorials" + # description: "End-to-end notebooks" + exhibits: [] + # ============================================================================= # Singleuser Idle Culler # ============================================================================= From 1c2fa0e81b7cb663cc86c7e3b47d8f1ce03b60cc Mon Sep 17 00:00:00 2001 From: Oren Fromberg Date: Mon, 8 Jun 2026 12:44:11 -0400 Subject: [PATCH 2/3] regenerate jupyterlab/pixi.lock for jupyterlab-gallery Resolves jupyterlab-gallery to 0.6.3 from conda-forge. Required for the Docker build's 'pixi install --locked' step in the jupyterlab/jupyterlab-base /jupyterlab-gpu/jupyterlab-gpu-base images. --- images/jupyterlab/pixi.lock | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/images/jupyterlab/pixi.lock b/images/jupyterlab/pixi.lock index 7b87c55..0dcdbb0 100644 --- a/images/jupyterlab/pixi.lock +++ b/images/jupyterlab/pixi.lock @@ -291,6 +291,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-base-5.1.0-pyh31011fe_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.2.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-favorites-3.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-gallery-0.6.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-git-0.51.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-pioneer-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-spellchecker-0.8.4-pyhd8ed1ab_0.conda @@ -420,7 +421,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/06/ad/5ba858ffc307ed2b51693d87642f9e9dd6013579840981ac364c6e24c38c/backports_zstd-1.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0f/73/bb1bc2529f852e7bf64a2dec885e89ff9f5cc7bbf6c9340eed30ff2c69c5/ruamel.yaml-0.18.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/11/bd/c35b56ffe2d9a99782a390515a4e0ea9616fb0cb25d7fa2dc1d52e6e1195/jupyterlab_gallery-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/1c/1b83ffb56adb76abe46df91439e9a613bba45e4612a4e6a7bbebca0b35bb/plotlydash_tornado_cmd-0.0.6-py3-none-any.whl @@ -764,6 +764,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-base-5.1.0-pyh31011fe_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.2.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-favorites-3.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-gallery-0.6.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-git-0.51.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-pioneer-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-spellchecker-0.8.4-pyhd8ed1ab_0.conda @@ -893,7 +894,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0f/73/bb1bc2529f852e7bf64a2dec885e89ff9f5cc7bbf6c9340eed30ff2c69c5/ruamel.yaml-0.18.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/11/bd/c35b56ffe2d9a99782a390515a4e0ea9616fb0cb25d7fa2dc1d52e6e1195/jupyterlab_gallery-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/1c/1b83ffb56adb76abe46df91439e9a613bba45e4612a4e6a7bbebca0b35bb/plotlydash_tornado_cmd-0.0.6-py3-none-any.whl @@ -7201,6 +7201,22 @@ packages: - pkg:pypi/jupyterlab-favorites?source=hash-mapping size: 24685 timestamp: 1711052887220 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-gallery-0.6.3-pyhcf101f3_0.conda + sha256: 008f74822b42d5e3476dcfa950cbf203b9db48fe159e1a0f9360288aa1b2024c + md5: 1c9354580595a235dbe1ee0db01bac1e + depends: + - python >=3.10,<4.0 + - jupyter_server >=2.0.1,<3 + - nbgitpuller >=1.2.1 + - gitpython >=3.1.43 + - python + constrains: + - jupyterhub >4 + license: Apache-2.0 AND BSD-3-Clause + purls: + - pkg:pypi/jupyterlab-gallery?source=hash-mapping + size: 53175 + timestamp: 1758622936798 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-git-0.51.0-pyhd8ed1ab_0.conda sha256: 35f573ef65d1b58554f8577205f13b5a25695dfdc9297fb01c2ee55b4cd475b7 md5: 053d26baf17b58d8d945d9a1bedbe441 @@ -8850,21 +8866,6 @@ packages: - ryd ; extra == 'docs' - mercurial>5.7 ; extra == 'docs' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/11/bd/c35b56ffe2d9a99782a390515a4e0ea9616fb0cb25d7fa2dc1d52e6e1195/jupyterlab_gallery-0.6.3-py3-none-any.whl - name: jupyterlab-gallery - version: 0.6.3 - sha256: 29b56630151b6d3dfa75eec46b820a2a80d7057b509de0a800db20492883da02 - requires_dist: - - gitpython>=3.1.43 - - jupyter-server>=2.0.1,<3 - - nbgitpuller>=1.2.1 - - ruff==0.4.4 ; extra == 'dev' - - coverage ; extra == 'test' - - pytest ; extra == 'test' - - pytest-asyncio ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-jupyter[server]>=0.6.0 ; extra == 'test' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl name: annotated-doc version: 0.0.4 From 3d669a129f587deface192f693735989098e118d Mon Sep 17 00:00:00 2001 From: Oren Fromberg Date: Mon, 8 Jun 2026 13:43:28 -0400 Subject: [PATCH 3/3] Drop redundant pixi.toml/lock changes jupyterlab-gallery==0.6.3 is already installed via pixi.toml's [pypi-dependencies] block (line 86). The [dependencies] entry I added in the first commit was a duplicate; the chart-level config plumbing in values.yaml + templates is the actual PR contribution. --- images/jupyterlab/pixi.lock | 35 +++++++++++++++++------------------ images/jupyterlab/pixi.toml | 1 - 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/images/jupyterlab/pixi.lock b/images/jupyterlab/pixi.lock index 0dcdbb0..7b87c55 100644 --- a/images/jupyterlab/pixi.lock +++ b/images/jupyterlab/pixi.lock @@ -291,7 +291,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-base-5.1.0-pyh31011fe_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.2.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-favorites-3.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-gallery-0.6.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-git-0.51.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-pioneer-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-spellchecker-0.8.4-pyhd8ed1ab_0.conda @@ -421,6 +420,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/06/ad/5ba858ffc307ed2b51693d87642f9e9dd6013579840981ac364c6e24c38c/backports_zstd-1.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0f/73/bb1bc2529f852e7bf64a2dec885e89ff9f5cc7bbf6c9340eed30ff2c69c5/ruamel.yaml-0.18.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/bd/c35b56ffe2d9a99782a390515a4e0ea9616fb0cb25d7fa2dc1d52e6e1195/jupyterlab_gallery-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/1c/1b83ffb56adb76abe46df91439e9a613bba45e4612a4e6a7bbebca0b35bb/plotlydash_tornado_cmd-0.0.6-py3-none-any.whl @@ -764,7 +764,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-base-5.1.0-pyh31011fe_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.2.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-favorites-3.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-gallery-0.6.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-git-0.51.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-pioneer-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-spellchecker-0.8.4-pyhd8ed1ab_0.conda @@ -894,6 +893,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0f/73/bb1bc2529f852e7bf64a2dec885e89ff9f5cc7bbf6c9340eed30ff2c69c5/ruamel.yaml-0.18.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/11/bd/c35b56ffe2d9a99782a390515a4e0ea9616fb0cb25d7fa2dc1d52e6e1195/jupyterlab_gallery-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/1c/1b83ffb56adb76abe46df91439e9a613bba45e4612a4e6a7bbebca0b35bb/plotlydash_tornado_cmd-0.0.6-py3-none-any.whl @@ -7201,22 +7201,6 @@ packages: - pkg:pypi/jupyterlab-favorites?source=hash-mapping size: 24685 timestamp: 1711052887220 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-gallery-0.6.3-pyhcf101f3_0.conda - sha256: 008f74822b42d5e3476dcfa950cbf203b9db48fe159e1a0f9360288aa1b2024c - md5: 1c9354580595a235dbe1ee0db01bac1e - depends: - - python >=3.10,<4.0 - - jupyter_server >=2.0.1,<3 - - nbgitpuller >=1.2.1 - - gitpython >=3.1.43 - - python - constrains: - - jupyterhub >4 - license: Apache-2.0 AND BSD-3-Clause - purls: - - pkg:pypi/jupyterlab-gallery?source=hash-mapping - size: 53175 - timestamp: 1758622936798 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-git-0.51.0-pyhd8ed1ab_0.conda sha256: 35f573ef65d1b58554f8577205f13b5a25695dfdc9297fb01c2ee55b4cd475b7 md5: 053d26baf17b58d8d945d9a1bedbe441 @@ -8866,6 +8850,21 @@ packages: - ryd ; extra == 'docs' - mercurial>5.7 ; extra == 'docs' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/11/bd/c35b56ffe2d9a99782a390515a4e0ea9616fb0cb25d7fa2dc1d52e6e1195/jupyterlab_gallery-0.6.3-py3-none-any.whl + name: jupyterlab-gallery + version: 0.6.3 + sha256: 29b56630151b6d3dfa75eec46b820a2a80d7057b509de0a800db20492883da02 + requires_dist: + - gitpython>=3.1.43 + - jupyter-server>=2.0.1,<3 + - nbgitpuller>=1.2.1 + - ruff==0.4.4 ; extra == 'dev' + - coverage ; extra == 'test' + - pytest ; extra == 'test' + - pytest-asyncio ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-jupyter[server]>=0.6.0 ; extra == 'test' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl name: annotated-doc version: 0.0.4 diff --git a/images/jupyterlab/pixi.toml b/images/jupyterlab/pixi.toml index ac8cb3c..376d36d 100644 --- a/images/jupyterlab/pixi.toml +++ b/images/jupyterlab/pixi.toml @@ -36,7 +36,6 @@ jupyterlab-spellchecker = ">=0.7.3" jupyterlab-pioneer = "*" jupyter-ai = "*" jupyterlab-favorites = ">=3.2.1" -jupyterlab-gallery = ">=0.5.0" # jupyter-scheduler is disabled until the service is configured # jupyter-scheduler = ">=2.8.0,<3.0.0"