From 8e5a0c1d6a9ef72501ba7a4040b49798a09b96f5 Mon Sep 17 00:00:00 2001 From: Tim Metzler Date: Fri, 1 Sep 2023 16:10:18 +0200 Subject: [PATCH 01/23] Add config app for diagram editor --- .../apps/diagram_editor/__init__.py | 3 ++ .../apps/diagram_editor/diagrameditor.py | 52 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 e2xgrader/server_extensions/apps/diagram_editor/__init__.py create mode 100644 e2xgrader/server_extensions/apps/diagram_editor/diagrameditor.py diff --git a/e2xgrader/server_extensions/apps/diagram_editor/__init__.py b/e2xgrader/server_extensions/apps/diagram_editor/__init__.py new file mode 100644 index 00000000..b9d39bd4 --- /dev/null +++ b/e2xgrader/server_extensions/apps/diagram_editor/__init__.py @@ -0,0 +1,3 @@ +from .diagrameditor import DiagramEditor + +__all__ = ["DiagramEditor"] diff --git a/e2xgrader/server_extensions/apps/diagram_editor/diagrameditor.py b/e2xgrader/server_extensions/apps/diagram_editor/diagrameditor.py new file mode 100644 index 00000000..38d18f6e --- /dev/null +++ b/e2xgrader/server_extensions/apps/diagram_editor/diagrameditor.py @@ -0,0 +1,52 @@ +import json + +from e2xcore import BaseApp +from e2xcore.handlers import E2xApiHandler +from nbgrader.apps.baseapp import NbGrader +from nbgrader.server_extensions.formgrader.base import check_xsrf +from tornado import web +from traitlets import List, Unicode + + +class DiagramConfigHandler(E2xApiHandler): + @web.authenticated + @check_xsrf + def get(self): + self.finish(json.dumps(self.settings.get("diagram_config", dict()))) + + +class DiagramEditor(NbGrader, BaseApp): + drawDomain = Unicode( + default_value=None, allow_none=True, help="The url to drawio" + ).tag(config=True) + drawOrigin = Unicode( + default_value=None, allow_none=True, help="The drawio origin" + ).tag(config=True) + libraries = List(default_value=[], help="A list of activated libraries").tag( + config=True + ) + + def __init__(self, **kwargs): + NbGrader.__init__(self, **kwargs) + BaseApp.__init__(self, **kwargs) + + def get_diagram_config(self): + # self.log.info("My drawDomain is", self.drawDomain) + config = dict() + if self.drawDomain: + config["drawDomain"] = self.drawDomain + if self.drawOrigin: + config["drawOrigin"] = self.drawOrigin + if len(self.libraries) > 0: + config["libs"] = self.libraries + return config + + def load_app(self): + self.log.info("Loading the diagrameditor app") + self.initialize([]) + self.update_tornado_settings(dict(diagram_config=self.get_diagram_config())) + self.add_handlers( + [ + (r"/e2x/diagrams/api", DiagramConfigHandler), + ] + ) From 29a35d636151247b6b535ff4445d84446e58cb9c Mon Sep 17 00:00:00 2001 From: Tim Metzler Date: Fri, 1 Sep 2023 16:10:36 +0200 Subject: [PATCH 02/23] Use config app for diagram editor in all modes --- e2xgrader/server_extensions/student/student.py | 3 ++- e2xgrader/server_extensions/teacher/teacher.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/e2xgrader/server_extensions/student/student.py b/e2xgrader/server_extensions/student/student.py index dbf8644b..c404d627 100644 --- a/e2xgrader/server_extensions/student/student.py +++ b/e2xgrader/server_extensions/student/student.py @@ -1,6 +1,7 @@ from traitlets import Any, List from ..apps.assignment_list import AssignmentList +from ..apps.diagram_editor import DiagramEditor from ..apps.help import Help from ..apps.validate_assignment import ValidateAssignment from ..base import BaseExtension @@ -9,7 +10,7 @@ class StudentExtension(BaseExtension): apps = List( trait=Any(), - default_value=[AssignmentList, ValidateAssignment, Help], + default_value=[AssignmentList, ValidateAssignment, Help, DiagramEditor], ).tag(config=True) diff --git a/e2xgrader/server_extensions/teacher/teacher.py b/e2xgrader/server_extensions/teacher/teacher.py index 50dceae0..7b608b42 100644 --- a/e2xgrader/server_extensions/teacher/teacher.py +++ b/e2xgrader/server_extensions/teacher/teacher.py @@ -2,6 +2,7 @@ from traitlets import Any, List from ..apps.assignment_list import AssignmentList +from ..apps.diagram_editor import DiagramEditor from ..apps.e2xgraderapi import E2xGraderApi from ..apps.formgrader import FormgradeApp from ..apps.help import Help @@ -21,6 +22,7 @@ class TeacherExtension(BaseExtension): ValidateAssignment, AssignmentList, Help, + DiagramEditor, ], ).tag(config=True) From d28cc80ac4b7f4f17cd5e74e3facb1a36a948dce Mon Sep 17 00:00:00 2001 From: Tim Metzler Date: Fri, 1 Sep 2023 16:11:09 +0200 Subject: [PATCH 03/23] Refactor diagram editor and load config from app --- packages/cells/src/utils/diagram-editor.js | 657 ++++++++++----------- 1 file changed, 299 insertions(+), 358 deletions(-) diff --git a/packages/cells/src/utils/diagram-editor.js b/packages/cells/src/utils/diagram-editor.js index e9b64379..9e1bc559 100644 --- a/packages/cells/src/utils/diagram-editor.js +++ b/packages/cells/src/utils/diagram-editor.js @@ -1,410 +1,351 @@ -import $ from "jquery"; - -/** - * Copyright (c) 2006-2020, JGraph Ltd - * Copyright (c) 2006-2020, Gaudenz Alder - * - * Usage: DiagramEditor.editElement(elt) where elt is an img or object with - * a data URI src or data attribute or an svg node with a content attribute. - * - * See https://jgraph.github.io/drawio-integration/javascript.html - */ -export function DiagramEditor(config, ui, done, initialized, urlParams, cell) { - this.config = config != null ? config : this.config; - this.ui = ui != null ? ui : this.ui; - this.done = done != null ? done : this.done; - this.initialized = initialized != null ? initialized : this.initialized; - this.urlParams = urlParams; - this.cell = cell; - - var self = this; - - this.handleMessageEvent = function (evt) { - if (evt.origin !== "https://embed.diagrams.net") { +import { urlJoin, requests } from "@e2xgrader/api"; +import Jupyter from "base/js/namespace"; + +let baseOptionsLoaded = false; +let baseOptions = {}; + +async function fetchBaseOptions() { + if (!baseOptionsLoaded) { + try { + baseOptions = await requests.get( + urlJoin(Jupyter.notebook.base_url, "e2x", "diagrams", "api") + ); + baseOptionsLoaded = true; + } catch (error) { + console.error("Error fetching DiagramEditor base options:", error); + baseOptionsLoaded = true; + } + } +} + +class DiagramEditor { + constructor(config, ui, done, initialized, urlParams, cell, options = {}) { + const mergedOptions = { ...baseOptions, ...options }; + this.config = config || this.config; + this.ui = ui || this.ui; + this.done = done || function () {}; + this.initialized = initialized || function () {}; + this.urlParams = urlParams; + this.cell = cell; + this.handleMessageEvent = this.handleMessageEvent.bind(this); + this.frame = null; + this.startElement = null; + this.format = "xml"; + this.xml = null; + this.drawDomain = mergedOptions.drawDomain || "https://embed.diagrams.net/"; + this.origin = mergedOptions.drawOrigin || "https://embed.diagrams.net/"; + this.frameStyle = + "position:absolute;bottom:0;border:0;width:100%;height:100%;"; + this.libs = mergedOptions.libs || []; + } + + handleMessageEvent(evt) { + const expectedOrigin = new URL(this.origin); + const actualOrigin = new URL(evt.origin); + if (actualOrigin.href !== expectedOrigin.href) { console.log( - "Message should come from https://embed.diagrams.net but came from " + - evt.origin + `Message should come from ${expectedOrigin.href} but came from ${actualOrigin.href}` ); } if ( - self.frame != null && - evt.source == self.frame.contentWindow && + this.frame != null && + evt.source == this.frame.contentWindow && evt.data.length > 0 ) { try { - var msg = JSON.parse(evt.data); + const msg = JSON.parse(evt.data); if (msg != null) { - self.handleMessage(msg); + this.handleMessage(msg); } } catch (e) { console.error(e); } } - }; -} + } -/** - * Static method to edit the diagram in the given img or object. - */ -DiagramEditor.editElement = function (cell, elt, config, ui, done, urlParams) { - if (!elt.diagramEditorStarting) { - elt.diagramEditorStarting = true; - - return new DiagramEditor( - config, - ui, - done, - function () { - delete elt.diagramEditorStarting; - }, - urlParams, - cell - ).editElement(elt); + static editElement(cell, elt, config, ui, done, urlParams, options = {}) { + if (!elt.diagramEditorStarting) { + elt.diagramEditorStarting = true; + + return new DiagramEditor( + config, + ui, + done, + function () { + delete elt.diagramEditorStarting; + }, + urlParams, + cell, + options + ).editElement(elt); + } } -}; - -/** - * Global configuration. - */ -DiagramEditor.prototype.config = null; - -/** - * Protocol and domain to use. - */ -DiagramEditor.prototype.drawDomain = "https://embed.diagrams.net/"; - -/** - * UI theme to be use. - */ -DiagramEditor.prototype.ui = "min"; - -/** - * Contains XML for pending image export. - */ -DiagramEditor.prototype.xml = null; - -/** - * Format to use. - */ -DiagramEditor.prototype.format = "xml"; - -/** - * Specifies if libraries should be enabled. - */ -DiagramEditor.prototype.libraries = true; - -/** - * CSS style for the iframe. - */ -DiagramEditor.prototype.frameStyle = - "position:absolute;bottom:0;border:0;width:100%;height:100%;"; - -/** - * Adds the iframe and starts editing. - */ -DiagramEditor.prototype.editElement = function (elem) { - var src = this.getElementData(elem); - this.startElement = elem; - var fmt = this.format; - - if (src.substring(0, 15) === "data:image/png;") { - fmt = "xmlpng"; - } else if ( - src.substring(0, 19) === "data:image/svg+xml;" || - elem.nodeName.toLowerCase() == "svg" + + static async editDiagram( + cell, + elt, + config, + ui, + done, + urlParams, + options = {} ) { - fmt = "xmlsvg"; + fetchBaseOptions().then(() => { + return this.editElement(cell, elt, config, ui, done, urlParams, options); + }); } - this.startEditing(src, fmt); - - return this; -}; + config = null; + ui = "min"; + xml = null; + format = "xml"; + libraries = true; + frameStyle = "position:absolute;bottom:0;border:0;width:100%;height:100%;"; + + editElement(elem) { + const src = this.getElementData(elem); + this.startElement = elem; + let fmt = this.format; + + if (src.substring(0, 15) === "data:image/png;") { + fmt = "xmlpng"; + } else if ( + src.substring(0, 19) === "data:image/svg+xml;" || + elem.nodeName.toLowerCase() == "svg" + ) { + fmt = "xmlsvg"; + } -/** - * Adds the iframe and starts editing. - */ -DiagramEditor.prototype.getElementData = function (elem) { - var name = elem.nodeName.toLowerCase(); + this.startEditing(src, fmt); - let attribute = ""; - if (name == "svg") { - attribute = "content"; - } else if (name == "img") { - attribute = "src"; - } else { - attribute = "data"; + return this; } - return elem.getAttribute(attribute); -}; + getElementData(elem) { + const name = elem.nodeName.toLowerCase(); -/** - * Adds the iframe and starts editing. - */ -DiagramEditor.prototype.setElementData = function (elem, data) { - var name = elem.nodeName.toLowerCase(); + let attribute = ""; + if (name == "svg") { + attribute = "content"; + } else if (name == "img") { + attribute = "src"; + } else { + attribute = "data"; + } - if (name == "svg") { - elem.outerHTML = atob(data.substring(data.indexOf(",") + 1)); - } else { - elem.setAttribute(name == "img" ? "src" : "data", data); + return elem.getAttribute(attribute); } - return elem; -}; - -/** - * Starts the editor for the given data. - */ -DiagramEditor.prototype.startEditing = function (data, format, title) { - if (this.frame == null) { - window.addEventListener("message", this.handleMessageEvent); - this.format = format != null ? format : this.format; - this.title = title != null ? title : this.title; - this.data = data; - - this.frame = this.createFrame(this.getFrameUrl(), this.getFrameStyle()); - document.body.appendChild(this.frame); - this.setWaiting(true); + setElementData(elem, data) { + const name = elem.nodeName.toLowerCase(); + + if (name == "svg") { + elem.outerHTML = atob(data.substring(data.indexOf(",") + 1)); + } else { + elem.setAttribute(name == "img" ? "src" : "data", data); + } + + return elem; } -}; - -/** - * Updates the waiting cursor. - */ -DiagramEditor.prototype.setWaiting = function (waiting) { - if (this.startElement != null) { - // Redirect cursor to parent for SVG and object - var elt = this.startElement; - var name = elt.nodeName.toLowerCase(); - - if (name == "svg" || name == "object") { - elt = elt.parentNode; + + startEditing(data, format, title) { + if (this.frame == null) { + window.addEventListener("message", this.handleMessageEvent); + this.format = format || this.format; + this.title = title || this.title; + this.data = data; + + this.frame = this.createFrame(this.getFrameUrl(), this.getFrameStyle()); + document.body.appendChild(this.frame); + this.setWaiting(true); } + } - if (elt != null) { - if (waiting) { - this.frame.style.pointerEvents = "none"; - this.previousCursor = elt.style.cursor; - elt.style.cursor = "wait"; - } else { - elt.style.cursor = this.previousCursor; - this.frame.style.pointerEvents = ""; + setWaiting(waiting) { + if (this.startElement != null) { + let elt = this.startElement; + const name = elt.nodeName.toLowerCase(); + + if (name == "svg" || name == "object") { + elt = elt.parentNode; + } + + if (elt != null) { + if (waiting) { + this.frame.style.pointerEvents = "none"; + this.previousCursor = elt.style.cursor; + elt.style.cursor = "wait"; + } else { + elt.style.cursor = this.previousCursor; + this.frame.style.pointerEvents = ""; + } } } } -}; - -/** - * Updates the waiting cursor. - */ -DiagramEditor.prototype.setActive = function (active) { - if (active) { - this.previousOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = this.previousOverflow; - } -}; - -/** - * Removes the iframe. - */ -DiagramEditor.prototype.stopEditing = function () { - if (this.frame != null) { - window.removeEventListener("message", this.handleMessageEvent); - document.body.removeChild(this.frame); - this.setActive(false); - this.frame = null; + + setActive(active) { + if (active) { + this.previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = this.previousOverflow; + } } -}; - -/** - * Send the given message to the iframe. - */ -DiagramEditor.prototype.postMessage = function (msg) { - if (this.frame != null) { - this.frame.contentWindow.postMessage( - JSON.stringify(msg), - "https://embed.diagrams.net" - ); + + stopEditing() { + if (this.frame != null) { + window.removeEventListener("message", this.handleMessageEvent); + document.body.removeChild(this.frame); + this.setActive(false); + this.frame = null; + } } -}; - -/** - * Returns the diagram data. - */ -DiagramEditor.prototype.getData = function () { - return this.data; -}; - -/** - * Returns the title for the editor. - */ -DiagramEditor.prototype.getTitle = function () { - return this.title; -}; - -/** - * Returns the CSS style for the iframe. - */ -DiagramEditor.prototype.getFrameStyle = function () { - return ( - this.frameStyle + - ";left:" + - document.body.scrollLeft + - "px;top:" + - document.body.scrollTop + - $("#header").height() + - "px;" - ); -}; - -/** - * Returns the URL for the iframe. - */ -DiagramEditor.prototype.getFrameUrl = function () { - var url = this.drawDomain + "?proto=json&spin=1"; - - if (this.ui != null) { - url += "&ui=" + this.ui; + + postMessage(msg) { + if (this.frame != null) { + this.frame.contentWindow.postMessage( + JSON.stringify(msg), + "https://embed.diagrams.net" + ); + } } - if (this.libraries != null) { - url += "&libraries=1"; + getData() { + return this.data; } - if (this.config != null) { - url += "&configure=1"; + getTitle() { + return this.title; } - if (this.urlParams != null) { - url += "&" + this.urlParams.join("&"); + getFrameStyle() { + const header = document.getElementById("header"); + return ( + this.frameStyle + + ";left:" + + document.body.scrollLeft + + "px;top:" + + document.body.scrollTop + + header.offsetHeight + + "px;" + ); } - return url; -}; - -/** - * Creates the iframe. - */ -DiagramEditor.prototype.createFrame = function (url, style) { - var frame = document.createElement("iframe"); - frame.setAttribute("frameborder", "0"); - frame.setAttribute("style", style); - frame.setAttribute("src", url); - - return frame; -}; - -/** - * Sets the status of the editor. - */ -DiagramEditor.prototype.setStatus = function (messageKey, modified) { - this.postMessage({ - action: "status", - messageKey: messageKey, - modified: modified, - }); -}; - -/** - * Handles the given message. - */ -DiagramEditor.prototype.handleMessage = function (msg) { - if (msg.event == "configure") { - this.configureEditor(); - } else if (msg.event == "init") { - this.initializeEditor(); - } else if (msg.event == "autosave") { - this.save(msg.xml, true, this.startElement); - } else if (msg.event == "export") { - this.cell.model.setAttachment("diagram.png", msg.data); - this.setElementData(this.startElement, msg.data); - this.stopEditing(); - this.xml = null; - } else if (msg.event == "save") { - this.save(msg.xml, false, this.startElement); - this.xml = msg.xml; + getFrameUrl() { + let url = this.drawDomain + "?proto=json&spin=1"; - if (msg.exit) { - msg.event = "exit"; - } else { - this.setStatus("allChangesSaved", false); + if (this.ui != null) { + url += "&ui=" + this.ui; + } + + if (this.libraries != null) { + url += "&libraries=1"; + } + + if (this.config != null) { + url += "&configure=1"; + } + + if (this.libs.length > 0) { + url += "&libs=" + this.libs.join(";"); } + + if (this.urlParams != null) { + url += "&" + this.urlParams.join("&"); + } + + console.log("The url", url); + + return url; + } + + createFrame(url, style) { + const frame = document.createElement("iframe"); + frame.setAttribute("frameborder", "0"); + frame.setAttribute("style", style); + frame.setAttribute("src", url); + + return frame; } - if (msg.event == "exit") { - this.handleExitMessage(msg); + setStatus(messageKey, modified) { + this.postMessage({ + action: "status", + messageKey: messageKey, + modified: modified, + }); } -}; - -/** - * Handles the exit message. - */ -DiagramEditor.prototype.handleExitMessage = function (msg) { - if (this.format != "xml") { - if (this.xml != null) { - this.postMessage({ - action: "export", - format: this.format, - xml: this.xml, - spinKey: "export", - }); + + handleMessage(msg) { + if (msg.event == "configure") { + this.configureEditor(); + } else if (msg.event == "init") { + this.initializeEditor(); + } else if (msg.event == "autosave") { + this.save(msg.xml, true, this.startElement); + } else if (msg.event == "export") { + this.cell.model.setAttachment("diagram.png", msg.data); + this.setElementData(this.startElement, msg.data); + this.stopEditing(); + this.xml = null; + } else if (msg.event == "save") { + this.save(msg.xml, false, this.startElement); + this.xml = msg.xml; + + if (msg.exit) { + msg.event = "exit"; + } else { + this.setStatus("allChangesSaved", false); + } + } + + if (msg.event == "exit") { + this.handleExitMessage(msg); + } + } + + handleExitMessage(msg) { + if (this.format != "xml") { + if (this.xml != null) { + this.postMessage({ + action: "export", + format: this.format, + xml: this.xml, + spinKey: "export", + }); + } else { + this.stopEditing(msg); + } } else { + if (msg.modified == null || msg.modified) { + this.save(msg.xml, false, this.startElement); + } this.stopEditing(msg); } - } else { - if (msg.modified == null || msg.modified) { - this.save(msg.xml, false, this.startElement); - } - this.stopEditing(msg); } -}; - -/** - * Posts configure message to editor. - */ -DiagramEditor.prototype.configureEditor = function () { - this.postMessage({ action: "configure", config: this.config }); -}; - -/** - * Posts load message to editor. - */ -DiagramEditor.prototype.initializeEditor = function () { - this.postMessage({ - action: "load", - autosave: 1, - saveAndExit: "1", - modified: "unsavedChanges", - xml: this.getData(), - title: this.getTitle(), - }); - this.setWaiting(false); - this.setActive(true); - this.initialized(); -}; - -/** - * Saves the given data. - */ -DiagramEditor.prototype.save = function (data, draft, elt) { - this.done(data, draft, elt); -}; - -/** - * Invoked after save. - */ -DiagramEditor.prototype.done = function () { - // hook for subclassers -}; - -/** - * Invoked after the editor has sent the init message. - */ -DiagramEditor.prototype.initialized = function () { - // hook for subclassers -}; + + configureEditor() { + this.postMessage({ action: "configure", config: this.config }); + } + + initializeEditor() { + this.postMessage({ + action: "load", + autosave: 1, + saveAndExit: "1", + modified: "unsavedChanges", + xml: this.getData(), + title: this.getTitle(), + }); + this.setWaiting(false); + this.setActive(true); + this.initialized(); + } + + save(data, draft, elt) { + this.done(data, draft, elt); + } +} + +export default DiagramEditor; From bbe8f639d12752bf50cc130f54a1051e2c51feba Mon Sep 17 00:00:00 2001 From: Tim Metzler Date: Fri, 1 Sep 2023 16:11:49 +0200 Subject: [PATCH 04/23] Add e2xgrader/api dependency --- packages/cells/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cells/package.json b/packages/cells/package.json index 993dc100..e556fd80 100644 --- a/packages/cells/package.json +++ b/packages/cells/package.json @@ -9,6 +9,9 @@ }, "author": "Tim Metzler", "license": "ISC", + "dependencies": { + "@e2xgrader/api": "0.1.1" + }, "devDependencies": { "@babel/preset-env": "^7.16.11", "css-loader": "^6.6.0", From 9ca405a27bf044a60e473e90d0eccdc16e28ebc5 Mon Sep 17 00:00:00 2001 From: Tim Metzler Date: Fri, 1 Sep 2023 16:12:22 +0200 Subject: [PATCH 05/23] Use new diagram editor in diagram cell --- packages/cells/src/cells/diagram-cell.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/cells/src/cells/diagram-cell.js b/packages/cells/src/cells/diagram-cell.js index ecfb38f2..002b8b02 100644 --- a/packages/cells/src/cells/diagram-cell.js +++ b/packages/cells/src/cells/diagram-cell.js @@ -1,6 +1,6 @@ import { E2xCell } from "./base"; import { AttachmentModel } from "../utils/attachment-model"; -import { DiagramEditor } from "../utils/diagram-editor"; +import DiagramEditor from "../utils/diagram-editor"; class DiagramCellModel extends AttachmentModel { postSaveHook() { @@ -56,7 +56,14 @@ export class DiagramCell extends E2xCell { let button = $("