jupyterhub / jupyterhub

Multi-user server for Jupyter notebooks
https://jupyterhub.readthedocs.io
Other
7.82k stars 2.02k forks source link

Missing "Hub Control Panel" option in the menu for classic (tree) view #4901

Open jvhaarst opened 2 months ago

jvhaarst commented 2 months ago

We show our users the tree view by default instead of jupyterlab, as they have been using that since we started with jupyterhub. In our old version (3.1.1) we see the button for access to the "Control Panel" in the top left corner. In our current version (5.0.0), that button is missing.

It also isn't in the menu on first startup. At first I thought it was missing completely, so we would have to instruct our users how to access the "Control Panel".

But.....

If I switch the view from tree to jupyterlab (in another window), the menu item to access the "Control Panel" suddenly appears in the tree view !

This sounds like a bug, not a feature..

How to reproduce

Set your jupyterhub to show the older tree view by default. (c.Spawner.default_url = '/tree') See that the "Control Panel" option is missing from the menu. Duplicate the tab with the view Use the View tab to start the jupyterlab view. Refresh the tree view, and the menu item is there !

See above

Expected behaviour

The menu item is there.

Actual behaviour

The menu item isn't there.

Your personal set up

Installed 5.0.0 from pip, like this:

python3 -m pip install jupyterhub
python3 -m pip install jupyterlab notebook
python3 -m pip install oauthenticator
# pycurl has an issue with SSL in packaged version
pip3 install pycurl --no-binary :all:
python3 -m pip install batchspawner sudospawner wrapspawner
python3 -m pip install ipywidgets jupyterlmod pythreejs
npm install -g configurable-http-proxy
Full environment ``` alembic==1.13.2 annotated-types==0.7.0 anyio==4.4.0 argon2-cffi==23.1.0 argon2-cffi-bindings==21.2.0 arrow==1.3.0 asttokens==2.4.1 async-lru==2.0.4 attrs==23.2.0 Babel==2.15.0 batchspawner==1.3.0 beautifulsoup4==4.12.3 bleach==6.1.0 certifi==2024.7.4 certipy==0.1.3 cffi==1.16.0 charset-normalizer==3.3.2 comm==0.2.2 cryptography==42.0.8 debugpy==1.8.2 decorator==5.1.1 defusedxml==0.7.1 executing==2.0.1 fastjsonschema==2.20.0 fqdn==1.5.1 greenlet==3.0.3 h11==0.14.0 httpcore==1.0.5 httpx==0.27.0 idna==3.7 ipydatawidgets==4.3.5 ipykernel==6.29.5 ipython==8.26.0 ipywidgets==8.1.3 isoduration==20.11.0 jedi==0.19.1 Jinja2==3.1.4 json5==0.9.25 jsonpointer==3.0.0 jsonschema==4.23.0 jsonschema-specifications==2023.12.1 jupyter-events==0.10.0 jupyter-lsp==2.2.5 jupyter_client==8.6.2 jupyter_core==5.7.2 jupyter_server==2.14.2 jupyter_server_terminals==0.5.3 jupyterhub==5.0.0 jupyterlab==4.2.4 jupyterlab_pygments==0.3.0 jupyterlab_server==2.27.3 jupyterlab_widgets==3.0.11 jupyterlmod==4.0.3 Mako==1.3.5 MarkupSafe==2.1.5 matplotlib-inline==0.1.7 mistune==3.0.2 nbclient==0.10.0 nbconvert==7.16.4 nbformat==5.10.4 nest-asyncio==1.6.0 notebook==7.2.1 notebook_shim==0.2.4 numpy==2.1.0 oauthenticator==16.3.1 oauthlib==3.2.2 overrides==7.7.0 packaging==24.1 pamela==1.1.0 pandocfilters==1.5.1 parso==0.8.4 pexpect==4.9.0 platformdirs==4.2.2 prometheus_client==0.20.0 prompt_toolkit==3.0.47 psutil==6.0.0 ptyprocess==0.7.0 pure-eval==0.2.2 pycparser==2.22 pycurl==7.45.3 pydantic==2.8.2 pydantic_core==2.20.1 Pygments==2.18.0 PyJWT==2.8.0 pyOpenSSL==24.1.0 python-dateutil==2.9.0.post0 python-json-logger==2.0.7 pythreejs==2.4.2 PyYAML==6.0.1 pyzmq==26.0.3 referencing==0.35.1 requests==2.32.3 rfc3339-validator==0.1.4 rfc3986-validator==0.1.1 rpds-py==0.19.0 ruamel.yaml==0.18.6 ruamel.yaml.clib==0.2.8 Send2Trash==1.8.3 setuptools==71.0.3 six==1.16.0 sniffio==1.3.1 soupsieve==2.5 SQLAlchemy==2.0.31 stack-data==0.6.3 sudospawner==0.5.2 terminado==0.18.1 tinycss2==1.3.0 tornado==6.4.1 traitlets==5.14.3 traittypes==0.2.1 types-python-dateutil==2.9.0.20240316 typing_extensions==4.12.2 uri-template==1.3.0 urllib3==2.2.2 wcwidth==0.2.13 webcolors==24.6.0 webencodings==0.5.1 websocket-client==1.8.0 widgetsnbextension==4.0.11 wrapspawner==1.0.1 ```
Configuration ```python c = get_config() import os, socket CURRENT_BASE_DIR=os.path.realpath(os.path.dirname(__file__)) SLURM_DIR='/shared/apps/slurm/current' HOSTNAME=socket.gethostname() c.Application.log_level = 'DEBUG' import os os.environ['OAUTH2_TOKEN_URL'] = 'https://auth.anunna.wur.nl/realms/anunna/protocol/openid-connect/token' os.environ['OAUTH2_AUTHORIZE_URL'] = 'https://auth.anunna.wur.nl/realms/anunna/protocol/openid-connect/auth' os.environ['OAUTH2_USERDATA_URL'] = 'https://auth.anunna.wur.nl/realms/anunna/protocol/openid-connect/userinfo' from oauthenticator.generic import GenericOAuthenticator c.JupyterHub.authenticator_class = 'oauthenticator.generic.GenericOAuthenticator' c.GenericOAuthenticator.login_service = 'keycloak' c.OAuthenticator.client_id = 'jupyterhub-dev' c.OAuthenticator.client_secret = '' c.GenericOAuthenticator.oauth_callback_url = 'https://dev.notebook.anunna.wur.nl/hub/oauth_callback' c.GenericOAuthenticator.userdata_url = os.environ['OAUTH2_USERDATA_URL'] c.GenericOAuthenticator.token_url = os.environ['OAUTH2_TOKEN_URL'] c.GenericOAuthenticator.userdata_token_method = 'GET' c.GenericOAuthenticator.userdata_params = {"state": "state"} c.GenericOAuthenticator.username_key = "preferred_username" c.GenericOAuthenticator.claim_groups_key = 'realm_access.roles' c.GenericOAuthenticator.admin_groups = ['admin_unix_admin'] c.GenericOAuthenticator.scope = ['openid', 'profile', 'roles'] c.Authenticator.auto_login = True c.JupyterHub.bind_url = 'http://127.0.0.1:8941' c.JupyterHub.db_url = 'sqlite:///jupyterhub.sqlite' c.JupyterHub.hub_connect_url = f'http://{HOSTNAME}:8945' c.JupyterHub.last_activity_interval = 300 c.ConfigurableHTTPProxy.api_url = 'http://127.0.0.1:8008' from wrapspawner import ProfilesSpawner class FormSpawner(ProfilesSpawner): def options_from_form(self, formdata): #self.log.error("Formdata:--------------------------------------------------------------") #self.log.error(formdata) #self.log.error("Profile data before changes:-------------------------------------------") #self.log.error(self.profiles) opts = formdata.copy() if 'profile' in opts: del opts['profile'] self.select_profile("HPC") for opt in opts: val = opts.get(opt, ['']) self.child_config[opt]=val[0] # Default to first profile if somehow none is provided return dict(profile=formdata.get('profile', [self.profiles[0][1]])[0]) def construct_child(self): super().construct_child() c.JupyterHub.spawner_class = FormSpawner c.JupyterHub.template_vars = { 'announcement': 'You\'re on an upgraded version of Jupyterhub. All should work as it did before. If you encounter any issues, please open a ticket. You can fall back to the old version on old.notebook.anunna.wur.nl, but we will deprecate it in the near future', 'announcement_spawn' : 'You\'re on an upgraded version of Jupyterhub. All should work as it did before. If you encounter any issues, please open a ticket. You can fall back to the old version on old.notebook.anunna.wur.nl, but we will deprecate it in the near future', } c.SudoSpawner.sudospawner_path=f'{CURRENT_BASE_DIR}/bin/sudospawner' c.Spawner.cmd = f'{CURRENT_BASE_DIR}/bin/jupyterhub-singleuser' import batchspawner c.Spawner.start_timeout=60 c.BatchSpawnerBase.batchspawner_singleuser_cmd=f'{CURRENT_BASE_DIR}/bin/batchspawner-singleuser' c.BatchSpawnerBase.batch_submit_cmd = f'{SLURM_DIR}/bin/sbatch --parsable' c.BatchSpawnerBase.batch_query_cmd = f'{SLURM_DIR}/bin/squeue -h -j {{job_id}} -o "%T %B"' c.BatchSpawnerBase.batch_cancel_cmd = f'{SLURM_DIR}/bin/scancel {{job_id}}' c.SlurmSpawner.batch_script = '''#!/bin/bash {% if partition %}#SBATCH --partition={{partition}} {% endif %}{% if runtime %}#SBATCH --time={{runtime}} {% endif %}{% if memory %}#SBATCH --mem={{memory}} {% endif %}{% if gres %}#SBATCH --gres={{gres}} {% endif %}{% if nprocs %}#SBATCH --cpus-per-task={{nprocs}} {% endif %}{% if reservation%}#SBATCH --reservation={{reservation}} {% endif %}{% if options %}#SBATCH {{options}}{% endif %} set -euo pipefail unset XDG_RUNTIME_DIR trap 'echo SIGTERM received' TERM {{prologue}} {{cmd}} {{epilogue}} ''' c.Spawner.default_url = '/tree' c.Spawner.environment = { 'LMOD_CMD':'/usr/share/lmod/lmod/libexec/lmod', 'MODULEPATH':'/shared/modulefiles:/shared/eb_modules/all:/shared/modulefiles/SHARED' } c.Spawner.env_keep = ['LMOD_CMD', 'MODULEPATH'] c.ProfilesSpawner.profiles = [ ('On the cluster', 'HPC', 'batchspawner.SlurmSpawner', {'start_timeout':300,'http_timeout':300, 'req_partition':'main', 'req_memory':'2000'} ), ('Cluster - EZO23306 (test)' , 'EZO23306', 'batchspawner.SlurmSpawner', {'start_timeout':300,'http_timeout':300,'req_partition':'main','req_nprocs':'1','req_memory':'2000M','req_runtime':'4:00:00'} ), ('Cluster - Psi4 (test)' , 'Psi4', 'batchspawner.SlurmSpawner', {'start_timeout':300,'http_timeout':300,'req_partition':'main','req_nprocs':'2','req_memory':'4000M','req_runtime':'4:00:00'} ), ('Sandbox (constrained resources)', 'local', 'sudospawner.SudoSpawner', {'ip':'0.0.0.0'} ), ] c.ProfilesSpawner.form_template="""

Jupyterhub on Anunna

""" c.Authenticator.admin_users = set(['haars001']) c.Authenticator.allow_all = True c.Authenticator.delete_invalid_users = True ```
Logs ``` # paste relevant logs here, if any ```
minrk commented 2 months ago

This is related to the page_config_hook we use to populate hub data into page_config. Notebook 7 didn't call it, but Lab does, meaning it would be correct once a Lab page was visited, but not when another page is.

I tried to track it down with a dev install of notebook, but the issue was gone when I installed from main. I think this is already fixed by https://github.com/jupyter/notebook/pull/7387 which is not yet released.

jvhaarst commented 2 months ago

I just patched our site-packages/notebook/app.py to add the lines from https://github.com/jupyter/notebook/pull/7387 , and indeed the menu item is now present ! Thanks for the fast response and solution, we'll just patch our deployment until the fix lands in the repositories.

jvhaarst commented 2 months ago

Now that this works, is there an easy way to also get the button for the "Control Panel" back in notebook 7 ?

minrk commented 2 months ago

The button in the top bar, as opposed to the menu item? I'm sure it's possible, but I'm not sure how to add it. It would be a javascript Extension.