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 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
+ [![GitHub Actions](https://github.com/stellarshenson/jupyterlab_notifications_extension/actions/workflows/build.yml/badge.svg)](https://github.com/stellarshenson/jupyterlab_notifications_extension/actions/workflows/build.yml)
4
+ [![npm version](https://img.shields.io/npm/v/jupyterlab_notifications_extension.svg)](https://www.npmjs.com/package/jupyterlab_notifications_extension)
5
+ [![PyPI version](https://img.shields.io/pypi/v/jupyterlab-notifications-extension.svg)](https://pypi.org/project/jupyterlab-notifications-extension/)
6
+ [![Total PyPI downloads](https://static.pepy.tech/badge/jupyterlab-notifications-extension)](https://pepy.tech/project/jupyterlab-notifications-extension)
7
+ [![JupyterLab 4](https://img.shields.io/badge/JupyterLab-4-orange.svg)](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
@@ -0,0 +1,6 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ /**
3
+ * Initialization data for the jupyterlab_notifications_extension extension.
4
+ */
5
+ declare const plugin: JupyterFrontEndPlugin<void>;
6
+ export default plugin;
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;
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
3
+ */
4
+
5
+ describe('jupyterlab_notifications_extension', () => {
6
+ it('should be tested', () => {
7
+ expect(1 + 1).toEqual(2);
8
+ });
9
+ });
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
@@ -0,0 +1,5 @@
1
+ /*
2
+ See the JupyterLab Developer Guide for useful CSS Patterns:
3
+
4
+ https://jupyterlab.readthedocs.io/en/stable/developer/css.html
5
+ */
@@ -0,0 +1 @@
1
+ @import url('base.css');
package/style/index.js ADDED
@@ -0,0 +1 @@
1
+ import './base.css';