From 2e43a206fd25d255f2248b63481ca043a66ea0a9 Mon Sep 17 00:00:00 2001 From: iris-liu0312 Date: Tue, 14 Dec 2021 14:52:57 -0700 Subject: [PATCH 01/12] support for SDF v1.3 --- app.py | 153 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 78 insertions(+), 75 deletions(-) diff --git a/app.py b/app.py index a527feb..caccf70 100644 --- a/app.py +++ b/app.py @@ -6,14 +6,14 @@ nodes = {} edges = [] -# TODO: look through SDF version documentation to decide what should be in the schema_key_dict -# TODO: move support to SDF version 1.3 - -# SDF version 1.2 +# SDF version 1.3 schema_key_dict = { - 'root': ['@id', 'name', 'description', 'comment', 'qnode', '@type', 'minDuration', 'maxDuration', 'repeatable', 'TA1explanation', 'importance', 'qlabel'], + 'root': ['@id', 'name', 'comment', 'description', 'aka', 'qnode', 'qlabel', 'minDuration', + 'maxDuration', 'goal', 'ta1explanation', 'importance', 'children_gate'], + # TODO: handle xor children_gates 'participant': ['@id', 'roleName', 'entity'], - 'child': ['child', 'comment', 'optional', 'importance', 'outlinks', 'outlink_gate', ] + 'child': ['child', 'comment', 'optional', 'importance', 'outlinks', 'outlink_gate'], + 'privateData': ['@type', 'template', 'repeatable', 'importance'] } def create_node(_id, _label, _type, _shape=''): @@ -72,58 +72,12 @@ def extend_node(node, obj): node['classes'] = 'optional' else: node['data'][key] = obj[key] + if 'privateData' in obj.keys(): + for key in obj['privateData'].keys(): + if key in schema_key_dict['privateData']: + node['data'][key] = obj['privateData'][key] return node -def handle_precondition(order, node_set, label='Precondition'): - """Adds edges between multiple previous and next nodes. - - Parameters: - order (dict): links to "before" and "after" nodes - node_set (dict): nodes to be linked - label (str): shows order on graph - - """ - e = [] - if isinstance(order['before'], list): - for before_id in order['before']: - if isinstance(order['after'], list): - for after_id in order['after']: - if before_id in node_set and after_id in node_set: - e.append(create_edge(before_id, after_id, label, 'step_child')) - else: - if before_id in node_set and order['after'] in node_set: - e.append(create_edge(before_id, order['after'], label, 'step_child')) - else: - if isinstance(order['after'], list): - for after_id in order['after']: - if order['before'] in node_set and after_id in node_set: - e.append(create_edge(order['before'], after_id, label, 'step_child')) - else: - if order['before'] in node_set and order['after'] in node_set: - e.append(create_edge(order['before'], order['after'], label, 'step_child')) - return e - -def handle_optional(_order, node_set): - """Calls handle_precondition with thet optional label. - - """ - return handle_precondition(_order, node_set, 'Optional') - -def handle_flags(_flag, _order, node_set): - """Calls handle_precondition or handle_optional based on flag. - - Parameters: - _flag (str): flag raised in order dictionary - _node_set (dict): set of nodes to be handled - - """ - switcher={ - 'precondition': handle_precondition, - 'optional': handle_optional - } - func = switcher.get(_flag.lower(), lambda *args: None) - return func(_order, node_set) - def get_nodes_and_edges(schema): """Creates lists of nodes and edges, through references and relations. @@ -137,20 +91,45 @@ def get_nodes_and_edges(schema): """ nodes = {} edges = [] + containers = [] + first_run = True for scheme in schema: - # top node + # create event node _label = scheme['name'].split('/')[-1].replace('_', ' ').replace('-', ' ') - nodes[scheme['@id']] = extend_node(create_node(scheme['@id'], _label, 'root', 'diamond'), scheme) - # not root node, change node type - if '@type' in nodes[scheme['@id']]['data']: - nodes[scheme['@id']]['data']['_type'] = 'parent' - # not hierarchical node, change node shape + scheme_id = scheme['@id'] + # node already exists + if scheme_id in nodes: + # add information + nodes[scheme_id]['data']['_type'] = 'root' + nodes[scheme_id]['data']['_label'] = _label + nodes[scheme_id] = extend_node(nodes[scheme_id], scheme) + # change type back if 'children' not in scheme: - nodes[scheme['@id']]['data']['_type'] = 'child' - nodes[scheme['@id']]['data']['_shape'] = 'ellipse' - if scheme['repeatable']: - edges.append(create_edge(scheme['@id'], scheme['@id'], _edge_type='child_outlink')) + nodes[scheme_id]['data']['_type'] = 'child' + elif 'outlinks' in nodes[scheme_id]['data']['name']: + nodes[scheme_id]['data']['_type'] = 'container' + containers.append(scheme_id) + else: + nodes[scheme_id]['data']['_type'] = 'parent' + nodes[scheme_id]['data']['_shape'] = 'diamond' + # new node + else: + nodes[scheme_id] = extend_node(create_node(scheme_id, _label, 'root', 'diamond'), scheme) + + if first_run: + first_run = False + else: + # not root node, change node type + nodes[scheme_id]['data']['_type'] = 'parent' + nodes[scheme_id]['data']['_shape'] = 'diamond' + # not hierarchical node, change node shape + if 'children' not in scheme: + nodes[scheme_id]['data']['_type'] = 'child' + nodes[scheme_id]['data']['_shape'] = 'ellipse' + # handle repeatable + if nodes[scheme_id]['data']['repeatable']: + edges.append(create_edge(scheme_id, scheme_id, _edge_type='child_outlink')) # participants if 'participants' in scheme: @@ -158,13 +137,23 @@ def get_nodes_and_edges(schema): _label = participant['roleName'].split('/')[-1].replace('_', '') nodes[participant['@id']] = extend_node(create_node(participant['@id'], _label, 'participant', 'square'), participant) - edges.append(create_edge(scheme['@id'], participant['@id'], _edge_type='step_participant')) + edges.append(create_edge(scheme_id, participant['@id'], _edge_type='step_participant')) # children if 'children' in scheme: for child in scheme['children']: - nodes[child['child']] = extend_node(create_node(child['child'], child['comment'], 'child', 'ellipse'), child) - edges.append(create_edge(scheme['@id'], child['child'], _edge_type='step_child')) + + child_id = child['child'] + # node already exists + if child_id in nodes: + prev_type = nodes[child_id]['data']['_type'] + nodes[child_id]['data']['_type'] = 'child' + nodes[child_id] = extend_node(nodes[child_id], child) + nodes[child_id]['data']['_type'] = prev_type + # new node + else: + nodes[child_id] = extend_node(create_node(child_id, child['comment'], 'child', 'ellipse'), child) + edges.append(create_edge(scheme_id, child_id, _edge_type='step_child')) # check for outlinks if len(child['outlinks']): @@ -172,12 +161,26 @@ def get_nodes_and_edges(schema): if outlink not in nodes: _label = outlink.split('/')[-1].replace('_', '') nodes[outlink] = create_node(outlink, _label, 'child', 'ellipse') - edges.append(create_edge(child['child'], outlink, _edge_type='child_outlink')) - - # TODO: and, xor gate - # TODO: optional nodes -- the option is in the cy-style json - # bug? it doesn't show up on parent node - # found: parent node does not have optional key and overwrites child node keys + edges.append(create_edge(child_id, outlink, _edge_type='child_outlink')) + + # handle containers, ie. connect previous node to all their successors + edges_to_remove = [] + for container in containers: + for edge in edges: + if 'searched' in edge['data'] and edge['data']['searched']: + continue + if edge['data']['target'] == container: + source_id = edge['data']['source'] + nodes[source_id]['data']['children_gate'] = nodes[container]['data']['children_gate'] + edges_to_remove.append(edge) + edge['data']['searched'] = True + if edge['data']['source'] == container: + edges.append(create_edge(source_id, edge['data']['target'], _edge_type='step_child')) + edges_to_remove.append(edge) + edge['data']['searched'] = True + + for index in edges_to_remove: + edges.remove(index) # === are these two necessary? / what are these for === # TODO: entities From 6557644b62da7c159860a0b5006f9e706c2a4ad4 Mon Sep 17 00:00:00 2001 From: iris-liu0312 Date: Tue, 14 Dec 2021 14:53:14 -0700 Subject: [PATCH 02/12] sidebar viewable again --- static/src/template/Canvas.jsx | 3 ++- static/src/template/SideBar.jsx | 4 +--- static/src/template/Viewer.jsx | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/static/src/template/Canvas.jsx b/static/src/template/Canvas.jsx index 6c75f4d..ca98ffc 100644 --- a/static/src/template/Canvas.jsx +++ b/static/src/template/Canvas.jsx @@ -107,7 +107,8 @@ class Canvas extends React.Component { this.runLayout(); } // show information of node - this.props.sidebarCallback(event.target.data()); + console.log(event.target.data()); + this.showSidebar(event.target.data()); }) }) } diff --git a/static/src/template/SideBar.jsx b/static/src/template/SideBar.jsx index fc5a051..6a7921f 100644 --- a/static/src/template/SideBar.jsx +++ b/static/src/template/SideBar.jsx @@ -19,9 +19,7 @@ function SideBar (props) { {Object.entries(props.data).map(([key, val]) => { if (!excluded_ids.includes(key)) { - if (Array.isArray(val)) { - val = val.join(' ') - } + val = val.toString() return (
diff --git a/static/src/template/Viewer.jsx b/static/src/template/Viewer.jsx index 83bdf17..62d998e 100644 --- a/static/src/template/Viewer.jsx +++ b/static/src/template/Viewer.jsx @@ -13,7 +13,7 @@ class Viewer extends Component { this.state = { schemaResponse: '', schemaName: '', - schmeaJson: '', + schemaJson: '', isOpen: false, nodeData: {} } From 6afca44730c12e357ced5c553bb37e0e94a36c38 Mon Sep 17 00:00:00 2001 From: iris-liu0312 Date: Tue, 14 Dec 2021 14:57:24 -0700 Subject: [PATCH 03/12] comment cleanup --- app.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app.py b/app.py index caccf70..722fde6 100644 --- a/app.py +++ b/app.py @@ -185,10 +185,6 @@ def get_nodes_and_edges(schema): # === are these two necessary? / what are these for === # TODO: entities # TODO: relations - # if 'relations' in schema and len(schema['relations']): - # # generalize, although at the moment UIUC Q7 only has these two predicates - # predicates = {'Q19267375':'proximity', 'Q6498684':'ownership'} - # for relation in schema['relations']: # if 'entityRelations' in schema and isinstance(schema['entityRelations'], list): # for entityRelation in schema['entityRelations']: From ff3e92bf65fb5b0804ef830df3a873ab10eeaa6f Mon Sep 17 00:00:00 2001 From: iris-liu0312 Date: Thu, 16 Dec 2021 12:36:16 -0700 Subject: [PATCH 04/12] added tasks --- app.py | 6 ++++++ static/src/template/Canvas.jsx | 5 +++++ static/src/template/JsonView.jsx | 5 +++++ static/src/template/UploadModal.jsx | 15 ++++++++++----- static/src/template/Viewer.jsx | 7 +++++-- 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index 722fde6..323dcb6 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,12 @@ from flask import Flask, render_template, request import json +# =============================================== +# app.py +# ------------ +# reads json data to send to viewer +# =============================================== + app = Flask(__name__, static_folder='./static', template_folder='./static') nodes = {} diff --git a/static/src/template/Canvas.jsx b/static/src/template/Canvas.jsx index ca98ffc..49b0d03 100644 --- a/static/src/template/Canvas.jsx +++ b/static/src/template/Canvas.jsx @@ -10,6 +10,11 @@ import RefreshIcon from '@material-ui/icons/Refresh'; import Background from '../public/canvas_bg.png'; import CyStyle from '../public/cy-style.json'; +/* Graph view of the data. + Includes reload button. */ + + +// TODO: add right-click menu cytoscape.use(klay) class Canvas extends React.Component { diff --git a/static/src/template/JsonView.jsx b/static/src/template/JsonView.jsx index 9c3f8ef..8404a06 100644 --- a/static/src/template/JsonView.jsx +++ b/static/src/template/JsonView.jsx @@ -6,6 +6,11 @@ import some from 'lodash/some'; import isArray from 'lodash/isArray'; import isObject from 'lodash/isObject'; +// TODO: top-level task: make a more user-friendly editor + // TODO: add blocks of JSON based off of type of JSON + // scheme + // participant + // child class JsonView extends Component { /* Handles the Json viewer and editor. diff --git a/static/src/template/UploadModal.jsx b/static/src/template/UploadModal.jsx index b77d9d7..93e982f 100644 --- a/static/src/template/UploadModal.jsx +++ b/static/src/template/UploadModal.jsx @@ -4,6 +4,9 @@ import { ToastContainer, toast } from 'react-toastify'; import axios from 'axios'; +/* Upload modal to upload JSON file. + Shows a pop-up window. */ + class UploadModal extends Component { constructor(props) { super(props); @@ -139,14 +142,15 @@ class UploadModal extends Component { setTimeout(this.toggle, 1000); }) .catch(err => { // then print response status - toast.error('upload fail'); + this.setState({ valid: false }); + toast.error('upload fail, check console'); }); } render() { /* - Renders the upload schema button. - Opens up a sub window where you can upload a file or cancel. + Opens up a sub window when Upload Schema button is pressed, + where you can upload a file or cancel. Checks the validity of the file. Upon pressing upload, shows an upload progress bar. */ @@ -175,13 +179,14 @@ class UploadModal extends Component { - {Math.round(this.state.loaded, 2)}% + {Math.round(this.state.loaded, 2)}% - {' '} diff --git a/static/src/template/Viewer.jsx b/static/src/template/Viewer.jsx index 62d998e..4979aa4 100644 --- a/static/src/template/Viewer.jsx +++ b/static/src/template/Viewer.jsx @@ -6,6 +6,9 @@ import Canvas from './Canvas'; import SideBar from './SideBar'; import JsonView from './JsonView'; +/* Viewer page for the schema interface. */ + +// TODO: export JSON button class Viewer extends Component { constructor(props) { super(props) @@ -63,14 +66,14 @@ class Viewer extends Component { {this.state.schemaName} ; - // graph (cytoscape) in the middle + // graph (cytoscape) canvas = ; - // json editor on the right + // json editor jsonViewer = Date: Thu, 16 Dec 2021 15:28:03 -0700 Subject: [PATCH 05/12] update to SDF v1.3 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6052f5..5e597a9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Introduction -This is a web tool to visualize KAIROS Schema Format generated schemas using Cytoscape.js and React.js. The tool also allows editing of these schemas for curation purpose. Current supported SDF version is **1.2**. +This is a web tool to visualize KAIROS Schema Format generated schemas using Cytoscape.js and React.js. The tool also allows editing of these schemas for curation purpose. Current supported SDF version is **1.3**. **This project is currently a work in progress and is in alpha testing. Feedbacks and suggestions are welcome.** From 603de33d55cb3a474210f41c1d6067f3ffd54565 Mon Sep 17 00:00:00 2001 From: iris-liu0312 Date: Thu, 16 Dec 2021 16:08:10 -0700 Subject: [PATCH 06/12] migrate todos --- static/src/template/Canvas.jsx | 5 +++++ static/src/template/JsonView.jsx | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/static/src/template/Canvas.jsx b/static/src/template/Canvas.jsx index 49b0d03..a00b8a3 100644 --- a/static/src/template/Canvas.jsx +++ b/static/src/template/Canvas.jsx @@ -15,6 +15,11 @@ import CyStyle from '../public/cy-style.json'; // TODO: add right-click menu +// TODO: top-level task: make a more user-friendly editor + // TODO: add blocks of JSON based off of type of JSON + // scheme + // participant + // child cytoscape.use(klay) class Canvas extends React.Component { diff --git a/static/src/template/JsonView.jsx b/static/src/template/JsonView.jsx index 8404a06..9c3f8ef 100644 --- a/static/src/template/JsonView.jsx +++ b/static/src/template/JsonView.jsx @@ -6,11 +6,6 @@ import some from 'lodash/some'; import isArray from 'lodash/isArray'; import isObject from 'lodash/isObject'; -// TODO: top-level task: make a more user-friendly editor - // TODO: add blocks of JSON based off of type of JSON - // scheme - // participant - // child class JsonView extends Component { /* Handles the Json viewer and editor. From d1c7615ea3f0d48c4458dd0b0599c63d580b3ae1 Mon Sep 17 00:00:00 2001 From: iris-liu0312 Date: Fri, 17 Dec 2021 13:42:22 -0700 Subject: [PATCH 07/12] form for creating schema, toast notifs --- static/src/App.scss | 10 ++ static/src/template/SchemaModal.jsx | 239 ++++++++++++++++++++++++++++ static/src/template/UploadModal.jsx | 5 +- static/src/template/Viewer.jsx | 18 +++ 4 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 static/src/template/SchemaModal.jsx diff --git a/static/src/App.scss b/static/src/App.scss index 42253cd..2d491f6 100644 --- a/static/src/App.scss +++ b/static/src/App.scss @@ -85,4 +85,14 @@ margin: 0 2vw; border: solid darkslateblue 2px; overflow-y: auto; +} + +input.form-check-input { +width: 15px; +height: 15px; +padding: 0; +margin: 0; +vertical-align: bottom; +position: relative; +top: -1px; } \ No newline at end of file diff --git a/static/src/template/SchemaModal.jsx b/static/src/template/SchemaModal.jsx new file mode 100644 index 0000000..ab07c20 --- /dev/null +++ b/static/src/template/SchemaModal.jsx @@ -0,0 +1,239 @@ +import React, { Component } from 'react'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input, Label, Form, FormGroup } from 'reactstrap'; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +import axios from 'axios'; +import AddCircleIcon from '@material-ui/icons/AddCircle'; +// import LineStyleIcon from '@material-ui/icons/LineStyle'; +// import PersonIcon from '@material-ui/icons/Person'; +// import ChildCareIcon from '@material-ui/icons/ChildCare'; + +/* Shows a pop-up window to create and edit JSON data. */ +// TODO: add submitted data to json +class SchemaModal extends Component { + constructor(props) { + super(props); + this.state = { + modal: false, + valid: false, + id: "", + name: "", + comment: "", + description: "", + aka: "", + qnode: "", + qlabel: "", + minDuration: "", + maxDuration: "", + goal: "", + ta1explanation: "", + importance: "1.0", + children_gate: "OR", + repeatable: false + } + this.toggle = this.toggle.bind(this); + this.onChangeHandler = this.onChangeHandler.bind(this); + this.validate = this.validate.bind(this); + this.onClickHandler = this.onClickHandler.bind(this); + } + + toggle() { + this.setState({ + modal: !this.state.modal, + id: "", + name: "", + comment: "", + description: "", + aka: "", + qnode: "", + qlabel: "", + minDuration: "", + maxDuration: "", + goal: "", + ta1explanation: "", + importance: "1.0", + children_gate: "OR" + }) + } + + validate() { + /* + Checks whether required text fields are completed + and are the right format. + */ + console.log('in validate'); + const err = []; + + // id, name, ta1explanation, importance + if (this.state.id.length === 0){ + console.log('id'); + err.push("@ID cannot be blank.\n"); + } + if (this.state.name.length === 0){ + console.log('name'); + err.push("Name cannot be blank.\n"); + } + if (this.state.ta1explanation.length === 0){ + console.log('explanation'); + err.push("TA1 explanation cannot be blank.\n"); + } + if (this.state.importance > 1 || this.state.importance < 0){ + console.log('importance'); + err.push("Importance must be a number between 0 and 1.\n"); + } + if (this.state.qnode.length !== 0){ + var isValidQNode = false; + if(/Q\d/.test(this.state.qnode)){ + isValidQNode = true; + } + + if (!isValidQNode){ + console.log('qnode'); + err.push("Invalid QNode.\n") + } + } + console.log(err) + console.log('show errors'); + // show errors + for (var z = 0; z < err.length; z++) { + toast.error(err[z]); + } + console.log('return'); + return err.length === 0; + } + + onChangeHandler(event) { + // Set state of inputs. + if (event.target.name === "repeatable"){ + event.target.value = !event.target.value + } + this.setState({ + [event.target.name]: event.target.value + }); + } + + onClickHandler() { + /* + Validates fields and handles add button. + Shows success or failure. + If success, closes the sub window. + */ + + validated = this.validate(); + if (validated){ + // const data = new FormData(); + // for (var x = 0; x < this.state.selectedFile.length; x++) { + // data.append('file', this.state.selectedFile[x]); + // } + // axios.post("/upload", data, { + // onUploadProgress: ProgressEvent => { + // this.setState({ + // loaded: (ProgressEvent.loaded / ProgressEvent.total * 100), + // }) + // } + // }) + // .then(res => { // then print response status + // this.props.parentCallback(res.data) + // toast.success('upload success'); + // setTimeout(this.toggle, 1000); + // }) + // .catch(err => { // then print response status + // this.setState({ valid: false }); + // toast.error('upload fail, check console'); + // }); + setTimeout(this.toggle, 1000); + } + } + + render() { + /* + Opens up a sub window when Upload Schema button is pressed, + where you can upload a file or cancel. + Checks the validity of the file. + Upon pressing upload, shows an upload progress bar. + */ + + const openModal = () => { + document.getElementById("btn-modal").blur(); + this.toggle(); + } + + return ( +
+
+ +
+ + + Add Scheme + + +
+ + + + + + + + + + + + + + + + + + + + +