feat: add optional volume descriptions for UI

Added VOLUME_DESCRIPTIONS config dict allowing optional user-friendly
descriptions for volumes shown in management UI.

Changes:
- Added VOLUME_DESCRIPTIONS dict in jupyterhub_config.py (optional, co-defined
  with DOCKER_SPAWNER_VOLUMES)
- Exposed volume_descriptions via c.JupyterHub.template_vars
- Updated home.html to conditionally display descriptions if defined
- If volume not in VOLUME_DESCRIPTIONS, no description shown (UI remains agnostic)

Configuration example:
VOLUME_DESCRIPTIONS = {
    'home': 'User home directory files, configurations',
    'workspace': 'Project files, notebooks, code',
    'cache': 'Temporary files, pip cache, conda cache'
}

Documentation:
- Updated .claude/CLAUDE.md with Manage Volumes implementation details
- Simplified Restart Server section for consistency
- Added template variables to doc/ui-template-customization.md

UI now shows volume name, Docker volume path, and optional description
(if configured). Fully backward compatible - descriptions are optional.
This commit is contained in:
stellarshenson
2025-11-09 22:50:13 +01:00
parent 96f3d546b0
commit 12953ee4d6
4 changed files with 47 additions and 30 deletions

View File

@@ -258,52 +258,52 @@ The startup script will automatically read this list and create missing groups.
## User Self-Service Features
The platform provides two self-service features accessible from the user control panel (`/hub/home`):
### Manage Volumes
### Reset Home Volume
**Purpose**: Allows users to delete their home directory volume and start fresh with a clean environment.
Selective volume reset allowing users to delete chosen persistent volumes (home, workspace, cache).
**Requirements**:
- User's JupyterLab server must be stopped
- Volume `jupyterlab-{username}_home` must exist
- Volumes dynamically read from `DOCKER_SPAWNER_VOLUMES` config
**Implementation**:
- API Endpoint: `DELETE /hub/api/users/{username}/reset-home-volume`
- Handler: `services/jupyterhub/conf/bin/custom_handlers.py::ResetHomeVolumeHandler`
- Uses Docker API to safely remove the volume
- Only affects home volume - workspace and cache volumes are preserved
- API Endpoint: `DELETE /hub/api/users/{username}/manage-volumes`
- Handler: `services/jupyterhub/conf/bin/custom_handlers.py::ManageVolumesHandler`
- Template: `services/jupyterhub/templates/home.html` with dynamic checkbox generation
- Volume list extracted via `get_user_volume_suffixes(DOCKER_SPAWNER_VOLUMES)`
- Optional descriptions from `VOLUME_DESCRIPTIONS` dict (config file)
- Validates requested volumes against `USER_VOLUME_SUFFIXES`
**Permissions**:
- Users can reset their own home volume
- Admins can reset any user's home volume
- Enforced via `@admin_or_self` decorator
**Configuration** (`config/jupyterhub_config.py`):
```python
DOCKER_SPAWNER_VOLUMES = {
"jupyterlab-{username}_home": "/home",
"jupyterlab-{username}_workspace": DOCKER_NOTEBOOK_DIR,
"jupyterlab-{username}_cache": "/home/lab/.cache",
}
VOLUME_DESCRIPTIONS = {
'home': 'User home directory files, configurations',
'workspace': 'Project files, notebooks, code',
'cache': 'Temporary files, pip cache, conda cache'
}
```
**Permissions**: Users can manage own volumes, admins can manage any user's volumes
### Restart Server
**Purpose**: Provides one-click Docker container restart without recreating the container.
One-click Docker container restart without recreation. Preserves volumes and configuration.
**Requirements**:
- User's JupyterLab server must be running
- Container `jupyterlab-{username}` must exist
**Requirements**: User's JupyterLab server must be running
**Implementation**:
- API Endpoint: `POST /hub/api/users/{username}/restart-server`
- Handler: `services/jupyterhub/conf/bin/custom_handlers.py::RestartServerHandler`
- Uses Docker's native `container.restart(timeout=10)` method
- Preserves container identity, volumes, and configuration
- Does NOT recreate container (unlike JupyterHub's stop/spawn cycle)
**Permissions**:
- Users can restart their own server
- Admins can restart any user's server
- Enforced via `@admin_or_self` decorator
**UI Location**:
- Custom template: `services/jupyterhub/templates/home.html`
- Reset button visible when server is stopped
- Restart button visible when server is running
- Both include confirmation modals with warnings
**Permissions**: Users can restart own server, admins can restart any user's server
## Troubleshooting

View File

@@ -120,6 +120,14 @@ DOCKER_SPAWNER_VOLUMES = {
"jupyterhub_shared": "/mnt/shared" # shared drive across hub
}
# Optional descriptions for user volumes (shown in UI)
# If a volume suffix is not listed here, no description will be shown
VOLUME_DESCRIPTIONS = {
'home': 'User home directory files, configurations',
'workspace': 'Project files, notebooks, code',
'cache': 'Temporary files, pip cache, conda cache'
}
# Helper function to extract user-specific volume suffixes
def get_user_volume_suffixes(volumes_dict):
"""Extract volume suffixes from volumes dict that follow jupyterlab-{username}_<suffix> pattern"""
@@ -145,9 +153,10 @@ if c is not None:
# Set volumes from constant
c.DockerSpawner.volumes = DOCKER_SPAWNER_VOLUMES
# Make volume suffixes available to templates
# Make volume suffixes and descriptions available to templates
c.JupyterHub.template_vars = {
'user_volume_suffixes': USER_VOLUME_SUFFIXES
'user_volume_suffixes': USER_VOLUME_SUFFIXES,
'volume_descriptions': VOLUME_DESCRIPTIONS
}
# Built-in groups that cannot be deleted (auto-recreated if missing)

View File

@@ -8,6 +8,10 @@ JupyterHub templates extended using Jinja2 to add custom UI features (server res
- Changes require Docker rebuild with `--no-cache` flag
- JupyterHub 5.4.2 uses Bootstrap 5 (not Bootstrap 4)
**Template Variables** (via `c.JupyterHub.template_vars`):
- `user_volume_suffixes`: List of volume suffixes from `DOCKER_SPAWNER_VOLUMES`
- `volume_descriptions`: Optional dict mapping suffixes to descriptions
**JavaScript Integration**:
All custom JavaScript wrapped in RequireJS to ensure library loading:
```javascript

View File

@@ -146,6 +146,10 @@
<strong>{{ volume_suffix }}</strong>
<br>
<code class="text-muted">jupyterlab-{{ user.name }}_{{ volume_suffix }}</code>
{% if volume_descriptions and volume_suffix in volume_descriptions %}
<br>
<small class="text-muted">{{ volume_descriptions[volume_suffix] }}</small>
{% endif %}
</label>
</div>
{% endfor %}