jupyterlab_notifications_extension 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +204 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +65 -0
- package/lib/request.d.ts +8 -0
- package/lib/request.js +35 -0
- package/package.json +207 -0
- package/src/__tests__/jupyterlab_notifications_extension.spec.ts +9 -0
- package/src/index.ts +107 -0
- package/src/request.ts +46 -0
- package/style/base.css +5 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Stellars Henson
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# jupyterlab_notifications_extension
|
|
2
|
+
|
|
3
|
+
[](https://github.com/stellarshenson/jupyterlab_notifications_extension/actions/workflows/build.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/jupyterlab_notifications_extension)
|
|
5
|
+
[](https://pypi.org/project/jupyterlab-notifications-extension/)
|
|
6
|
+
[](https://pepy.tech/project/jupyterlab-notifications-extension)
|
|
7
|
+
[](https://jupyterlab.readthedocs.io/en/stable/)
|
|
8
|
+
|
|
9
|
+
JupyterLab extension enabling external systems to send notifications that appear in JupyterLab's notification center. Administrators, monitoring systems, and CI/CD pipelines broadcast alerts and status updates to users via a simple REST API.
|
|
10
|
+
|
|
11
|
+
The extension provides a POST endpoint for notification ingestion and polls every 30 seconds to display new notifications using JupyterLab's native notification system. Supports multiple notification types, configurable auto-close behavior, and optional action buttons.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install jupyterlab_notifications_extension
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Requirements**: JupyterLab >= 4.0.0
|
|
20
|
+
|
|
21
|
+
## API Reference
|
|
22
|
+
|
|
23
|
+
### POST /jupyterlab-notifications-extension/ingest
|
|
24
|
+
|
|
25
|
+
Send notifications to JupyterLab users. Requires authentication via `Authorization: token <TOKEN>` header or `?token=<TOKEN>` query parameter.
|
|
26
|
+
|
|
27
|
+
**Endpoint**: `POST /jupyterlab-notifications-extension/ingest`
|
|
28
|
+
|
|
29
|
+
**Request Body** (application/json):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"message": "Your notification message",
|
|
34
|
+
"type": "info",
|
|
35
|
+
"autoClose": 5000,
|
|
36
|
+
"actions": [
|
|
37
|
+
{
|
|
38
|
+
"label": "Click here",
|
|
39
|
+
"caption": "Additional info",
|
|
40
|
+
"displayType": "accent"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Request Parameters**:
|
|
47
|
+
|
|
48
|
+
| Field | Type | Required | Default | Description |
|
|
49
|
+
| ----------- | -------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| `message` | string | Yes | - | Notification text displayed to users |
|
|
51
|
+
| `type` | string | No | `"info"` | Visual style: `default`, `info`, `success`, `warning`, `error`, `in-progress` |
|
|
52
|
+
| `autoClose` | number/boolean | No | `5000` | Milliseconds before auto-dismiss. `false` = manual dismiss only. `0` = silent mode (notification center only, no toast) |
|
|
53
|
+
| `actions` | array | No | `[]` | Action buttons (see below) |
|
|
54
|
+
|
|
55
|
+
**Action Button Schema**:
|
|
56
|
+
|
|
57
|
+
| Field | Type | Required | Default | Description |
|
|
58
|
+
| ------------- | ------ | -------- | ----------- | ------------------------------------------------- |
|
|
59
|
+
| `label` | string | Yes | - | Button text |
|
|
60
|
+
| `caption` | string | No | `""` | Tooltip text |
|
|
61
|
+
| `displayType` | string | No | `"default"` | Visual style: `default`, `accent`, `warn`, `link` |
|
|
62
|
+
|
|
63
|
+
**Response** (200 OK):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"success": true,
|
|
68
|
+
"notification_id": "notif_1762549476180_0"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Error Responses**:
|
|
73
|
+
|
|
74
|
+
- `400 Bad Request` - Missing `message` field or invalid JSON
|
|
75
|
+
- `401 Unauthorized` - Missing or invalid authentication token
|
|
76
|
+
- `500 Internal Server Error` - Server-side processing error
|
|
77
|
+
|
|
78
|
+
## Usage Examples
|
|
79
|
+
|
|
80
|
+
### Python Script
|
|
81
|
+
|
|
82
|
+
The included script auto-detects tokens from `JUPYTERHUB_API_TOKEN`, `JPY_API_TOKEN`, or `JUPYTER_TOKEN` environment variables:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Basic notification
|
|
86
|
+
./scripts/send_notification.py --message "Deployment complete" --type success
|
|
87
|
+
|
|
88
|
+
# Persistent warning (no auto-close)
|
|
89
|
+
./scripts/send_notification.py --message "System maintenance in 1 hour" --type warning --no-auto-close
|
|
90
|
+
|
|
91
|
+
# Silent mode (notification center only)
|
|
92
|
+
./scripts/send_notification.py --message "Background task finished" --auto-close 0
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### cURL
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Basic info notification
|
|
99
|
+
curl -X POST http://localhost:8888/jupyterlab-notifications-extension/ingest \
|
|
100
|
+
-H "Content-Type: application/json" \
|
|
101
|
+
-H "Authorization: token YOUR_TOKEN" \
|
|
102
|
+
-d '{"message": "Build completed", "type": "info"}'
|
|
103
|
+
|
|
104
|
+
# Error notification with action button
|
|
105
|
+
curl -X POST http://localhost:8888/jupyterlab-notifications-extension/ingest \
|
|
106
|
+
-H "Content-Type: application/json" \
|
|
107
|
+
-H "Authorization: token YOUR_TOKEN" \
|
|
108
|
+
-d '{
|
|
109
|
+
"message": "Build failed on main branch",
|
|
110
|
+
"type": "error",
|
|
111
|
+
"autoClose": false,
|
|
112
|
+
"actions": [{
|
|
113
|
+
"label": "View Logs",
|
|
114
|
+
"caption": "Open build logs",
|
|
115
|
+
"displayType": "accent"
|
|
116
|
+
}]
|
|
117
|
+
}'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Architecture
|
|
121
|
+
|
|
122
|
+
Broadcast-only model - all notifications delivered to all users.
|
|
123
|
+
|
|
124
|
+
**Flow**: External system POSTs to `/jupyterlab-notifications-extension/ingest` -> Server queues in memory -> Frontend polls `/jupyterlab-notifications-extension/notifications` every 30 seconds -> Displays via JupyterLab notification manager -> Clears queue after fetch.
|
|
125
|
+
|
|
126
|
+
## Troubleshooting
|
|
127
|
+
|
|
128
|
+
**Frontend installed but not working**:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
jupyter server extension list # Verify server extension enabled
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Server extension enabled but frontend missing**:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
jupyter labextension list # Verify frontend extension installed
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Notifications not appearing**: Check browser console for polling errors or verify JupyterLab was restarted after installation.
|
|
141
|
+
|
|
142
|
+
## Uninstall
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pip uninstall jupyterlab_notifications_extension
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
### Setup
|
|
151
|
+
|
|
152
|
+
Requires NodeJS to build the extension. Uses `jlpm` (JupyterLab's pinned yarn) for package management.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Install in development mode
|
|
156
|
+
python -m venv .venv
|
|
157
|
+
source .venv/bin/activate
|
|
158
|
+
pip install --editable ".[dev,test]"
|
|
159
|
+
|
|
160
|
+
# Link extension with JupyterLab
|
|
161
|
+
jupyter labextension develop . --overwrite
|
|
162
|
+
jupyter server extension enable jupyterlab_notifications_extension
|
|
163
|
+
|
|
164
|
+
# Build TypeScript
|
|
165
|
+
jlpm build
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Development workflow
|
|
169
|
+
|
|
170
|
+
Run `jlpm watch` in one terminal to auto-rebuild on changes, and `jupyter lab` in another. Refresh browser after rebuilds to load changes.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
jlpm watch # Auto-rebuild on file changes
|
|
174
|
+
jupyter lab # Run JupyterLab
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Cleanup
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
jupyter server extension disable jupyterlab_notifications_extension
|
|
181
|
+
pip uninstall jupyterlab_notifications_extension
|
|
182
|
+
# Remove symlink: find via `jupyter labextension list`
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Testing
|
|
186
|
+
|
|
187
|
+
**Python tests** (Pytest):
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
pip install -e ".[test]"
|
|
191
|
+
pytest -vv -r ap --cov jupyterlab_notifications_extension
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Frontend tests** (Jest):
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
jlpm test
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Integration tests** (Playwright/Galata): See [ui-tests/README.md](ui-tests/README.md)
|
|
201
|
+
|
|
202
|
+
### Packaging
|
|
203
|
+
|
|
204
|
+
See [RELEASE.md](RELEASE.md) for release procedures.
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { requestAPI } from './request';
|
|
2
|
+
/**
|
|
3
|
+
* Poll interval in milliseconds (30 seconds)
|
|
4
|
+
*/
|
|
5
|
+
const POLL_INTERVAL = 30000;
|
|
6
|
+
/**
|
|
7
|
+
* Fetch and display notifications from the server
|
|
8
|
+
*/
|
|
9
|
+
async function fetchAndDisplayNotifications(app) {
|
|
10
|
+
try {
|
|
11
|
+
const response = await requestAPI('notifications');
|
|
12
|
+
if (response.notifications && response.notifications.length > 0) {
|
|
13
|
+
console.log(`Received ${response.notifications.length} notification(s) from server`);
|
|
14
|
+
response.notifications.forEach(notif => {
|
|
15
|
+
// Build options object
|
|
16
|
+
const options = {
|
|
17
|
+
autoClose: notif.autoClose
|
|
18
|
+
};
|
|
19
|
+
// Build actions array if present (actions are passed as part of options)
|
|
20
|
+
if (notif.actions && notif.actions.length > 0) {
|
|
21
|
+
options.actions = notif.actions.map(action => ({
|
|
22
|
+
label: action.label,
|
|
23
|
+
caption: action.caption || '',
|
|
24
|
+
displayType: action.displayType || 'default',
|
|
25
|
+
callback: () => {
|
|
26
|
+
console.log(`Action clicked: ${action.label}`);
|
|
27
|
+
}
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
// Display notification using JupyterLab's command
|
|
31
|
+
app.commands
|
|
32
|
+
.execute('apputils:notify', {
|
|
33
|
+
message: notif.message,
|
|
34
|
+
type: notif.type,
|
|
35
|
+
options: options
|
|
36
|
+
})
|
|
37
|
+
.catch(err => {
|
|
38
|
+
console.error('Failed to display notification:', err);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (reason) {
|
|
44
|
+
console.error('Failed to fetch notifications from server:', reason);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Initialization data for the jupyterlab_notifications_extension extension.
|
|
49
|
+
*/
|
|
50
|
+
const plugin = {
|
|
51
|
+
id: 'jupyterlab_notifications_extension:plugin',
|
|
52
|
+
description: 'Jupyterlab extension to receive and display notifications in the main panel. Those can be from the jupyterjub administrator or from other places.',
|
|
53
|
+
autoStart: true,
|
|
54
|
+
activate: (app) => {
|
|
55
|
+
console.log('JupyterLab extension jupyterlab_notifications_extension is activated!');
|
|
56
|
+
// Fetch notifications immediately on startup
|
|
57
|
+
fetchAndDisplayNotifications(app);
|
|
58
|
+
// Set up periodic polling for new notifications
|
|
59
|
+
setInterval(() => {
|
|
60
|
+
fetchAndDisplayNotifications(app);
|
|
61
|
+
}, POLL_INTERVAL);
|
|
62
|
+
console.log(`Notification polling started (interval: ${POLL_INTERVAL / 1000}s)`);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
export default plugin;
|
package/lib/request.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call the server extension
|
|
3
|
+
*
|
|
4
|
+
* @param endPoint API REST end point for the extension
|
|
5
|
+
* @param init Initial values for the request
|
|
6
|
+
* @returns The response body interpreted as JSON
|
|
7
|
+
*/
|
|
8
|
+
export declare function requestAPI<T>(endPoint?: string, init?: RequestInit): Promise<T>;
|
package/lib/request.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { URLExt } from '@jupyterlab/coreutils';
|
|
2
|
+
import { ServerConnection } from '@jupyterlab/services';
|
|
3
|
+
/**
|
|
4
|
+
* Call the server extension
|
|
5
|
+
*
|
|
6
|
+
* @param endPoint API REST end point for the extension
|
|
7
|
+
* @param init Initial values for the request
|
|
8
|
+
* @returns The response body interpreted as JSON
|
|
9
|
+
*/
|
|
10
|
+
export async function requestAPI(endPoint = '', init = {}) {
|
|
11
|
+
// Make request to Jupyter API
|
|
12
|
+
const settings = ServerConnection.makeSettings();
|
|
13
|
+
const requestUrl = URLExt.join(settings.baseUrl, 'jupyterlab-notifications-extension', // our server extension's API namespace
|
|
14
|
+
endPoint);
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await ServerConnection.makeRequest(requestUrl, init, settings);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw new ServerConnection.NetworkError(error);
|
|
21
|
+
}
|
|
22
|
+
let data = await response.text();
|
|
23
|
+
if (data.length > 0) {
|
|
24
|
+
try {
|
|
25
|
+
data = JSON.parse(data);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.log('Not a JSON response body.', response);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new ServerConnection.ResponseError(response, data.message || data);
|
|
33
|
+
}
|
|
34
|
+
return data;
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jupyterlab_notifications_extension",
|
|
3
|
+
"version": "1.0.18",
|
|
4
|
+
"description": "Jupyterlab extension to receive and display notifications in the main panel. Those can be from the jupyterjub administrator or from other places.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"jupyter",
|
|
7
|
+
"jupyterlab",
|
|
8
|
+
"jupyterlab-extension"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/stellarshenson/jupyterlab_notifications_extension",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/stellarshenson/jupyterlab_notifications_extension/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "BSD-3-Clause",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Stellars Henson",
|
|
17
|
+
"email": "konrad.jelen@gmail.com"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
|
|
21
|
+
"style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
|
|
22
|
+
"src/**/*.{ts,tsx}"
|
|
23
|
+
],
|
|
24
|
+
"main": "lib/index.js",
|
|
25
|
+
"types": "lib/index.d.ts",
|
|
26
|
+
"style": "style/index.css",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/stellarshenson/jupyterlab_notifications_extension.git"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "jlpm build:lib && jlpm build:labextension:dev",
|
|
33
|
+
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
|
|
34
|
+
"build:labextension": "jupyter labextension build .",
|
|
35
|
+
"build:labextension:dev": "jupyter labextension build --development True .",
|
|
36
|
+
"build:lib": "tsc --sourceMap",
|
|
37
|
+
"build:lib:prod": "tsc",
|
|
38
|
+
"clean": "jlpm clean:lib",
|
|
39
|
+
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
|
|
40
|
+
"clean:lintcache": "rimraf .eslintcache .stylelintcache",
|
|
41
|
+
"clean:labextension": "rimraf jupyterlab_notifications_extension/labextension jupyterlab_notifications_extension/_version.py",
|
|
42
|
+
"clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
|
|
43
|
+
"eslint": "jlpm eslint:check --fix",
|
|
44
|
+
"eslint:check": "eslint . --cache --ext .ts,.tsx",
|
|
45
|
+
"install:extension": "jlpm build",
|
|
46
|
+
"lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
|
|
47
|
+
"lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
|
|
48
|
+
"prettier": "jlpm prettier:base --write --list-different",
|
|
49
|
+
"prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
|
|
50
|
+
"prettier:check": "jlpm prettier:base --check",
|
|
51
|
+
"stylelint": "jlpm stylelint:check --fix",
|
|
52
|
+
"stylelint:check": "stylelint --cache \"style/**/*.css\"",
|
|
53
|
+
"test": "jest --coverage",
|
|
54
|
+
"watch": "run-p watch:src watch:labextension",
|
|
55
|
+
"watch:src": "tsc -w --sourceMap",
|
|
56
|
+
"watch:labextension": "jupyter labextension watch ."
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@jupyterlab/application": "^4.0.0",
|
|
60
|
+
"@jupyterlab/coreutils": "^6.0.0",
|
|
61
|
+
"@jupyterlab/services": "^7.0.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@jupyterlab/builder": "^4.0.0",
|
|
65
|
+
"@jupyterlab/testutils": "^4.0.0",
|
|
66
|
+
"@types/jest": "^29.2.0",
|
|
67
|
+
"@types/json-schema": "^7.0.11",
|
|
68
|
+
"@types/react": "^18.0.26",
|
|
69
|
+
"@types/react-addons-linked-state-mixin": "^0.14.22",
|
|
70
|
+
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
|
71
|
+
"@typescript-eslint/parser": "^6.1.0",
|
|
72
|
+
"css-loader": "^6.7.1",
|
|
73
|
+
"eslint": "^8.36.0",
|
|
74
|
+
"eslint-config-prettier": "^8.8.0",
|
|
75
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
76
|
+
"jest": "^29.2.0",
|
|
77
|
+
"mkdirp": "^1.0.3",
|
|
78
|
+
"npm-run-all2": "^7.0.1",
|
|
79
|
+
"prettier": "^3.0.0",
|
|
80
|
+
"rimraf": "^5.0.10",
|
|
81
|
+
"source-map-loader": "^1.0.2",
|
|
82
|
+
"style-loader": "^3.3.1",
|
|
83
|
+
"stylelint": "^15.10.1",
|
|
84
|
+
"stylelint-config-recommended": "^13.0.0",
|
|
85
|
+
"stylelint-config-standard": "^34.0.0",
|
|
86
|
+
"stylelint-csstree-validator": "^3.0.0",
|
|
87
|
+
"stylelint-prettier": "^4.0.0",
|
|
88
|
+
"typescript": "~5.8.0",
|
|
89
|
+
"yjs": "^13.5.0"
|
|
90
|
+
},
|
|
91
|
+
"sideEffects": [
|
|
92
|
+
"style/*.css",
|
|
93
|
+
"style/index.js"
|
|
94
|
+
],
|
|
95
|
+
"styleModule": "style/index.js",
|
|
96
|
+
"publishConfig": {
|
|
97
|
+
"access": "public"
|
|
98
|
+
},
|
|
99
|
+
"jupyterlab": {
|
|
100
|
+
"discovery": {
|
|
101
|
+
"server": {
|
|
102
|
+
"managers": [
|
|
103
|
+
"pip"
|
|
104
|
+
],
|
|
105
|
+
"base": {
|
|
106
|
+
"name": "jupyterlab_notifications_extension"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"extension": true,
|
|
111
|
+
"outputDir": "jupyterlab_notifications_extension/labextension"
|
|
112
|
+
},
|
|
113
|
+
"eslintIgnore": [
|
|
114
|
+
"node_modules",
|
|
115
|
+
"dist",
|
|
116
|
+
"coverage",
|
|
117
|
+
"**/*.d.ts",
|
|
118
|
+
"tests",
|
|
119
|
+
"**/__tests__",
|
|
120
|
+
"ui-tests"
|
|
121
|
+
],
|
|
122
|
+
"eslintConfig": {
|
|
123
|
+
"extends": [
|
|
124
|
+
"eslint:recommended",
|
|
125
|
+
"plugin:@typescript-eslint/eslint-recommended",
|
|
126
|
+
"plugin:@typescript-eslint/recommended",
|
|
127
|
+
"plugin:prettier/recommended"
|
|
128
|
+
],
|
|
129
|
+
"parser": "@typescript-eslint/parser",
|
|
130
|
+
"parserOptions": {
|
|
131
|
+
"project": "tsconfig.json",
|
|
132
|
+
"sourceType": "module"
|
|
133
|
+
},
|
|
134
|
+
"plugins": [
|
|
135
|
+
"@typescript-eslint"
|
|
136
|
+
],
|
|
137
|
+
"rules": {
|
|
138
|
+
"@typescript-eslint/naming-convention": [
|
|
139
|
+
"error",
|
|
140
|
+
{
|
|
141
|
+
"selector": "interface",
|
|
142
|
+
"format": [
|
|
143
|
+
"PascalCase"
|
|
144
|
+
],
|
|
145
|
+
"custom": {
|
|
146
|
+
"regex": "^I[A-Z]",
|
|
147
|
+
"match": true
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"@typescript-eslint/no-unused-vars": [
|
|
152
|
+
"warn",
|
|
153
|
+
{
|
|
154
|
+
"args": "none"
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
158
|
+
"@typescript-eslint/no-namespace": "off",
|
|
159
|
+
"@typescript-eslint/no-use-before-define": "off",
|
|
160
|
+
"@typescript-eslint/quotes": [
|
|
161
|
+
"error",
|
|
162
|
+
"single",
|
|
163
|
+
{
|
|
164
|
+
"avoidEscape": true,
|
|
165
|
+
"allowTemplateLiterals": false
|
|
166
|
+
}
|
|
167
|
+
],
|
|
168
|
+
"curly": [
|
|
169
|
+
"error",
|
|
170
|
+
"all"
|
|
171
|
+
],
|
|
172
|
+
"eqeqeq": "error",
|
|
173
|
+
"prefer-arrow-callback": "error"
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
"prettier": {
|
|
177
|
+
"singleQuote": true,
|
|
178
|
+
"trailingComma": "none",
|
|
179
|
+
"arrowParens": "avoid",
|
|
180
|
+
"endOfLine": "auto",
|
|
181
|
+
"overrides": [
|
|
182
|
+
{
|
|
183
|
+
"files": "package.json",
|
|
184
|
+
"options": {
|
|
185
|
+
"tabWidth": 4
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
"stylelint": {
|
|
191
|
+
"extends": [
|
|
192
|
+
"stylelint-config-recommended",
|
|
193
|
+
"stylelint-config-standard",
|
|
194
|
+
"stylelint-prettier/recommended"
|
|
195
|
+
],
|
|
196
|
+
"plugins": [
|
|
197
|
+
"stylelint-csstree-validator"
|
|
198
|
+
],
|
|
199
|
+
"rules": {
|
|
200
|
+
"csstree/validator": true,
|
|
201
|
+
"property-no-vendor-prefix": null,
|
|
202
|
+
"selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
|
|
203
|
+
"selector-no-vendor-prefix": null,
|
|
204
|
+
"value-no-vendor-prefix": null
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JupyterFrontEnd,
|
|
3
|
+
JupyterFrontEndPlugin
|
|
4
|
+
} from '@jupyterlab/application';
|
|
5
|
+
|
|
6
|
+
import { requestAPI } from './request';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Notification interface matching backend payload
|
|
10
|
+
*/
|
|
11
|
+
interface INotificationData {
|
|
12
|
+
id: string;
|
|
13
|
+
message: string;
|
|
14
|
+
type: 'default' | 'info' | 'success' | 'warning' | 'error' | 'in-progress';
|
|
15
|
+
autoClose: number | false;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
actions?: Array<{
|
|
18
|
+
label: string;
|
|
19
|
+
caption?: string;
|
|
20
|
+
displayType?: 'default' | 'accent' | 'warn' | 'link';
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Poll interval in milliseconds (30 seconds)
|
|
26
|
+
*/
|
|
27
|
+
const POLL_INTERVAL = 30000;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Fetch and display notifications from the server
|
|
31
|
+
*/
|
|
32
|
+
async function fetchAndDisplayNotifications(
|
|
33
|
+
app: JupyterFrontEnd
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
try {
|
|
36
|
+
const response = await requestAPI<{ notifications: INotificationData[] }>(
|
|
37
|
+
'notifications'
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (response.notifications && response.notifications.length > 0) {
|
|
41
|
+
console.log(
|
|
42
|
+
`Received ${response.notifications.length} notification(s) from server`
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
response.notifications.forEach(notif => {
|
|
46
|
+
// Build options object
|
|
47
|
+
const options: any = {
|
|
48
|
+
autoClose: notif.autoClose
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Build actions array if present (actions are passed as part of options)
|
|
52
|
+
if (notif.actions && notif.actions.length > 0) {
|
|
53
|
+
options.actions = notif.actions.map(action => ({
|
|
54
|
+
label: action.label,
|
|
55
|
+
caption: action.caption || '',
|
|
56
|
+
displayType: action.displayType || 'default',
|
|
57
|
+
callback: () => {
|
|
58
|
+
console.log(`Action clicked: ${action.label}`);
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Display notification using JupyterLab's command
|
|
64
|
+
app.commands
|
|
65
|
+
.execute('apputils:notify', {
|
|
66
|
+
message: notif.message,
|
|
67
|
+
type: notif.type,
|
|
68
|
+
options: options
|
|
69
|
+
})
|
|
70
|
+
.catch(err => {
|
|
71
|
+
console.error('Failed to display notification:', err);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
} catch (reason) {
|
|
76
|
+
console.error('Failed to fetch notifications from server:', reason);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Initialization data for the jupyterlab_notifications_extension extension.
|
|
82
|
+
*/
|
|
83
|
+
const plugin: JupyterFrontEndPlugin<void> = {
|
|
84
|
+
id: 'jupyterlab_notifications_extension:plugin',
|
|
85
|
+
description:
|
|
86
|
+
'Jupyterlab extension to receive and display notifications in the main panel. Those can be from the jupyterjub administrator or from other places.',
|
|
87
|
+
autoStart: true,
|
|
88
|
+
activate: (app: JupyterFrontEnd) => {
|
|
89
|
+
console.log(
|
|
90
|
+
'JupyterLab extension jupyterlab_notifications_extension is activated!'
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Fetch notifications immediately on startup
|
|
94
|
+
fetchAndDisplayNotifications(app);
|
|
95
|
+
|
|
96
|
+
// Set up periodic polling for new notifications
|
|
97
|
+
setInterval(() => {
|
|
98
|
+
fetchAndDisplayNotifications(app);
|
|
99
|
+
}, POLL_INTERVAL);
|
|
100
|
+
|
|
101
|
+
console.log(
|
|
102
|
+
`Notification polling started (interval: ${POLL_INTERVAL / 1000}s)`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export default plugin;
|
package/src/request.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { URLExt } from '@jupyterlab/coreutils';
|
|
2
|
+
|
|
3
|
+
import { ServerConnection } from '@jupyterlab/services';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Call the server extension
|
|
7
|
+
*
|
|
8
|
+
* @param endPoint API REST end point for the extension
|
|
9
|
+
* @param init Initial values for the request
|
|
10
|
+
* @returns The response body interpreted as JSON
|
|
11
|
+
*/
|
|
12
|
+
export async function requestAPI<T>(
|
|
13
|
+
endPoint = '',
|
|
14
|
+
init: RequestInit = {}
|
|
15
|
+
): Promise<T> {
|
|
16
|
+
// Make request to Jupyter API
|
|
17
|
+
const settings = ServerConnection.makeSettings();
|
|
18
|
+
const requestUrl = URLExt.join(
|
|
19
|
+
settings.baseUrl,
|
|
20
|
+
'jupyterlab-notifications-extension', // our server extension's API namespace
|
|
21
|
+
endPoint
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
let response: Response;
|
|
25
|
+
try {
|
|
26
|
+
response = await ServerConnection.makeRequest(requestUrl, init, settings);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new ServerConnection.NetworkError(error as any);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let data: any = await response.text();
|
|
32
|
+
|
|
33
|
+
if (data.length > 0) {
|
|
34
|
+
try {
|
|
35
|
+
data = JSON.parse(data);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.log('Not a JSON response body.', response);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new ServerConnection.ResponseError(response, data.message || data);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return data;
|
|
46
|
+
}
|
package/style/base.css
ADDED
package/style/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import url('base.css');
|
package/style/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './base.css';
|