@fails-components/jupyter-react-edit 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 +29 -0
- package/README.md +65 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/jupyteredit.d.ts +53 -0
- package/lib/jupyteredit.js +288 -0
- package/package.json +185 -0
- package/src/index.ts +1 -0
- package/src/jupyteredit.tsx +385 -0
- package/style/index.css +7 -0
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,65 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Fancy automated internet lecture system (**FAILS**) - components (jupyter react edit)
|
|
4
|
+
|
|
5
|
+
(c) 2024 Marten Richter
|
|
6
|
+
|
|
7
|
+
The package provides a React component to include jupyter within FAILS, but may be usuable outside of fails.
|
|
8
|
+
|
|
9
|
+
This package is part of FAILS.
|
|
10
|
+
A web-based lecture system developed out of university lectures.
|
|
11
|
+
|
|
12
|
+
While FAILS as a whole is licensed via GNU Affero GPL version 3.0, this package is licensed under a BSD-style license that can be found in the LICENSE file.
|
|
13
|
+
This package is licensed more permissive since it can be useful outside of the FAILS environment, esspecially if you want to integrate Jupyter Lite into your application.
|
|
14
|
+
So the default license from Jupyter is used to be license compatible.
|
|
15
|
+
|
|
16
|
+
## Installation and usage
|
|
17
|
+
|
|
18
|
+
### Installation
|
|
19
|
+
|
|
20
|
+
You can install the package directly via npm from node.js:
|
|
21
|
+
|
|
22
|
+
For installation simply run:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
npm install @fails-components/jupyter-react-edit
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Usage
|
|
29
|
+
|
|
30
|
+
You can integrate the React component via
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
import { JupyterEdit } from '@fails-components/jupyter-react-edit'
|
|
34
|
+
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
<JupyterEdit
|
|
38
|
+
editActivated={/* true or false, to control activation */}
|
|
39
|
+
jupyterurl={window.location.origin + '/jupyter/index.html' /* provides the URL of your jupyter lite deployment */}
|
|
40
|
+
pointerOff={
|
|
41
|
+
/* turn off and on mouse pointer inter actzion*/
|
|
42
|
+
}
|
|
43
|
+
rerunAtStartup={/* if true rerun all cells at startup*/}
|
|
44
|
+
installScreenShotPatches={
|
|
45
|
+
/* if true patches are installed within jupyter to allow for screenshots of applets*/
|
|
46
|
+
}
|
|
47
|
+
ref={/* you may want to track the reference to interact with the control*/}
|
|
48
|
+
document={/* initial javascript object representing the jupyter file */}
|
|
49
|
+
filename={/* file name */}
|
|
50
|
+
appid={/* you can set an id to show only a specific applet, instead of full editing */}
|
|
51
|
+
stateCallback={/* call back to process changes of the state of the jupyter applets */}
|
|
52
|
+
appletSizeChanged={(appid, width, height) => {
|
|
53
|
+
/* callback invoked if the size of an applet changes */
|
|
54
|
+
}}
|
|
55
|
+
kernelStatusCallback={status => {
|
|
56
|
+
/* callback, if the kernel status changed */
|
|
57
|
+
}}
|
|
58
|
+
receiveInterceptorUpdate={({ path, mime, state }) => {
|
|
59
|
+
/* information from the interceptor about a state update */
|
|
60
|
+
}}
|
|
61
|
+
/>;
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For more information please look at the usuage of the componenent inside fails, notably inside `@fails-components\app` and `@fails-components\lectureapp`.
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './jupyteredit.js';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './jupyteredit.js';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React, { Component } from 'react';
|
|
2
|
+
import { IFailsToJupyterMessage, IInterceptorUpdate, IScreenShotOpts, IFailsAppletSize, IGDPRProxyInfo } from '@fails-components/jupyter-launcher';
|
|
3
|
+
import { JSONObject } from '@lumino/coreutils';
|
|
4
|
+
import '../style/index.css';
|
|
5
|
+
interface IJupyterState {
|
|
6
|
+
dirty?: boolean;
|
|
7
|
+
failsApp?: JSONObject;
|
|
8
|
+
kernelspec?: JSONObject;
|
|
9
|
+
}
|
|
10
|
+
interface IJupyterEditProps {
|
|
11
|
+
stateCallback?: (state: IJupyterState) => void;
|
|
12
|
+
receiveInterceptorUpdate?: (update: IInterceptorUpdate) => void;
|
|
13
|
+
kernelStatusCallback?: (status: string) => void;
|
|
14
|
+
appletSizeChanged?: (appid: string, width: number, height: number) => void;
|
|
15
|
+
jupyterurl: string;
|
|
16
|
+
filename: string;
|
|
17
|
+
document: JSONObject | undefined;
|
|
18
|
+
appid?: string;
|
|
19
|
+
rerunAtStartup: boolean;
|
|
20
|
+
installScreenShotPatches: boolean;
|
|
21
|
+
GDPRProxy?: IGDPRProxyInfo;
|
|
22
|
+
editActivated?: boolean;
|
|
23
|
+
pointerOff?: boolean;
|
|
24
|
+
}
|
|
25
|
+
interface IJupyterEditState {
|
|
26
|
+
appletSizes?: {
|
|
27
|
+
[key: string]: IFailsAppletSize;
|
|
28
|
+
} | undefined;
|
|
29
|
+
dirty: boolean;
|
|
30
|
+
appLoading: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare class JupyterEdit extends Component<IJupyterEditProps, IJupyterEditState> {
|
|
33
|
+
constructor(props: IJupyterEditProps);
|
|
34
|
+
componentDidMount(): void;
|
|
35
|
+
componentDidUpdate(prevProps: IJupyterEditProps): void;
|
|
36
|
+
componentWillUnmount(): void;
|
|
37
|
+
loadJupyter(): void;
|
|
38
|
+
saveJupyter(): Promise<any>;
|
|
39
|
+
screenShot({ dpi }: IScreenShotOpts): Promise<any>;
|
|
40
|
+
activateApp(): Promise<any>;
|
|
41
|
+
getLicenses(): Promise<any>;
|
|
42
|
+
restartKernelAndRunCells(): Promise<any>;
|
|
43
|
+
activateInterceptor(activate: boolean): Promise<any>;
|
|
44
|
+
sendInterceptorUpdate({ path, mime, state }: IInterceptorUpdate): Promise<any>;
|
|
45
|
+
onMessage(event: MessageEvent): void;
|
|
46
|
+
sendToIFrame(message: IFailsToJupyterMessage): void;
|
|
47
|
+
sendToIFrameAndReceive(message: IFailsToJupyterMessage): Promise<any>;
|
|
48
|
+
render(): React.JSX.Element;
|
|
49
|
+
private _iframe;
|
|
50
|
+
private _requestId;
|
|
51
|
+
private _requests;
|
|
52
|
+
}
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/*
|
|
2
|
+
BSD 3-Clause License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024, Marten Richter
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
|
8
|
+
modification, are permitted provided that the following conditions are met:
|
|
9
|
+
|
|
10
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
11
|
+
list of conditions and the following disclaimer.
|
|
12
|
+
|
|
13
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
and/or other materials provided with the distribution.
|
|
16
|
+
|
|
17
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
18
|
+
contributors may be used to endorse or promote products derived from
|
|
19
|
+
this software without specific prior written permission.
|
|
20
|
+
|
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
22
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
23
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
24
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
25
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
26
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
27
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
28
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
29
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
30
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
31
|
+
*/
|
|
32
|
+
import React, { Component, Fragment } from 'react';
|
|
33
|
+
import '../style/index.css';
|
|
34
|
+
export class JupyterEdit extends Component {
|
|
35
|
+
constructor(props) {
|
|
36
|
+
super(props);
|
|
37
|
+
this._iframe = null;
|
|
38
|
+
this._requestId = 1; // id, if we request something
|
|
39
|
+
this._requests = new Map();
|
|
40
|
+
this.state = { dirty: false, appLoading: true };
|
|
41
|
+
this.onMessage = this.onMessage.bind(this);
|
|
42
|
+
if (props.stateCallback) {
|
|
43
|
+
props.stateCallback({ dirty: false });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
componentDidMount() {
|
|
47
|
+
if (!this.props.jupyterurl) {
|
|
48
|
+
throw new Error('No jupyter url passed');
|
|
49
|
+
}
|
|
50
|
+
window.addEventListener('message', this.onMessage);
|
|
51
|
+
if (this.props.receiveInterceptorUpdate) {
|
|
52
|
+
this.activateInterceptor(true);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
componentDidUpdate(prevProps) {
|
|
56
|
+
if (!this.props.jupyterurl) {
|
|
57
|
+
throw new Error('No jupyter url passed');
|
|
58
|
+
}
|
|
59
|
+
if (this.props.appid !== prevProps.appid) {
|
|
60
|
+
this.activateApp();
|
|
61
|
+
}
|
|
62
|
+
if (this.props.receiveInterceptorUpdate !== prevProps.receiveInterceptorUpdate) {
|
|
63
|
+
this.activateInterceptor(!!this.props.receiveInterceptorUpdate);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
componentWillUnmount() {
|
|
67
|
+
window.removeEventListener('message', this.onMessage);
|
|
68
|
+
}
|
|
69
|
+
loadJupyter() {
|
|
70
|
+
var _a, _b, _c;
|
|
71
|
+
const data = this.props.document;
|
|
72
|
+
const metadata = data === null || data === void 0 ? void 0 : data.metadata;
|
|
73
|
+
if (metadata === null || metadata === void 0 ? void 0 : metadata.kernelspec) {
|
|
74
|
+
const kernelspec = metadata === null || metadata === void 0 ? void 0 : metadata.kernelspec;
|
|
75
|
+
if ((kernelspec === null || kernelspec === void 0 ? void 0 : kernelspec.name) !== 'python' && (kernelspec === null || kernelspec === void 0 ? void 0 : kernelspec.name) !== 'xpython') {
|
|
76
|
+
// replace the kernel
|
|
77
|
+
kernelspec.name = 'python';
|
|
78
|
+
kernelspec.display_name = 'Python (Pyodide)';
|
|
79
|
+
kernelspec.language = 'python';
|
|
80
|
+
kernelspec.name = 'python';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
this.sendToIFrame({
|
|
84
|
+
type: 'loadJupyter',
|
|
85
|
+
inLecture: !!this.props.appid,
|
|
86
|
+
rerunAtStartup: !!this.props.rerunAtStartup,
|
|
87
|
+
installScreenShotPatches: !!this.props.installScreenShotPatches,
|
|
88
|
+
installGDPRProxy: this.props.GDPRProxy,
|
|
89
|
+
appid: this.props.appid,
|
|
90
|
+
fileName: this.props.filename || 'example.ipynb',
|
|
91
|
+
fileData: data,
|
|
92
|
+
kernelName: (_c = (_b = (_a = data === null || data === void 0 ? void 0 : data.metadata) === null || _a === void 0 ? void 0 : _a.kernelspec) === null || _b === void 0 ? void 0 : _b.name) !== null && _c !== void 0 ? _c : 'python'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async saveJupyter() {
|
|
96
|
+
const fileToSaveObj = await this.sendToIFrameAndReceive({
|
|
97
|
+
type: 'saveJupyter',
|
|
98
|
+
fileName: this.props.filename || 'example.ipynb'
|
|
99
|
+
});
|
|
100
|
+
if (!fileToSaveObj.fileData) {
|
|
101
|
+
throw new Error('Empty saveJupyter response');
|
|
102
|
+
}
|
|
103
|
+
return fileToSaveObj.fileData;
|
|
104
|
+
}
|
|
105
|
+
async screenShot({ dpi }) {
|
|
106
|
+
const { screenshot } = await this.sendToIFrameAndReceive({
|
|
107
|
+
type: 'screenshotApp',
|
|
108
|
+
dpi
|
|
109
|
+
});
|
|
110
|
+
return screenshot;
|
|
111
|
+
}
|
|
112
|
+
activateApp() {
|
|
113
|
+
const appid = this.props.appid;
|
|
114
|
+
return this.sendToIFrameAndReceive({
|
|
115
|
+
type: 'activateApp',
|
|
116
|
+
inLecture: !!appid,
|
|
117
|
+
appid
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async getLicenses() {
|
|
121
|
+
return this.sendToIFrameAndReceive({
|
|
122
|
+
type: 'getLicenses'
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async restartKernelAndRunCells() {
|
|
126
|
+
return this.sendToIFrameAndReceive({
|
|
127
|
+
type: 'restartKernelAndRerunCells'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
activateInterceptor(activate) {
|
|
131
|
+
return this.sendToIFrameAndReceive({
|
|
132
|
+
type: 'activateInterceptor',
|
|
133
|
+
activate
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
sendInterceptorUpdate({ path, mime, state }) {
|
|
137
|
+
return this.sendToIFrameAndReceive({
|
|
138
|
+
type: 'receiveInterceptorUpdate',
|
|
139
|
+
path,
|
|
140
|
+
mime,
|
|
141
|
+
state
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
onMessage(event) {
|
|
145
|
+
var _a, _b, _c, _d, _e, _f;
|
|
146
|
+
if (event.source === window) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (event.source !== ((_a = this._iframe) === null || _a === void 0 ? void 0 : _a.contentWindow)) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (event.origin !== new URL(this.props.jupyterurl).origin) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const data = event.data;
|
|
156
|
+
if (event.data.requestId) {
|
|
157
|
+
const requestId = event.data.requestId;
|
|
158
|
+
if (this._requests.has(requestId)) {
|
|
159
|
+
const request = this._requests.get(requestId);
|
|
160
|
+
this._requests.delete(requestId);
|
|
161
|
+
if (event.data.error) {
|
|
162
|
+
request.reject(new Error(event.data.error));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
request.resolve(event.data);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
switch (data === null || data === void 0 ? void 0 : data.task) {
|
|
170
|
+
case 'appLoaded':
|
|
171
|
+
this.setState({ appLoading: false });
|
|
172
|
+
this.loadJupyter();
|
|
173
|
+
break;
|
|
174
|
+
case 'docDirty':
|
|
175
|
+
{
|
|
176
|
+
const { dirty = undefined } = data;
|
|
177
|
+
if (this.props.stateCallback && typeof dirty !== 'undefined') {
|
|
178
|
+
this.props.stateCallback({ dirty });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
case 'reportMetadata':
|
|
183
|
+
{
|
|
184
|
+
const { failsApp = undefined, kernelspec = undefined } = (_b = data === null || data === void 0 ? void 0 : data.metadata) !== null && _b !== void 0 ? _b : {};
|
|
185
|
+
if (this.props.stateCallback &&
|
|
186
|
+
(typeof failsApp !== 'undefined' ||
|
|
187
|
+
typeof kernelspec !== 'undefined')) {
|
|
188
|
+
this.props.stateCallback({ failsApp, kernelspec });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
case 'reportFailsAppletSizes':
|
|
193
|
+
{
|
|
194
|
+
const { appletSizes = undefined } = data;
|
|
195
|
+
if (typeof appletSizes !== 'undefined') {
|
|
196
|
+
this.setState(state => {
|
|
197
|
+
var _a, _b, _c;
|
|
198
|
+
const retState = {};
|
|
199
|
+
for (const appletSize of Object.values(appletSizes)) {
|
|
200
|
+
const { appid, height, width } = appletSize;
|
|
201
|
+
if ((_a = state === null || state === void 0 ? void 0 : state.appletSizes) === null || _a === void 0 ? void 0 : _a[appid]) {
|
|
202
|
+
const oldsize = state.appletSizes[appid];
|
|
203
|
+
if (oldsize.height === height && oldsize.width === width) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!retState.appletSizes) {
|
|
208
|
+
retState.appletSizes = {};
|
|
209
|
+
}
|
|
210
|
+
retState.appletSizes[appid] = { width, height, appid };
|
|
211
|
+
(_c = (_b = this.props) === null || _b === void 0 ? void 0 : _b.appletSizeChanged) === null || _c === void 0 ? void 0 : _c.call(_b, appid, width, height);
|
|
212
|
+
}
|
|
213
|
+
return retState;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
case 'reportKernelStatus':
|
|
219
|
+
(_d = (_c = this.props) === null || _c === void 0 ? void 0 : _c.kernelStatusCallback) === null || _d === void 0 ? void 0 : _d.call(_c, data.status);
|
|
220
|
+
break;
|
|
221
|
+
case 'sendInterceptorUpdate':
|
|
222
|
+
{
|
|
223
|
+
const { path, mime, state } = data;
|
|
224
|
+
(_f = (_e = this.props) === null || _e === void 0 ? void 0 : _e.receiveInterceptorUpdate) === null || _f === void 0 ? void 0 : _f.call(_e, { path, mime, state });
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
default:
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
sendToIFrame(message) {
|
|
231
|
+
var _a;
|
|
232
|
+
if (this._iframe) {
|
|
233
|
+
(_a = this._iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(message, this.props.jupyterurl);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async sendToIFrameAndReceive(message) {
|
|
237
|
+
const requestId = this._requestId++;
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
this._requests.set(requestId, {
|
|
240
|
+
requestId,
|
|
241
|
+
resolve,
|
|
242
|
+
reject
|
|
243
|
+
});
|
|
244
|
+
this.sendToIFrame({
|
|
245
|
+
// @ts-expect-error requestId is not included in type
|
|
246
|
+
requestId,
|
|
247
|
+
...message
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
render() {
|
|
252
|
+
// launch debugging in the following way:
|
|
253
|
+
// jupyter lab --allow-root --ServerApp.allow_origin='*' --ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors self *'}}" --ServerApp.allow_websocket_origin='*' --ServerApp.cookie_options="{'samesite': 'None', 'secure': True}"
|
|
254
|
+
// jupyter lab --allow-root --ServerApp.allow_origin='*' --ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors self *'}}" --ServerApp.allow_websocket_origin='*' --ServerApp.cookie_options="{'samesite': 'None', 'secure': True}" --LabServerApp.app_settings_dir=/workspaces/jupyterfails/development/config/app-edit
|
|
255
|
+
// do it only in a container!
|
|
256
|
+
if (!this.props.editActivated) {
|
|
257
|
+
return React.createElement(Fragment, null, "JupyterEdit is not activated");
|
|
258
|
+
}
|
|
259
|
+
let width = '100%';
|
|
260
|
+
let height = '99%';
|
|
261
|
+
if (this.props.appid) {
|
|
262
|
+
const appletSize = this.state.appletSizes && this.state.appletSizes[this.props.appid];
|
|
263
|
+
if (appletSize) {
|
|
264
|
+
width = Math.ceil(appletSize.width * 1.01) + 'px';
|
|
265
|
+
height = Math.ceil(appletSize.height * 1.01) + 'px';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
let className = 'jpt-edit-iframe';
|
|
269
|
+
if (this.props.pointerOff) {
|
|
270
|
+
className += ' jpyt-edit-iframe-pointeroff';
|
|
271
|
+
}
|
|
272
|
+
return (React.createElement(Fragment, null,
|
|
273
|
+
React.createElement("iframe", { style: { width, height }, className: className, src: this.props.jupyterurl, ref: el => {
|
|
274
|
+
this._iframe = el;
|
|
275
|
+
}, onLoad: () => {
|
|
276
|
+
console.log('Jupyter iframe loaded');
|
|
277
|
+
}, allow: "",
|
|
278
|
+
// @ts-expect-error credentialless
|
|
279
|
+
credentialless: "true", sandbox: "allow-scripts allow-downloads allow-same-origin allow-popups" // we need allow-forms for a local jupyter server, remove for jupyterlite
|
|
280
|
+
, title: "jupyteredit" }),
|
|
281
|
+
this.state.appLoading && (React.createElement("h2", { style: {
|
|
282
|
+
position: 'absolute',
|
|
283
|
+
top: '50%',
|
|
284
|
+
left: '50%',
|
|
285
|
+
transform: 'translate(-50%, -50%)'
|
|
286
|
+
} }, "Jupyter is loading, be patient..."))));
|
|
287
|
+
}
|
|
288
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fails-components/jupyter-react-edit",
|
|
3
|
+
"version": "0.0.1-alpha.10",
|
|
4
|
+
"description": "A react control, that lets you embed jupyter lite in your application including applets with screenshots etc.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"jupyter",
|
|
7
|
+
"jupyterlab",
|
|
8
|
+
"react",
|
|
9
|
+
"iframe"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/fails-components/jupyterfails",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/fails-components/jupyterfails/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "BSD-3-Clause",
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "Marten Richter",
|
|
18
|
+
"email": "marten.richter@freenet.de"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
|
|
22
|
+
"style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
|
|
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",
|
|
36
|
+
"build:prod": "jlpm clean && jlpm build:lib:prod",
|
|
37
|
+
"build:lib": "tsc --sourceMap",
|
|
38
|
+
"build:lib:prod": "tsc",
|
|
39
|
+
"clean": "jlpm clean:lib",
|
|
40
|
+
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
|
|
41
|
+
"clean:lintcache": "rimraf .eslintcache .stylelintcache",
|
|
42
|
+
"clean:all": "jlpm clean:lib && jlpm clean:lintcache",
|
|
43
|
+
"eslint": "jlpm eslint:check --fix",
|
|
44
|
+
"eslint:check": "eslint . --cache --ext .ts,.tsx",
|
|
45
|
+
"lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
|
|
46
|
+
"lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
|
|
47
|
+
"prepublishOnly": "npm run build",
|
|
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 --passWithNoTests",
|
|
54
|
+
"watch": "run-p watch:src",
|
|
55
|
+
"watch:src": "tsc -w --sourceMap"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@fails-components/jupyter-launcher": "^0.0.1-alpha.10",
|
|
59
|
+
"@types/jest": "^29.2.0",
|
|
60
|
+
"@types/json-schema": "^7.0.11",
|
|
61
|
+
"@types/react": "^18.0.26",
|
|
62
|
+
"@types/react-addons-linked-state-mixin": "^0.14.22",
|
|
63
|
+
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
|
64
|
+
"@typescript-eslint/parser": "^6.1.0",
|
|
65
|
+
"css-loader": "^6.7.1",
|
|
66
|
+
"eslint": "^8.36.0",
|
|
67
|
+
"eslint-config-prettier": "^8.8.0",
|
|
68
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
69
|
+
"jest": "^29.2.0",
|
|
70
|
+
"npm-run-all": "^4.1.5",
|
|
71
|
+
"prettier": "^3.0.0",
|
|
72
|
+
"rimraf": "^5.0.1",
|
|
73
|
+
"source-map-loader": "^1.0.2",
|
|
74
|
+
"style-loader": "^3.3.1",
|
|
75
|
+
"stylelint": "^15.10.1",
|
|
76
|
+
"stylelint-config-recommended": "^13.0.0",
|
|
77
|
+
"stylelint-config-standard": "^34.0.0",
|
|
78
|
+
"stylelint-csstree-validator": "^3.0.0",
|
|
79
|
+
"stylelint-prettier": "^4.0.0",
|
|
80
|
+
"typescript": "~5.0.2",
|
|
81
|
+
"yjs": "^13.5.0"
|
|
82
|
+
},
|
|
83
|
+
"sideEffects": [
|
|
84
|
+
"style/*.css",
|
|
85
|
+
"style/index.js"
|
|
86
|
+
],
|
|
87
|
+
"styleModule": "style/index.js",
|
|
88
|
+
"publishConfig": {
|
|
89
|
+
"access": "public"
|
|
90
|
+
},
|
|
91
|
+
"eslintIgnore": [
|
|
92
|
+
"node_modules",
|
|
93
|
+
"dist",
|
|
94
|
+
"coverage",
|
|
95
|
+
"**/*.d.ts",
|
|
96
|
+
"tests",
|
|
97
|
+
"**/__tests__",
|
|
98
|
+
"ui-tests"
|
|
99
|
+
],
|
|
100
|
+
"eslintConfig": {
|
|
101
|
+
"extends": [
|
|
102
|
+
"eslint:recommended",
|
|
103
|
+
"plugin:@typescript-eslint/eslint-recommended",
|
|
104
|
+
"plugin:@typescript-eslint/recommended",
|
|
105
|
+
"plugin:prettier/recommended"
|
|
106
|
+
],
|
|
107
|
+
"parser": "@typescript-eslint/parser",
|
|
108
|
+
"parserOptions": {
|
|
109
|
+
"project": "tsconfig.json",
|
|
110
|
+
"sourceType": "module"
|
|
111
|
+
},
|
|
112
|
+
"plugins": [
|
|
113
|
+
"@typescript-eslint"
|
|
114
|
+
],
|
|
115
|
+
"rules": {
|
|
116
|
+
"@typescript-eslint/naming-convention": [
|
|
117
|
+
"error",
|
|
118
|
+
{
|
|
119
|
+
"selector": "interface",
|
|
120
|
+
"format": [
|
|
121
|
+
"PascalCase"
|
|
122
|
+
],
|
|
123
|
+
"custom": {
|
|
124
|
+
"regex": "^I[A-Z]",
|
|
125
|
+
"match": true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
"@typescript-eslint/no-unused-vars": [
|
|
130
|
+
"warn",
|
|
131
|
+
{
|
|
132
|
+
"args": "none"
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
136
|
+
"@typescript-eslint/no-namespace": "off",
|
|
137
|
+
"@typescript-eslint/no-use-before-define": "off",
|
|
138
|
+
"@typescript-eslint/quotes": [
|
|
139
|
+
"error",
|
|
140
|
+
"single",
|
|
141
|
+
{
|
|
142
|
+
"avoidEscape": true,
|
|
143
|
+
"allowTemplateLiterals": false
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
"curly": [
|
|
147
|
+
"error",
|
|
148
|
+
"all"
|
|
149
|
+
],
|
|
150
|
+
"eqeqeq": "error",
|
|
151
|
+
"prefer-arrow-callback": "error"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"prettier": {
|
|
155
|
+
"singleQuote": true,
|
|
156
|
+
"trailingComma": "none",
|
|
157
|
+
"arrowParens": "avoid",
|
|
158
|
+
"endOfLine": "auto",
|
|
159
|
+
"overrides": [
|
|
160
|
+
{
|
|
161
|
+
"files": "package.json",
|
|
162
|
+
"options": {
|
|
163
|
+
"tabWidth": 4
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
"stylelint": {
|
|
169
|
+
"extends": [
|
|
170
|
+
"stylelint-config-recommended",
|
|
171
|
+
"stylelint-config-standard",
|
|
172
|
+
"stylelint-prettier/recommended"
|
|
173
|
+
],
|
|
174
|
+
"plugins": [
|
|
175
|
+
"stylelint-csstree-validator"
|
|
176
|
+
],
|
|
177
|
+
"rules": {
|
|
178
|
+
"csstree/validator": true,
|
|
179
|
+
"property-no-vendor-prefix": null,
|
|
180
|
+
"selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
|
|
181
|
+
"selector-no-vendor-prefix": null,
|
|
182
|
+
"value-no-vendor-prefix": null
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './jupyteredit.js';
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/*
|
|
2
|
+
BSD 3-Clause License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024, Marten Richter
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
|
8
|
+
modification, are permitted provided that the following conditions are met:
|
|
9
|
+
|
|
10
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
11
|
+
list of conditions and the following disclaimer.
|
|
12
|
+
|
|
13
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
and/or other materials provided with the distribution.
|
|
16
|
+
|
|
17
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
18
|
+
contributors may be used to endorse or promote products derived from
|
|
19
|
+
this software without specific prior written permission.
|
|
20
|
+
|
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
22
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
23
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
24
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
25
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
26
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
27
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
28
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
29
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
30
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
31
|
+
*/
|
|
32
|
+
import React, { Component, Fragment } from 'react';
|
|
33
|
+
import {
|
|
34
|
+
IJupyterToFailsMessage,
|
|
35
|
+
IFailsToJupyterMessage,
|
|
36
|
+
IInterceptorUpdate,
|
|
37
|
+
IReportFailsAppletSizes,
|
|
38
|
+
IScreenShotOpts,
|
|
39
|
+
IDocDirty,
|
|
40
|
+
IFailsAppletSize,
|
|
41
|
+
IGDPRProxyInfo
|
|
42
|
+
} from '@fails-components/jupyter-launcher';
|
|
43
|
+
import { JSONObject, PartialJSONObject } from '@lumino/coreutils';
|
|
44
|
+
import '../style/index.css';
|
|
45
|
+
|
|
46
|
+
interface IJupyterState {
|
|
47
|
+
dirty?: boolean;
|
|
48
|
+
failsApp?: JSONObject;
|
|
49
|
+
kernelspec?: JSONObject;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface IJupyterEditProps {
|
|
53
|
+
stateCallback?: (state: IJupyterState) => void;
|
|
54
|
+
receiveInterceptorUpdate?: (update: IInterceptorUpdate) => void;
|
|
55
|
+
kernelStatusCallback?: (status: string) => void;
|
|
56
|
+
appletSizeChanged?: (appid: string, width: number, height: number) => void;
|
|
57
|
+
jupyterurl: string; // url of the jupyter lite distribution
|
|
58
|
+
filename: string; // filename of the document
|
|
59
|
+
document: JSONObject | undefined; // the document as javascript object
|
|
60
|
+
appid?: string; // id of the applet or undefined if not in applet mode
|
|
61
|
+
rerunAtStartup: boolean;
|
|
62
|
+
installScreenShotPatches: boolean; // install patches to allow screenshots of plotly
|
|
63
|
+
GDPRProxy?: IGDPRProxyInfo;
|
|
64
|
+
editActivated?: boolean; // whether the jupyter edit is activated.
|
|
65
|
+
pointerOff?: boolean; // if true, no pointer interaction with jupyter is possible
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface IJupyterEditState {
|
|
69
|
+
appletSizes?:
|
|
70
|
+
| {
|
|
71
|
+
[key: string]: IFailsAppletSize;
|
|
72
|
+
}
|
|
73
|
+
| undefined;
|
|
74
|
+
dirty: boolean;
|
|
75
|
+
appLoading: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface IDocumentMetadata extends PartialJSONObject {
|
|
79
|
+
kernelspec?: JSONObject;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class JupyterEdit extends Component<
|
|
83
|
+
IJupyterEditProps,
|
|
84
|
+
IJupyterEditState
|
|
85
|
+
> {
|
|
86
|
+
constructor(props: IJupyterEditProps) {
|
|
87
|
+
super(props);
|
|
88
|
+
this.state = { dirty: false, appLoading: true };
|
|
89
|
+
this.onMessage = this.onMessage.bind(this);
|
|
90
|
+
if (props.stateCallback) {
|
|
91
|
+
props.stateCallback({ dirty: false });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
componentDidMount() {
|
|
96
|
+
if (!this.props.jupyterurl) {
|
|
97
|
+
throw new Error('No jupyter url passed');
|
|
98
|
+
}
|
|
99
|
+
window.addEventListener('message', this.onMessage);
|
|
100
|
+
if (this.props.receiveInterceptorUpdate) {
|
|
101
|
+
this.activateInterceptor(true);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
componentDidUpdate(prevProps: IJupyterEditProps) {
|
|
106
|
+
if (!this.props.jupyterurl) {
|
|
107
|
+
throw new Error('No jupyter url passed');
|
|
108
|
+
}
|
|
109
|
+
if (this.props.appid !== prevProps.appid) {
|
|
110
|
+
this.activateApp();
|
|
111
|
+
}
|
|
112
|
+
if (
|
|
113
|
+
this.props.receiveInterceptorUpdate !== prevProps.receiveInterceptorUpdate
|
|
114
|
+
) {
|
|
115
|
+
this.activateInterceptor(!!this.props.receiveInterceptorUpdate);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
componentWillUnmount() {
|
|
120
|
+
window.removeEventListener('message', this.onMessage);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
loadJupyter() {
|
|
124
|
+
const data = this.props.document;
|
|
125
|
+
const metadata = data?.metadata as IDocumentMetadata | null;
|
|
126
|
+
if (metadata?.kernelspec) {
|
|
127
|
+
const kernelspec = metadata?.kernelspec;
|
|
128
|
+
if (kernelspec?.name !== 'python' && kernelspec?.name !== 'xpython') {
|
|
129
|
+
// replace the kernel
|
|
130
|
+
kernelspec.name = 'python';
|
|
131
|
+
kernelspec.display_name = 'Python (Pyodide)';
|
|
132
|
+
kernelspec.language = 'python';
|
|
133
|
+
kernelspec.name = 'python';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
this.sendToIFrame({
|
|
137
|
+
type: 'loadJupyter',
|
|
138
|
+
inLecture: !!this.props.appid,
|
|
139
|
+
rerunAtStartup: !!this.props.rerunAtStartup,
|
|
140
|
+
installScreenShotPatches: !!this.props.installScreenShotPatches,
|
|
141
|
+
installGDPRProxy: this.props.GDPRProxy,
|
|
142
|
+
appid: this.props.appid,
|
|
143
|
+
fileName: this.props.filename || 'example.ipynb',
|
|
144
|
+
fileData: data,
|
|
145
|
+
kernelName:
|
|
146
|
+
((data?.metadata as IDocumentMetadata)?.kernelspec?.name as
|
|
147
|
+
| 'python'
|
|
148
|
+
| 'xpython'
|
|
149
|
+
| undefined) ?? 'python'
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async saveJupyter() {
|
|
154
|
+
const fileToSaveObj = await this.sendToIFrameAndReceive({
|
|
155
|
+
type: 'saveJupyter',
|
|
156
|
+
fileName: this.props.filename || 'example.ipynb'
|
|
157
|
+
});
|
|
158
|
+
if (!fileToSaveObj.fileData) {
|
|
159
|
+
throw new Error('Empty saveJupyter response');
|
|
160
|
+
}
|
|
161
|
+
return fileToSaveObj.fileData;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async screenShot({ dpi }: IScreenShotOpts) {
|
|
165
|
+
const { screenshot } = await this.sendToIFrameAndReceive({
|
|
166
|
+
type: 'screenshotApp',
|
|
167
|
+
dpi
|
|
168
|
+
});
|
|
169
|
+
return screenshot;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
activateApp() {
|
|
173
|
+
const appid = this.props.appid;
|
|
174
|
+
return this.sendToIFrameAndReceive({
|
|
175
|
+
type: 'activateApp',
|
|
176
|
+
inLecture: !!appid,
|
|
177
|
+
appid
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async getLicenses() {
|
|
182
|
+
return this.sendToIFrameAndReceive({
|
|
183
|
+
type: 'getLicenses'
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async restartKernelAndRunCells() {
|
|
188
|
+
return this.sendToIFrameAndReceive({
|
|
189
|
+
type: 'restartKernelAndRerunCells'
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
activateInterceptor(activate: boolean) {
|
|
194
|
+
return this.sendToIFrameAndReceive({
|
|
195
|
+
type: 'activateInterceptor',
|
|
196
|
+
activate
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
sendInterceptorUpdate({ path, mime, state }: IInterceptorUpdate) {
|
|
201
|
+
return this.sendToIFrameAndReceive({
|
|
202
|
+
type: 'receiveInterceptorUpdate',
|
|
203
|
+
path,
|
|
204
|
+
mime,
|
|
205
|
+
state
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
onMessage(event: MessageEvent) {
|
|
210
|
+
if (event.source === window) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (event.source !== this._iframe?.contentWindow) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (event.origin !== new URL(this.props.jupyterurl).origin) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const data = event.data as IJupyterToFailsMessage;
|
|
220
|
+
if (event.data.requestId) {
|
|
221
|
+
const requestId = event.data.requestId;
|
|
222
|
+
if (this._requests.has(requestId)) {
|
|
223
|
+
const request = this._requests.get(requestId);
|
|
224
|
+
this._requests.delete(requestId);
|
|
225
|
+
if (event.data.error) {
|
|
226
|
+
request.reject(new Error(event.data.error));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
request.resolve(event.data);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
switch (data?.task) {
|
|
234
|
+
case 'appLoaded':
|
|
235
|
+
this.setState({ appLoading: false });
|
|
236
|
+
this.loadJupyter();
|
|
237
|
+
break;
|
|
238
|
+
case 'docDirty':
|
|
239
|
+
{
|
|
240
|
+
const { dirty = undefined } = data as IDocDirty;
|
|
241
|
+
if (this.props.stateCallback && typeof dirty !== 'undefined') {
|
|
242
|
+
this.props.stateCallback({ dirty });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
case 'reportMetadata':
|
|
247
|
+
{
|
|
248
|
+
const { failsApp = undefined, kernelspec = undefined } =
|
|
249
|
+
data?.metadata ?? {};
|
|
250
|
+
if (
|
|
251
|
+
this.props.stateCallback &&
|
|
252
|
+
(typeof failsApp !== 'undefined' ||
|
|
253
|
+
typeof kernelspec !== 'undefined')
|
|
254
|
+
) {
|
|
255
|
+
this.props.stateCallback({ failsApp, kernelspec });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
case 'reportFailsAppletSizes':
|
|
260
|
+
{
|
|
261
|
+
const { appletSizes = undefined } = data as IReportFailsAppletSizes;
|
|
262
|
+
if (typeof appletSizes !== 'undefined') {
|
|
263
|
+
this.setState(state => {
|
|
264
|
+
const retState: {
|
|
265
|
+
appletSizes?: {
|
|
266
|
+
[key: string]: IFailsAppletSize;
|
|
267
|
+
};
|
|
268
|
+
} = {};
|
|
269
|
+
for (const appletSize of Object.values(appletSizes)) {
|
|
270
|
+
const { appid, height, width } = appletSize;
|
|
271
|
+
if (state?.appletSizes?.[appid]) {
|
|
272
|
+
const oldsize = state.appletSizes[appid];
|
|
273
|
+
if (oldsize.height === height && oldsize.width === width) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!retState.appletSizes) {
|
|
278
|
+
retState.appletSizes = {};
|
|
279
|
+
}
|
|
280
|
+
retState.appletSizes[appid] = { width, height, appid };
|
|
281
|
+
this.props?.appletSizeChanged?.(appid, width, height);
|
|
282
|
+
}
|
|
283
|
+
return retState;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
case 'reportKernelStatus':
|
|
289
|
+
this.props?.kernelStatusCallback?.(data.status);
|
|
290
|
+
break;
|
|
291
|
+
case 'sendInterceptorUpdate':
|
|
292
|
+
{
|
|
293
|
+
const { path, mime, state } = data;
|
|
294
|
+
this.props?.receiveInterceptorUpdate?.({ path, mime, state });
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
default:
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
sendToIFrame(message: IFailsToJupyterMessage) {
|
|
302
|
+
if (this._iframe) {
|
|
303
|
+
this._iframe.contentWindow?.postMessage(message, this.props.jupyterurl);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async sendToIFrameAndReceive(message: IFailsToJupyterMessage): Promise<any> {
|
|
308
|
+
const requestId = this._requestId++;
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
this._requests.set(requestId, {
|
|
311
|
+
requestId,
|
|
312
|
+
resolve,
|
|
313
|
+
reject
|
|
314
|
+
});
|
|
315
|
+
this.sendToIFrame({
|
|
316
|
+
// @ts-expect-error requestId is not included in type
|
|
317
|
+
requestId,
|
|
318
|
+
...message
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
render() {
|
|
324
|
+
// launch debugging in the following way:
|
|
325
|
+
// jupyter lab --allow-root --ServerApp.allow_origin='*' --ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors self *'}}" --ServerApp.allow_websocket_origin='*' --ServerApp.cookie_options="{'samesite': 'None', 'secure': True}"
|
|
326
|
+
// jupyter lab --allow-root --ServerApp.allow_origin='*' --ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors self *'}}" --ServerApp.allow_websocket_origin='*' --ServerApp.cookie_options="{'samesite': 'None', 'secure': True}" --LabServerApp.app_settings_dir=/workspaces/jupyterfails/development/config/app-edit
|
|
327
|
+
// do it only in a container!
|
|
328
|
+
|
|
329
|
+
if (!this.props.editActivated) {
|
|
330
|
+
return <Fragment>JupyterEdit is not activated</Fragment>;
|
|
331
|
+
}
|
|
332
|
+
let width = '100%';
|
|
333
|
+
let height = '99%';
|
|
334
|
+
|
|
335
|
+
if (this.props.appid) {
|
|
336
|
+
const appletSize =
|
|
337
|
+
this.state.appletSizes && this.state.appletSizes[this.props.appid];
|
|
338
|
+
if (appletSize) {
|
|
339
|
+
width = Math.ceil(appletSize.width * 1.01) + 'px';
|
|
340
|
+
height = Math.ceil(appletSize.height * 1.01) + 'px';
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
let className = 'jpt-edit-iframe';
|
|
344
|
+
if (this.props.pointerOff) {
|
|
345
|
+
className += ' jpyt-edit-iframe-pointeroff';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<Fragment>
|
|
350
|
+
<iframe
|
|
351
|
+
style={{ width, height }}
|
|
352
|
+
className={className}
|
|
353
|
+
src={this.props.jupyterurl}
|
|
354
|
+
ref={el => {
|
|
355
|
+
this._iframe = el;
|
|
356
|
+
}}
|
|
357
|
+
onLoad={() => {
|
|
358
|
+
console.log('Jupyter iframe loaded');
|
|
359
|
+
}}
|
|
360
|
+
allow=""
|
|
361
|
+
// @ts-expect-error credentialless
|
|
362
|
+
credentialless="true"
|
|
363
|
+
sandbox="allow-scripts allow-downloads allow-same-origin allow-popups" // we need allow-forms for a local jupyter server, remove for jupyterlite
|
|
364
|
+
title="jupyteredit"
|
|
365
|
+
></iframe>
|
|
366
|
+
{this.state.appLoading && (
|
|
367
|
+
<h2
|
|
368
|
+
style={{
|
|
369
|
+
position: 'absolute',
|
|
370
|
+
top: '50%',
|
|
371
|
+
left: '50%',
|
|
372
|
+
transform: 'translate(-50%, -50%)'
|
|
373
|
+
}}
|
|
374
|
+
>
|
|
375
|
+
Jupyter is loading, be patient...
|
|
376
|
+
</h2>
|
|
377
|
+
)}
|
|
378
|
+
</Fragment>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private _iframe: HTMLIFrameElement | null = null;
|
|
383
|
+
private _requestId: number = 1; // id, if we request something
|
|
384
|
+
private _requests = new Map();
|
|
385
|
+
}
|