diff --git a/package-lock.json b/package-lock.json index abec60f..5f9b0d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@fails-components/config": "^1.5.4", "@fails-components/data": "^1.4.1", + "@fails-components/jupyter-react-edit": "file:../jupyterfails/packages/jupyterreactedit", "axios": "^1.7.4", "dexie": "^3.2.2", "image-blob-reduce": "^3.0.1", @@ -45,6 +46,36 @@ "vite-plugin-top-level-await": "^1.3.1" } }, + "../jupyterfails/packages/jupyterreactedit": { + "version": "0.0.1", + "license": "BSD-3-Clause", + "devDependencies": { + "@fails-components/jupyter-launcher": "workspace:^", + "@types/jest": "^29.2.0", + "@types/json-schema": "^7.0.11", + "@types/react": "^18.0.26", + "@types/react-addons-linked-state-mixin": "^0.14.22", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.2.0", + "npm-run-all": "^4.1.5", + "prettier": "^3.0.0", + "rimraf": "^5.0.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^15.10.1", + "stylelint-config-recommended": "^13.0.0", + "stylelint-config-standard": "^34.0.0", + "stylelint-csstree-validator": "^3.0.0", + "stylelint-prettier": "^4.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -2507,6 +2538,10 @@ "node": ">=16" } }, + "node_modules/@fails-components/jupyter-react-edit": { + "resolved": "../jupyterfails/packages/jupyterreactedit", + "link": true + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", @@ -9176,6 +9211,35 @@ "color": "^3.1.3" } }, + "@fails-components/jupyter-react-edit": { + "version": "file:../jupyterfails/packages/jupyterreactedit", + "requires": { + "@fails-components/jupyter-launcher": "workspace:^", + "@types/jest": "^29.2.0", + "@types/json-schema": "^7.0.11", + "@types/react": "^18.0.26", + "@types/react-addons-linked-state-mixin": "^0.14.22", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", + "css-loader": "^6.7.1", + "eslint": "^8.36.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.2.0", + "npm-run-all": "^4.1.5", + "prettier": "^3.0.0", + "rimraf": "^5.0.1", + "source-map-loader": "^1.0.2", + "style-loader": "^3.3.1", + "stylelint": "^15.10.1", + "stylelint-config-recommended": "^13.0.0", + "stylelint-config-standard": "^34.0.0", + "stylelint-csstree-validator": "^3.0.0", + "stylelint-prettier": "^4.0.0", + "typescript": "~5.0.2", + "yjs": "^13.5.0" + } + }, "@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", diff --git a/package.json b/package.json index 3418173..b52c790 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@fails-components/app", "version": "1.2.13", - "description": "Fails app ", + "description": "Fails app", "author": "Marten Richter", "license": "AGPL-3.0-or-later", "repository": { @@ -12,6 +12,7 @@ "dependencies": { "@fails-components/config": "^1.5.4", "@fails-components/data": "^1.4.1", + "@fails-components/jupyter-react-edit": "file:../jupyterfails/packages/jupyterreactedit", "axios": "^1.7.4", "dexie": "^3.2.2", "image-blob-reduce": "^3.0.1", diff --git a/src/app.jsx b/src/app.jsx index dce31ae..2091701 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -52,7 +52,7 @@ import failsLogoLongExp from './logo/logo1exp.svg' import Dexie from 'dexie' import JSZip from 'jszip' import QrScanner from 'qr-scanner' -import { JupyterEdit } from './jupyteredit.jsx' +import { JupyterEdit } from '@fails-components/jupyter-react-edit' QrScanner.WORKER_PATH = new URL( '../node_modules/qr-scanner/qr-scanner-worker.min.js', diff --git a/src/jupyteredit.css b/src/jupyteredit.css deleted file mode 100644 index 6ce154d..0000000 --- a/src/jupyteredit.css +++ /dev/null @@ -1,7 +0,0 @@ -.jpt-edit-iframe { - box-sizing: content-box; -} - -.jpyt-edit-iframe-pointeroff { - pointer-events: none; -} \ No newline at end of file diff --git a/src/jupyteredit.jsx b/src/jupyteredit.jsx deleted file mode 100644 index 52b1766..0000000 --- a/src/jupyteredit.jsx +++ /dev/null @@ -1,288 +0,0 @@ -/* - Fails Components (Fancy Automated Internet Lecture System - Components) - Copyright (C) 2015-2017 (original FAILS), - 2021- (FAILS Components) Marten Richter - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -import React, { Component, Fragment } from 'react' -import './jupyteredit.css' - -export class JupyterEdit extends Component { - constructor(props) { - super(props) - this.state = { dirty: false, appLoading: true } - this.onMessage = this.onMessage.bind(this) - if (props.stateCallback) props.stateCallback({ dirty: false }) - this._requestId = 1 // id, if we request something - this._requests = new Map() - } - - componentDidMount() { - if (!this.props.jupyterurl) throw new Error('No jupyter url passed') - window.addEventListener('message', this.onMessage) - if (this.props.receiveInterceptorUpdate) { - this.activateInterceptor(true) - } - } - - componentDidUpdate(prevProps) { - if (!this.props.jupyterurl) throw new Error('No jupyter url passed') - if (this.props.appid !== prevProps.appid) { - this.activateApp() - } - if ( - this.props.receiveInterceptorUpdate !== prevProps.receiveInterceptorUpdate - ) { - this.activateInterceptor(!!this.props.receiveInterceptorUpdate) - } - } - - componentWillUnmount() { - window.removeEventListener('message', this.onMessage) - } - - loadJupyter() { - const data = this.props.document - if (data?.metadata?.kernelspec) { - const kernelspec = data?.metadata?.kernelspec - if (kernelspec?.name !== 'python' && kernelspec?.name !== 'xpython') { - // replace the kernel - kernelspec.name = 'python' - kernelspec.display_name = 'Python (Pyodide)' - kernelspec.language = 'python' - kernelspec.name = 'python' - } - } - this.sendToIFrame({ - type: 'loadJupyter', - inLecture: !!this.props.appid, - rerunAtStartup: !!this.props.rerunAtStartup, - installScreenShotPatches: !!this.props.installScreenShotPatches, - appid: this.props.appid, - fileName: this.props.filename || 'example.ipynb', - fileData: data, - kernelName: data?.metadata?.kernelspec ?? 'python' - }) - } - - async saveJupyter() { - const fileToSaveObj = await this.sendToIFrameAndReceive({ - type: 'saveJupyter', - fileName: this.props.filename || 'example.ipynb' - }) - if (!fileToSaveObj.fileData) throw new Error('Empty saveJupyter response') - return fileToSaveObj.fileData - } - - async screenShot({ dpi }) { - const { screenshot } = await this.sendToIFrameAndReceive({ - type: 'screenshotApp', - dpi - }) - return screenshot - } - - activateApp() { - const appid = this.props.appid - return this.sendToIFrameAndReceive({ - type: 'activateApp', - inLecture: !!appid, - appid - }) - } - - async getLicenses() { - return this.sendToIFrameAndReceive({ - type: 'getLicenses' - }) - } - - async restartKernelAndRunCells() { - return this.sendToIFrameAndReceive({ - type: 'restartKernelAndRerunCells' - }) - } - - activateInterceptor(activate) { - return this.sendToIFrameAndReceive({ - type: 'activateInterceptor', - activate - }) - } - - sendInterceptorUpdate({ path, mime, state }) { - return this.sendToIFrameAndReceive({ - type: 'receiveInterceptorUpdate', - path, - mime, - state - }) - } - - onMessage(event) { - if (event.source === window) return - if (event.source !== this.iframe.contentWindow) return - if (event.origin !== new URL(this.props.jupyterurl).origin) return - const data = event.data - if (event.data.requestId) { - const requestId = event.data.requestId - if (this._requests.has(requestId)) { - const request = this._requests.get(requestId) - this._requests.delete(requestId) - if (event.data.error) { - request.reject(new Error(event.data.error)) - return - } - request.resolve(event.data) - return - } - } - switch (data?.task) { - case 'appLoaded': - this.setState({ appLoading: false }) - this.loadJupyter() - break - case 'docDirty': - { - const { dirty = undefined } = data - if (this.props.stateCallback && typeof dirty !== 'undefined') { - this.props.stateCallback({ dirty }) - } - } - break - case 'reportMetadata': - { - const { failsApp = undefined, kernelspec = undefined } = - data?.metadata - if ( - this.props.stateCallback && - (typeof failsApp !== 'undefined' || - typeof kernelspec !== 'undefined') - ) { - this.props.stateCallback({ failsApp, kernelspec }) - } - } - break - case 'reportFailsAppletSizes': - { - const { appletSizes = undefined } = data - if (typeof appletSizes !== 'undefined') { - this.setState((state) => { - const retState = {} - for (const appletSize of Object.values(appletSizes)) { - const { appid, height, width } = appletSize - if (state?.appletSizes?.[appid]) { - const oldsize = state.appletSizes[appid] - if (oldsize.height === height && oldsize.width === width) - continue - } - if (!retState.appletSizes) retState.appletSizes = {} - retState.appletSizes[appid] = { width, height } - this.props?.appletSizeChanged?.(appid, width, height) - } - return retState - }) - } - } - break - case 'reportKernelStatus': - this.props?.kernelStatusCallback?.(data.status) - break - case 'sendInterceptorUpdate': - { - const { path, mime, state } = data - this.props?.receiveInterceptorUpdate?.({ path, mime, state }) - } - break - default: - } - } - - sendToIFrame(message) { - if (this.iframe) - this.iframe.contentWindow.postMessage(message, this.props.jupyterurl) - } - - async sendToIFrameAndReceive(message) { - const requestId = this._requestId++ - return new Promise((resolve, reject) => { - this._requests.set(requestId, { - requestId, - resolve, - reject - }) - this.sendToIFrame({ - requestId, - ...message - }) - }) - } - - render() { - // launch debugging in the following way: - // 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}" - // 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 - // do it only in a container! - - if (!this.props.editActivated) { - return JupyterEdit is not activated - } - let width = '100%' - let height = '99%' - - if (this.props.appid) { - const appletSize = - this.state.appletSizes && this.state.appletSizes[this.props.appid] - if (appletSize) { - width = Math.ceil(appletSize.width * 1.01) + 'px' - height = Math.ceil(appletSize.height * 1.01) + 'px' - } - } - let className = 'jpt-edit-iframe' - if (this.props.pointerOff) className += ' jpyt-edit-iframe-pointeroff' - - return ( - - - {this.state.appLoading && ( -

- Jupyter is loading, be patient... -

- )} -
- ) - } -}