diff --git a/actions/generals.js b/actions/generals.js index d3ea7e1a2..6d1fd7298 100644 --- a/actions/generals.js +++ b/actions/generals.js @@ -6,8 +6,7 @@ export const VFB_UI_UPDATED = 'VFB_UI_UPDATED'; export const INSTANCE_ADDED = 'INSTANCE_ADDED'; export const SHOW_GRAPH = 'SHOW_GRAPH'; export const UPDATE_GRAPH = 'UPDATE_GRAPH'; -export const LOAD_CIRCUIT_BROWSER = 'LOAD_CIRCUIT_BROWSER'; -export const UPDATE_CIRCUIT_BROWSER = 'UPDATE_CIRCUIT_BROWSER'; +export const UPDATE_CIRCUIT_QUERY = 'UPDATE_CIRCUIT_QUERY'; export const INSTANCE_SELECTED = 'INSTANCE_SELECTION'; export const INSTANCE_DELETED = 'INSTANCE_DELETED'; export const INSTANCE_VISIBILITY_CHANGED = 'INSTANCE_VISIBILITY_CHANGED'; @@ -40,7 +39,7 @@ export const vfbGraph = (type, instance, queryIndex) => ({ }); export const vfbCircuitBrowser = (type, instance) => ({ - type: LOAD_CIRCUIT_BROWSER, + type: type, data: { instance : instance } }); diff --git a/components/VFBMain.js b/components/VFBMain.js index 27fd1ebe4..e2aaf2e5a 100644 --- a/components/VFBMain.js +++ b/components/VFBMain.js @@ -17,7 +17,7 @@ import VFBQuickHelp from './interface/VFBOverview/QuickHelp'; import VFBGraph from './interface/VFBGraph/VFBGraph'; import VFBCircuitBrowser from './interface/VFBCircuitBrowser/VFBCircuitBrowser'; import { connect } from "react-redux"; -import { SHOW_GRAPH, LOAD_CIRCUIT_BROWSER, VFB_LOAD_TERM_INFO, SHOW_LIST_VIEWER } from './../actions/generals'; +import { SHOW_GRAPH, UPDATE_CIRCUIT_QUERY, VFB_LOAD_TERM_INFO, SHOW_LIST_VIEWER } from './../actions/generals'; require('../css/base.less'); require('../css/VFBMain.less'); @@ -1110,7 +1110,7 @@ class VFBMain extends React.Component { } } - if ( this.props.generals.type == LOAD_CIRCUIT_BROWSER ) { + if ( this.props.generals.type == UPDATE_CIRCUIT_QUERY ) { if ( !this.state.circuitBrowserVisible ) { this.setState({ UIUpdated: true, diff --git a/components/configuration/VFBGraph/graphConfiguration.js b/components/configuration/VFBGraph/graphConfiguration.js index 77d0b7037..d622b01e8 100644 --- a/components/configuration/VFBGraph/graphConfiguration.js +++ b/components/configuration/VFBGraph/graphConfiguration.js @@ -1,4 +1,4 @@ -var locationCypherQuery = instance => ({ +var whatIsCypherQuery = instance => ({ "statements": [ { "statement": "MATCH p=(n:Entity {short_form:'" + instance + "'})-[r:INSTANCEOF|part_of|has_synaptic_terminal_in|has_presynaptic_terminal_in|" diff --git a/components/configuration/VFBListViewer/controlsMenuConfiguration.js b/components/configuration/VFBListViewer/controlsMenuConfiguration.js index 6203e90fd..9b1997469 100644 --- a/components/configuration/VFBListViewer/controlsMenuConfiguration.js +++ b/components/configuration/VFBListViewer/controlsMenuConfiguration.js @@ -1,5 +1,20 @@ import React from "react"; +var ACTIONS = { + COLOR : 'color', + INFO : 'info', + DELETE : 'delete', + SELECT : 'select', + DESELECT : 'deselect', + SHOW : 'hide', + HIDE : 'show', + ZOOM_TO : 'zoom_to', + SHOW_VOLUME : 'show_volume', + HIDE_VOLUME : 'hide_volume', + SHOW_SKELETON : 'show_skeleton', + HIDE_SKELETON : 'hide_skeleton', +}; + const controlsMenuConf = { itemOptions: { customArrow: }, // Global configuration for Menu buttons and drop down @@ -51,6 +66,7 @@ const controlsMenuConf = { } } }, + actions : ACTIONS, // Buttons to display inside the Menu buttons: [ { @@ -70,21 +86,22 @@ const controlsMenuConf = { { label: "Show Info", icon: "fa fa-info", - action: "info" + action: { handlerAction: ACTIONS.INFO } }, { toggle : { condition : entity => entity.isSelected(), + isVisible : entity => entity.isVisible(), options : { false : { label: "Select", icon: "fa fa-check-circle-o", - action: entity => entity.select() + action: { handlerAction: ACTIONS.SELECT, } }, true : { label: "Unselect", icon: "fa fa-check-circle", - action: entity => entity.deselect() + action: { handlerAction: ACTIONS.DESELECT, } } } } @@ -96,12 +113,12 @@ const controlsMenuConf = { false : { label: "Show", icon: "fa fa-eye", - action: entity => entity.show() + action: { handlerAction: ACTIONS.SHOW, } }, true : { label: "Hide", icon: "fa fa-eye-slash", - action: entity => entity.hide() + action: { handlerAction: ACTIONS.HIDE, } } } } @@ -110,36 +127,73 @@ const controlsMenuConf = { label: "Delete", icon: "fa fa-trash", isVisible : entity => entity.getId() != window.templateID, - action: 'delete' + action: { handlerAction: ACTIONS.DELETE }, }, { label: "Zoom To", icon: "fa fa-search-plus", - action: entity => GEPPETTO.SceneController.zoomTo([entity]) + action: { handlerAction: ACTIONS.ZOOM_TO }, + isVisible : entity => entity.isVisible() }, { - label: "Show As", + label: "Show Volume", icon: "", - action: "", + action: {}, position: "right-start", list: [ { toggle : { - condition : entity => entity.isVisible(), + condition : entity => { + var visible = false; + if (entity.getType()[entity.getId() + "_obj"] != undefined && entity[entity.getId() + "_obj"] != undefined) { + visible = GEPPETTO.SceneController.isVisible([entity[entity.getId() + "_obj"]]); + } + return visible; + }, + isVisible : entity => entity.getType().hasVariable(entity.getId() + '_obj'), options : { false : { - label: "Show 3D Volume", + label: "Enable 3D Volume", icon: "gpt-shapeshow", - action: entity => { - entity.show() - } + action: { handlerAction: ACTIONS.SHOW_VOLUME } }, true : { - label: "Hide 3D Volume", + label: "Disable 3D Volume", icon: "gpt-shapehide", - action: entity => { - entity.hide() - } + action: { handlerAction: ACTIONS.HIDE_VOLUME } + } + } + } + }, + ] + }, + { + label: "Show Skeleton", + icon: "", + action: {}, + position: "right-start", + list: [ + { + toggle : { + condition : entity => { + var visible = false; + if (entity.getType()[entity.getId() + "_swc"] != undefined && entity.getType()[entity.getId() + "_swc"].getType().getMetaType() != GEPPETTO.Resources.IMPORT_TYPE && entity[entity.getId() + "_swc"] != undefined) { + visible = GEPPETTO.SceneController.isVisible([entity[entity.getId() + "_swc"]]); + } + return visible; + }, + isVisible : entity => entity.getType().hasVariable(entity.getId() + '_swc'), + options : { + false : { + label: "Enable 3D Skeleton", + icon: "gpt-3dhide", + tooltip : "Show 3D Skeleton", + action: { handlerAction: ACTIONS.SHOW_SKELETON } + }, + true : { + label: "Disable 3D Skeleton", + icon: "gpt-3dshow", + action: { handlerAction: ACTIONS.HIDE_SKELETON } } } } @@ -149,7 +203,8 @@ const controlsMenuConf = { { label: "Color", icon: "fa fa-tint", - action: 'color' + action: { handlerAction: ACTIONS.COLOR }, + isVisible : entity => entity.isVisible() }, ] } diff --git a/components/configuration/VFBToolbar/vfbtoolbarHTML.js b/components/configuration/VFBToolbar/vfbtoolbarHTML.js index 5e7cd675b..a8e3c85b5 100644 --- a/components/configuration/VFBToolbar/vfbtoolbarHTML.js +++ b/components/configuration/VFBToolbar/vfbtoolbarHTML.js @@ -12,8 +12,8 @@ var feedback + "you can engage directly with our developer community on GitHub " + "[VirtualFlyBrain/VFB2].

" + "

If you have a GitHub account you can easily raise a new issue: " - + "

" - + "" + + "" + + "" + "
" + "

" + "

This could simply be a question or a new feature request, but if you have found a bug we missed please copy in " diff --git a/components/interface/VFBCircuitBrowser/Controls.js b/components/interface/VFBCircuitBrowser/Controls.js index f3dea2a64..16dbb0323 100644 --- a/components/interface/VFBCircuitBrowser/Controls.js +++ b/components/interface/VFBCircuitBrowser/Controls.js @@ -22,6 +22,8 @@ import IconButton from '@material-ui/core/IconButton'; import DeleteIcon from '@material-ui/icons/Delete'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; +import { connect } from "react-redux"; +import { UPDATE_CIRCUIT_QUERY } from './../../../actions/generals'; /** * Create a local theme to override some default values in material-ui components @@ -122,7 +124,7 @@ class Controls extends Component { this.state = { typingTimeout: 0, expanded : true, - neuronFields : this.props.neurons + neuronFields : ["", ""] }; this.addNeuron = this.addNeuron.bind(this); this.neuronTextfieldModified = this.neuronTextfieldModified.bind(this); @@ -135,8 +137,12 @@ class Controls extends Component { } componentDidMount () { - this.setState( { expanded : !this.props.resultsAvailable() } ); + let neurons = [...this.props.neurons]; + this.setState( { expanded : !this.props.resultsAvailable(), neuronFields : neurons } ); + this.circuitQuerySelected = this.props.circuitQuerySelected; } + + componentDidUpdate () {} /** * Deletes neuron field, updates control component right after @@ -146,12 +152,17 @@ class Controls extends Component { if ( event.target.id === "" ) { id = parseInt(event.target.parentElement.id); } + // remove neuron textfield let neurons = this.state.neuronFields; neurons.splice(id,1); + + this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, neurons); + // Update state with one less neuron textfield this.setState( { neuronFields : neurons } ); + // If neuron fields are validated, let the VFBCircuitBrowser component know, it will do a graph update if ( this.fieldsValidated(neurons) ) { this.props.queriesUpdated(neurons); } @@ -176,7 +187,7 @@ class Controls extends Component { * Validates neurons ID's are valid, checks there's at least 8 numbers in it */ fieldsValidated (neurons) { - var pattern = /\d{8}/; + var pattern = /^[a-zA-Z0-9].*_[a-zA-Z0-9]{8}$/; for ( var i = 0 ; i < neurons.length ; i++ ){ if ( neurons[i] === "" ) { return false; @@ -195,6 +206,8 @@ class Controls extends Component { typingTimeout (target) { let neurons = this.state.neuronFields; neurons[target.id] = target.value; + this.circuitQuerySelected = neurons; + this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, neurons); if ( this.fieldsValidated(neurons) ) { this.setState( { neuronFields : neurons } ); this.props.queriesUpdated(neurons); @@ -230,40 +243,36 @@ class Controls extends Component { */ getUpdatedNeuronFields () { let neuronFields = this.state.neuronFields; - let neuronMatch = false; - // Query preselected - let queriesPassed = Object.keys(this.circuitQuerySelected).length > 0; - - if ( queriesPassed) { - // If query is preselected and is not on the list already - if ( !this.state.neuronFields.includes(this.circuitQuerySelected) ) { - for ( var i = 0 ; i < neuronFields.length ; i++ ) { - if ( this.state.neuronFields[i] === "" ) { - neuronFields[i] = this.circuitQuerySelected; - neuronMatch = true; + let added = false; + for ( var i = 0; i < this.props.circuitQuerySelected.length; i++ ){ + if ( !this.state.neuronFields.includes(this.props.circuitQuerySelected[i])) { + for ( var j = 0 ; j < neuronFields.length ; j++ ) { + if ( this.state.neuronFields[j] === "" ) { + neuronFields[j] = this.props.circuitQuerySelected[i]; + added = true; break; } } - } else if ( queriesPassed && neuronFields.includes(this.circuitQuerySelected) ) { - neuronMatch = true; - } - } - - // If preselected query is not on list of existing queries - if ( !neuronMatch ) { - if ( queriesPassed ) { - if ( neuronFields.length < configuration.maxNeurons ) { - neuronFields.push(this.circuitQuerySelected); + + if ( this.props.circuitQuerySelected.length > neuronFields.length && !this.state.neuronFields.includes(this.circuitQuerySelected[i])) { + if ( neuronFields.length < configuration.maxNeurons && this.props.circuitQuerySelected !== "" ) { + neuronFields.push(this.props.circuitQuerySelected[i]); + } } } } + if ( this.fieldsValidated(neuronFields) ) { + this.props.queriesUpdated(neuronFields); + } + return neuronFields; } render () { let self = this; const { classes } = this.props; + this.circuitQuerySelected = this.props.circuitQuerySelected; let neuronFields = this.getUpdatedNeuronFields() let expanded = this.state.expanded; @@ -390,4 +399,12 @@ class Controls extends Component { Controls.propTypes = { classes: PropTypes.object.isRequired }; -export default withStyles(styles)(Controls); +function mapStateToProps (state) { + return { ...state } +} + +function mapDispatchToProps (dispatch) { + return { vfbCircuitBrowser: (type, path) => dispatch ( { type : type, data : { instance : path } }), } +} + +export default connect(mapStateToProps, mapDispatchToProps, null, { forwardRef : true } )(withStyles(styles)(Controls)); diff --git a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js index 56de50645..b2810b8bd 100644 --- a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js +++ b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js @@ -162,6 +162,7 @@ class VFBCircuitBrowser extends Component { graph : { nodes : [], links : [] } , legend : {}, loading : true, + queryLoaded : false, dropDownAnchorEl : null, neurons : ["", ""], hops : Math.ceil((configuration.maxHops - configuration.minHops) / 2), @@ -194,6 +195,8 @@ class VFBCircuitBrowser extends Component { let self = this; this.__isMounted = true; this.updateGraph(this.state.neurons , Math.ceil((configuration.maxHops - configuration.minHops) / 2)); + const { circuitQuerySelected } = this.props; + this.circuitQuerySelected = circuitQuerySelected; } componentDidUpdate () { @@ -207,6 +210,10 @@ class VFBCircuitBrowser extends Component { } else if ( !this.props.visible ) { this.focused = false; } + + if ( this.circuitQuerySelected !== this.props.circuitQuerySelected ) { + this.circuitQuerySelected = this.props.circuitQuerySelected; + } } componentWillUnmount () { @@ -214,10 +221,19 @@ class VFBCircuitBrowser extends Component { } /** - * New neurons have been entered by user, update graph + * New neurons have been entered by user, update graph. + * @neurons (array): New list of neurons user has entered */ queriesUpdated (neurons) { - this.updateGraph(neurons, this.state.hops); + // Check if new list of neurons is the same as the ones already rendered on last update + var is_same = (this.state.neurons.length == neurons.length) && this.state.neurons.every(function (element, index) { + return element === neurons[index]; + }); + + // Request graph update if the list of new neurons is not the same + if ( !this.state.loading && !is_same ) { + this.updateGraph(neurons, this.state.hops); + } } /** @@ -225,6 +241,7 @@ class VFBCircuitBrowser extends Component { */ updateHops (hops) { this.setState({ hops : hops }); + this.updateGraph(this.state.neurons, hops); } resetCamera () { @@ -234,7 +251,7 @@ class VFBCircuitBrowser extends Component { } } - resize(){ + resize (){ this.graphResized = true; this.setState( { reload : !this.state.reload } ); } @@ -271,7 +288,7 @@ class VFBCircuitBrowser extends Component { updateGraph (neurons, hops) { if (this.__isMounted){ // Show loading spinner while cypher query search occurs - this.setState({ loading : true , neurons : neurons, hops : hops }); + this.setState({ loading : true , neurons : neurons, hops : hops, queryLoaded : false }); // Perform cypher query this.queryResults(cypherQuery(neurons.map(d => `'${d}'`).join(','), hops)); } @@ -308,7 +325,7 @@ class VFBCircuitBrowser extends Component { worker.onmessage = function (e) { switch (e.data.resultMessage) { case "OK": - self.setState( { graph : e.data.params.results , legend : e.data.params.colorLabels, loading : false }); + self.setState( { graph : e.data.params.results , legend : e.data.params.colorLabels, loading : false, queryLoaded : true }); self.objectsLoaded = e.data.params.results.nodes.length; setTimeout( function () { self.resetCamera(); @@ -353,17 +370,10 @@ class VFBCircuitBrowser extends Component { render () { let self = this; - const { classes } = this.props; // Detect when the first load of the Graph component happens - if ( !this.state.loading && this.firstLoad ) { - // Reset CircuitQuerySelected value after first load - this.circuitQuerySelected = ""; - } - if ( !this.state.loading && !this.firstLoad ) { - this.firstLoad = true; - } - + const { classes, circuitQuerySelected } = this.props; + this.circuitQuerySelected = circuitQuerySelected; return ( this.state.loading ? this.state.graph.nodes.length > 0 } resetCamera={self.resetCamera} @@ -493,10 +504,7 @@ class VFBCircuitBrowser extends Component { VFBCircuitBrowser.propTypes = { classes: PropTypes.object.isRequired }; function mapStateToProps (state) { - return { - circuitQuerySelected : state.generals.circuitQuerySelected, - circuitBrowserSelected : state.generals.circuitBrowserSelected - } + return { circuitQuerySelected : state.generals.circuitQuerySelected } } export default connect(mapStateToProps, null, null, { forwardRef : true } )(withStyles(styles)(VFBCircuitBrowser)); diff --git a/components/interface/VFBGraph/VFBGraph.js b/components/interface/VFBGraph/VFBGraph.js index 6df87af9c..e272726b8 100644 --- a/components/interface/VFBGraph/VFBGraph.js +++ b/components/interface/VFBGraph/VFBGraph.js @@ -5,6 +5,7 @@ import CircularProgress from '@material-ui/core/CircularProgress'; import Menu from '@material-ui/core/Menu'; import MenuItem from '@material-ui/core/MenuItem'; import Tooltip from '@material-ui/core/Tooltip'; +import { UPDATE_GRAPH } from './../../../actions/generals'; import { connect } from "react-redux"; /** @@ -115,20 +116,18 @@ function refineData (e) { }); // Worker is done, notify main thread - this.postMessage({ resultMessage: "OK", params: { results: { nodes, links } } }); + this.postMessage({ resultMessage: "OK", params: { results: { nodes, links }, id : e.data.params.value } }); } class VFBGraph extends Component { constructor (props) { super(props); - this.state = { - graph : { nodes : [], links : [] }, - loading : true, - currentQuery : this.props.instance.id, + this.state = { + graph : { nodes : [], links : [] }, + currentQuery : this.props.instance != null ? this.props.instance.id : "", dropDownAnchorEl : null, optionsIconColor : stylingConfiguration.defaultRefreshIconColor, - nodeSelected : { title : "", id : "" }, reload : false } this.updateGraph = this.updateGraph.bind(this); @@ -143,6 +142,7 @@ class VFBGraph extends Component { this.zoomOut = this.zoomOut.bind(this); this.selectedNodeLoaded = this.selectedNodeLoaded.bind(this); this.resize = this.resize.bind(this); + this.sync = this.sync.bind(this); this.highlightNodes = new Set(); this.highlightLinks = new Set(); @@ -157,19 +157,23 @@ class VFBGraph extends Component { this.graphResized = false; this.focusedInstance = { id : "" }; this.selectedDropDownQuery = -1; + this.loading = true; + this.firstLoad = true; + this.nodeSelectedID = ""; } componentDidMount () { let self = this; this.__isMounted = true; - if (this.state.currentQuery !== undefined && this.state.currentQuery !== null){ - if (this.props.instance.getParent() !== null) { - this.focusedInstance = this.props.instance.getParent(); - } else { - this.focusedInstance = this.props.instance; + if (this.state.currentQuery !== undefined && this.state.currentQuery !== "" && this.state.currentQuery !== null){ + if ( this.props.instance !== null ) { + if (this.props.instance.getParent() !== null) { + this.focusedInstance = this.props.instance.getParent(); + } else { + this.focusedInstance = this.props.instance; + } } - this.updateGraph(); } @@ -184,21 +188,68 @@ class VFBGraph extends Component { if (event.isComposing || event.keyCode === 16) { self.shiftOn = false; } - }); + }); } componentDidUpdate () { let self = this; + + if (this.loading && this.firstLoad) { + if (this.state.currentQuery === undefined || this.state.currentQuery === "" || this.state.currentQuery === null){ + if (this.props.instance !== null && this.props.instance !== undefined) { + if (this.props.instance.getParent() !== null) { + this.focusedInstance = this.props.instance.getParent(); + } else { + this.focusedInstance = this.props.instance; + } + this.firstLoad = false; + this.updateGraph(); + } else if (this.props.instanceOnFocus !== null && this.props.instanceOnFocus !== undefined) { + if (this.props.instanceOnFocus.getParent() !== null) { + this.focusedInstance = this.props.instanceOnFocus.getParent(); + } else { + this.focusedInstance = this.props.instanceOnFocus; + } + this.firstLoad = false; + this.updateGraph(); + } + } + } + + // Reset camera if graph component is visible, not focused or has been resized if ( this.props.visible && ( !this.focused || this.graphResized ) ) { - setTimeout( function () { + /* + * Update graph with selected query index from configuration dropdown selection, this is to allow to lauch the component to be launched + * with specific configuration dropdown query. + */ + stylingConfiguration.dropDownQueries.map((item, index) => { + if ( parseInt(self.props.graphQueryIndex) >= 0 && self.firstLoad ) { + if ( parseInt(self.props.graphQueryIndex) === index ) { + self.selectedDropDownQuery = index; + self.loading = true; + let idToSearch = self.props.instanceOnFocus.id; + if (self.props.instanceOnFocus.getParent() !== null) { + idToSearch = self.props.instanceOnFocus.getParent().id; + } + self.queryResults(item.query(idToSearch), idToSearch); + } + } + }); + // Reset camera view after graph component becomes visible + setTimeout( function () { self.resetCamera(); self.focused = true; + self.loading = false; self.graphResized = false; - }, (self.objectsLoaded * 20)); + }, (self.objectsLoaded * 50)); } else if ( !this.props.visible ) { this.focused = false; - this.selectedDropDownQuery = -1; + if ( parseInt(this.props.graphQueryIndex) >= 0 && !this.firstLoad ) { + this.selectedDropDownQuery = -1; + this.props.vfbGraph(UPDATE_GRAPH, this.focusedInstance, -1); + } + this.firstLoad = true; } } @@ -212,8 +263,8 @@ class VFBGraph extends Component { this.focused = true; } } - - resize(){ + + resize (){ this.graphResized = true; this.setState( { reload : !this.state.reload } ); } @@ -240,6 +291,7 @@ class VFBGraph extends Component { * Handle Left click on Nodes */ handleNodeLeftClick (node, event) { + this.nodeSelectedID = node.title; this.queryNewInstance(node); } @@ -250,6 +302,11 @@ class VFBGraph extends Component { this.graphRef.current.ggv.current.centerAt(node.x , node.y, 1000); this.graphRef.current.ggv.current.zoom(2, 1000); } + + sync () { + this.instanceFocusChange(this.props.instanceOnFocus); + this.updateGraph(); + } /** * Handle Menu drop down clicks @@ -257,9 +314,10 @@ class VFBGraph extends Component { handleMenuClick (query) { if (this.__isMounted){ // Show loading spinner while cypher query search occurs - this.setState({ loading : true , dropDownAnchorEl : null }); + this.loading = true; + this.setState({ dropDownAnchorEl : null }); // Perform cypher query - this.queryResults(query(this.state.currentQuery)) + this.queryResults(query(this.state.currentQuery), this.state.currentQuery) } } @@ -268,21 +326,20 @@ class VFBGraph extends Component { */ queryNewInstance (node) { window.addVfbId(node.title); - this.setState({ loading : true, nodeSelected : node, currentQuery : node.title, optionsIconColor : stylingConfiguration.defaultRefreshIconColor }); + this.loading = true; + this.setState({ optionsIconColor : stylingConfiguration.defaultRefreshIconColor }); // Perform cypher query this.queryResults(cypherQuery(node.title), node.title) } selectedNodeLoaded (instance) { - var loadedId = null; + var loadedId = instance.id; if (instance.getParent() !== null) { loadedId = instance.getParent().id; - } else { - loadedId = instance.id; } - if ( this.state.nodeSselected ) { - if ( this.state.nodeSelected.title === loadedId ) { + if ( this.nodeSselected ) { + if ( this.nodeSelectedID === loadedId ) { return true; } } @@ -307,20 +364,19 @@ class VFBGraph extends Component { * Re-render graph with a new instance */ updateGraph () { - var idToSearch = null; + var idToSearch = this.focusedInstance.id; /* * function handler called by the VFBMain whenever there is an update of the instance on focus, * this will reflect and move to the node (if it exists) that we have on focus. */ if (this.focusedInstance.getParent() !== null) { idToSearch = this.focusedInstance.getParent().id; - } else { - idToSearch = this.focusedInstance.id; } if (this.__isMounted){ // Show loading spinner while cypher query search occurs - this.setState({ loading : true, currentQuery : idToSearch, optionsIconColor : stylingConfiguration.defaultRefreshIconColor }); + this.loading = true; + this.setState({ optionsIconColor : stylingConfiguration.defaultRefreshIconColor }); // Perform cypher query this.queryResults(cypherQuery(idToSearch), idToSearch); } @@ -357,7 +413,10 @@ class VFBGraph extends Component { worker.onmessage = function (e) { switch (e.data.resultMessage) { case "OK": - self.setState( { graph : e.data.params.results , loading : false }); + self.loading = false; + self.firstLoad = false; + self.focusedInstance = e.data.params; + self.setState( { graph : e.data.params.results, currentQuery : e.data.params.id }); self.objectsLoaded = e.data.params.results.nodes.length; setTimeout( function () { self.resetCamera(); @@ -373,7 +432,7 @@ class VFBGraph extends Component { worker.postMessage({ message: "refine", params: { results: response.data, value: instanceID, configuration : configuration, NODE_WIDTH : NODE_WIDTH, NODE_HEIGHT : NODE_HEIGHT } }); }) .catch( function (error) { - self.setState( { loading : false } ); + self.loading = false; }) } @@ -401,46 +460,43 @@ class VFBGraph extends Component { render () { let self = this; - const { instanceOnFocus, graphQueryIndex } = this.props; - + const { graphQueryIndex } = this.props; + let syncColor = this.state.optionsIconColor; - let loading = this.state.loading; - if ( this.focusedInstance.id !== "" && instanceOnFocus !== this.focusedInstance.id ) { - this.instanceFocusChange(instanceOnFocus); - + if (Object.keys(this.props.instanceOnFocus).length === 0 && this.props.instanceOnFocus.constructor === Object) { + return ( +

Model not loaded, graph not available yet

+ ); + } + + if ( this.focusedInstance.id !== "" && !this.props.instanceOnFocus.id.includes(this.focusedInstance.id) ) { + // If the length of the graph is 0, request a new query using the instanceOnFocus if ( this.state.graph.nodes.length === 0 && this.state.graph.links.length === 0 && this.focusedInstance.id !== this.state.currentQuery ){ - let idToSearch = ""; + let idToSearch = this.focusedInstance.id; if (this.focusedInstance.getParent() !== null) { idToSearch = this.focusedInstance.getParent().id; - } else { - idToSearch = this.focusedInstance.id; } + syncColor = stylingConfiguration.defaultRefreshIconColor; // Perform cypher query - loading = true; + this.loading = true; this.queryResults(cypherQuery(idToSearch), idToSearch) } + + // Out of sync if instanceOnFocus is not what's on display if ( this.focusedInstance.id !== this.state.currentQuery ) { syncColor = stylingConfiguration.outOfSyncIconColor; } - } else if (this.focusedInstance.id !== "" && instanceOnFocus === this.focusedInstance.id ){ - stylingConfiguration.dropDownQueries.map((item, index) => { - if ( self.selectedDropDownQuery === -1 || self.selectedDropDownQuery !== parseInt(graphQueryIndex) ) { - if ( parseInt(graphQueryIndex) === index ) { - self.selectedDropDownQuery = index; - loading = true; - self.queryResults(item.query(instanceOnFocus)); - } - } - }) } - - if ( !instanceOnFocus.id.includes(this.focusedInstance.id) ) { + + // Out of sync if instanceOnFocus is not what's on display + if ( !this.props.instanceOnFocus.id.includes(this.focusedInstance.id) ) { syncColor = stylingConfiguration.outOfSyncIconColor; } + return ( - loading + this.loading ? - Reset View}> + Reset View}> - Zoom In}> + Zoom In}> - Zoom Out}> + Zoom Out}> - Refresh}> + Refresh for {this.focusedInstance.name} }> + onClick={self.sync}> Options}> @@ -691,4 +747,8 @@ function mapStateToProps (state) { } } -export default connect(mapStateToProps, null, null, { forwardRef : true } )(VFBGraph); +function mapDispatchToProps (dispatch) { + return { vfbGraph: (type, path, index) => dispatch ( { type : type, data : { instance : path, queryIndex : index } } ) } +} + +export default connect(mapStateToProps, mapDispatchToProps, null, { forwardRef : true } )(VFBGraph); diff --git a/components/interface/VFBListViewer/ListViewerControlsMenu.js b/components/interface/VFBListViewer/ListViewerControlsMenu.js index 429ab9e38..44abe0df8 100644 --- a/components/interface/VFBListViewer/ListViewerControlsMenu.js +++ b/components/interface/VFBListViewer/ListViewerControlsMenu.js @@ -1,14 +1,11 @@ import React, { Component } from "react"; import Menu from "@geppettoengine/geppetto-ui/menu/Menu"; import { connect } from 'react-redux'; -import controlsConfiguration from '../../configuration/VFBListViewer/controlsMenuConfiguration'; import { SliderPicker } from 'react-color'; import { setTermInfo, SHOW_LIST_VIEWER, INSTANCE_DELETED } from './../../../actions/generals'; -// Special control actions handled by the menu handler -const COLOR = 'color'; -const INFO = 'info'; -const DELETE = 'delete'; +const controlsConfiguration = require('../../configuration/VFBListViewer/controlsMenuConfiguration').default; +const ACTIONS = controlsConfiguration.actions; /** * Menu component to display controls for VFB List Viewer @@ -47,12 +44,28 @@ class ListViewerControlsMenu extends Component { * Handles menu option selection */ menuHandler (action, component) { - // If action belongs to color control, display the color picker - if ( action === COLOR ) { - this.setState({ displayColorPicker: true }); - } else if ( action === INFO ) { - // If action belongs to 'info' control, display term info - let self = this; + switch (action.handlerAction){ + case ACTIONS.SHOW: + this.props.instance.show(); + break; + case ACTIONS.HIDE: + this.props.instance.hide(); + break; + case ACTIONS.SELECT: + this.props.instance.select(); + break; + case ACTIONS.DESELECT: + this.props.instance.deselect(); + break; + case ACTIONS.ZOOM_TO: + GEPPETTO.SceneController.zoomTo([this.props.instance]); + break; + case ACTIONS.DELETE: + this.props.instance.delete(); + this.props.instanceDeleted(INSTANCE_DELETED, this.props.instance); + break; + case ACTIONS.INFO: + var self = this; /** * Needs a 100 ms delay before calling the setTermInfo method, this is due to Menu taking * a few ms to close. @@ -60,12 +73,60 @@ class ListViewerControlsMenu extends Component { setTimeout ( () => { self.props.setTermInfo(self.props.instance, true); }, 100); - } else if ( action === DELETE ) { - this.props.instance.delete(); - this.props.instanceDeleted(INSTANCE_DELETED, this.props.instance); - } else { - // For every other action execute the action as is - action(this.props.instance); + break; + case ACTIONS.COLOR: + this.setState({ displayColorPicker: true }); + break; + case ACTIONS.SHOW_VOLUME: + var color = this.props.instance.getColor(); + var instance = this.props.instance[this.props.instance.getId() + "_obj"]; + if ( instance === undefined ) { + instance = this.props.instance.getType()[this.props.instance.getId() + "_obj"]; + } + if (instance.getType().getMetaType() == GEPPETTO.Resources.IMPORT_TYPE) { + var self = this; + instance.getType().resolve(function () { + self.props.instance.setColor(color); + GEPPETTO.trigger('experiment:visibility_changed', instance); + GEPPETTO.ControlPanel.refresh(); + }); + } else { + if (GEPPETTO.SceneController.isInstancePresent(instance)) { + GEPPETTO.SceneController.show([instance]); + } else { + GEPPETTO.SceneController.display(instance); instance.setColor(color); + } + } + break; + case ACTIONS.HIDE_VOLUME: + var instance = this.props.instance[this.props.instance.getId() + "_obj"]; + if ( instance === undefined ) { + instance = this.props.instance.getType()[this.props.instance.getId() + "_obj"]; + } + instance.hide(); + break; + case ACTIONS.SHOW_SKELETON: + var color = this.props.instance.getColor(); + var instance = this.props.instance[this.props.instance.getId() + "_swc"]; + if (instance.getType().getMetaType() == GEPPETTO.Resources.IMPORT_TYPE) { + var col = instance.getParent().getColor(); + instance.getType().resolve( function () { + instance.setColor(col); + GEPPETTO.trigger('experiment:visibility_changed', instance); + GEPPETTO.ControlPanel.refresh(); + }); + } else { + if (GEPPETTO.SceneController.isInstancePresent(instance)) { + GEPPETTO.SceneController.show([instance]); + } else { + GEPPETTO.SceneController.display(instance); + instance.setColor(color); + } + } + break; + case ACTIONS.HIDE_SKELETON: + GEPPETTO.SceneController.hide([this.props.instance[this.props.instance.getId() + "_swc"]]); + break; } } @@ -83,7 +144,14 @@ class ListViewerControlsMenu extends Component { // Button configuration has two options, perform condition to determine which button to use if ( item.toggle ){ let condition = item.toggle.condition(this.props.instance); - list.push(item.toggle.options[condition]); + if ( item.toggle.isVisible !== undefined) { + let visible = item.toggle.isVisible(this.props.instance); + if ( visible ) { + list.push(item.toggle.options[condition]); + } + } else { + list.push(item.toggle.options[condition]); + } } else { if ( item.isVisible !== undefined) { let visible = item.isVisible(this.props.instance); @@ -96,6 +164,7 @@ class ListViewerControlsMenu extends Component { } } + /** * Iterate through button list in Menu configuration */ diff --git a/components/interface/VFBListViewer/VFBListViewer.js b/components/interface/VFBListViewer/VFBListViewer.js index 35fc8f66d..6f0ff6d4e 100644 --- a/components/interface/VFBListViewer/VFBListViewer.js +++ b/components/interface/VFBListViewer/VFBListViewer.js @@ -7,6 +7,7 @@ import { connect } from 'react-redux'; require('../../../css/VFBListViewer.less'); const VISUAL_TYPE = "VisualType"; +const COMPOSITE_VISUAL_TYPE = "CompositeVisualType"; /** * Wrapper class that connects geppetto-client's ListViewer component with VFB. @@ -36,19 +37,19 @@ class VFBListViewer extends Component { getInstances () { // Retrieve all instances from the ModelFactory let entities = GEPPETTO.ModelFactory.allPaths; - var visuals = []; + var visuals = {}; const { instanceDeleted, idsMap, idsList } = this.props; // Match Visual Types from ModelFactory for (var i = 0; i < entities.length; i++) { - if (entities[i].metaType === VISUAL_TYPE ) { - if (idsList.includes(entities[i].path.split(".")[0])){ - visuals.push(entities[i]); + if (entities[i].metaType === VISUAL_TYPE || entities[i].metaType === COMPOSITE_VISUAL_TYPE ) { + if (idsList.includes(entities[i].path.split(".")[0]) && visuals[entities[i].path] === undefined ){ + visuals[entities[i].path.split(".")[0]] = entities[i]; } } } - return visuals; + return Object.values(visuals); } render () { @@ -60,7 +61,7 @@ class VFBListViewer extends Component { handler={this} filter={() => true} columnConfiguration={this.getColumnConfiguration()} - showPagination={false} + infiniteScroll={true} /> } diff --git a/components/interface/VFBTermInfo/VFBTermInfo.js b/components/interface/VFBTermInfo/VFBTermInfo.js index 889e210a2..ab37a2c72 100644 --- a/components/interface/VFBTermInfo/VFBTermInfo.js +++ b/components/interface/VFBTermInfo/VFBTermInfo.js @@ -4,7 +4,7 @@ import Slider from "react-slick"; import Collapsible from 'react-collapsible'; import HTMLViewer from '@geppettoengine/geppetto-ui/html-viewer/HTMLViewer'; import ButtonBarComponent from '@geppettoengine/geppetto-client/components/widgets/popup/ButtonBarComponent'; -import { SHOW_GRAPH, LOAD_CIRCUIT_BROWSER } from './../../../actions/generals'; +import { SHOW_GRAPH, UPDATE_CIRCUIT_QUERY } from './../../../actions/generals'; import { connect } from "react-redux"; var $ = require('jquery'); @@ -126,14 +126,26 @@ class VFBTermInfo extends React.Component { // Look for root node, create a Variable object with the graphs configuration, and attach it to root type object if (type.getMetaType() == GEPPETTO.Resources.COMPOSITE_TYPE_NODE) { - var graphType = new Type({ wrappedObj : { name : GRAPHS, eClass : GRAPHS } }) + let variables = type.getVariables(); + let present = false; - // Variable object holding the information for the graph links - var graphsVariable = new Variable({ wrappedObj : { name : "Graph for" }, values : graphs }); - graphsVariable.setTypes([graphType]); + // Check if link has been added already, if it has, don't add it again + for ( var i = 0; i < variables.length; i++ ){ + if ( variables[i].types[0].wrappedObj.name === GRAPHS ){ + present = true; + } + } + + if ( !present ) { + var graphType = new Type({ wrappedObj : { name : GRAPHS, eClass : GRAPHS } }) + + // Variable object holding the information for the graph links + var graphsVariable = new Variable({ wrappedObj : { name : "Graph for" }, values : graphs }); + graphsVariable.setTypes([graphType]); - // Add graphs Variable to root node - type.getVariables().push(graphsVariable); + // Add graphs Variable to root node + type.getVariables().push(graphsVariable); + } } } @@ -153,14 +165,25 @@ class VFBTermInfo extends React.Component { // Look for root node, create a Variable object with the graphs configuration, and attach it to root type object if (type.getMetaType() == GEPPETTO.Resources.COMPOSITE_TYPE_NODE) { - var circuitBrowserType = new Type({ wrappedObj : { name : CIRCUIT_BROWSER, eClass : CIRCUIT_BROWSER } }) + let variables = type.getVariables(); + let present = false; + // Check if link has been added already, if it has, don't add it again + for ( var i = 0; i < variables.length; i++ ){ + if ( variables[i].types[0].wrappedObj.name === CIRCUIT_BROWSER ){ + present = true; + } + } - // Variable object holding the information for the graph links - var circuitBrowserVariable = new Variable({ wrappedObj : { name : "Circuit Browser for" }, values : circuitBrowser }); - circuitBrowserVariable.setTypes([circuitBrowserType]); + if ( !present ) { + var circuitBrowserType = new Type({ wrappedObj : { name : CIRCUIT_BROWSER, eClass : CIRCUIT_BROWSER } }) - // Add graphs Variable to root node - type.getVariables().push(circuitBrowserVariable); + // Variable object holding the information for the graph links + var circuitBrowserVariable = new Variable({ wrappedObj : { name : "Circuit Browser for" }, values : circuitBrowser }); + circuitBrowserVariable.setTypes([circuitBrowserType]); + + // Add graphs Variable to root node + type.getVariables().push(circuitBrowserVariable); + } } } @@ -711,10 +734,9 @@ class VFBTermInfoWidget extends React.Component { // Show Circuit Browser const { vfbCircuitBrowser } = this.props; /* - * Path contains the instance and the index of the drop down query options - * Path is of type : "instance_path, query_index" + * Path contains the instancE ID passed to the circuit browser */ - vfbCircuitBrowser(LOAD_CIRCUIT_BROWSER, path.split(',')[1]); + vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, path.split(',')[1]); // Notify VFBMain UI needs to be updated this.props.uiUpdated(); diff --git a/components/interface/VFBToolbar/VFBToolBar.js b/components/interface/VFBToolbar/VFBToolBar.js index fc0fbb0bc..5a3ad5252 100644 --- a/components/interface/VFBToolbar/VFBToolBar.js +++ b/components/interface/VFBToolbar/VFBToolBar.js @@ -95,10 +95,6 @@ export default class VFBToolBar extends React.Component { clickFeedback () { var htmlContent = this.feedbackHTML; window.ga('vfb.send', 'pageview', (window.location.pathname + '?page=Feedback')); - // add clinet data to console - $.getJSON('http://gd.geobytes.com/GetCityDetails?callback=?', function (data) { - console.log('USER: ' + data.geobytesipaddress + ' ' + data.geobytesfqcn); - }); // report console log for agrigated analysis window.ga('vfb.send', 'feedback', window.location.href, window.console.logs.join('\n').replace(/\#/g,escape('#')), ); @@ -151,26 +147,10 @@ export default class VFBToolBar extends React.Component { } // return as much of the log up to the last 10 events < 1000 characters: var logLength = -50; - var limitedLog = window.console.logs.slice(logLength).join('%0A').replace( - /\&/g,escape('&') - ).replace( - /\#/g,escape('#') - ).replace( - /\-/g,'%2D' - ).replace( - /\+/g,'%2B' - ); - while (limitedLog.length > 1000 && logLength < 0) { + var limitedLog = window.console.logs.slice(logLength).join('/n'); + while (limitedLog.length < 1000 && logLength < 0) { logLength += 1; - limitedLog = window.console.logs.slice(logLength).join('%0A').replace( - /\&/g,escape('&') - ).replace( - /\#/g,escape('#') - ).replace( - /\-/g,'%2D' - ).replace( - /\+/g,'%2B' - ); + limitedLog = window.console.logs.slice(logLength).join('/n'); } this.props.htmlOutputHandler( htmlContent.replace( diff --git a/reducers/generals.js b/reducers/generals.js index 50350b189..ed054b7c4 100644 --- a/reducers/generals.js +++ b/reducers/generals.js @@ -8,7 +8,8 @@ import { SHOW_LIST_VIEWER, LOAD_CYPHER_QUERIES, SHOW_GRAPH, - LOAD_CIRCUIT_BROWSER, + UPDATE_GRAPH, + UPDATE_CIRCUIT_QUERY, INSTANCE_SELECTED, INSTANCE_VISIBILITY_CHANGED, VFB_LOAD_TERM_INFO @@ -25,15 +26,14 @@ export const GENERAL_DEFAULT_STATE = { stepsToLoad: 1, stepsLoaded: 0, loading: false, - graphQueryIndex : {}, + graphQueryIndex : -1, instanceOnFocus : {}, instanceSelection : {}, instanceDeleted : {}, instanceVisibilityChanged : false, termInfoVisible : false, listViewerInfoVisible : true, - circuitBrowserSelected : false, - circuitQuerySelected : {}, + circuitQuerySelected : [], layout: { "ThreeDViewer": true, "StackViewer": true, @@ -140,13 +140,6 @@ function generalReducer (state, action) { var idsLoaded = state.idsLoaded; var newMap = { ...state.idsMap }; - if (newMap[action.data.id] === undefined ) { - return { - ...state, - error: "instance " + action.data.id + "is not present anymore in the map" - }; - } - if (newMap[action.data.id] !== undefined && newMap[action.data.id].components[action.data.component]) { var newComponents = { ...newMap[action.data.id].components }; newMap[action.data.id].components = newComponents; @@ -195,7 +188,9 @@ function generalReducer (state, action) { stepsToLoad: 0, stepsLoaded: 0, idsMap: newMap, - loading: loading + loading: loading, + instanceOnFocus : Instances[action.data.id] != null ? Instances[action.data.id] : {}, + idsList : !state.idsList.includes(action.data.id) ? [ ...state.idsList, action.data.id ] : [ ...state.idsList ] }; } case VFB_UI_UPDATED: @@ -209,11 +204,22 @@ function generalReducer (state, action) { graphQueryIndex : action.data.queryIndex, instanceOnFocus : action.data.instance }; - case LOAD_CIRCUIT_BROWSER: + case UPDATE_GRAPH: + return { + ...state, + graphQueryIndex : action.data.queryIndex + }; + case UPDATE_CIRCUIT_QUERY: + var newQueryMap = []; + if ( Array.isArray(action.data.instance) ) { + newQueryMap = action.data.instance; + } else { + !state.circuitQuerySelected.includes(action.data.instance) ? newQueryMap = [...state.circuitQuerySelected, action.data.instance] : newQueryMap = [...state.circuitQuerySelected]; + } + return { ...state, - circuitQuerySelected : action.data.instance, - circuitBrowserSelected : true + circuitQuerySelected : newQueryMap, }; case INSTANCE_ADDED: var newMap = { ...state.idsMap }; @@ -239,7 +245,9 @@ function generalReducer (state, action) { } return { ...state, - idsMap: newMap + idsMap: newMap, + instanceOnFocus : Instances[newInstance[0]] != null ? Instances[newInstance[0]] : {}, + idsList : !state.idsList.includes(action.data) ? [ ...state.idsList, action.data ] : [ ...state.idsList ] }; case INSTANCE_SELECTED: return {