Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#985 and #986 - Uses SOLR in Circuit Browser and instance selection done by name in Circuit Browser #1014

Merged
merged 4 commits into from
Mar 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion components/VFBMain.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class VFBMain extends React.Component {
canvasVisible: true,
listViewerVisible: true,
graphVisible : true,
circuitBrowserVisible : false,
circuitBrowserVisible : true,
htmlFromToolbar: undefined,
idSelected: undefined,
instanceOnFocus: undefined,
Expand Down Expand Up @@ -1022,6 +1022,10 @@ class VFBMain extends React.Component {
} else if (component === "vfbCircuitBrowser") {
let circuitBrowserVisibility = node.isVisible();
node.setEventListener("close", () => {
self.setState({
UIUpdated: true,
circuitBrowserVisible: false
});
self.props.vfbCircuitBrowser(ACTIONS.UPDATE_CIRCUIT_QUERY,null,false);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
var locationCypherQuery = ( instances, hops ) => ({
"statements": [
var locationCypherQuery = ( instances, hops , weight ) => ({
statements: [
{
"statement" : "WITH [" + instances + "] AS neurons"
+ " MATCH p=(x:Class)-[:synapsed_to*.." + hops.toString() + "]->(y:Class)"
+ " WHERE x.short_form in neurons and y.short_form in neurons"
+ " RETURN p, neurons",
"statement" : "WITH [" + instances + "] AS neurons"
+ " WITH neurons[0] as root, neurons[1..] AS neurons"
+ " MATCH p=(x:Neuron {short_form: root})-[:synapsed_to*.." + hops.toString() + "]->(y:Neuron)"
+ " WHERE y.short_form IN neurons AND"
+ " ALL(rel in relationships(p) WHERE exists(rel.weight) AND rel.weight[0] > " + weight.toString() + ")"
+ " WITH root, relationships(p) as fu"
+ " UNWIND fu as r"
+ " WITH root, startNode(r) AS a, endNode(r) AS b"
+ " MATCH p=(a)-[:synapsed_to]-(b)"
+ " RETURN p, root",
"resultDataContents": ["graph"]
}
]
Expand Down
5 changes: 5 additions & 0 deletions components/configuration/VFBMain/layoutModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ var modelJson = {
"type": "tab",
"name": "Layers",
"component": "vfbListViewer"
},
{
"type": "tab",
"name": "Circuit Browser",
"component": "vfbCircuitBrowser"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,14 @@ var toolbarMenu = {
parameters: ["graphVisible"]
}
},
{
label: "Circuit Browser",
icon: "fa fa-cogs",
action: {
handlerAction: "UIElementHandler",
parameters: ["circuitBrowserVisible"]
}
},
{
label: "NBLAST",
icon: "",
Expand Down
117 changes: 85 additions & 32 deletions components/interface/VFBCircuitBrowser/Controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Accordion from '@material-ui/core/Accordion';
import AccordionDetails from '@material-ui/core/AccordionDetails';
import AccordionSummary from '@material-ui/core/AccordionSummary';
import AccordionActions from '@material-ui/core/AccordionActions';
import Autocomplete from '@material-ui/lab/Autocomplete';
import Typography from '@material-ui/core/Typography';
import Chip from '@material-ui/core/Chip';
import Divider from '@material-ui/core/Divider';
Expand All @@ -24,6 +25,8 @@ 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';
import { DatasourceTypes } from '@geppettoengine/geppetto-ui/search/datasources/datasources';
import { getResultsSOLR } from "@geppettoengine/geppetto-ui/search/datasources/SOLRclient";

/**
* Create a local theme to override some default values in material-ui components
Expand Down Expand Up @@ -97,6 +100,9 @@ const restPostConfig = require('../../configuration/VFBCircuitBrowser/circuitBro
const cypherQuery = require('../../configuration/VFBCircuitBrowser/circuitBrowserConfiguration').locationCypherQuery;
const stylingConfiguration = require('../../configuration/VFBCircuitBrowser/circuitBrowserConfiguration').styling;

const searchConfiguration = require('./../../configuration/VFBMain/searchConfiguration').searchConfiguration;
const datasourceConfiguration = require('./../../configuration/VFBMain/searchConfiguration').datasourceConfiguration;

/**
* Create custom marks for Hops slider.
* Only show the label for the minimum and maximum hop, hide the rest
Expand Down Expand Up @@ -124,7 +130,8 @@ class Controls extends Component {
this.state = {
typingTimeout: 0,
expanded : true,
neuronFields : ["", ""]
neuronFields : [{ id : "", label : "" } , { id : "", label : "" }],
filteredResults : {}
};
this.addNeuron = this.addNeuron.bind(this);
this.neuronTextfieldModified = this.neuronTextfieldModified.bind(this);
Expand All @@ -133,13 +140,16 @@ class Controls extends Component {
this.fieldsValidated = this.fieldsValidated.bind(this);
this.deleteNeuronField = this.deleteNeuronField.bind(this);
this.getUpdatedNeuronFields = this.getUpdatedNeuronFields.bind(this);
this.handleResults = this.handleResults.bind(this);
this.resultSelectedChanged = this.resultSelectedChanged.bind(this);
this.circuitQuerySelected = this.props.circuitQuerySelected;
}

componentDidMount () {
let neurons = [...this.props.neurons];
this.setState( { expanded : !this.props.resultsAvailable(), neuronFields : neurons } );
this.circuitQuerySelected = this.props.circuitQuerySelected;
this.setInputValue = {};
}

componentDidUpdate () {}
Expand All @@ -159,7 +169,7 @@ class Controls extends Component {

this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, neurons);

// Update state with one less neuron textfield
// Update state with one fewer neuron textfield
this.setState( { neuronFields : neurons } );

// If neuron fields are validated, let the VFBCircuitBrowser component know, it will do a graph update
Expand All @@ -174,7 +184,7 @@ class Controls extends Component {
addNeuron () {
let neuronFields = this.state.neuronFields;
// Add emptry string for now to text field
neuronFields.push("");
neuronFields.push({ id : "", label : "" });
// User has added the maximum number of neurons allowed in query search
if ( configuration.maxNeurons <= neuronFields.length ) {
this.setState({ neuronFields : neuronFields });
Expand All @@ -189,43 +199,72 @@ class Controls extends Component {
fieldsValidated (neurons) {
var pattern = /^[a-zA-Z0-9].*_[a-zA-Z0-9]{8}$/;
for ( var i = 0 ; i < neurons.length ; i++ ){
if ( neurons[i] === "" ) {
if ( neurons?.[i].id == "" ) {
return false;
} else if ( !neurons[i].match(pattern) ) {
} else if ( !neurons?.[i].id?.match(pattern) ) {
return false;
}
}

return true;
}

/**
* Receives SOLR results and creates an map with those results that match the input text
*/
handleResults (status, data, value) {
let results = {};
data?.map(result => {
// Match results by short_form id
if ( result?.short_form?.toLowerCase().includes(value?.toLowerCase()) ){
results[result?.label] = result;
} else if ( result?.label?.toLowerCase().includes(value?.toLowerCase()) ){
results[result?.label] = result;
}
});

this.setState({ filteredResults : results })
}

/**
* Waits some time before performing new search, this to avoid performing search everytime
* enters a new character in neuron fields
*/
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);
}
this.setInputValue = target.id;
getResultsSOLR( target.value, this.handleResults,searchConfiguration.sorter,datasourceConfiguration );
}

/**
* Neuron text field has been modified.
*/
neuronTextfieldModified (event) {
const self = this;
this.resultsHeight = event.target.offsetTop + 15;
// Remove old typing timeout interval
if (self.state.typingTimeout) {
if (this.state.typingTimeout) {
clearTimeout(this.typingTimeout);
}
let target = event.target;
// Create a setTimeout interval, to avoid performing searches on every stroke
setTimeout(this.typingTimeout, 500, target);
setTimeout(this.typingTimeout, 500, event.target);
}

/**
* Handle SOLR result selection, activated by selecting from drop down menu under textfield
*/
resultSelectedChanged (event, value) {
// Copy neurons and add selection to correct array index
let neurons = this.state.neuronFields;
neurons[this.setInputValue] = { id : this.state.filteredResults?.[value].short_form, label : value };

// Keep track of query selected, and send an event to redux store that circuit has been updated
this.circuitQuerySelected = neurons;
this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, neurons);

// If text fields contain valid ids, perform query
if ( this.fieldsValidated(neurons) ) {
this.setState( { neuronFields : neurons } );
this.props.queriesUpdated(neurons);
}
}

/**
Expand All @@ -245,18 +284,22 @@ class Controls extends Component {
let neuronFields = this.state.neuronFields;
let added = false;
for ( var i = 0; i < this.props.circuitQuerySelected.length; i++ ){
if ( !this.state.neuronFields.includes(this.props.circuitQuerySelected[i])) {
var fieldExists = this.state.neuronFields.filter(entry =>
entry.id === this.props.circuitQuerySelected[i]
);

if ( !fieldExists) {
for ( var j = 0 ; j < neuronFields.length ; j++ ) {
if ( this.state.neuronFields[j] === "" ) {
neuronFields[j] = this.props.circuitQuerySelected[i];
if ( this.state.neuronFields?.[j].id === "" ) {
neuronFields[j] = { id : this.props.circuitQuerySelected[i], label : "" };
added = true;
break;
}
}

if ( this.props.circuitQuerySelected.length > neuronFields.length && !this.state.neuronFields.includes(this.circuitQuerySelected[i])) {
if ( this.props.circuitQuerySelected.length > neuronFields.length && !fieldExists) {
if ( neuronFields.length < configuration.maxNeurons && this.props.circuitQuerySelected !== "" ) {
neuronFields.push(this.props.circuitQuerySelected[i]);
neuronFields.push({ id : this.props.circuitQuerySelected[i], label : "" });
}
}
}
Expand Down Expand Up @@ -325,21 +368,31 @@ class Controls extends Component {
</div>
</Grid>
<Grid item sm={11}>
{ neuronFields.map((value, index) => {
{ neuronFields.map((field, index) => {
let label = "Neuron " + (index + 1) .toString();
return <Grid container alignItems="center" justify="center" key={"TextFieldContainer" + index}>
<Grid item sm={neuronColumnSize} key={"TextFieldItem" + index}>
<TextField
<Autocomplete
freeSolo
fullWidth
margin="dense"
defaultValue={value}
placeholder={label}
key={value}
onChange={this.neuronTextfieldModified}
disableClearable
autoHighlight
value={field.label}
id={index.toString()}
inputProps={{ style: { color: "white" } }}
InputLabelProps={{ style: { color: "white" } }}
/></Grid>
onChange={this.resultSelectedChanged}
options={Object.keys(this.state.filteredResults).map(option => this.state.filteredResults[option].label)}
renderInput={params => (
<TextField
{...params}
label={"Neuron " + ( index + 1 ).toString()}
key={field.id}
onChange={this.neuronTextfieldModified}
inputProps={{ ...params.inputProps, style: { color: "white" , paddingLeft : "10px" } }}
InputLabelProps={{ ...params.inputProps,style: { color: "white" } }}
/>
)}
/>
</Grid>
{ deleteIconVisible ? <Grid item sm={1}>
<IconButton
key={"TextFieldIcon-" + index}
Expand Down
19 changes: 9 additions & 10 deletions components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class VFBCircuitBrowser extends Component {
loading : true,
queryLoaded : false,
dropDownAnchorEl : null,
neurons : ["", ""],
neurons : [{ id : "", label : "" } , { id : "", label : "" }],
hops : Math.ceil((configuration.maxHops - configuration.minHops) / 2),
reload : false
}
Expand Down Expand Up @@ -118,12 +118,12 @@ class VFBCircuitBrowser extends Component {
*/
queriesUpdated (neurons) {
// 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];
var matched = (this.state.neurons.length == neurons.length) && this.state.neurons.every(function (element, index) {
return element.id === neurons[index].id;
});

// Request graph update if the list of new neurons is not the same
if ( !this.state.loading && !is_same ) {
if ( !this.state.loading && !matched ) {
this.updateGraph(neurons, this.state.hops);
}
}
Expand Down Expand Up @@ -181,8 +181,8 @@ class VFBCircuitBrowser extends Component {
if (this.__isMounted){
// Show loading spinner while cypher query search occurs
this.setState({ loading : true , neurons : neurons, hops : hops, queryLoaded : false });
// Perform cypher query
this.queryResults(cypherQuery(neurons.map(d => `'${d}'`).join(','), hops));
// Perform cypher query. TODO: Remove hardcoded weight once edge weight is implemented
this.queryResults(cypherQuery(neurons.map(a => `'${a.id}'`).join(","), hops, 70));
}
}

Expand Down Expand Up @@ -233,7 +233,6 @@ class VFBCircuitBrowser extends Component {
worker.postMessage({ message: "refine", params: { results: response.data, configuration : configuration, styling : stylingConfiguration, NODE_WIDTH : NODE_WIDTH, NODE_HEIGHT : NODE_HEIGHT } });
})
.catch( function (error) {
console.log("HTTP Request Error: ", error);
self.setState( { loading : false } );
})
}
Expand Down Expand Up @@ -268,8 +267,8 @@ class VFBCircuitBrowser extends Component {
this.circuitQuerySelected = circuitQuerySelected;

let errorMessage = "Not enough input queries to create a graph, needs 2.";
if ( this.state.neurons[0] != "" && this.state.neurons[1] != "" ){
errorMessage = "Graph not available for " + this.state.neurons.join(",");
if ( this.state.neurons?.[0].id != "" && this.state.neurons?.[1].id != "" ){
errorMessage = "Graph not available for " + this.state.neurons.map(a => `'${a.id}'`).join(",");
}
return (
this.state.loading
Expand Down Expand Up @@ -352,6 +351,7 @@ class VFBCircuitBrowser extends Component {
// bu = Bottom Up, creates Graph with root at bottom
dagMode="lr"
dagLevelDistance = {100}
onDagError={loopNodeIds => {}}
// Handles clicking event on an individual node
onNodeClick = { (node,event) => this.handleNodeLeftClick(node,event) }
ref={this.graphRef}
Expand All @@ -373,7 +373,6 @@ class VFBCircuitBrowser extends Component {
zoomIn={self.zoomIn}
zoomOut={self.zoomOut}
circuitQuerySelected={this.circuitQuerySelected}
datasource="SOLR"
legend = {self.state.legend}
/>
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@babel/plugin-transform-runtime": "^7.4.5",
"@geppettoengine/geppetto-client": "file:./geppetto-client",
"@material-ui/icons": "3.0.1",
"@material-ui/lab": "^4.0.0-alpha.57",
"@types/react-rnd": "^8.0.0",
"axios": "^0.19.2",
"babel-loader": "^8.0.6",
Expand Down