Repository: nebari-dev/jupyterlab-gallery

Issue:
Should the gallery section be hidden in the launcher if no exhibits are configured?
Should the gallery section be hidden in the launcher if no exhibits are configured? As of today it will show some pre-defined (and not very useful) example repositories (nebari and nebari docker images):

https://github.com/nebari-dev/jupyterlab-gallery/blob/f90920445a5b8e988167895912204497df1334be/jupyterlab_gallery/manager.py#L50-L63

Discussion:
github-actions[bot]: [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/andrewfulton9/jupyterlab-gallery/hide_gallery_without_exhibits?urlpath=lab) :point_left: Launch a Binder on branch _andrewfulton9/jupyterlab-gallery/hide_gallery_without_exhibits_

Patch:
diff --git a/binder/environment.yml b/binder/environment.yml
index dcb27bc..bf2a725 100644
--- a/binder/environment.yml
+++ b/binder/environment.yml
@@ -17,5 +17,5 @@ dependencies:
   - nodejs >=18,<19
   - pip
   - wheel
-  # additional packages for demos
-  # - ipywidgets
+  - pip:
+    - jupyterlab-launchpad
diff --git a/binder/jupyter_gallery_config.json b/binder/jupyter_gallery_config.json
new file mode 100644
index 0000000..47abd79
--- /dev/null
+++ b/binder/jupyter_gallery_config.json
@@ -0,0 +1,33 @@
+{
+  "GalleryManager": {
+    "title": "Tutorials",
+    "destination": "tutorials",
+    "exhibits": [
+      {
+        "git": "https://github.com/numba/nvidia-cuda-tutorial.git",
+        "homepage": "https://github.com/numba/nvidia-cuda-tutorial",
+        "title": "Numba for CUDA Programmers",
+        "description": "Nvidia contributed CUDA tutorial for Numba"
+      },
+      {
+        "git": "https://github.com/yunjey/pytorch-tutorial.git",
+        "homepage": "https://github.com/yunjey/pytorch-tutorial",
+        "title": "PyTorch Tutorial",
+        "description": "PyTorch Tutorial for Deep Learning Researchers",
+        "icon": "https://github.com/yunjey/pytorch-tutorial/raw/master/logo/pytorch_logo_2018.svg"
+      },
+      {
+        "git": "https://github.com/jupyter-widgets/tutorial.git",
+        "homepage": "https://github.com/jupyter-widgets/tutorial",
+        "title": "Jupyter Widgets Tutorial",
+        "description": "The Jupyter Widget Ecosystem"
+      },
+      {
+        "git": "https://github.com/amueller/scipy-2018-sklearn.git",
+        "homepage": "https://github.com/amueller/scipy-2018-sklearn",
+        "title": "Scikit-learn Tutorial 2018",
+        "description": "SciPy 2018 Scikit-learn Tutorial"
+      }
+    ]
+  }
+}
diff --git a/binder/start b/binder/start
new file mode 100644
index 0000000..14ece7f
--- /dev/null
+++ b/binder/start
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+set -e
+
+echo $@
+
+exec jupyter-lab "${@:4}" --config binder/jupyter_gallery_config.json
diff --git a/jupyterlab_gallery/handlers.py b/jupyterlab_gallery/handlers.py
index ab11de8..c02c568 100644
--- a/jupyterlab_gallery/handlers.py
+++ b/jupyterlab_gallery/handlers.py
@@ -27,6 +27,7 @@ class GalleryHandler(BaseHandler):
                 {
                     "title": self.gallery_manager.title,
                     "exhibitsConfigured": len(self.gallery_manager.exhibits) != 0,
+                    "hideGalleryWithoutExhibits": self.gallery_manager.hide_gallery_without_exhibits,
                     "apiVersion": "1.0",
                 }
             )
@@ -63,7 +64,12 @@ class PullHandler(BaseHandler, SyncHandlerBase):
     async def post(self):
         data = self.get_json_body()
         exhibit_id = data["exhibit_id"]
-        exhibit = self.gallery_manager.exhibits[exhibit_id]
+        try:
+            exhibit = self.gallery_manager.exhibits[exhibit_id]
+        except IndexError:
+            self.set_status(406)
+            self.finish(json.dumps({"message": f"exhibit_id {exhibit_id} not found"}))
+            return
         return await super()._pull(
             repo=exhibit["git"],
             exhibit_id=exhibit_id,
diff --git a/jupyterlab_gallery/manager.py b/jupyterlab_gallery/manager.py
index c7dece6..0f3298d 100644
--- a/jupyterlab_gallery/manager.py
+++ b/jupyterlab_gallery/manager.py
@@ -5,7 +5,7 @@ from typing import Optional
 from threading import Thread
 
 from traitlets.config.configurable import LoggingConfigurable
-from traitlets import Dict, List, Unicode
+from traitlets import Dict, List, Unicode, Bool
 
 from .git_utils import (
     extract_repository_owner,
@@ -69,6 +69,12 @@ class GalleryManager(LoggingConfigurable):
         config=True,
     )
 
+    hide_gallery_without_exhibits = Bool(
+        help="Hide Gallery if no exhibits are configured",
+        default_value=False,
+        config=True
+    )
+
     def get_local_path(self, exhibit) -> Path:
         clone_destination = Path(self.destination)
         repository_name = extract_repository_name(exhibit["git"])
diff --git a/src/gallery.tsx b/src/gallery.tsx
index c069877..398213f 100644
--- a/src/gallery.tsx
+++ b/src/gallery.tsx
@@ -53,9 +53,16 @@ export class GalleryWidget extends ReactWidget {
           };
           this._stream.connect(promiseResolver);
         });
+        const xsrfTokenMatch = document.cookie.match('\\b_xsrf=([^;]*)\\b');
+        const args: Record<string, string | number> = {
+          exhibit_id: exhibit.id
+        };
+        if (xsrfTokenMatch) {
+          args['_xsrf'] = xsrfTokenMatch[1];
+        }
         await requestAPI('pull', {
           method: 'POST',
-          body: JSON.stringify({ exhibit_id: exhibit.id })
+          body: JSON.stringify(args)
         });
         await done;
       }
diff --git a/src/handler.ts b/src/handler.ts
index 7df8492..8b3b43e 100644
--- a/src/handler.ts
+++ b/src/handler.ts
@@ -70,11 +70,17 @@ export function eventStream(
   onError: (error: Event) => void
 ): EventSource {
   const settings = ServerConnection.makeSettings();
-  const requestUrl = URLExt.join(
+  let requestUrl = URLExt.join(
     settings.baseUrl,
     'jupyterlab-gallery', // API Namespace
     endPoint
   );
+  const xsrfTokenMatch = document.cookie.match('\\b_xsrf=([^;]*)\\b');
+  if (xsrfTokenMatch) {
+    const fullUrl = new URL(requestUrl);
+    fullUrl.searchParams.append('_xsrf', xsrfTokenMatch[1]);
+    requestUrl = fullUrl.toString();
+  }
   const eventSource = new EventSource(requestUrl);
   eventSource.addEventListener('message', event => {
     const data = JSON.parse(event.data);
diff --git a/src/index.ts b/src/index.ts
index c9dd2e6..27ed4d6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -61,10 +61,17 @@ const plugin: JupyterFrontEndPlugin<void> = {
         `jupyter-gallery API version out of sync, expected ${expectedVersion}, got ${data.apiVersion}`
       );
     }
-
     const title = data.title === 'Gallery' ? trans.__('Gallery') : data.title;
+
+    // hide the widget if no exhibits are configured
+    if (data.hideGalleryWithoutExhibits && !data.exhibitsConfigured) {
+      console.log(
+        'Gallery extension will not add any UI elements because no exhibits are configured'
+      );
+      return;
+    }
     // add the widget to sidebar before waiting for server reply to reduce UI jitter
-    if (launcher && isNewLauncher(launcher) && data.exhibitsConfigured) {
+    if (launcher && isNewLauncher(launcher)) {
       launcher.addSection({
         title,
         className: 'jp-Launcher-openExample',
diff --git a/src/types.ts b/src/types.ts
index 628f0fc..8bc67bc 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -2,6 +2,7 @@ export interface IGalleryReply {
   title: string;
   apiVersion: string;
   exhibitsConfigured: boolean;
+  hideGalleryWithoutExhibits: boolean;
 }
 
 export interface IExhibitReply {


Test patch:
diff --git a/jupyterlab_gallery/tests/test_handlers.py b/jupyterlab_gallery/tests/test_handlers.py
index 6403dae..fe63342 100644
--- a/jupyterlab_gallery/tests/test_handlers.py
+++ b/jupyterlab_gallery/tests/test_handlers.py
@@ -1,5 +1,7 @@
 import json
 
+from jupyter_server.utils import url_path_join
+
 
 async def test_exhibits(jp_fetch):
     response = await jp_fetch("jupyterlab-gallery", "exhibits")
@@ -13,3 +15,20 @@ async def test_gallery(jp_fetch):
     assert response.code == 200
     payload = json.loads(response.body)
     assert payload["apiVersion"] == "1.0"
+
+
+async def test_pull_token_can_be_used_instead_of_xsrf(jp_serverapp, jp_base_url, http_server_client):
+    token = jp_serverapp.identity_provider.token
+    response = await http_server_client.fetch(
+        url_path_join(jp_base_url, "jupyterlab-gallery", "pull"),
+        body=b'{"exhibit_id": 100}',
+        method="POST",
+        headers={
+            "Authorization": f"token {token}",
+            "Cookie": ""
+        },
+        raise_error=False,
+    )
+    assert response.code == 406
+    payload = json.loads(response.body)
+    assert payload["message"] == "exhibit_id 100 not found"