Skip to content

Commit e9c46d5

Browse files
authored
Pinned services via JAppsConfig (#636)
* make pinned services configurable via JAppsConfig * unpin Argo * rename pinned_services to additional_services * Add more detail about basis C4 encoded thumbnails * refactor * update to use new trait
1 parent f855918 commit e9c46d5

File tree

8 files changed

+225
-53
lines changed

8 files changed

+225
-53
lines changed

docs/docs/configuration.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,52 @@ Specifies the version of `jhub-app-proxy` to install when deploying apps.
144144
- This sets the default version globally for all apps
145145
- Can be overridden per-app by setting the `JHUB_APP_PROXY_VERSION` environment variable in the app's configuration
146146
- Priority: App-specific `JHUB_APP_PROXY_VERSION` env var > Global config > Default
147+
148+
### `additional_services`
149+
150+
List of additional external services to display in JupyterHub's services menu. Services with `pinned=True`
151+
will also appear in the quick access section for easy access.
152+
153+
- **Example**:
154+
```python
155+
c.JAppsConfig.additional_services = [
156+
{
157+
"name": "Monitoring",
158+
"url": "/grafana",
159+
"description": "System monitoring dashboard",
160+
"pinned": True,
161+
"thumbnail": "https://example.com/grafana-logo.svg",
162+
},
163+
{
164+
"name": "Argo",
165+
"url": "/argo",
166+
},
167+
{
168+
"name": "Environments",
169+
"url": "/conda-store",
170+
"description": "Conda environment manager",
171+
"pinned": True,
172+
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mNkYPhfz0AEYBxVSF+FAP5FDvcfRYWgAAAAAElFTkSuQmCC",
173+
},
174+
]
175+
```
176+
- **Fields**:
177+
- `name` (required): Display name of the service
178+
- `url` (required): URL path for the service
179+
- `description` (optional): Description of the service shown in the UI
180+
- `pinned` (optional): Whether the service should appear in the quick access section (default: `False`)
181+
- `thumbnail` (optional): URL or base64-encoded data URL for the service icon (e.g., `"https://..."` or `"data:image/png;base64,..."`)
182+
- **Notes**:
183+
- This replaces the older approach of manually extending `c.JupyterHub.services` with custom service dictionaries
184+
- For advanced use cases, you can still use the programmatic approach with the `service_for_jhub_apps` helper:
185+
```python
186+
from jhub_apps import service_for_jhub_apps
187+
c.JupyterHub.services.extend([
188+
service_for_jhub_apps(
189+
name="Custom Service",
190+
url="/custom",
191+
description="My custom service",
192+
pinned=True,
193+
),
194+
])
195+
```

jhub_apps/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pathlib import Path
22

33
from jhub_apps.config_utils import JAppsConfig # noqa: F401
4+
from jhub_apps.service_utils import service_for_jhub_apps # noqa: F401
45

56
HERE = Path(__file__).parent.resolve()
67

jhub_apps/config_utils.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from traitlets.config import SingletonConfigurable, Enum
77

8-
from jhub_apps.service.models import StartupApp
8+
from jhub_apps.service.models import StartupApp, AdditionalService
99

1010

1111
# jhub-app-proxy configuration constants
@@ -137,3 +137,33 @@ class JAppsConfig(SingletonConfigurable):
137137
DEFAULT_JHUB_APP_PROXY_VERSION,
138138
help="Version of jhub-app-proxy to install. Can be overridden by JHUB_APP_PROXY_VERSION environment variable.",
139139
).tag(config=True)
140+
141+
additional_services = List(
142+
trait=PydanticModelTrait(AdditionalService),
143+
description="List of additional external services to display in JupyterHub UI.",
144+
default_value=[],
145+
help="""
146+
List of additional external services to display in JupyterHub's services menu.
147+
Services with pinned=True will also appear in the quick access section.
148+
Each service should be an AdditionalService model instance or dict with keys:
149+
name (str), url (str), description (str, optional), pinned (bool, optional),
150+
thumbnail (str, optional) - can be a URL or base64-encoded data URL.
151+
152+
Example:
153+
c.JAppsConfig.additional_services = [
154+
{
155+
"name": "Monitoring",
156+
"url": "/grafana",
157+
"pinned": True,
158+
"description": "System monitoring",
159+
"thumbnail": "https://example.com/logo.svg",
160+
},
161+
{
162+
"name": "Environments",
163+
"url": "/conda-store",
164+
"pinned": True,
165+
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...",
166+
},
167+
]
168+
""",
169+
).tag(config=True)

jhub_apps/configuration.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ def install_jhub_apps(c, spawner_to_subclass, *, oauth_no_confirm=False):
100100
]
101101
)
102102

103+
# Add additional services from JAppsConfig
104+
if japps_config.additional_services:
105+
from jhub_apps.service_utils import additional_service_to_service_dict
106+
additional_service_dicts = [
107+
additional_service_to_service_dict(service)
108+
for service in japps_config.additional_services
109+
]
110+
c.JupyterHub.services.extend(additional_service_dicts)
111+
103112
services_roles = [
104113
{
105114
"name": "japps-service-role", # name the role

jhub_apps/service/models.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,17 @@ class JHubAppUserOptions(UserOptions):
100100

101101
class StartupApp(ServerCreation):
102102
username: str
103-
user_options: JHubAppUserOptions
103+
user_options: JHubAppUserOptions
104+
105+
106+
class AdditionalService(BaseModel):
107+
"""Configuration for an additional external service in JupyterHub.
108+
109+
These services appear in the JupyterHub UI services menu.
110+
Services with pinned=True also appear in the quick access section.
111+
"""
112+
name: str
113+
url: str
114+
description: Optional[str] = None
115+
pinned: bool = False
116+
thumbnail: Optional[str] = None

jhub_apps/service_utils.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Utilities for configuring JupyterHub services."""
2+
from typing import Dict, Any, Optional
3+
4+
from jhub_apps.service.models import AdditionalService
5+
6+
7+
def service_for_jhub_apps(
8+
name: str,
9+
url: str,
10+
description: Optional[str] = None,
11+
pinned: bool = False,
12+
thumbnail: Optional[str] = None
13+
) -> Dict[str, Any]:
14+
"""Create a service configuration dict for JupyterHub services.
15+
16+
This helper function creates the proper structure for external services
17+
that appear in the JupyterHub UI services menu. Services with pinned=True
18+
also appear in the quick access section. It validates the input using
19+
the AdditionalService Pydantic model.
20+
21+
Args:
22+
name: Display name of the service
23+
url: URL path for the service
24+
description: Optional description of the service
25+
pinned: Whether the service should appear in the quick access section
26+
thumbnail: Optional thumbnail URL or base64-encoded data URL for the service icon
27+
28+
Returns:
29+
Dictionary with JupyterHub service configuration
30+
31+
Raises:
32+
ValidationError: If the input parameters don't pass Pydantic validation
33+
34+
Example:
35+
>>> from jhub_apps import service_for_jhub_apps
36+
>>> service = service_for_jhub_apps(
37+
... name="Monitoring",
38+
... url="/grafana",
39+
... pinned=True,
40+
... description="System monitoring dashboard"
41+
... )
42+
"""
43+
# Validate inputs using Pydantic model
44+
additional_service = AdditionalService(
45+
name=name,
46+
url=url,
47+
description=description,
48+
pinned=pinned,
49+
thumbnail=thumbnail,
50+
)
51+
52+
return {
53+
"name": additional_service.name,
54+
"display": True,
55+
"info": {
56+
"name": additional_service.name,
57+
"description": additional_service.description,
58+
"url": additional_service.url,
59+
"external": True,
60+
"pinned": additional_service.pinned,
61+
"thumbnail": additional_service.thumbnail,
62+
},
63+
}
64+
65+
66+
def additional_service_to_service_dict(additional_service: AdditionalService) -> Dict[str, Any]:
67+
"""Convert an AdditionalService model to a JupyterHub service dict.
68+
69+
Args:
70+
additional_service: AdditionalService model instance
71+
72+
Returns:
73+
Dictionary with JupyterHub service configuration
74+
"""
75+
return service_for_jhub_apps(
76+
name=additional_service.name,
77+
url=additional_service.url,
78+
description=additional_service.description,
79+
pinned=additional_service.pinned,
80+
thumbnail=additional_service.thumbnail,
81+
)

jupyterhub_config.py

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,30 @@
1717
c.JAppsConfig.jupyterhub_config_path = "jupyterhub_config.py"
1818
c.JAppsConfig.conda_envs = []
1919
c.JAppsConfig.service_workers = 1
20+
21+
# Configure additional services via JAppsConfig
22+
c.JAppsConfig.additional_services = [
23+
{
24+
"name": "Argo",
25+
"url": "/argo",
26+
},
27+
{
28+
"name": "Users",
29+
"url": "/auth/admin/nebari/console/",
30+
},
31+
{
32+
"name": "Environments",
33+
"url": "/conda-store",
34+
"description": "This is conda-store, your environments manager.",
35+
"pinned": True,
36+
"thumbnail": "https://raw.githubusercontent.com/conda-incubator/conda-store/main/docusaurus-docs/community/assets/logos/conda-store-logo-vertical-lockup.svg",
37+
},
38+
{
39+
"name": "Monitoring",
40+
"url": "/monitoring",
41+
},
42+
]
43+
2044
c.JAppsConfig.startup_apps = [
2145
{
2246
"username": "admin",
@@ -65,36 +89,14 @@
6589

6690
c.JupyterHub.template_paths = theme_template_paths
6791

68-
69-
def service_for_jhub_apps(name, url, description=None, pinned=False, thumbnail=None):
70-
return {
71-
"name": name,
72-
"display": True,
73-
"info": {
74-
"name": name,
75-
"description": description,
76-
"url": url,
77-
"external": True,
78-
"pinned": pinned,
79-
"thumbnail": thumbnail,
80-
},
81-
}
82-
83-
84-
c.JupyterHub.services.extend(
85-
[
86-
service_for_jhub_apps(name="Argo", url="/argo"),
87-
service_for_jhub_apps(name="Users", url="/auth/admin/nebari/console/"),
88-
service_for_jhub_apps(
89-
name="Environments",
90-
description="This is conda-store, your environments manager.",
91-
url="/conda-store",
92-
pinned=True,
93-
thumbnail="https://raw.githubusercontent.com/conda-incubator/conda-store/main/docusaurus-docs/community/assets/logos/conda-store-logo-vertical-lockup.svg",
94-
),
95-
service_for_jhub_apps(name="Monitoring", url="/monitoring"),
96-
]
97-
)
92+
# NOTE: Additional services are now configured via c.JAppsConfig.additional_services above.
93+
# The old approach was to use c.JupyterHub.services.extend() with service_for_jhub_apps() helper.
94+
# You can still use that approach if needed:
95+
#
96+
# from jhub_apps import service_for_jhub_apps
97+
# c.JupyterHub.services.extend([
98+
# service_for_jhub_apps(name="Custom Service", url="/custom", pinned=True),
99+
# ])
98100

99101
# nebari will control these as ways to customize the template
100102
c.JupyterHub.template_vars = {

k3s-dev/config/00-jhub-apps.py

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
3131
c.JAppsConfig.conda_envs = [] # No conda-store in dev environment
3232
c.Spawner.debug = True
3333

34+
# Configure additional services via JAppsConfig
35+
c.JAppsConfig.additional_services = [
36+
{
37+
"name": "MyAwesomeService",
38+
"url": "/services/japps",
39+
},
40+
]
41+
3442
# Install jhub-apps - wraps KubeSpawner with jhub-apps functionality
3543
c = install_jhub_apps(c, spawner_to_subclass=KubeSpawner, oauth_no_confirm=True)
3644

@@ -41,27 +49,6 @@
4149
from jhub_apps import theme_template_paths
4250
c.JupyterHub.template_paths = theme_template_paths
4351

44-
def service_for_jhub_apps(name, url, description=None, pinned=False, thumbnail=None):
45-
return {
46-
"name": name,
47-
"display": True,
48-
"info": {
49-
"name": name,
50-
"description": description,
51-
"url": url,
52-
"external": True,
53-
"pinned": pinned,
54-
"thumbnail": thumbnail,
55-
},
56-
}
57-
58-
59-
c.JupyterHub.services.extend(
60-
[
61-
service_for_jhub_apps(name="MyAwesomeService", url="/services/japps"),
62-
]
63-
)
64-
6552
# Theme configuration (mimics Nebari's look and feel)
6653
from jhub_apps import themes
6754
c.JupyterHub.template_vars = {

0 commit comments

Comments
 (0)