jupyterlab_notifications_extension 1.0.20 → 1.1.10

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/README.md CHANGED
@@ -6,9 +6,30 @@
6
6
  [![Total PyPI downloads](https://static.pepy.tech/badge/jupyterlab-notifications-extension)](https://pepy.tech/project/jupyterlab-notifications-extension)
7
7
  [![JupyterLab 4](https://img.shields.io/badge/JupyterLab-4-orange.svg)](https://jupyterlab.readthedocs.io/en/stable/)
8
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.
9
+ JupyterLab extension for sending notifications using the native JupyterLab notification system. External systems and extensions send alerts and status updates that appear in JupyterLab's notification center.
10
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.
11
+ Five notification types with distinct visual styling provide clear status communication:
12
+
13
+ ![Notification Types](.resources/screenshot-notifications.png)
14
+
15
+ Access via command palette for quick manual notification sending:
16
+
17
+ ![Command Palette](.resources/screenshot-palette.png)
18
+
19
+ Interactive dialog with message input, type selection, auto-close timing, and action button options:
20
+
21
+ ![Send Dialog](.resources/screenshot-command.png)
22
+
23
+ **Key Features:**
24
+
25
+ - REST API for external systems to POST notifications with authentication
26
+ - Command palette integration with interactive dialog
27
+ - Programmatic command API for extensions and automation
28
+ - Five notification types (info, success, warning, error, in-progress)
29
+ - Configurable auto-close with millisecond precision or manual dismiss
30
+ - Optional action buttons (currently dismiss only)
31
+ - Broadcast delivery via 30-second polling
32
+ - In-memory queue cleared after delivery
12
33
 
13
34
  ## Installation
14
35
 
@@ -22,7 +43,7 @@ pip install jupyterlab_notifications_extension
22
43
 
23
44
  ### POST /jupyterlab-notifications-extension/ingest
24
45
 
25
- Send notifications to JupyterLab users. Requires authentication via `Authorization: token <TOKEN>` header or `?token=<TOKEN>` query parameter.
46
+ Send notifications to JupyterLab. Requires authentication via `Authorization: token <TOKEN>` header or `?token=<TOKEN>` query parameter.
26
47
 
27
48
  **Endpoint**: `POST /jupyterlab-notifications-extension/ingest`
28
49
 
@@ -47,7 +68,7 @@ Send notifications to JupyterLab users. Requires authentication via `Authorizati
47
68
 
48
69
  | Field | Type | Required | Default | Description |
49
70
  | ----------- | -------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
50
- | `message` | string | Yes | - | Notification text displayed to users |
71
+ | `message` | string | Yes | - | Notification text (max 140 characters) |
51
72
  | `type` | string | No | `"info"` | Visual style: `default`, `info`, `success`, `warning`, `error`, `in-progress` |
52
73
  | `autoClose` | number/boolean | No | `5000` | Milliseconds before auto-dismiss. `false` = manual dismiss only. `0` = silent mode (notification center only, no toast) |
53
74
  | `actions` | array | No | `[]` | Action buttons (see below) |
@@ -79,6 +100,32 @@ Note: Action buttons are purely visual. Clicking any button dismisses the notifi
79
100
 
80
101
  ## Usage Examples
81
102
 
103
+ ### From JupyterLab Extensions
104
+
105
+ Send notifications programmatically from other extensions:
106
+
107
+ ```javascript
108
+ // Basic notification
109
+ await app.commands.execute('jupyterlab-notifications:send', {
110
+ message: 'Operation complete'
111
+ });
112
+
113
+ // Custom type and auto-close
114
+ await app.commands.execute('jupyterlab-notifications:send', {
115
+ message: 'Build finished successfully',
116
+ type: 'success',
117
+ autoClose: 3000
118
+ });
119
+
120
+ // With action button
121
+ await app.commands.execute('jupyterlab-notifications:send', {
122
+ message: 'Error processing data',
123
+ type: 'error',
124
+ autoClose: false,
125
+ actions: [{ label: 'View Details', displayType: 'accent' }]
126
+ });
127
+ ```
128
+
82
129
  ### Python Script
83
130
 
84
131
  The included script auto-detects tokens from `JUPYTERHUB_API_TOKEN`, `JPY_API_TOKEN`, or `JUPYTER_TOKEN` environment variables:
@@ -121,7 +168,7 @@ curl -X POST http://localhost:8888/jupyterlab-notifications-extension/ingest \
121
168
 
122
169
  ## Architecture
123
170
 
124
- Broadcast-only model - all notifications delivered to all users.
171
+ Broadcast-only model - all notifications delivered to the JupyterLab server.
125
172
 
126
173
  **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.
127
174
 
package/lib/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { ICommandPalette, Dialog } from '@jupyterlab/apputils';
2
+ import { Widget } from '@lumino/widgets';
1
3
  import { requestAPI } from './request';
2
4
  /**
3
5
  * Poll interval in milliseconds (30 seconds)
@@ -55,8 +57,154 @@ const plugin = {
55
57
  id: 'jupyterlab_notifications_extension:plugin',
56
58
  description: 'Jupyterlab extension to receive and display notifications in the main panel. Those can be from the jupyterjub administrator or from other places.',
57
59
  autoStart: true,
58
- activate: (app) => {
60
+ requires: [ICommandPalette],
61
+ activate: (app, palette) => {
59
62
  console.log('JupyterLab extension jupyterlab_notifications_extension is activated!');
63
+ // Register command to send notifications
64
+ const commandId = 'jupyterlab-notifications:send';
65
+ app.commands.addCommand(commandId, {
66
+ label: 'Send Notification',
67
+ caption: 'Send a notification to all JupyterLab users',
68
+ execute: async (args) => {
69
+ let message = args.message;
70
+ let type = args.type || 'info';
71
+ let autoClose = args.autoClose !== undefined ? args.autoClose : 5000;
72
+ let actions = args.actions || [];
73
+ const data = args.data;
74
+ // If no message provided, show input dialog
75
+ if (!message) {
76
+ // Create dialog body with form elements
77
+ const body = document.createElement('div');
78
+ body.style.display = 'flex';
79
+ body.style.flexDirection = 'column';
80
+ body.style.gap = '10px';
81
+ // Message input
82
+ const messageLabel = document.createElement('label');
83
+ messageLabel.textContent = 'Message:';
84
+ const messageInput = document.createElement('input');
85
+ messageInput.type = 'text';
86
+ messageInput.placeholder = 'Enter notification message';
87
+ messageInput.style.width = '100%';
88
+ messageInput.style.padding = '5px';
89
+ // Type select
90
+ const typeLabel = document.createElement('label');
91
+ typeLabel.textContent = 'Type:';
92
+ const typeSelect = document.createElement('select');
93
+ typeSelect.style.width = '100%';
94
+ typeSelect.style.padding = '5px';
95
+ ['info', 'success', 'warning', 'error', 'in-progress'].forEach(t => {
96
+ const option = document.createElement('option');
97
+ option.value = t;
98
+ option.textContent = t;
99
+ typeSelect.appendChild(option);
100
+ });
101
+ // Auto-close checkbox and seconds input
102
+ const autoCloseContainer = document.createElement('div');
103
+ autoCloseContainer.style.display = 'flex';
104
+ autoCloseContainer.style.alignItems = 'center';
105
+ autoCloseContainer.style.gap = '10px';
106
+ const autoCloseCheckbox = document.createElement('input');
107
+ autoCloseCheckbox.type = 'checkbox';
108
+ autoCloseCheckbox.id = 'autoCloseCheckbox';
109
+ autoCloseCheckbox.checked = true;
110
+ const autoCloseLabel = document.createElement('label');
111
+ autoCloseLabel.htmlFor = 'autoCloseCheckbox';
112
+ autoCloseLabel.textContent = 'Auto-close after';
113
+ autoCloseLabel.style.cursor = 'pointer';
114
+ const autoCloseInput = document.createElement('input');
115
+ autoCloseInput.type = 'number';
116
+ autoCloseInput.value = '5';
117
+ autoCloseInput.min = '1';
118
+ autoCloseInput.style.width = '60px';
119
+ autoCloseInput.style.padding = '3px';
120
+ const secondsLabel = document.createElement('span');
121
+ secondsLabel.textContent = 'seconds';
122
+ autoCloseContainer.appendChild(autoCloseCheckbox);
123
+ autoCloseContainer.appendChild(autoCloseLabel);
124
+ autoCloseContainer.appendChild(autoCloseInput);
125
+ autoCloseContainer.appendChild(secondsLabel);
126
+ // Disable/enable input based on checkbox
127
+ autoCloseCheckbox.addEventListener('change', () => {
128
+ autoCloseInput.disabled = !autoCloseCheckbox.checked;
129
+ });
130
+ // Dismiss button checkbox
131
+ const dismissCheckbox = document.createElement('input');
132
+ dismissCheckbox.type = 'checkbox';
133
+ dismissCheckbox.id = 'dismissCheckbox';
134
+ const dismissLabel = document.createElement('label');
135
+ dismissLabel.htmlFor = 'dismissCheckbox';
136
+ dismissLabel.textContent = ' Include dismiss button';
137
+ dismissLabel.style.display = 'flex';
138
+ dismissLabel.style.alignItems = 'center';
139
+ dismissLabel.style.gap = '5px';
140
+ dismissLabel.style.cursor = 'pointer';
141
+ dismissLabel.prepend(dismissCheckbox);
142
+ body.appendChild(messageLabel);
143
+ body.appendChild(messageInput);
144
+ body.appendChild(typeLabel);
145
+ body.appendChild(typeSelect);
146
+ body.appendChild(autoCloseContainer);
147
+ body.appendChild(dismissLabel);
148
+ const widget = new Widget({ node: body });
149
+ const dialog = new Dialog({
150
+ title: 'Send Notification',
151
+ body: widget,
152
+ buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'Send' })]
153
+ });
154
+ const result = await dialog.launch();
155
+ if (result.button.accept) {
156
+ message = messageInput.value;
157
+ if (!message) {
158
+ return; // No message entered
159
+ }
160
+ // Override with dialog values
161
+ type = typeSelect.value;
162
+ // Set autoClose based on checkbox and input
163
+ if (autoCloseCheckbox.checked) {
164
+ autoClose = parseInt(autoCloseInput.value) * 1000; // Convert to milliseconds
165
+ }
166
+ else {
167
+ autoClose = false;
168
+ }
169
+ actions = dismissCheckbox.checked
170
+ ? [
171
+ {
172
+ label: 'Dismiss',
173
+ caption: 'Close this notification',
174
+ displayType: 'default'
175
+ }
176
+ ]
177
+ : [];
178
+ }
179
+ else {
180
+ return; // User cancelled
181
+ }
182
+ }
183
+ try {
184
+ const payload = {
185
+ message,
186
+ type,
187
+ autoClose
188
+ };
189
+ if (actions.length > 0) {
190
+ payload.actions = actions;
191
+ }
192
+ if (data !== undefined) {
193
+ payload.data = data;
194
+ }
195
+ await requestAPI('ingest', {
196
+ method: 'POST',
197
+ body: JSON.stringify(payload)
198
+ });
199
+ console.log('Notification sent successfully');
200
+ }
201
+ catch (error) {
202
+ console.error('Failed to send notification:', error);
203
+ }
204
+ }
205
+ });
206
+ // Add command to palette
207
+ palette.addItem({ command: commandId, category: 'Notifications' });
60
208
  // Fetch notifications immediately on startup
61
209
  fetchAndDisplayNotifications(app);
62
210
  // Set up periodic polling for new notifications
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_notifications_extension",
3
- "version": "1.0.20",
3
+ "version": "1.1.10",
4
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
5
  "keywords": [
6
6
  "jupyter",
package/src/index.ts CHANGED
@@ -3,6 +3,9 @@ import {
3
3
  JupyterFrontEndPlugin
4
4
  } from '@jupyterlab/application';
5
5
 
6
+ import { ICommandPalette, Dialog } from '@jupyterlab/apputils';
7
+ import { Widget } from '@lumino/widgets';
8
+
6
9
  import { requestAPI } from './request';
7
10
 
8
11
  /**
@@ -91,11 +94,180 @@ const plugin: JupyterFrontEndPlugin<void> = {
91
94
  description:
92
95
  'Jupyterlab extension to receive and display notifications in the main panel. Those can be from the jupyterjub administrator or from other places.',
93
96
  autoStart: true,
94
- activate: (app: JupyterFrontEnd) => {
97
+ requires: [ICommandPalette],
98
+ activate: (app: JupyterFrontEnd, palette: ICommandPalette) => {
95
99
  console.log(
96
100
  'JupyterLab extension jupyterlab_notifications_extension is activated!'
97
101
  );
98
102
 
103
+ // Register command to send notifications
104
+ const commandId = 'jupyterlab-notifications:send';
105
+ app.commands.addCommand(commandId, {
106
+ label: 'Send Notification',
107
+ caption: 'Send a notification to all JupyterLab users',
108
+ execute: async (args: any) => {
109
+ let message = args.message as string;
110
+ let type = (args.type as string) || 'info';
111
+ let autoClose = args.autoClose !== undefined ? args.autoClose : 5000;
112
+ let actions = args.actions || [];
113
+ const data = args.data;
114
+
115
+ // If no message provided, show input dialog
116
+ if (!message) {
117
+ // Create dialog body with form elements
118
+ const body = document.createElement('div');
119
+ body.style.display = 'flex';
120
+ body.style.flexDirection = 'column';
121
+ body.style.gap = '10px';
122
+
123
+ // Message input
124
+ const messageLabel = document.createElement('label');
125
+ messageLabel.textContent = 'Message:';
126
+ const messageInput = document.createElement('input');
127
+ messageInput.type = 'text';
128
+ messageInput.placeholder = 'Enter notification message';
129
+ messageInput.style.width = '100%';
130
+ messageInput.style.padding = '5px';
131
+
132
+ // Type select
133
+ const typeLabel = document.createElement('label');
134
+ typeLabel.textContent = 'Type:';
135
+ const typeSelect = document.createElement('select');
136
+ typeSelect.style.width = '100%';
137
+ typeSelect.style.padding = '5px';
138
+ ['info', 'success', 'warning', 'error', 'in-progress'].forEach(t => {
139
+ const option = document.createElement('option');
140
+ option.value = t;
141
+ option.textContent = t;
142
+ typeSelect.appendChild(option);
143
+ });
144
+
145
+ // Auto-close checkbox and seconds input
146
+ const autoCloseContainer = document.createElement('div');
147
+ autoCloseContainer.style.display = 'flex';
148
+ autoCloseContainer.style.alignItems = 'center';
149
+ autoCloseContainer.style.gap = '10px';
150
+
151
+ const autoCloseCheckbox = document.createElement('input');
152
+ autoCloseCheckbox.type = 'checkbox';
153
+ autoCloseCheckbox.id = 'autoCloseCheckbox';
154
+ autoCloseCheckbox.checked = true;
155
+
156
+ const autoCloseLabel = document.createElement('label');
157
+ autoCloseLabel.htmlFor = 'autoCloseCheckbox';
158
+ autoCloseLabel.textContent = 'Auto-close after';
159
+ autoCloseLabel.style.cursor = 'pointer';
160
+
161
+ const autoCloseInput = document.createElement('input');
162
+ autoCloseInput.type = 'number';
163
+ autoCloseInput.value = '5';
164
+ autoCloseInput.min = '1';
165
+ autoCloseInput.style.width = '60px';
166
+ autoCloseInput.style.padding = '3px';
167
+
168
+ const secondsLabel = document.createElement('span');
169
+ secondsLabel.textContent = 'seconds';
170
+
171
+ autoCloseContainer.appendChild(autoCloseCheckbox);
172
+ autoCloseContainer.appendChild(autoCloseLabel);
173
+ autoCloseContainer.appendChild(autoCloseInput);
174
+ autoCloseContainer.appendChild(secondsLabel);
175
+
176
+ // Disable/enable input based on checkbox
177
+ autoCloseCheckbox.addEventListener('change', () => {
178
+ autoCloseInput.disabled = !autoCloseCheckbox.checked;
179
+ });
180
+
181
+ // Dismiss button checkbox
182
+ const dismissCheckbox = document.createElement('input');
183
+ dismissCheckbox.type = 'checkbox';
184
+ dismissCheckbox.id = 'dismissCheckbox';
185
+ const dismissLabel = document.createElement('label');
186
+ dismissLabel.htmlFor = 'dismissCheckbox';
187
+ dismissLabel.textContent = ' Include dismiss button';
188
+ dismissLabel.style.display = 'flex';
189
+ dismissLabel.style.alignItems = 'center';
190
+ dismissLabel.style.gap = '5px';
191
+ dismissLabel.style.cursor = 'pointer';
192
+ dismissLabel.prepend(dismissCheckbox);
193
+
194
+ body.appendChild(messageLabel);
195
+ body.appendChild(messageInput);
196
+ body.appendChild(typeLabel);
197
+ body.appendChild(typeSelect);
198
+ body.appendChild(autoCloseContainer);
199
+ body.appendChild(dismissLabel);
200
+
201
+ const widget = new Widget({ node: body });
202
+
203
+ const dialog = new Dialog({
204
+ title: 'Send Notification',
205
+ body: widget,
206
+ buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'Send' })]
207
+ });
208
+
209
+ const result = await dialog.launch();
210
+
211
+ if (result.button.accept) {
212
+ message = messageInput.value;
213
+ if (!message) {
214
+ return; // No message entered
215
+ }
216
+
217
+ // Override with dialog values
218
+ type = typeSelect.value;
219
+
220
+ // Set autoClose based on checkbox and input
221
+ if (autoCloseCheckbox.checked) {
222
+ autoClose = parseInt(autoCloseInput.value) * 1000; // Convert to milliseconds
223
+ } else {
224
+ autoClose = false;
225
+ }
226
+
227
+ actions = dismissCheckbox.checked
228
+ ? [
229
+ {
230
+ label: 'Dismiss',
231
+ caption: 'Close this notification',
232
+ displayType: 'default'
233
+ }
234
+ ]
235
+ : [];
236
+ } else {
237
+ return; // User cancelled
238
+ }
239
+ }
240
+
241
+ try {
242
+ const payload: any = {
243
+ message,
244
+ type,
245
+ autoClose
246
+ };
247
+
248
+ if (actions.length > 0) {
249
+ payload.actions = actions;
250
+ }
251
+
252
+ if (data !== undefined) {
253
+ payload.data = data;
254
+ }
255
+
256
+ await requestAPI('ingest', {
257
+ method: 'POST',
258
+ body: JSON.stringify(payload)
259
+ });
260
+
261
+ console.log('Notification sent successfully');
262
+ } catch (error) {
263
+ console.error('Failed to send notification:', error);
264
+ }
265
+ }
266
+ });
267
+
268
+ // Add command to palette
269
+ palette.addItem({ command: commandId, category: 'Notifications' });
270
+
99
271
  // Fetch notifications immediately on startup
100
272
  fetchAndDisplayNotifications(app);
101
273