diff --git a/static/package-lock.json b/static/package-lock.json index d3d03f1..348a8e8 100644 --- a/static/package-lock.json +++ b/static/package-lock.json @@ -1,12 +1,12 @@ { "name": "schema-interface", - "version": "0.0.0", + "version": "0.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "schema-interface", - "version": "0.0.0", + "version": "0.5.1", "license": "ISC", "dependencies": { "@material-ui/core": "^4.10.1", @@ -14,6 +14,7 @@ "axios": "^0.25.0", "bootstrap": "^5.1.3", "cytoscape": "^3.20.0", + "cytoscape-context-menus": "^4.1.0", "cytoscape-klay": "^3.1.4", "fast-deep-equal": "^3.1.3", "jsoneditor": "^9.6.0", @@ -39,8 +40,6 @@ "sass": "^1.49.0", "sass-loader": "^12.4.0", "style-loader": "^3.3.1", - "svg-inline-loader": "^0.8.2", - "svg-url-loader": "^7.1.1", "webpack": "^5.67.0", "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.7.3" @@ -3531,6 +3530,14 @@ "node": ">=0.10" } }, + "node_modules/cytoscape-context-menus": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-context-menus/-/cytoscape-context-menus-4.1.0.tgz", + "integrity": "sha512-qbrdRfz8JQuZBZamhzmExPpJvUNR4VA3rcN4qChu4eVplWYU4n8N98A0ksrr2kfmUvCAL529N04KzwZd8xra7w==", + "peerDependencies": { + "cytoscape": "^2.7.0 || ^3.0.0" + } + }, "node_modules/cytoscape-klay": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/cytoscape-klay/-/cytoscape-klay-3.1.4.tgz", @@ -7948,12 +7955,6 @@ "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", "dev": true }, - "node_modules/simple-html-tokenizer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.1.1.tgz", - "integrity": "sha1-BcLuxXn//+FFoDCsJs/qYbmA+r4=", - "dev": true - }, "node_modules/slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", @@ -8315,47 +8316,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/svg-inline-loader": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/svg-inline-loader/-/svg-inline-loader-0.8.2.tgz", - "integrity": "sha512-kbrcEh5n5JkypaSC152eGfGcnT4lkR0eSfvefaUJkLqgGjRQJyKDvvEE/CCv5aTSdfXuc+N98w16iAojhShI3g==", - "dev": true, - "dependencies": { - "loader-utils": "^1.1.0", - "object-assign": "^4.0.1", - "simple-html-tokenizer": "^0.1.1" - } - }, - "node_modules/svg-url-loader": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/svg-url-loader/-/svg-url-loader-7.1.1.tgz", - "integrity": "sha512-NlsMCePODm7FQhU9aEZyGLPx5Xe1QRI1cSEUE6vTq5LJc9l9pStagvXoEIyZ9O3r00w6G3+Wbkimb+SC3DI/Aw==", - "dev": true, - "dependencies": { - "file-loader": "~6.2.0", - "loader-utils": "~2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/svg-url-loader/node_modules/loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -11967,6 +11927,12 @@ "lodash.debounce": "^4.0.8" } }, + "cytoscape-context-menus": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-context-menus/-/cytoscape-context-menus-4.1.0.tgz", + "integrity": "sha512-qbrdRfz8JQuZBZamhzmExPpJvUNR4VA3rcN4qChu4eVplWYU4n8N98A0ksrr2kfmUvCAL529N04KzwZd8xra7w==", + "requires": {} + }, "cytoscape-klay": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/cytoscape-klay/-/cytoscape-klay-3.1.4.tgz", @@ -15374,12 +15340,6 @@ "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", "dev": true }, - "simple-html-tokenizer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.1.1.tgz", - "integrity": "sha1-BcLuxXn//+FFoDCsJs/qYbmA+r4=", - "dev": true - }, "slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", @@ -15662,40 +15622,6 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, - "svg-inline-loader": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/svg-inline-loader/-/svg-inline-loader-0.8.2.tgz", - "integrity": "sha512-kbrcEh5n5JkypaSC152eGfGcnT4lkR0eSfvefaUJkLqgGjRQJyKDvvEE/CCv5aTSdfXuc+N98w16iAojhShI3g==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "object-assign": "^4.0.1", - "simple-html-tokenizer": "^0.1.1" - } - }, - "svg-url-loader": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/svg-url-loader/-/svg-url-loader-7.1.1.tgz", - "integrity": "sha512-NlsMCePODm7FQhU9aEZyGLPx5Xe1QRI1cSEUE6vTq5LJc9l9pStagvXoEIyZ9O3r00w6G3+Wbkimb+SC3DI/Aw==", - "dev": true, - "requires": { - "file-loader": "~6.2.0", - "loader-utils": "~2.0.0" - }, - "dependencies": { - "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - } - } - }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", diff --git a/static/package.json b/static/package.json index 741284c..65e165c 100644 --- a/static/package.json +++ b/static/package.json @@ -1,8 +1,8 @@ { "name": "schema-interface", - "version": "0.0.0", - "description": "", - "main": "index.js", + "version": "0.5.1", + "description": "Schema Curation Interface", + "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "webpack-dev-server --mode production", @@ -22,8 +22,6 @@ "sass": "^1.49.0", "sass-loader": "^12.4.0", "style-loader": "^3.3.1", - "svg-inline-loader": "^0.8.2", - "svg-url-loader": "^7.1.1", "webpack": "^5.67.0", "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.7.3" @@ -34,6 +32,7 @@ "axios": "^0.25.0", "bootstrap": "^5.1.3", "cytoscape": "^3.20.0", + "cytoscape-context-menus": "^4.1.0", "cytoscape-klay": "^3.1.4", "fast-deep-equal": "^3.1.3", "jsoneditor": "^9.6.0", diff --git a/static/src/public/cy-style.json b/static/src/public/cy-style.json index 17e836a..62e6dee 100644 --- a/static/src/public/cy-style.json +++ b/static/src/public/cy-style.json @@ -124,10 +124,9 @@ "layout": { "name": "klay", "nodeDimensionsIncludeLabels": false, - "fit": true, + "fit": false, "padding": 20, "animate": false, - "animationDuration": 500, "klay": { "addUnnecessaryBendpoints": false, "aspectRatio": 1.6, diff --git a/static/src/template/Canvas.jsx b/static/src/template/Canvas.jsx index a848c69..987697b 100644 --- a/static/src/template/Canvas.jsx +++ b/static/src/template/Canvas.jsx @@ -2,21 +2,21 @@ import React from 'react'; import CytoscapeComponent from 'react-cytoscapejs'; import cytoscape from 'cytoscape'; import klay from 'cytoscape-klay'; +import contextMenus from 'cytoscape-context-menus' // want to use https://github.com/iVis-at-Bilkent/cytoscape.js-expand-collapse import axios from 'axios'; import equal from 'fast-deep-equal'; import RefreshIcon from '@material-ui/icons/Refresh'; +import OpenIcon from '@material-ui/icons/OpenWith'; import Background from '../public/canvas_bg.png'; import CyStyle from '../public/cy-style.json'; +import 'cytoscape-context-menus/cytoscape-context-menus.css'; -// TODO: add right-click menu -// TODO: allow removing edges and nodes -// TODO: add uncollapse without complete reload -// add pan-zoom menu because it's hard to pinpoint the exact area of nodes +// TODO: add uncollapse / unselect without complete reload cytoscape.use(klay); -// cytoscape.use(contextMenus); +cytoscape.use(contextMenus); /* Graph view of the data. Includes reload button. */ @@ -45,6 +45,7 @@ class Canvas extends React.Component { this.reloadCanvas = this.reloadCanvas.bind(this); this.removeObject = this.removeObject.bind(this); this.restore = this.restore.bind(this); + this.fitCanvas = this.fitCanvas.bind(this); } showSidebar(data) { @@ -113,18 +114,23 @@ class Canvas extends React.Component { } } + fitCanvas(){ + this.cy.fit(); + } + componentDidMount() { this.cy.ready(() => { // left-click this.cy.on('tap', event => { var eventTarget = event.target; - // click background, reset canvas + //click background if (eventTarget === this.cy) { - this.reloadCanvas(); + // do nothing // click node, show subtree } else if (eventTarget.isNode()) { let node = eventTarget.data(); this.showSubTree(node); + this.cy.center(node); } }); @@ -140,115 +146,28 @@ class Canvas extends React.Component { }) // right-click menu - // var contextMenu = this.cy.contextMenus({ - // menuItems: [ - // { - // id: 'remove', - // content: 'remove', - // tooltipText: 'remove', - // selector: 'node, edge', - // onClickFunction: this.removeObject, - // hasTrailingDivider: true - // }, - // { - // id: 'undo-last-remove', - // content: 'undo last remove', - // selector: 'node, edge', - // disabled: this.state.removed ? true : false, - // show: true, - // coreAsWell: true, - // onClickFunction: this.restore, - // hasTrailingDivider: true - // }, - // // { - // // id: 'add-node', - // // content: 'add node', - // // tooltipText: 'add node', - // // coreAsWell: true, - // // onClickFunction: function (event) { - // // var data = { - // // group: 'nodes' - // // }; - - // // var pos = event.position || event.cyPosition; - - // // cy.add({ - // // data: data, - // // position: { - // // x: pos.x, - // // y: pos.y - // // } - // // }); - // // }, - // // }, - // // { - // // id: 'add-edge', - // // content: 'add edge', - // // tooltipText: 'add edge', - // // coreAsWell: true, - // // onClickFunction: function (event) { - // // var data = { - // // group: 'edges' - // // }; - - // // var pos = event.position || event.cyPosition; - - // // cy.add({ - // // data: data, - // // position: { - // // x: pos.x, - // // y: pos.y - // // } - // // }); - // // }, - // // hasTrailingDivider: true - // // }, - // { - // id: 'edit-node', - // content: 'edit node', - // tooltipText: 'edit node', - // selector: 'node', - // coreAsWell: true, - // onClickFunction: function (event) { - // var data = { - // group: 'nodes' - // }; - - // var pos = event.position || event.cyPosition; - - // cy.add({ - // data: data, - // position: { - // x: pos.x, - // y: pos.y - // } - // }); - // }, - // }, - // { - // id: 'edit-edge', - // content: 'edit edge', - // tooltipText: 'edit edge', - // selector: 'edge', - // coreAsWell: true, - // onClickFunction: function (event) { - // var data = { - // group: 'edges' - // }; - - // var pos = event.position || event.cyPosition; - - // cy.add({ - // data: data, - // position: { - // x: pos.x, - // y: pos.y - // } - // }); - // }, - // } - // ] - // }) + var contextMenu = this.cy.contextMenus({ + menuItems: [ + { + id: 'remove', + content: 'remove', + tooltipText: 'remove', + selector: 'node, edge', + onClickFunction: this.removeObject, + hasTrailingDivider: true + }, + { + id: 'undo-last-remove', + content: 'undo last remove', + selector: 'node, edge', + disabled: this.state.removed ? true : false, + show: true, + coreAsWell: true, + onClickFunction: this.restore, + hasTrailingDivider: true + } + ] + }) }) } @@ -274,10 +193,11 @@ class Canvas extends React.Component { style={style} stylesheet={CyStyle.stylesheet} cy={(cy) => { this.cy = cy }} - wheelSensitivity={0.5} maxZoom={2} minZoom={0.5} + maxZoom={3} minZoom={0.5} />
+
); diff --git a/static/src/template/JsonEdit.jsx b/static/src/template/JsonEdit.jsx index fecdf83..206e7d3 100644 --- a/static/src/template/JsonEdit.jsx +++ b/static/src/template/JsonEdit.jsx @@ -3,21 +3,32 @@ import React, {Component} from 'react'; import JSONEditor from 'jsoneditor'; import 'jsoneditor/dist/jsoneditor.min.css'; -// TODO: be able to add empty string - // currently reloads whenever a key is pressed, change to onBlur somehow export default class JSONEdit extends Component { + constructor(props){ + super(props); + + this.handleEvent = this.handleEvent.bind(this); + } + + // sends JSON back to Viewer when field is out of focus + handleEvent(node, event){ + if (event.type === 'blur'){ + this.props.parentCallback(this.jsoneditor.get()); + } + } + componentDidMount () { const options = { mode: 'tree', enableTransform: false, - onChangeJSON: this.props.parentCallback, + onEvent: this.handleEvent, templates: [ { text: 'Event', title: 'Insert an Event Node', field: '', value: { - '@id': 'resin:Events/10000/resin:Events_', + '@id': 'Events/10000/Event', 'name': 'Event Name', 'comment': 'description', 'qnode': 'Q1234567', @@ -42,9 +53,9 @@ export default class JSONEdit extends Component { title: 'Insert Participant', field: '', value: { - '@id': 'resin:Participants/20000/kairos:Primitives_Events_Disaster.Diseaseoutbreak.Unspecified:1_entity', + '@id': 'Participants/20000/Participant', 'roleName': 'consult_XPO', - 'entity': 'resin:Entities/00001/' + 'entity': 'Entities/00001/' } }, { @@ -52,7 +63,7 @@ export default class JSONEdit extends Component { title: 'Insert Child', field: '', value: { - 'child': 'resin:Events/10023/Steps_kairos', + 'child': 'Events/10023/Steps_kairos', 'comment': 'name', 'optional': false, 'importance': 1, @@ -62,6 +73,28 @@ export default class JSONEdit extends Component { 'outlink' ] } + }, + { + text: 'Entity', + title: 'Insert Entity', + field: '', + value: { + 'child': 'Entities/00023/', + 'comment': 'name', + 'qnode': 'Q1234567', + 'qlabel': 'qlabel' + } + }, + { + text: 'Relation', + title: 'Insert Relation', + field: '', + value: { + 'relationSubject': 'Entities/00023/', + 'relationPredicate': 'Q1234567', + 'relationObject': 'Entities/00023/', + '@id': 'Relations/30000/' + } } ] }; diff --git a/static/webpack.config.js b/static/webpack.config.js index 209c156..d746c3c 100644 --- a/static/webpack.config.js +++ b/static/webpack.config.js @@ -28,14 +28,7 @@ module.exports = { }, { test: /\.svg$/, - use: [ - { - loader: 'svg-url-loader', - options: { - iesafe: true, - }, - }, - ], + type: 'asset/inline' }, { test: /\.(woff|woff2|eot|ttf|otf)$/,