@fails-components/jupyter-interceptor 0.0.1-alpha.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/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Marten Richter
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,102 @@
1
+ # fails_components_jupyter_interceptor
2
+
3
+ [![Github Actions Status](https://github.com/fails-components/jupyterfails/workflows/Build/badge.svg)](https://github.com/fails-components/jupyterfails/actions/workflows/build.yml)
4
+
5
+ This is an extension, that intercepts message's to and from output views and its widgets, in order to puppet external jupyter deployments of clients listening to a central instructor.
6
+ It can be used together with the fails_components_jupyter_launcher.
7
+ Currently, only widgets based on ipywidgets are supported.
8
+
9
+ ## Requirements
10
+
11
+ - JupyterLab >= 4.0.0
12
+
13
+ ## Install
14
+
15
+ To install the extension, execute:
16
+
17
+ ```bash
18
+ pip install fails_components_jupyter_interceptor
19
+ ```
20
+
21
+ ## Uninstall
22
+
23
+ To remove the extension, execute:
24
+
25
+ ```bash
26
+ pip uninstall fails_components_jupyter_interceptor
27
+ ```
28
+
29
+ ## Contributing
30
+
31
+ ### Development install
32
+
33
+ Note: You will need NodeJS to build the extension package.
34
+
35
+ The `jlpm` command is JupyterLab's pinned version of
36
+ [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
37
+ `yarn` or `npm` in lieu of `jlpm` below.
38
+
39
+ ```bash
40
+ # Clone the repo to your local environment
41
+ # Change directory to the fails_components_jupyter_interceptor directory
42
+ jlpm build
43
+ # Install package in development mode
44
+ pip install -e "."
45
+ # Link your development version of the extension with JupyterLab
46
+ jupyter labextension develop . --overwrite
47
+ # Rebuild extension Typescript source after making changes
48
+ jlpm build
49
+ ```
50
+
51
+ You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
52
+
53
+ ```bash
54
+ # Watch the source directory in one terminal, automatically rebuilding when needed
55
+ jlpm watch
56
+ # Run JupyterLab in another terminal
57
+ jupyter lab
58
+ ```
59
+
60
+ With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt).
61
+
62
+ By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command:
63
+
64
+ ```bash
65
+ jupyter lab build --minimize=False
66
+ ```
67
+
68
+ ### Development uninstall
69
+
70
+ ```bash
71
+ pip uninstall fails_components_jupyter_interceptor
72
+ ```
73
+
74
+ In development mode, you will also need to remove the symlink created by `jupyter labextension develop`
75
+ command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions`
76
+ folder is located. Then you can remove the symlink named `@fails-components/jupyter-applet-view` within that folder.
77
+
78
+ ### Testing the extension
79
+
80
+ #### Frontend tests
81
+
82
+ This extension is using [Jest](https://jestjs.io/) for JavaScript code testing.
83
+
84
+ To execute them, execute:
85
+
86
+ ```sh
87
+ jlpm
88
+ jlpm test
89
+ ```
90
+
91
+ <!--
92
+ #### Integration tests
93
+
94
+ This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests).
95
+ More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab.
96
+
97
+ More information are provided within the [ui-tests](./ui-tests/README.md) README.
98
+
99
+ ### Packaging the extension
100
+
101
+ See [RELEASE](RELEASE.md)
102
+ -->
package/lib/index.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ import { WidgetModel } from '@jupyter-widgets/base';
3
+ import { Token } from '@lumino/coreutils';
4
+ import { IFailsInterceptorUpdateMessage, IAppletWidgetRegistry, IFailsLauncherInfo } from '@fails-components/jupyter-launcher';
5
+ export interface IFailsInterceptor {
6
+ isMimeTypeSupported: (mimeType: string) => boolean;
7
+ }
8
+ export declare const IFailsInterceptor: Token<IFailsInterceptor>;
9
+ export declare class AppletWidgetRegistry implements IAppletWidgetRegistry {
10
+ constructor(launcher: IFailsLauncherInfo);
11
+ registerModel(path: string, modelId: string, model: WidgetModel): void;
12
+ unregisterPath(path: string): void;
13
+ unregisterModel(modelId: string): void;
14
+ getModelId(path: string): string;
15
+ getModel(path: string): WidgetModel;
16
+ getPath(modelId: string): string;
17
+ dispatchMessage(message: IFailsInterceptorUpdateMessage): void;
18
+ private _modelIdToPath;
19
+ private _pathToModelId;
20
+ private _pathToModel;
21
+ private _updateMessageArrived;
22
+ }
23
+ declare const plugins: JupyterFrontEndPlugin<any>[];
24
+ export default plugins;
package/lib/index.js ADDED
@@ -0,0 +1,392 @@
1
+ import { INotebookTracker } from '@jupyterlab/notebook';
2
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
3
+ import { Token } from '@lumino/coreutils';
4
+ import { IFailsLauncherInfo } from '@fails-components/jupyter-launcher';
5
+ import { Signal } from '@lumino/signaling';
6
+ export const IFailsInterceptor = new Token('@fails-components/jupyter-fails:IFailsInterceptor', 'A service to talk with FAILS interceptor.');
7
+ // List of static Mimetypes, where intercepting is not necessary
8
+ const staticMimeTypes = new Set([
9
+ 'text/html',
10
+ 'text/plain',
11
+ 'image/bmp',
12
+ 'image/png',
13
+ 'image/jpeg',
14
+ 'image/gif',
15
+ 'image/webp',
16
+ 'text/latex',
17
+ 'text/markdown',
18
+ 'image/svg+xml',
19
+ 'application/vnd.jupyter.stderr',
20
+ 'application/vnd.jupyter.stdout'
21
+ /* 'text/javascript', 'application/javascript' We need to check, what do about it
22
+ */
23
+ ]);
24
+ // List of dynamic Mimetypes, where intercepting is currently handled
25
+ const dynamicMimeTypes = new Set([
26
+ 'application/vnd.jupyter.widget-view+json'
27
+ /* 'application/vnd.plotly.v1+json' */
28
+ ]);
29
+ export class AppletWidgetRegistry {
30
+ constructor(launcher) {
31
+ this._modelIdToPath = {};
32
+ this._pathToModelId = {};
33
+ this._pathToModel = {};
34
+ this._updateMessageArrived = new Signal(this);
35
+ launcher.updateMessageArrived = this._updateMessageArrived;
36
+ }
37
+ registerModel(path, modelId, model) {
38
+ this._modelIdToPath[modelId] = path;
39
+ this._pathToModelId[path] = modelId;
40
+ this._pathToModel[path] = model;
41
+ }
42
+ unregisterPath(path) {
43
+ const modelId = this._pathToModelId[path];
44
+ if (modelId) {
45
+ delete this._modelIdToPath[modelId];
46
+ }
47
+ delete this._pathToModelId[path];
48
+ delete this._pathToModel[path];
49
+ }
50
+ unregisterModel(modelId) {
51
+ const path = this._modelIdToPath[modelId];
52
+ delete this._modelIdToPath[modelId];
53
+ if (path) {
54
+ delete this._pathToModelId[path];
55
+ delete this._pathToModel[path];
56
+ }
57
+ }
58
+ getModelId(path) {
59
+ return this._pathToModelId[path];
60
+ }
61
+ getModel(path) {
62
+ return this._pathToModel[path];
63
+ }
64
+ getPath(modelId) {
65
+ return this._modelIdToPath[modelId];
66
+ }
67
+ dispatchMessage(message) {
68
+ this._updateMessageArrived.emit(message);
69
+ // console.log(path, mime, message);
70
+ }
71
+ }
72
+ function activateWidgetInterceptor(app, notebookTracker, rendermimeRegistry, launcher) {
73
+ const wRegistry = new AppletWidgetRegistry(launcher);
74
+ if (app.namespace === 'JupyterLite Server') {
75
+ return {
76
+ isMimeTypeSupported: (mimeType) => false
77
+ };
78
+ }
79
+ const addKernelInterceptor = (kernel) => {
80
+ kernel.anyMessage.connect((sender, args) => {
81
+ /* console.log(
82
+ 'Intercept any message',
83
+ args,
84
+ args?.msg?.header?.msg_id,
85
+ args?.msg?.header?.msg_type
86
+ ); */
87
+ const { direction, msg } = args;
88
+ if (direction === 'send') {
89
+ // send from the control
90
+ const { content, channel } = msg;
91
+ if (channel === 'shell') {
92
+ const { data, comm_id: commId } = content;
93
+ if ((data === null || data === void 0 ? void 0 : data.method) === 'update') {
94
+ // got an update
95
+ const path = wRegistry.getPath(commId);
96
+ // console.log('Send an update', data.state, commId, path);
97
+ if (path && typeof data.state === 'object') {
98
+ // const inform = { path, commId, value, index, event };
99
+ const state = { ...data.state };
100
+ if (state.outputs) {
101
+ delete state.outputs;
102
+ }
103
+ if (Object.keys(state).length !== 0) {
104
+ wRegistry.dispatchMessage({
105
+ path,
106
+ mime: widgetsMime,
107
+ state
108
+ });
109
+ }
110
+ }
111
+ }
112
+ }
113
+ // now fish all messages of control out
114
+ }
115
+ });
116
+ launcher.remoteUpdateMessageArrived.connect(async (slot, args) => {
117
+ // const modelId = wRegistry.getModelId(args.path);
118
+ const model = wRegistry.getModel(args.path);
119
+ const state = await model.constructor._deserialize_state(args.state, model.widget_manager);
120
+ // console.log('got an remote interceptor message', args, state);
121
+ for (const [key, value] of Object.entries(state)) {
122
+ model.set(key, value);
123
+ }
124
+ model.sync('patch', model, { attrs: state });
125
+ });
126
+ };
127
+ const widgetsMime = 'application/vnd.jupyter.widget-view+json';
128
+ // add interceptors for mimerenderers, whose javascript, we need to patch
129
+ // deactivated, as one can use always widgets!
130
+ // eslint-disable-next-line no-constant-condition
131
+ if (rendermimeRegistry && false) {
132
+ const rmRegistry = rendermimeRegistry;
133
+ const mimetypes = ['application/vnd.plotly.v1+json']; // mimetypes to patch
134
+ mimetypes.forEach(mime => {
135
+ const factory = rmRegistry.getFactory(mime);
136
+ if (!factory) {
137
+ console.log('Plotly seems to be not installed! So I can not add an interceptor');
138
+ return;
139
+ }
140
+ // ok, lets add an interceptor
141
+ const createRendererOld = factory.createRenderer;
142
+ factory.createRenderer = function (options) {
143
+ const renderer = createRendererOld(options);
144
+ console.log('intercepted renderer', mime, renderer, renderer.node);
145
+ // we have also the replace renderModel
146
+ const renderModelOld = renderer.renderModel.bind(renderer);
147
+ renderer.renderModel = async (model) => {
148
+ let result = await renderModelOld(model);
149
+ console.log('intercepted renderer model', model);
150
+ if (!renderer.hasGraphElement()) {
151
+ result = await renderer.createGraph(renderer === null || renderer === void 0 ? void 0 : renderer._model);
152
+ }
153
+ if (renderer.node.on) {
154
+ const messages = [
155
+ 'relayout',
156
+ 'hover',
157
+ 'unhover',
158
+ 'selected',
159
+ 'selecting',
160
+ 'restyle'
161
+ ];
162
+ messages.forEach(mess => {
163
+ renderer.node.on('plotly_' + mess, (data) => {
164
+ var _a, _b;
165
+ const path = (_a = model.metadata) === null || _a === void 0 ? void 0 : _a.appPath;
166
+ if (path) {
167
+ wRegistry.dispatchMessage({
168
+ path: path + ':' + mess,
169
+ mime,
170
+ state: data
171
+ });
172
+ }
173
+ console.log('plotly', mess, data, (_b = model.metadata) === null || _b === void 0 ? void 0 : _b.appPath, path);
174
+ });
175
+ });
176
+ }
177
+ console.log('renderer layout rM',
178
+ // @ts-expect-error plotly
179
+ renderer.node.layout,
180
+ // @ts-expect-error plotly
181
+ !!renderer.node.on, renderer);
182
+ //@ts-expect-error result is different from void
183
+ console.log('renderer result', result, !!(result === null || result === void 0 ? void 0 : result.on));
184
+ return result;
185
+ };
186
+ // special code for plotly
187
+ // @ts-expect-error plotly
188
+ console.log('renderer layout', renderer.node.layout);
189
+ /* if (!(renderer as any).hasGraphElement()) {
190
+ (renderer as any).createGraph((renderer as any)['_model]']);
191
+ } */
192
+ /* //@ts-expect-error on not found
193
+ renderer.node.on('plotly_relayout', (update: any) => {
194
+ console.log('relayout', update);
195
+ }); */
196
+ // special code for plotly
197
+ return renderer;
198
+ };
199
+ });
200
+ }
201
+ notebookTracker.widgetAdded.connect((sender, panel) => {
202
+ var _a;
203
+ if ((_a = panel.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel) {
204
+ addKernelInterceptor(panel.sessionContext.session.kernel);
205
+ }
206
+ panel.sessionContext.kernelChanged.connect((sender, args) => {
207
+ if (args.newValue) {
208
+ addKernelInterceptor(args.newValue);
209
+ // TODO remove old interceptor?
210
+ }
211
+ });
212
+ const widgetManagerPromise = panel.context.sessionContext.ready.then(() => {
213
+ return new Promise((resolve, reject) => {
214
+ requestAnimationFrame(async () => {
215
+ // ensure it is handled after the widgetmanager is installed.
216
+ const rendermime = panel.content.rendermime;
217
+ const widgetFactory = rendermime.getFactory(widgetsMime);
218
+ if (widgetFactory) {
219
+ // now create a dummy widget
220
+ const dummyWidget = widgetFactory.createRenderer({
221
+ mimeType: widgetsMime,
222
+ sanitizer: {
223
+ sanitize: (dirty, options) => ''
224
+ },
225
+ resolver: {
226
+ getDownloadUrl: url => Promise.resolve(''),
227
+ resolveUrl: url => Promise.resolve('')
228
+ },
229
+ latexTypesetter: null,
230
+ linkHandler: null
231
+ });
232
+ resolve(await dummyWidget._manager.promise);
233
+ dummyWidget.dispose();
234
+ }
235
+ else {
236
+ reject(new Error('No widgetFactory found for widget view'));
237
+ }
238
+ });
239
+ });
240
+ });
241
+ widgetManagerPromise === null || widgetManagerPromise === void 0 ? void 0 : widgetManagerPromise.then(widgetManager => {
242
+ const notebookModel = panel.model;
243
+ if (notebookModel) {
244
+ const trackedCells = new WeakSet();
245
+ const pendingModels = [];
246
+ const iterateWidgets = async (path, widget_model_id) => {
247
+ if (widgetManager === null || widgetManager === void 0 ? void 0 : widgetManager.has_model(widget_model_id)) {
248
+ const widget = await (widgetManager === null || widgetManager === void 0 ? void 0 : widgetManager.get_model(widget_model_id));
249
+ // const children = widget.attributes.children as
250
+ /* widget.attributes.children.forEach((child) => {
251
+ iterateWidgets(path + '/' + child. ,)
252
+ }) */
253
+ const mypath = path + (widget.name ? '/' + widget.name : '');
254
+ const state = widget.get_state();
255
+ const children = state.children;
256
+ if (children) {
257
+ children.forEach((child, index) => {
258
+ iterateWidgets(mypath + '/' + index, child.model_id);
259
+ });
260
+ }
261
+ // console.log('show widget model', path, widget.get_state());
262
+ wRegistry.registerModel(mypath, widget.model_id, widget);
263
+ /* console.log(
264
+ 'model registred',
265
+ mypath,
266
+ widget.model_id /*, state*,
267
+ widget
268
+ ); */
269
+ }
270
+ else {
271
+ // console.log('model missing', widget_model_id);
272
+ pendingModels.push({ path, widget_model_id });
273
+ }
274
+ };
275
+ /* const labWidgetManager = widgetManager as LabWidgetManager;
276
+
277
+ if (labWidgetManager) {
278
+ labWidgetManager.restored.connect(lWManager => {
279
+ // we may be able to continue for some missing widgets
280
+ console.log('RESTORED');
281
+ const stillPendingModels: {
282
+ path: string;
283
+ widget_model_id: string;
284
+ }[] = [];
285
+ while (pendingModels.length > 0) {
286
+ const pModel = pendingModels.pop();
287
+ if (!pModel) {
288
+ break;
289
+ }
290
+ if (widgetManager?.has_model(pModel.widget_model_id)) {
291
+ console.log('Resume model search', pModel.path);
292
+ iterateWidgets(pModel.path, pModel.widget_model_id);
293
+ } else {
294
+ stillPendingModels.push(pModel);
295
+ }
296
+ }
297
+ pendingModels.push(...stillPendingModels);
298
+ });
299
+ } */
300
+ const onCellsChanged = (cell) => {
301
+ if (!trackedCells.has(cell)) {
302
+ trackedCells.add(cell);
303
+ const updateMimedata = () => {
304
+ if (cell.type === 'code') {
305
+ // now we figure out, if all widgets are registered
306
+ const sharedModel = cell.sharedModel;
307
+ let index = 0;
308
+ for (const output of sharedModel.outputs) {
309
+ const appPath = cell.id + '/' + index;
310
+ let addPath = false;
311
+ // tag all sharedmodels with the path
312
+ switch (output.output_type) {
313
+ case 'display_data':
314
+ case 'update_display_data':
315
+ case 'execute_result':
316
+ {
317
+ const result = output;
318
+ // console.log('Mimebundle', result.data); // to do parse this also
319
+ // console.log('Metadata', result.metadata);
320
+ // console.log('Result', result);
321
+ const mimebundle = result.data;
322
+ if (mimebundle[widgetsMime]) {
323
+ const { model_id } = mimebundle[widgetsMime];
324
+ iterateWidgets(appPath, model_id);
325
+ }
326
+ // Deactivate, as we are not using it, use a plotly widget instead.
327
+ if (
328
+ // eslint-disable-next-line no-constant-condition
329
+ mimebundle['application/vnd.plotly.v1+json'] &&
330
+ false) {
331
+ const bundle = mimebundle['application/vnd.plotly.v1+json'];
332
+ console.log('Plotly bundle', bundle);
333
+ console.log('plotly cell', cell);
334
+ if (result.metadata.appPath !== appPath) {
335
+ result.metadata.appPath = appPath;
336
+ addPath = true;
337
+ }
338
+ }
339
+ }
340
+ break;
341
+ case 'stream':
342
+ case 'error':
343
+ default:
344
+ }
345
+ if (addPath) {
346
+ // should happen only once
347
+ sharedModel.updateOutputs(index, index + 1, [output]);
348
+ }
349
+ index++;
350
+ }
351
+ }
352
+ };
353
+ cell.contentChanged.connect(updateMimedata);
354
+ updateMimedata();
355
+ }
356
+ };
357
+ for (const cell of notebookModel.cells) {
358
+ onCellsChanged(cell);
359
+ }
360
+ notebookModel.cells.changed.connect((cellist, changedList) => {
361
+ const { /*newIndex,*/ newValues /* oldIndex, oldValues, type */ } = changedList;
362
+ newValues.forEach(newcell => {
363
+ onCellsChanged(newcell);
364
+ // console.log('changed cells', newcell);
365
+ });
366
+ });
367
+ }
368
+ });
369
+ });
370
+ return {
371
+ isMimeTypeSupported: (mimeType) => {
372
+ if (staticMimeTypes.has(mimeType)) {
373
+ return true;
374
+ }
375
+ if (dynamicMimeTypes.has(mimeType)) {
376
+ return true;
377
+ }
378
+ return false;
379
+ }
380
+ };
381
+ }
382
+ const appletWidgetInterceptor = {
383
+ id: '@fails-components/jupyter-applet-widget:interceptor',
384
+ description: 'Tracks and intercepts widget communication',
385
+ autoStart: true,
386
+ activate: activateWidgetInterceptor,
387
+ provides: IFailsInterceptor,
388
+ requires: [INotebookTracker, IRenderMimeRegistry, IFailsLauncherInfo],
389
+ optional: []
390
+ };
391
+ const plugins = [appletWidgetInterceptor];
392
+ export default plugins;
package/package.json ADDED
@@ -0,0 +1,214 @@
1
+ {
2
+ "name": "@fails-components/jupyter-interceptor",
3
+ "version": "0.0.1-alpha.10",
4
+ "description": "A collections of plugins intercepting kernel messages and other message, to steer outputs remotely",
5
+ "keywords": [
6
+ "jupyter",
7
+ "jupyterlab",
8
+ "jupyterlab-extension"
9
+ ],
10
+ "homepage": "https://github.com/fails-components/jupyterfails",
11
+ "bugs": {
12
+ "url": "https://github.com/fails-components/jupyterfails/issues"
13
+ },
14
+ "license": "BSD-3-Clause",
15
+ "author": {
16
+ "name": "Marten Richter",
17
+ "email": "marten.richter@freenet.de"
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
+ "schema/*.json",
23
+ "src/**/*.{ts,tsx}",
24
+ "LICENSE",
25
+ "README.md"
26
+ ],
27
+ "main": "lib/index.js",
28
+ "types": "lib/index.d.ts",
29
+ "style": "style/index.css",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/fails-components/jupyterfails.git"
33
+ },
34
+ "scripts": {
35
+ "build": "jlpm build:lib && jlpm build:labextension:dev",
36
+ "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
37
+ "build:labextension": "jupyter labextension build .",
38
+ "build:labextension:dev": "jupyter labextension build --development True .",
39
+ "build:lib": "tsc --sourceMap",
40
+ "build:lib:prod": "tsc",
41
+ "clean": "jlpm clean:lib",
42
+ "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
43
+ "clean:lintcache": "rimraf .eslintcache .stylelintcache",
44
+ "clean:labextension": "rimraf fails_components_jupyter_interceptor/labextension fails_components_jupyter_interceptor/_version.py",
45
+ "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
46
+ "eslint": "jlpm eslint:check --fix",
47
+ "eslint:check": "eslint . --cache --ext .ts,.tsx",
48
+ "install:extension": "jlpm build",
49
+ "lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
50
+ "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
51
+ "prepublishOnly": "npm run build",
52
+ "prettier": "jlpm prettier:base --write --list-different",
53
+ "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
54
+ "prettier:check": "jlpm prettier:base --check",
55
+ "stylelint": "jlpm stylelint:check --fix",
56
+ "stylelint:check": "stylelint --cache \"style/**/*.css\"",
57
+ "test": "jest --coverage --passWithNoTests",
58
+ "watch": "run-p watch:src watch:labextension",
59
+ "watch:src": "tsc -w --sourceMap",
60
+ "watch:labextension": "jupyter labextension watch ."
61
+ },
62
+ "dependencies": {
63
+ "@fails-components/jupyter-launcher": "^0.0.1-alpha.10",
64
+ "@jupyter-widgets/base": "^6.0.8",
65
+ "@jupyter-widgets/jupyterlab-manager": "^5.0.11",
66
+ "@jupyter/ydoc": "^3.0.0",
67
+ "@jupyterlab/application": "^4.3.4",
68
+ "@jupyterlab/cells": "^4.3.4",
69
+ "@jupyterlab/nbformat": "^4.3.4",
70
+ "@jupyterlab/notebook": "^4.3.4",
71
+ "@jupyterlab/rendermime": "^4.3.4",
72
+ "@jupyterlab/services": "^7.3.4",
73
+ "@jupyterlite/application-extension": "^0.5.0",
74
+ "@jupyterlite/contents": "^0.5.0",
75
+ "@jupyterlite/server": "^0.5.0",
76
+ "@jupyterlite/settings": "^0.5.0",
77
+ "@lumino/coreutils": "^2.2.0",
78
+ "json5": "^2.2.3"
79
+ },
80
+ "devDependencies": {
81
+ "@jupyterlab/builder": "^4.0.0",
82
+ "@jupyterlab/testutils": "^4.0.0",
83
+ "@types/jest": "^29.2.0",
84
+ "@types/json-schema": "^7.0.11",
85
+ "@types/react": "^18.0.26",
86
+ "@types/react-addons-linked-state-mixin": "^0.14.22",
87
+ "@typescript-eslint/eslint-plugin": "^6.1.0",
88
+ "@typescript-eslint/parser": "^6.1.0",
89
+ "css-loader": "^6.7.1",
90
+ "eslint": "^8.36.0",
91
+ "eslint-config-prettier": "^8.8.0",
92
+ "eslint-plugin-prettier": "^5.0.0",
93
+ "jest": "^29.2.0",
94
+ "npm-run-all": "^4.1.5",
95
+ "prettier": "^3.0.0",
96
+ "rimraf": "^5.0.1",
97
+ "source-map-loader": "^1.0.2",
98
+ "style-loader": "^3.3.1",
99
+ "stylelint": "^15.10.1",
100
+ "stylelint-config-recommended": "^13.0.0",
101
+ "stylelint-config-standard": "^34.0.0",
102
+ "stylelint-csstree-validator": "^3.0.0",
103
+ "stylelint-prettier": "^4.0.0",
104
+ "typescript": "~5.0.2",
105
+ "yjs": "^13.5.0"
106
+ },
107
+ "sideEffects": [
108
+ "style/*.css",
109
+ "style/index.js"
110
+ ],
111
+ "styleModule": "style/index.js",
112
+ "publishConfig": {
113
+ "access": "public"
114
+ },
115
+ "jupyterlab": {
116
+ "extension": true,
117
+ "outputDir": "fails_components_jupyter_interceptor/labextension",
118
+ "schemaDir": "schema"
119
+ },
120
+ "eslintIgnore": [
121
+ "node_modules",
122
+ "dist",
123
+ "coverage",
124
+ "**/*.d.ts",
125
+ "tests",
126
+ "**/__tests__",
127
+ "ui-tests"
128
+ ],
129
+ "eslintConfig": {
130
+ "extends": [
131
+ "eslint:recommended",
132
+ "plugin:@typescript-eslint/eslint-recommended",
133
+ "plugin:@typescript-eslint/recommended",
134
+ "plugin:prettier/recommended"
135
+ ],
136
+ "parser": "@typescript-eslint/parser",
137
+ "parserOptions": {
138
+ "project": "tsconfig.json",
139
+ "sourceType": "module"
140
+ },
141
+ "plugins": [
142
+ "@typescript-eslint"
143
+ ],
144
+ "rules": {
145
+ "@typescript-eslint/naming-convention": [
146
+ "error",
147
+ {
148
+ "selector": "interface",
149
+ "format": [
150
+ "PascalCase"
151
+ ],
152
+ "custom": {
153
+ "regex": "^I[A-Z]",
154
+ "match": true
155
+ }
156
+ }
157
+ ],
158
+ "@typescript-eslint/no-unused-vars": [
159
+ "warn",
160
+ {
161
+ "args": "none"
162
+ }
163
+ ],
164
+ "@typescript-eslint/no-explicit-any": "off",
165
+ "@typescript-eslint/no-namespace": "off",
166
+ "@typescript-eslint/no-use-before-define": "off",
167
+ "@typescript-eslint/quotes": [
168
+ "error",
169
+ "single",
170
+ {
171
+ "avoidEscape": true,
172
+ "allowTemplateLiterals": false
173
+ }
174
+ ],
175
+ "curly": [
176
+ "error",
177
+ "all"
178
+ ],
179
+ "eqeqeq": "error",
180
+ "prefer-arrow-callback": "error"
181
+ }
182
+ },
183
+ "prettier": {
184
+ "singleQuote": true,
185
+ "trailingComma": "none",
186
+ "arrowParens": "avoid",
187
+ "endOfLine": "auto",
188
+ "overrides": [
189
+ {
190
+ "files": "package.json",
191
+ "options": {
192
+ "tabWidth": 4
193
+ }
194
+ }
195
+ ]
196
+ },
197
+ "stylelint": {
198
+ "extends": [
199
+ "stylelint-config-recommended",
200
+ "stylelint-config-standard",
201
+ "stylelint-prettier/recommended"
202
+ ],
203
+ "plugins": [
204
+ "stylelint-csstree-validator"
205
+ ],
206
+ "rules": {
207
+ "csstree/validator": true,
208
+ "property-no-vendor-prefix": null,
209
+ "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
210
+ "selector-no-vendor-prefix": null,
211
+ "value-no-vendor-prefix": null
212
+ }
213
+ }
214
+ }
package/src/index.ts ADDED
@@ -0,0 +1,507 @@
1
+ import {
2
+ INotebookTracker,
3
+ NotebookPanel,
4
+ INotebookModel
5
+ } from '@jupyterlab/notebook';
6
+ import {
7
+ JupyterFrontEnd,
8
+ JupyterFrontEndPlugin
9
+ } from '@jupyterlab/application';
10
+ import { IWidgetManager, WidgetModel } from '@jupyter-widgets/base';
11
+ import {
12
+ IExecuteResult,
13
+ IDisplayData,
14
+ IDisplayUpdate
15
+ } from '@jupyterlab/nbformat';
16
+ import {
17
+ IRenderMime,
18
+ IRenderMimeRegistry,
19
+ RenderMimeRegistry
20
+ } from '@jupyterlab/rendermime';
21
+ import { ICellModel } from '@jupyterlab/cells';
22
+ import { ISharedCodeCell } from '@jupyter/ydoc';
23
+ import { JSONObject, PromiseDelegate, Token } from '@lumino/coreutils';
24
+ import { Kernel /*, KernelMessage */ } from '@jupyterlab/services';
25
+ import {
26
+ IFailsInterceptorUpdateMessage,
27
+ IAppletWidgetRegistry,
28
+ IFailsLauncherInfo
29
+ } from '@fails-components/jupyter-launcher';
30
+ import { Signal } from '@lumino/signaling';
31
+
32
+ export interface IFailsInterceptor {
33
+ isMimeTypeSupported: (mimeType: string) => boolean;
34
+ }
35
+
36
+ export const IFailsInterceptor = new Token<IFailsInterceptor>(
37
+ '@fails-components/jupyter-fails:IFailsInterceptor',
38
+ 'A service to talk with FAILS interceptor.'
39
+ );
40
+
41
+ // List of static Mimetypes, where intercepting is not necessary
42
+ const staticMimeTypes = new Set([
43
+ 'text/html',
44
+ 'text/plain',
45
+ 'image/bmp',
46
+ 'image/png',
47
+ 'image/jpeg',
48
+ 'image/gif',
49
+ 'image/webp',
50
+ 'text/latex',
51
+ 'text/markdown',
52
+ 'image/svg+xml',
53
+ 'application/vnd.jupyter.stderr',
54
+ 'application/vnd.jupyter.stdout'
55
+ /* 'text/javascript', 'application/javascript' We need to check, what do about it
56
+ */
57
+ ]);
58
+ // List of dynamic Mimetypes, where intercepting is currently handled
59
+ const dynamicMimeTypes = new Set<string>([
60
+ 'application/vnd.jupyter.widget-view+json'
61
+ /* 'application/vnd.plotly.v1+json' */
62
+ ]);
63
+
64
+ export class AppletWidgetRegistry implements IAppletWidgetRegistry {
65
+ constructor(launcher: IFailsLauncherInfo) {
66
+ launcher.updateMessageArrived = this._updateMessageArrived;
67
+ }
68
+ registerModel(path: string, modelId: string, model: WidgetModel) {
69
+ this._modelIdToPath[modelId] = path;
70
+ this._pathToModelId[path] = modelId;
71
+ this._pathToModel[path] = model;
72
+ }
73
+
74
+ unregisterPath(path: string) {
75
+ const modelId = this._pathToModelId[path];
76
+ if (modelId) {
77
+ delete this._modelIdToPath[modelId];
78
+ }
79
+ delete this._pathToModelId[path];
80
+ delete this._pathToModel[path];
81
+ }
82
+
83
+ unregisterModel(modelId: string) {
84
+ const path = this._modelIdToPath[modelId];
85
+ delete this._modelIdToPath[modelId];
86
+ if (path) {
87
+ delete this._pathToModelId[path];
88
+ delete this._pathToModel[path];
89
+ }
90
+ }
91
+
92
+ getModelId(path: string) {
93
+ return this._pathToModelId[path];
94
+ }
95
+
96
+ getModel(path: string) {
97
+ return this._pathToModel[path];
98
+ }
99
+
100
+ getPath(modelId: string) {
101
+ return this._modelIdToPath[modelId];
102
+ }
103
+
104
+ dispatchMessage(message: IFailsInterceptorUpdateMessage) {
105
+ this._updateMessageArrived.emit(message);
106
+ // console.log(path, mime, message);
107
+ }
108
+
109
+ private _modelIdToPath: { [key: string]: string } = {};
110
+ private _pathToModelId: { [key: string]: string } = {};
111
+ private _pathToModel: { [key: string]: WidgetModel } = {};
112
+ private _updateMessageArrived = new Signal<
113
+ IAppletWidgetRegistry,
114
+ IFailsInterceptorUpdateMessage
115
+ >(this);
116
+ }
117
+
118
+ function activateWidgetInterceptor(
119
+ app: JupyterFrontEnd,
120
+ notebookTracker: INotebookTracker,
121
+ rendermimeRegistry: IRenderMimeRegistry,
122
+ launcher: IFailsLauncherInfo
123
+ ): IFailsInterceptor {
124
+ const wRegistry = new AppletWidgetRegistry(launcher);
125
+ if (app.namespace === 'JupyterLite Server') {
126
+ return {
127
+ isMimeTypeSupported: (mimeType: string) => false
128
+ };
129
+ }
130
+ const addKernelInterceptor = (kernel: Kernel.IKernelConnection) => {
131
+ kernel.anyMessage.connect((sender, args) => {
132
+ /* console.log(
133
+ 'Intercept any message',
134
+ args,
135
+ args?.msg?.header?.msg_id,
136
+ args?.msg?.header?.msg_type
137
+ ); */
138
+ const { direction, msg } = args;
139
+ if (direction === 'send') {
140
+ // send from the control
141
+ const { content, channel } = msg;
142
+ if (channel === 'shell') {
143
+ const { data, comm_id: commId } = content as {
144
+ comm_id: string;
145
+ data: JSONObject;
146
+ };
147
+
148
+ if (data?.method === 'update') {
149
+ // got an update
150
+ const path = wRegistry.getPath(commId);
151
+ // console.log('Send an update', data.state, commId, path);
152
+ if (path && typeof data.state === 'object') {
153
+ // const inform = { path, commId, value, index, event };
154
+ const state = { ...data.state } as JSONObject;
155
+ if (state.outputs) {
156
+ delete state.outputs;
157
+ }
158
+ if (Object.keys(state).length !== 0) {
159
+ wRegistry.dispatchMessage({
160
+ path,
161
+ mime: widgetsMime,
162
+ state
163
+ });
164
+ }
165
+ }
166
+ }
167
+ }
168
+ // now fish all messages of control out
169
+ }
170
+ });
171
+ launcher.remoteUpdateMessageArrived.connect(
172
+ async (
173
+ slot: IFailsLauncherInfo,
174
+ args: IFailsInterceptorUpdateMessage
175
+ ) => {
176
+ // const modelId = wRegistry.getModelId(args.path);
177
+ const model = wRegistry.getModel(args.path);
178
+ const state = await (
179
+ model.constructor as typeof WidgetModel
180
+ )._deserialize_state(args.state, model.widget_manager);
181
+ // console.log('got an remote interceptor message', args, state);
182
+
183
+ for (const [key, value] of Object.entries(state)) {
184
+ model.set(key, value);
185
+ }
186
+ model.sync('patch', model, { attrs: state });
187
+ }
188
+ );
189
+ };
190
+
191
+ const widgetsMime = 'application/vnd.jupyter.widget-view+json';
192
+
193
+ // add interceptors for mimerenderers, whose javascript, we need to patch
194
+ // deactivated, as one can use always widgets!
195
+ // eslint-disable-next-line no-constant-condition
196
+ if (rendermimeRegistry && false) {
197
+ const rmRegistry = rendermimeRegistry as RenderMimeRegistry;
198
+ const mimetypes = ['application/vnd.plotly.v1+json']; // mimetypes to patch
199
+
200
+ mimetypes.forEach(mime => {
201
+ const factory = rmRegistry.getFactory(
202
+ mime
203
+ ) as IRenderMime.IRendererFactory;
204
+ if (!factory) {
205
+ console.log(
206
+ 'Plotly seems to be not installed! So I can not add an interceptor'
207
+ );
208
+ return;
209
+ }
210
+ // ok, lets add an interceptor
211
+ const createRendererOld = factory.createRenderer;
212
+ factory.createRenderer = function (
213
+ options: IRenderMime.IRendererOptions
214
+ ) {
215
+ const renderer = createRendererOld(options);
216
+ console.log('intercepted renderer', mime, renderer, renderer.node);
217
+ // we have also the replace renderModel
218
+ const renderModelOld = renderer.renderModel.bind(renderer);
219
+ renderer.renderModel = async (model: IRenderMime.IMimeModel) => {
220
+ let result = await renderModelOld(model);
221
+ console.log('intercepted renderer model', model);
222
+ if (!(<any>renderer).hasGraphElement()) {
223
+ result = await (renderer as any).createGraph(
224
+ (renderer as any)?._model
225
+ );
226
+ }
227
+ if ((<any>renderer.node).on) {
228
+ const messages = [
229
+ 'relayout',
230
+ 'hover',
231
+ 'unhover',
232
+ 'selected',
233
+ 'selecting',
234
+ 'restyle'
235
+ ];
236
+ messages.forEach(mess => {
237
+ (<any>renderer.node).on('plotly_' + mess, (data: any) => {
238
+ const path = model.metadata?.appPath as string;
239
+ if (path) {
240
+ wRegistry.dispatchMessage({
241
+ path: path + ':' + mess,
242
+ mime,
243
+ state: data
244
+ });
245
+ }
246
+ console.log(
247
+ 'plotly',
248
+ mess,
249
+ data,
250
+ model.metadata?.appPath,
251
+ path
252
+ );
253
+ });
254
+ });
255
+ }
256
+ console.log(
257
+ 'renderer layout rM',
258
+ // @ts-expect-error plotly
259
+ renderer.node.layout,
260
+ // @ts-expect-error plotly
261
+ !!renderer.node.on,
262
+ renderer
263
+ );
264
+ //@ts-expect-error result is different from void
265
+ console.log('renderer result', result, !!result?.on);
266
+
267
+ return result;
268
+ };
269
+ // special code for plotly
270
+ // @ts-expect-error plotly
271
+ console.log('renderer layout', renderer.node.layout);
272
+ /* if (!(renderer as any).hasGraphElement()) {
273
+ (renderer as any).createGraph((renderer as any)['_model]']);
274
+ } */
275
+
276
+ /* //@ts-expect-error on not found
277
+ renderer.node.on('plotly_relayout', (update: any) => {
278
+ console.log('relayout', update);
279
+ }); */
280
+
281
+ // special code for plotly
282
+ return renderer;
283
+ };
284
+ });
285
+ }
286
+
287
+ notebookTracker.widgetAdded.connect(
288
+ (sender: INotebookTracker, panel: NotebookPanel) => {
289
+ if (panel.sessionContext.session?.kernel) {
290
+ addKernelInterceptor(panel.sessionContext.session.kernel);
291
+ }
292
+ panel.sessionContext.kernelChanged.connect((sender, args) => {
293
+ if (args.newValue) {
294
+ addKernelInterceptor(args.newValue);
295
+ // TODO remove old interceptor?
296
+ }
297
+ });
298
+
299
+ const widgetManagerPromise: Promise<IWidgetManager> =
300
+ panel.context.sessionContext.ready.then(() => {
301
+ return new Promise((resolve, reject) => {
302
+ requestAnimationFrame(async () => {
303
+ // ensure it is handled after the widgetmanager is installed.
304
+ const rendermime = panel.content.rendermime;
305
+ const widgetFactory = rendermime.getFactory(widgetsMime);
306
+ if (widgetFactory) {
307
+ // now create a dummy widget
308
+ const dummyWidget = widgetFactory.createRenderer({
309
+ mimeType: widgetsMime,
310
+ sanitizer: {
311
+ sanitize: (dirty, options) => ''
312
+ },
313
+ resolver: {
314
+ getDownloadUrl: url => Promise.resolve(''),
315
+ resolveUrl: url => Promise.resolve('')
316
+ },
317
+ latexTypesetter: null,
318
+ linkHandler: null
319
+ });
320
+ resolve(
321
+ await (
322
+ dummyWidget as unknown as {
323
+ _manager: PromiseDelegate<IWidgetManager>;
324
+ }
325
+ )._manager.promise
326
+ );
327
+ dummyWidget.dispose();
328
+ } else {
329
+ reject(new Error('No widgetFactory found for widget view'));
330
+ }
331
+ });
332
+ });
333
+ });
334
+
335
+ widgetManagerPromise?.then(widgetManager => {
336
+ const notebookModel = panel.model as INotebookModel;
337
+ if (notebookModel) {
338
+ const trackedCells = new WeakSet<ICellModel>();
339
+
340
+ const pendingModels: { path: string; widget_model_id: string }[] = [];
341
+
342
+ const iterateWidgets = async (
343
+ path: string,
344
+ widget_model_id: string
345
+ ) => {
346
+ if (widgetManager?.has_model(widget_model_id)) {
347
+ const widget = await widgetManager?.get_model(widget_model_id);
348
+ // const children = widget.attributes.children as
349
+ /* widget.attributes.children.forEach((child) => {
350
+ iterateWidgets(path + '/' + child. ,)
351
+ }) */
352
+ const mypath = path + (widget.name ? '/' + widget.name : '');
353
+ const state = widget.get_state();
354
+ const children = state.children as unknown as [WidgetModel];
355
+ if (children) {
356
+ children.forEach((child, index) => {
357
+ iterateWidgets(mypath + '/' + index, child.model_id);
358
+ });
359
+ }
360
+ // console.log('show widget model', path, widget.get_state());
361
+ wRegistry.registerModel(mypath, widget.model_id, widget);
362
+ /* console.log(
363
+ 'model registred',
364
+ mypath,
365
+ widget.model_id /*, state*,
366
+ widget
367
+ ); */
368
+ } else {
369
+ // console.log('model missing', widget_model_id);
370
+ pendingModels.push({ path, widget_model_id });
371
+ }
372
+ };
373
+ /* const labWidgetManager = widgetManager as LabWidgetManager;
374
+
375
+ if (labWidgetManager) {
376
+ labWidgetManager.restored.connect(lWManager => {
377
+ // we may be able to continue for some missing widgets
378
+ console.log('RESTORED');
379
+ const stillPendingModels: {
380
+ path: string;
381
+ widget_model_id: string;
382
+ }[] = [];
383
+ while (pendingModels.length > 0) {
384
+ const pModel = pendingModels.pop();
385
+ if (!pModel) {
386
+ break;
387
+ }
388
+ if (widgetManager?.has_model(pModel.widget_model_id)) {
389
+ console.log('Resume model search', pModel.path);
390
+ iterateWidgets(pModel.path, pModel.widget_model_id);
391
+ } else {
392
+ stillPendingModels.push(pModel);
393
+ }
394
+ }
395
+ pendingModels.push(...stillPendingModels);
396
+ });
397
+ } */
398
+
399
+ const onCellsChanged = (cell: ICellModel) => {
400
+ if (!trackedCells.has(cell)) {
401
+ trackedCells.add(cell);
402
+ const updateMimedata = () => {
403
+ if (cell.type === 'code') {
404
+ // now we figure out, if all widgets are registered
405
+ const sharedModel = cell.sharedModel as ISharedCodeCell;
406
+ let index = 0;
407
+ for (const output of sharedModel.outputs) {
408
+ const appPath = cell.id + '/' + index;
409
+ let addPath = false;
410
+ // tag all sharedmodels with the path
411
+ switch (output.output_type) {
412
+ case 'display_data':
413
+ case 'update_display_data':
414
+ case 'execute_result':
415
+ {
416
+ const result = output as
417
+ | IExecuteResult
418
+ | IDisplayUpdate
419
+ | IDisplayData;
420
+
421
+ // console.log('Mimebundle', result.data); // to do parse this also
422
+ // console.log('Metadata', result.metadata);
423
+ // console.log('Result', result);
424
+ const mimebundle = result.data;
425
+ if (mimebundle[widgetsMime]) {
426
+ const { model_id } = mimebundle[widgetsMime] as {
427
+ model_id: string;
428
+ };
429
+ iterateWidgets(appPath, model_id);
430
+ }
431
+
432
+ // Deactivate, as we are not using it, use a plotly widget instead.
433
+ if (
434
+ // eslint-disable-next-line no-constant-condition
435
+ mimebundle['application/vnd.plotly.v1+json'] &&
436
+ false
437
+ ) {
438
+ const bundle =
439
+ mimebundle['application/vnd.plotly.v1+json'];
440
+ console.log('Plotly bundle', bundle);
441
+ console.log('plotly cell', cell);
442
+ if ((<any>result.metadata).appPath !== appPath) {
443
+ (<any>result.metadata).appPath = appPath;
444
+ addPath = true;
445
+ }
446
+ }
447
+ }
448
+ break;
449
+
450
+ case 'stream':
451
+ case 'error':
452
+ default:
453
+ }
454
+ if (addPath) {
455
+ // should happen only once
456
+ sharedModel.updateOutputs(index, index + 1, [output]);
457
+ }
458
+ index++;
459
+ }
460
+ }
461
+ };
462
+
463
+ cell.contentChanged.connect(updateMimedata);
464
+ updateMimedata();
465
+ }
466
+ };
467
+ for (const cell of notebookModel.cells) {
468
+ onCellsChanged(cell);
469
+ }
470
+ notebookModel.cells.changed.connect((cellist, changedList) => {
471
+ const { /*newIndex,*/ newValues /* oldIndex, oldValues, type */ } =
472
+ changedList;
473
+ newValues.forEach(newcell => {
474
+ onCellsChanged(newcell);
475
+ // console.log('changed cells', newcell);
476
+ });
477
+ });
478
+ }
479
+ });
480
+ }
481
+ );
482
+ return {
483
+ isMimeTypeSupported: (mimeType: string) => {
484
+ if (staticMimeTypes.has(mimeType)) {
485
+ return true;
486
+ }
487
+ if (dynamicMimeTypes.has(mimeType)) {
488
+ return true;
489
+ }
490
+ return false;
491
+ }
492
+ };
493
+ }
494
+
495
+ const appletWidgetInterceptor: JupyterFrontEndPlugin<IFailsInterceptor> = {
496
+ id: '@fails-components/jupyter-applet-widget:interceptor',
497
+ description: 'Tracks and intercepts widget communication',
498
+ autoStart: true,
499
+ activate: activateWidgetInterceptor,
500
+ provides: IFailsInterceptor,
501
+ requires: [INotebookTracker, IRenderMimeRegistry, IFailsLauncherInfo],
502
+ optional: []
503
+ };
504
+
505
+ const plugins: JupyterFrontEndPlugin<any>[] = [appletWidgetInterceptor];
506
+
507
+ export default plugins;
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,2 @@
1
+ import './base.css';
2
+ import './index.css';