diff --git a/Dockerfile b/Dockerfile index 01dc133d1..a82f3095b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ ARG geppettoDatasourceRelease=vfb_20200604_a ARG geppettoModelSwcRelease=v1.0.1 ARG geppettoFrontendRelease=development ARG geppettoClientRelease=VFBv2.2.0.7 -ARG ukAcVfbGeppettoRelease=pipeline2 +ARG ukAcVfbGeppettoRelease=download ARG mvnOpt="-Dhttps.protocols=TLSv1.2 -DskipTests --quiet -Pmaster" diff --git a/components/VFBMain.js b/components/VFBMain.js index bfe37e536..e16e44f06 100644 --- a/components/VFBMain.js +++ b/components/VFBMain.js @@ -9,6 +9,7 @@ import VFBTermInfoWidget from './interface/VFBTermInfo/VFBTermInfo'; import Logo from '@geppettoengine/geppetto-client/components/interface/logo/Logo'; import Canvas from '@geppettoengine/geppetto-client/components/interface/3dCanvas/Canvas'; import QueryBuilder from '@geppettoengine/geppetto-client/components/interface/query/queryBuilder'; +import VFBUploader from './interface/VFBUploader/VFBUploader'; import HTMLViewer from '@geppettoengine/geppetto-ui/html-viewer/HTMLViewer'; import VFBListViewer from './interface/VFBListViewer/VFBListViewer'; import * as FlexLayout from '@geppettoengine/geppetto-ui/flex-layout/src/index'; @@ -51,6 +52,7 @@ class VFBMain extends React.Component { quickHelpVisible: undefined, UIUpdated: true, wireframeVisible: false, + uploaderContentsVisible : true }; this.addVfbId = this.addVfbId.bind(this); @@ -85,7 +87,7 @@ class VFBMain extends React.Component { this.instanceOnFocus = undefined; this.idFromURL = undefined; this.idsFromURL = []; - this.urlQueryLoader = undefined; + this.urlQueryLoader = []; this.quickHelpRender = undefined; this.firstLoad = true; this.quickHelpOpen = true; @@ -525,6 +527,9 @@ class VFBMain extends React.Component { case 'triggerSetTermInfo': this.handlerInstanceUpdate(click.value[0]); break; + case 'uploaderContentsVisible': + this.refs.uploaderContentsRef?.openDialog(); + break; case 'triggerRunQuery': GEPPETTO.trigger('spin_logo'); var that = this; @@ -914,7 +919,7 @@ class VFBMain extends React.Component { self.setState({ UIUpdated: true }) }} focusTermRef={this.focusTermReference} - exclude={["ClassQueriesFrom", "Debug"]} + exclude={["ClassQueriesFrom", "Debug", "DownloadMeta"]} order={['Symbol', 'Title', 'Name', @@ -934,7 +939,9 @@ class VFBMain extends React.Component { 'Related Individuals', 'Relationships', 'Query for', - 'Graph for', + 'Graphs For', + 'Graphs for', + 'Add Neuron to Circuit Browser Query', 'Circuit Browser for', 'Description', 'Cross References', @@ -1058,7 +1065,7 @@ class VFBMain extends React.Component { if ( nextProps.generals.instanceOnFocus !== undefined && this.instanceOnFocus !== undefined) { if ( Object.keys(nextProps.generals.instanceOnFocus).length > 0 ) { if ( nextProps.generals.instanceOnFocus !== this.instanceOnFocus.getId() ){ - this.instanceOnFocus == nextProps.generals.instanceOnFocus; + this.instanceOnFocus = nextProps.generals.instanceOnFocus; } } } @@ -1342,10 +1349,15 @@ class VFBMain extends React.Component { } idsList = idList[list].replace("i=","") + idsList; } else if (idList[list].indexOf("q=") > -1) { - this.urlQueryLoader = idList[list].replace("q=","").replace("%20", " ").split(","); + const multipleQueries = idList[list].replace("q=","").replace("%20", " ").split(";"); + let that = this; + multipleQueries?.forEach( query => { + const querySplit = query.split(","); + that.urlQueryLoader.push({ id : querySplit[0].trim(), selection : querySplit[1].trim() }); + }); // if no other ids are loaded the query target is added. if (idsList.length == 0 && this.urlQueryLoader.length > 1) { - idsList = this.urlQueryLoader[0]; + idsList = this.urlQueryLoader[0].id; } } } @@ -1380,6 +1392,13 @@ class VFBMain extends React.Component { GEPPETTO.on(GEPPETTO.Events.Instance_added, function (instance) { that.props.instanceAdded(instance); }); + + GEPPETTO.on(GEPPETTO.Events.Instances_created, function (instances) { + // Set template Instance to be not clickable in 3D viewer + if ( instances[0]?.id?.includes(window.templateID) ) { + that.canvasReference.engine.meshes ? that.canvasReference.engine.meshes[window.templateID + "." + instances[0]?.id].children[0].clickThrough = true : null; + } + }); GEPPETTO.on(GEPPETTO.Events.Instance_deleted, function (instancePath) { let id = instancePath.split(".")[0]; @@ -1390,10 +1409,23 @@ class VFBMain extends React.Component { that.addVfbId(that.idsFinalList); var callback = function () { - // check if any results with count flag - if (that.refs.querybuilderRef.props.model.count > 0) { + if ( that.urlQueryLoader.length == 0 && that.refs.querybuilderRef.props.model.count > 0 ) { // runQuery if any results that.refs.querybuilderRef.runQuery(); + } else if (that.urlQueryLoader.length > 0 && that.refs.querybuilderRef.props.model.count > 0) { + // Remove query from stack, and perform the next query + that.urlQueryLoader.shift(); + const query = that.urlQueryLoader[0]; + // Fetch variable and addQuery, if no more queries left then run query. + query + ? window[query.id] === undefined + ? window.fetchVariableThenRun(query.id, function () { + that.refs.querybuilderRef.addQueryItem({ term: "", id: query.id, queryObj: Model[query.selection] }, callback) + }) + : that.refs.querybuilderRef.addQueryItem({ term: "", id: query.id, queryObj: Model[query.selection] }, callback) + : that.refs.querybuilderRef.props.model.count > 0 + ? that.refs.querybuilderRef.runQuery() + : null } else { that.refs.querybuilderRef.switchView(false); } @@ -1403,14 +1435,15 @@ class VFBMain extends React.Component { GEPPETTO.trigger('stop_spin_logo'); }; + // Initial queries specified on URL if (that.urlQueryLoader !== undefined) { if (window[that.urlQueryLoader[0]] == undefined) { - window.fetchVariableThenRun(that.urlQueryLoader[0], function () { - that.refs.querybuilderRef.addQueryItem({ term: "", id: that.urlQueryLoader[0], queryObj: Model[that.urlQueryLoader[1]] }, callback) + that.urlQueryLoader[0]?.id && window.fetchVariableThenRun(that.urlQueryLoader[0]?.id, function () { + that.refs.querybuilderRef.addQueryItem({ term: "", id: that.urlQueryLoader[0]?.id, queryObj: Model[that.urlQueryLoader[0]?.selection] }, callback) }); } else { setTimeout(function () { - that.refs.querybuilderRef.addQueryItem({ term: "", id: that.urlQueryLoader[0], queryObj: Model[that.urlQueryLoader[1]] }, callback); + that.refs.querybuilderRef.addQueryItem({ term: "", id: that.urlQueryLoader[0]?.id, queryObj: Model[that.urlQueryLoader[0]?.selection] }, callback); }, 100); } } @@ -1719,6 +1752,7 @@ class VFBMain extends React.Component { searchConfiguration={this.searchConfiguration} datasourceConfiguration={this.datasourceConfiguration} /> + {this.htmlToolbarRender} ); diff --git a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js index 456b5dda7..66339c535 100644 --- a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js +++ b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js @@ -3,10 +3,10 @@ var locationCypherQuery = ( instances, paths, weight ) => ({ { "statement" : "WITH [" + instances + "] AS neurons" + " WITH neurons[0] as a, neurons[1] AS b" - + " MATCH (source:has_neuron_connectivity {short_form: a}), (target:Neuron {short_form: b})" + + " MATCH (source:Neuron:has_neuron_connectivity {short_form: a}), (target:Neuron:has_neuron_connectivity {short_form: b})" + " CALL gds.beta.shortestPath.yens.stream({" - + " nodeQuery: 'MATCH (n:Neuron) RETURN id(n) AS id'," - + " relationshipQuery: 'MATCH (a:Neuron:has_neuron_connectivity)-[r:synapsed_to]->(b:Neuron) WHERE exists(r.weight) AND r.weight[0] >= " + + " nodeQuery: 'MATCH (n:Neuron:has_neuron_connectivity) RETURN id(n) AS id'," + + " relationshipQuery: 'MATCH (a:Neuron:has_neuron_connectivity)-[r:synapsed_to]->(b:Neuron:has_neuron_connectivity) WHERE exists(r.weight) AND r.weight[0] >= " + weight?.toString() + " RETURN id(a) AS source, id(b) AS target, type(r) as type, 5000-r.weight[0] as weight_p'," + " sourceNode: id(source)," + " targetNode: id(target)," @@ -18,12 +18,13 @@ var locationCypherQuery = ( instances, paths, weight ) => ({ + " YIELD index, sourceNode, targetNode, nodeIds, path" + " WITH * ORDER BY index DESC" + " UNWIND relationships(path) as sr" - + " OPTIONAL MATCH cp=(x)-[:synapsed_to]-(y) WHERE x=apoc.rel.startNode(sr) AND y=apoc.rel.endNode(sr) OPTIONAL MATCH fp=(x)-[r:synapsed_to]->(y)" + + " OPTIONAL MATCH cp=(x:Neuron:has_neuron_connectivity)-[:synapsed_to]-(y:Neuron:has_neuron_connectivity) WHERE x=apoc.rel.startNode(sr) AND y=apoc.rel.endNode(sr) OPTIONAL MATCH fp=(x)-[r:synapsed_to]->(y) WHERE r.weight[0] >= " + weight?.toString() + " RETURN distinct a as root, collect(distinct fp) as pp, collect(distinct cp) as p, collect(distinct id(r)) as fr, sourceNode as source, targetNode as target, max(length(path)) as maxHops, collect(distinct toString(id(r))+':'+toString(index)) as relationshipY ", "resultDataContents": ["row", "graph"] } ] }); +// See query explanation on https://github.com/VirtualFlyBrain/graph_queries/blob/main/weighted_path.md var configuration = { resultsMapping: diff --git a/components/configuration/VFBCircuitBrowser/datasources/SOLRclient.tsx b/components/configuration/VFBCircuitBrowser/datasources/SOLRclient.tsx new file mode 100644 index 000000000..dbf04e499 --- /dev/null +++ b/components/configuration/VFBCircuitBrowser/datasources/SOLRclient.tsx @@ -0,0 +1,439 @@ +import axios from 'axios'; + +const globalConfiguration:any = { + "url": "https://solr-dev.virtualflybrain.org/solr/ontology/select", + "query_settings": + { + "q": "$SEARCH_TERM$ OR $SEARCH_TERM$* OR *$SEARCH_TERM$*", + "defType": "edismax", + "qf": "label synonym label_autosuggest_ws label_autosuggest_e label_autosuggest synonym_autosuggest_ws synonym_autosuggest_e synonym_autosuggest shortform_autosuggest has_narrow_synonym_annotation has_broad_synonym_annotation", + "indent": "true", + "fl": "short_form,label,synonym,id,type,has_narrow_synonym_annotation,has_broad_synonym_annotation,facets_annotation", + "start": "0", + "fq": [ + "type:class OR type:individual OR type:property", + "ontology_name:(vfb)", + "facets_annotation:has_neuron_connectivity", + "shortform_autosuggest:VFB* OR shortform_autosuggest:FB* OR is_defining_ontology:true" + ], + "rows": "100", + "wt": "json", + "bq": "is_obsolete:false^100.0 shortform_autosuggest:VFB*^110.0 shortform_autosuggest:FBbt*^100.0 is_defining_ontology:true^100.0 label_s:\"\"^2 synonym_s:\"\" in_subset_annotation:BRAINNAME^3 short_form:FBbt_00003982^2" + } +}; + +let solrConfiguration:any = { + params: { + json: { + params: globalConfiguration.query_settings + } + } +} + +export function getResultsSOLR ( searchString: string, returnResults: Function, sorter: Function, configuration?: any) { + var url:string = configuration.url; + + if (configuration.url === undefined) { + url = globalConfiguration.url; + } + if (configuration.query_settings !== undefined) { + solrConfiguration.params.json.params = configuration.query_settings; + } + + // hack to clone the object + let tempConfig:any = JSON.parse(JSON.stringify(solrConfiguration)); + tempConfig.params.json.params.q = solrConfiguration.params.json.params.q.replace(/\$SEARCH_TERM\$/g, searchString); + + axios.get(`${url}`, tempConfig) + .then(function(response) { + var blob = new Blob(["onmessage = " + refineResults]); + var blobUrl = window.URL.createObjectURL(blob); + + var worker = new Worker(blobUrl); + worker.onmessage = function (e) { + switch(e.data.resultMessage) { + case "OK": + returnResults("OK", e.data.params.results, searchString); + window.URL.revokeObjectURL(blobUrl); + break; + } + }; + worker.postMessage({message: "refine", params: {results: response.data.response.docs, value: searchString}}); + + // refineResults(searchString, response.data.response.docs, returnResults); + }) + .catch(function(error) { + console.log('%c --- SOLR datasource error --- ', 'background: black; color: red'); + console.log(error); + returnResults("ERROR", undefined, searchString); + }) +}; + +function refineResults(e) { + const sorter = function (a, b) { + var InputString = self.spotlightString; + + // move exact matches to top + if (InputString == a.label) { + return -1; + } + if (InputString == b.label) { + return 1; + } + // close match without case matching + if (InputString.toLowerCase() == a.label.toLowerCase()) { + return -1; + } + if (InputString.toLowerCase() == b.label.toLowerCase()) { + return 1; + } + // match ignoring joinging nonwords + if (InputString.toLowerCase().split(/\W+/).join(' ') == a.label.toLowerCase().split(/\W+/).join(' ')) { + return -1; + } + if (InputString.toLowerCase().split(/\W+/).join(' ') == b.label.toLowerCase().split(/\W+/).join(' ')) { + return 1; + } + // match against id + if (InputString.toLowerCase() == a.id.toLowerCase()) { + return -1; + } + if (InputString.toLowerCase() == b.id.toLowerCase()) { + return 1; + } + // pick up any match without nonword join character match + if (a.label.toLowerCase().split(/\W+/).join(' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ')) < 0 && b.label.toLowerCase().split(/\W+/).join(' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ')) > -1) { + return 1; + } + if (b.label.toLowerCase().split(/\W+/).join(' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ')) < 0 && a.label.toLowerCase().split(/\W+/).join(' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ')) > -1) { + return -1; + } + // also with underscores ignored + if (a.label.toLowerCase().split(/\W+/).join(' ').replace('_', ' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ').replace('_', ' ')) < 0 && b.label.toLowerCase().split(/\W+/).join(' ').replace('_', ' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ').replace('_', ' ')) > -1) { + return 1; + } + if (b.label.toLowerCase().split(/\W+/).join(' ').replace('_', ' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ').replace('_', ' ')) < 0 && a.label.toLowerCase().split(/\W+/).join(' ').replace('_', ' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ').replace('_', ' ')) > -1) { + return -1; + } + // if not found in one then advance the other + if (a.label.toLowerCase().indexOf(InputString.toLowerCase()) < 0 && b.label.toLowerCase().indexOf(InputString.toLowerCase()) > -1) { + return 1; + } + if (b.label.toLowerCase().indexOf(InputString.toLowerCase()) < 0 && a.label.toLowerCase().indexOf(InputString.toLowerCase()) > -1) { + return -1; + } + // if the match is closer to start than the other move up + if (a.label.toLowerCase().indexOf(InputString.toLowerCase()) > -1 && a.label.toLowerCase().indexOf(InputString.toLowerCase()) < b.label.toLowerCase().indexOf(InputString.toLowerCase())) { + return -1; + } + if (b.label.toLowerCase().indexOf(InputString.toLowerCase()) > -1 && b.label.toLowerCase().indexOf(InputString.toLowerCase()) < a.label.toLowerCase().indexOf(InputString.toLowerCase())) { + return 1; + } + // if the match in the id is closer to start then move up + if (a.id.toLowerCase().indexOf(InputString.toLowerCase()) > -1 && a.id.toLowerCase().indexOf(InputString.toLowerCase()) < b.id.toLowerCase().indexOf(InputString.toLowerCase())) { + return -1; + } + if (b.id.toLowerCase().indexOf(InputString.toLowerCase()) > -1 && b.id.toLowerCase().indexOf(InputString.toLowerCase()) < a.id.toLowerCase().indexOf(InputString.toLowerCase())) { + return 1; + } + // move the shorter synonyms to the top + if (a.label < b.label) { + return -1; + } else if (a.label > b.label) { + return 1; + } else { + return 0; + } // if nothing found then do nothing. + }; + + self.spotlightString = e.data.params.value; + var refinedResults:Array = []; + e.data.params.results.map(item => { + if (item.hasOwnProperty("synonym")) { + item.synonym.map(innerItem => { + let newRecord:any = {} + if (innerItem !== item.label) { + Object.keys(item).map(key => { + switch(key) { + case "label": + newRecord[key] = innerItem + " (" + item.label + ")"; + break; + case "synonym": + break; + default: + newRecord[key] = item[key]; + } + }); + refinedResults.push(newRecord); + } + }); + let newRecord:any = {} + Object.keys(item).map(key => { + if (key !== "synonym") { + if (key === "label") { + newRecord[key] = item[key] + " (" + item["short_form"] + ")"; + } else { + newRecord[key] = item[key]; + } + } + }); + refinedResults.push(newRecord); + } else { + let newRecord:any = {} + Object.keys(item).map(key => { + if (key === "label") { + newRecord[key] = item[key] + " (" + item["short_form"] + ")"; + } else { + newRecord[key] = item[key]; + } + }); + refinedResults.push(newRecord); + } + }); + + var sortedResults: Array = refinedResults.sort(sorter); + this.postMessage({resultMessage: "OK", params: {results: sortedResults}}); + self.close(); +} + +export const datasourceConfiguration = { + "url": "https://solr-dev.virtualflybrain.org/solr/ontology/select", + "query_settings": + { + "q": "$SEARCH_TERM$ OR $SEARCH_TERM$* OR *$SEARCH_TERM$*", + "defType": "edismax", + "qf": "label^100 synonym^100 label_autosuggest_ws label_autosuggest_e label_autosuggest synonym_autosuggest_ws synonym_autosuggest shortform_autosuggest", + "indent": "true", + "fl": "short_form,label,synonym,id,facets_annotation", + "start": "0", + "pf":"true", + "fq": [ + "facets_annotation:has_neuron_connectivity", + "shortform_autosuggest:VFB* OR shortform_autosuggest:FB*" + ], + "rows": "100", + "wt": "json", + "bq": "shortform_autosuggest:VFB*^110.0 shortform_autosuggest:FBbt*^100.0 label_s:\"\"^2 synonym_s:\"\" short_form:FBbt_00003982^2 facets_annotation:Deprecated^0.001" + } +}; + +export const searchConfiguration = { + "resultsMapping": + { + "name": "label", + "id": "short_form" + }, + "filters_expanded": true, + "filters": [ + { + "key": "facets_annotation", + "filter_name": "Filters", + "type": "array", + "enabled": "disabled", + "disableGlobal": true, + "values": [ + { + "key": "Adult", + "filter_name": "Adult", + "enabled": "disabled", + }, + { + "key": "Larva", + "filter_name": "Larva", + "enabled": "disabled", + }, + { + "key": "Nervous_system", + "filter_name": "Nervous System", + "enabled": "disabled", + }, + { + "key": "Anatomy", + "filter_name": "Anatomy", + "enabled": "disabled", + }, + { + "key": "Neuron", + "filter_name": "Neuron", + "enabled": "disabled", + }, + { + "key": "has_image", + "filter_name": "Image", + "enabled": "disabled", + }, + { + "key": "Split", + "filter_name": "Split Expression", + "enabled": "disabled", + }, + { + "key": "Expression_pattern", + "filter_name": "Expression Pattern", + "enabled": "disabled", + }, + { + "key": "Expression_pattern_fragment", + "filter_name": "Expression Pattern Fragment", + "enabled": "disabled", + }, + { + "key": "has_neuron_connectivity", + "filter_name": "Neuron with Connectivity", + "enabled": "disabled", + }, + { + "key": "NBLAST", + "filter_name": "Neuron Similarity (NBLAST)", + "enabled": "disabled", + }, + { + "key": "NBLASTexp", + "filter_name": "Expression Similarity (NBLAST)", + "enabled": "disabled", + }, + { + "key": "Synaptic_neuropil_domain", + "filter_name": "Synaptic Neuropil", + "enabled": "disabled", + }, + { + "key": "DataSet", + "filter_name": "Dataset", + "enabled": "disabled", + }, + { + "key": "Deprecated", + "filter_name": "Deprecated", + "enabled": "negative", + } + ] + }, + ], + "sorter": function (a, b) { + var InputString = window.spotlightString; + // move exact matches to top + if (InputString == a.label) { + return -1; + } + if (InputString == b.label) { + return 1; + } + // close match without case matching + if (InputString.toLowerCase() == a.label.toLowerCase()) { + return -1; + } + if (InputString.toLowerCase() == b.label.toLowerCase()) { + return 1; + } + // match ignoring joinging nonwords + if (InputString.toLowerCase().split(/\W+/).join(' ') == a.label.toLowerCase().split(/\W+/).join(' ')) { + return -1; + } + if (InputString.toLowerCase().split(/\W+/).join(' ') == b.label.toLowerCase().split(/\W+/).join(' ')) { + return 1; + } + // match against id + if (InputString.toLowerCase() == a.id.toLowerCase()) { + return -1; + } + if (InputString.toLowerCase() == b.id.toLowerCase()) { + return 1; + } + // pick up any match without nonword join character match + if (a.label.toLowerCase().split(/\W+/).join(' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ')) < 0 && b.label.toLowerCase().split(/\W+/).join(' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ')) > -1) { + return 1; + } + if (b.label.toLowerCase().split(/\W+/).join(' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ')) < 0 && a.label.toLowerCase().split(/\W+/).join(' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ')) > -1) { + return -1; + } + // also with underscores ignored + if (a.label.toLowerCase().split(/\W+/).join(' ').replace('_', ' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ').replace('_', ' ')) < 0 && b.label.toLowerCase().split(/\W+/).join(' ').replace('_', ' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ').replace('_', ' ')) > -1) { + return 1; + } + if (b.label.toLowerCase().split(/\W+/).join(' ').replace('_', ' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ').replace('_', ' ')) < 0 && a.label.toLowerCase().split(/\W+/).join(' ').replace('_', ' ').indexOf(InputString.toLowerCase().split(/\W+/).join(' ').replace('_', ' ')) > -1) { + return -1; + } + // find all matching spaced words + if (InputString.toLowerCase().indexOf(' ') > -1) { + var lcInputStingFac = InputString.toLowerCase().split(' '); + var compare = (a1, a2) => a1.filter(v => a2.includes(v)).length; + var cA = compare(lcInputStingFac, a.label.toLowerCase().split(' ')); + var cB = compare(lcInputStingFac, b.label.toLowerCase().split(' ')); + if (cA > 0 || cB > 0) { + if (cA > cB) { + return -1; + } + if (cA < cB) { + return 1; + } + } + } + // find all tokenised word matches + if (InputString.split(/\W+/).length > 1) { + var lcInputStingFac = InputString.toLowerCase().split(/\W+/); + var compare = (a1, a2) => a1.filter(v => a2.includes(v)).length; + var cA = compare(lcInputStingFac, a.label.toLowerCase().split(/\W+/)); + var cB = compare(lcInputStingFac, b.label.toLowerCase().split(/\W+/)); + if (cA > 0 || cB > 0) { + if (cA > cB) { + return -1; + } + if (cA < cB) { + return 1; + } + } + } + // prioritise matches in the primary label + if (InputString.split(/\W+/).length > 1) { + var lcInputStingFac = InputString.toLowerCase().split(/\W+/); + var compare = (a1, a2) => a1.filter(v => a2.includes(v)).length; + var aLabel = a.label.split(' ('); + var aEnd = aLabel.pop(aLabel.length); + aLabel = aLabel.join(' ('); + var bLabel = b.label.split(' ('); + var bEnd = bLabel.pop(bLabel.length); + bLabel = bLabel.join(' ('); + var cA = compare(lcInputStingFac, aLabel.toLowerCase().split(/\W+/)); + var cB = compare(lcInputStingFac, bLabel.toLowerCase().split(/\W+/)); + if (cA > 0 || cB > 0) { + if (cA > cB) { + return -1; + } + if (cA < cB) { + return 1; + } + } + } + // if not found in one then advance the other + if (a.label.toLowerCase().indexOf(InputString.toLowerCase()) < 0 && b.label.toLowerCase().indexOf(InputString.toLowerCase()) > -1) { + return 1; + } + if (b.label.toLowerCase().indexOf(InputString.toLowerCase()) < 0 && a.label.toLowerCase().indexOf(InputString.toLowerCase()) > -1) { + return -1; + } + // if the match is closer to start than the other move up + if (a.label.toLowerCase().indexOf(InputString.toLowerCase()) > -1 && a.label.toLowerCase().indexOf(InputString.toLowerCase()) < b.label.toLowerCase().indexOf(InputString.toLowerCase())) { + return -1; + } + if (b.label.toLowerCase().indexOf(InputString.toLowerCase()) > -1 && b.label.toLowerCase().indexOf(InputString.toLowerCase()) < a.label.toLowerCase().indexOf(InputString.toLowerCase())) { + return 1; + } + // if the match in the id is closer to start then move up + if (a.id.toLowerCase().indexOf(InputString.toLowerCase()) > -1 && a.id.toLowerCase().indexOf(InputString.toLowerCase()) < b.id.toLowerCase().indexOf(InputString.toLowerCase())) { + return -1; + } + if (b.id.toLowerCase().indexOf(InputString.toLowerCase()) > -1 && b.id.toLowerCase().indexOf(InputString.toLowerCase()) < a.id.toLowerCase().indexOf(InputString.toLowerCase())) { + return 1; + } + // move the shorter synonyms to the top + if (a.label < b.label) { + return -1; + } else if (a.label > b.label) { + return 1; + } else { + return 0; + } // if nothing found then do nothing. + } +}; + diff --git a/components/configuration/VFBCircuitBrowser/datasources/datasources.tsx b/components/configuration/VFBCircuitBrowser/datasources/datasources.tsx new file mode 100644 index 000000000..1b5496503 --- /dev/null +++ b/components/configuration/VFBCircuitBrowser/datasources/datasources.tsx @@ -0,0 +1,4 @@ +export enum DatasourceTypes { + CUSTOM = 'CUSTOM', + SOLRClient = 'SOLR' +} \ No newline at end of file diff --git a/components/configuration/VFBMain/searchConfiguration.js b/components/configuration/VFBMain/searchConfiguration.js index 1ddf7d1fb..e0eea803d 100644 --- a/components/configuration/VFBMain/searchConfiguration.js +++ b/components/configuration/VFBMain/searchConfiguration.js @@ -139,8 +139,8 @@ var searchConfiguration = { "enabled": "disabled", }, { - "key": "Expression_pattern", - "filter_name": "Expression Pattern", + "key": "Neuron", + "filter_name": "Neuron", "enabled": "disabled", }, { @@ -148,19 +148,39 @@ var searchConfiguration = { "filter_name": "Image", "enabled": "disabled", }, + { + "key": "Split", + "filter_name": "Split Expression", + "enabled": "disabled", + }, + { + "key": "Expression_pattern", + "filter_name": "Expression Pattern", + "enabled": "disabled", + }, + { + "key": "Expression_pattern_fragment", + "filter_name": "Expression Pattern Fragment", + "enabled": "disabled", + }, { "key": "has_neuron_connectivity", - "filter_name": "Neurons with Connectivity", + "filter_name": "Neuron with Connectivity", "enabled": "disabled", }, { - "key": "Synaptic_neuropil_domain", - "filter_name": "Synaptic Neuropil", + "key": "NBLAST", + "filter_name": "Neuron Similarity (NBLAST)", "enabled": "disabled", }, { - "key": "Neuron", - "filter_name": "Neuron", + "key": "NBLASTexp", + "filter_name": "Expression Similarity (NBLAST)", + "enabled": "disabled", + }, + { + "key": "Synaptic_neuropil_domain", + "filter_name": "Synaptic Neuropil", "enabled": "disabled", }, { diff --git a/components/configuration/VFBTermInfo/VFBTermInfoConfiguration.js b/components/configuration/VFBTermInfo/VFBTermInfoConfiguration.js index 93841525d..91b2d2aec 100644 --- a/components/configuration/VFBTermInfo/VFBTermInfoConfiguration.js +++ b/components/configuration/VFBTermInfo/VFBTermInfoConfiguration.js @@ -136,13 +136,13 @@ const buttonBarControls = { const linksConfiguration = { // Graph Links configuration, name of key "Graph" must not be changed "Graph": { - "title": "Graphs For", + "title": "Graphs for", "visibility": true, "superType": "Anatomy" }, // CircuitBrowser Links configuration, name of key "CircuitBrowser" must not be changed "CircuitBrowser": { - "title": "Add Neuron to Circuit Browser", + "title": "Add Neuron to Circuit Browser Query", "visibility": true, "superType": "has_neuron_connectivity" } diff --git a/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js b/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js index 67169b050..a85df3499 100644 --- a/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js +++ b/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js @@ -253,6 +253,14 @@ var toolbarMenu = { parameters: ["circuitBrowserVisible"] } }, + { + label: "NBLAST Uploader", + icon: "fa fa-upload", + action: { + handlerAction: "uploaderContentsVisible", + parameters: [] + } + }, { label: "NBLAST", icon: "", @@ -447,6 +455,14 @@ var toolbarMenu = { parameters: ["undefinedAction"] }, list: [ + { + label: "Adult Head (McKellar)", + icon: "", + action: { + handlerAction: "openNewTab", + parameters: ["/org.geppetto.frontend/geppetto?i=VFB_00110000"] + } + }, { label: "Adult Brain (JFRC2/2010)", icon: "", @@ -560,6 +576,14 @@ var toolbarMenu = { parameters: ["undefinedAction"] }, list: [ + { + label: "Adult Head (McKellar)", + icon: "", + action: { + handlerAction: "triggerRunQuery", + parameters: ["AlignedDatasets,VFB_00110000,adult head template McKellar"] + } + }, { label: "Adult Brain (JFRC2/2010)", icon: "", diff --git a/components/configuration/VFBUploader/configuration.json b/components/configuration/VFBUploader/configuration.json new file mode 100644 index 000000000..a6d1a1091 --- /dev/null +++ b/components/configuration/VFBUploader/configuration.json @@ -0,0 +1,12 @@ +{ + "nblastURL" : "https://zip.virtualflybrain.org/download", + "contentType" : "", + "templates" : [ + { "VFB_00101567" : "VFB_00101567 Template" }, + { "VFB_00000001" : "VFB_00000001 Template" } + ], + "acceptedFiles" : [".swc"], + "filesLimit" : 10, + "maxFileSize" : 3000000, + "dropZoneMessage" : "Drag and drop a SWC file here or click" +} \ No newline at end of file diff --git a/components/interface/VFBCircuitBrowser/Controls.js b/components/interface/VFBCircuitBrowser/Controls.js index 8184987b6..a6c6e1838 100644 --- a/components/interface/VFBCircuitBrowser/Controls.js +++ b/components/interface/VFBCircuitBrowser/Controls.js @@ -27,8 +27,7 @@ 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"; +import { getResultsSOLR } from "../../configuration/VFBCircuitBrowser/datasources/SOLRclient"; /** * Create a local theme to override some default values in material-ui components @@ -100,7 +99,18 @@ const styles = theme => ({ backgroundColor: "#80808040 !important", paddingLeft : "10px !important" }, - weightInputDiv : { width : "100% !important" } + weightInputDiv : { width : "100% !important" }, + refreshButton : { + backgroundColor : "#0AB7FE", + flexBasis: "100%", + fontWeight : 600, + }, + clearButton : { + backgroundColor : "#E53935", + flexBasis: "100%", + fontWeight : 600, + }, + slider : { color: '#0AB7FE' } }); /** @@ -111,8 +121,8 @@ 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; +const searchConfiguration = require('./../../configuration/VFBCircuitBrowser/datasources/SOLRclient').searchConfiguration; +const datasourceConfiguration = require('./../../configuration/VFBCircuitBrowser/datasources/SOLRclient').datasourceConfiguration; /** * Create custom marks for Paths slider. @@ -136,6 +146,7 @@ class AutocompleteResults extends Component { super(props); this.state = { filteredResults: {} }; this.handleResults = this.handleResults.bind(this); + this.fieldLabel = this.props.field.label; } /** @@ -144,14 +155,9 @@ class AutocompleteResults extends Component { 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; - } + results[result?.label] = result; }); - + this.setState({ filteredResults : results }); } @@ -159,9 +165,15 @@ class AutocompleteResults extends Component { return this.state.filteredResults; } + shouldComponentUpdate(nextProps, nextState) { + this.fieldLabel = nextProps.getLatestNeuronFields()[this.props.index].label; + return true; + } + render () { const label = "Neuron " + (this.props.index + 1) .toString(); - + const options = Object.keys(this.state.filteredResults).map(option => this.state.filteredResults[option].label); + return ( @@ -315,7 +328,6 @@ class Controls extends Component { neurons.push({ id : target.value, label : target.value }); } - // this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, neurons); this.neuronFields = neurons; getResultsSOLR( target.value, this.autocompleteRef[this.setInputValue].current.handleResults,searchConfiguration.sorter,datasourceConfiguration ); } @@ -329,20 +341,36 @@ class Controls extends Component { if (this.state.typingTimeout) { clearTimeout(this.typingTimeout); } - // Create a setTimeout interval, to avoid performing searches on every stroke - setTimeout(this.typingTimeout, 10, event.target); + + this.setInputValue = event.target.id; + if ( event.target.id.id === "" ) { + this.setInputValue = event.target.parentElement.id; + } + let neurons = this.neuronFields; + + if ( neurons[parseInt(event.target.id)] ) { + neurons[parseInt(event.target.id)] = { id : event.target.value, label : event.target.value }; + } else { + neurons.push({ id : event.target.value, label : event.target.value }); + } + + if ( event?.nativeEvent?.inputType === "deleteContentBackward" && neurons?.find( (neuron, index) => neuron.id === "" && index.toString() === event.target.id )){ + this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, neurons); + } else { + getResultsSOLR( event.target.value, this.autocompleteRef[this.setInputValue].current.handleResults,searchConfiguration.sorter,datasourceConfiguration ); + } + this.neuronFields = neurons; } /** * Handle SOLR result selection, activated by selecting from drop down menu under textfield */ - resultSelectedChanged (event, value) { + resultSelectedChanged (event, value, index) { // Copy neurons and add selection to correct array index let neurons = this.neuronFields; let textFieldId = event.target.id.toString().split("-")[0]; let shortForm = this.autocompleteRef[textFieldId].current.getFilteredResults()[value] && this.autocompleteRef[textFieldId].current.getFilteredResults()[value].short_form; - let index = neurons.findIndex(neuron => neuron.id === shortForm); - index > -1 ? neurons[index] = { id : shortForm, label : value } : null + neurons[index] = { id : shortForm, label : value }; // Keep track of query selected, and send an event to redux store that circuit has been updated this.circuitQuerySelected = neurons; @@ -370,6 +398,7 @@ class Controls extends Component { while (this?.props?.circuitQuerySelected.length > 0) { this?.props?.circuitQuerySelected.pop(); } + this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, []) this.setState({ key: Math.random() }); } /** @@ -384,17 +413,19 @@ class Controls extends Component { ); if ( !fieldExists) { - for ( var j = 0 ; j < neuronFields.length ; j++ ) { - if ( neuronFields?.[j].id === "" ) { - neuronFields[j] = { id : this.props.circuitQuerySelected[i].id ? this.props.circuitQuerySelected[i].id : this.props.circuitQuerySelected[i], label : this.props.circuitQuerySelected[i].label ? this.props.circuitQuerySelected[i].label : this.props.circuitQuerySelected[i] }; - added = true; - fieldExists = true; - break; - } + const emptyIndex = neuronFields.findIndex( field => field.id === ""); + if ( emptyIndex >= 0 ) { + neuronFields[emptyIndex] = { id : this.props.circuitQuerySelected[i].id ? this.props.circuitQuerySelected[i].id : this.props.circuitQuerySelected[i], label : this.props.circuitQuerySelected[i].label ? this.props.circuitQuerySelected[i].label : this.props.circuitQuerySelected[i] }; + added = true; + fieldExists = true; + break; + } else { + neuronFields.pop(); + neuronFields.push({ id : this.props.circuitQuerySelected[i].id ? this.props.circuitQuerySelected[i].id : this.props.circuitQuerySelected[i], label : this.props.circuitQuerySelected[i].label ? this.props.circuitQuerySelected[i].label : this.props.circuitQuerySelected[i] }) } if ( this.props.circuitQuerySelected.length > neuronFields.length && !fieldExists && this.props.circuitQuerySelected?.[i]?.id != "") { - if ( neuronFields.length < configuration.maxNeurons && this.props.circuitQuerySelected !== "" ) { + if ( this.props.circuitQuerySelected !== "" ) { neuronFields.push({ id : this.props.circuitQuerySelected[i].id ? this.props.circuitQuerySelected[i].id : this.props.circuitQuerySelected[i], label : this.props.circuitQuerySelected[i].label ? this.props.circuitQuerySelected[i].label : this.props.circuitQuerySelected[i] }); } } @@ -409,7 +440,7 @@ class Controls extends Component { const { classes } = this.props; this.circuitQuerySelected = this.props.circuitQuerySelected; let neuronFields = this.getUpdatedNeuronFields(); - + let expanded = this.state.expanded; if ( this.props.resultsAvailable() ){ expanded = true; @@ -433,9 +464,10 @@ class Controls extends Component { { this.props.resultsAvailable() ?
    { this.props.legend.map((label, index) => ( -
  • {label}
  • +
  • {label}
  • )) } +
  • WEIGHT -Forward [Reverse]→
: null } @@ -467,7 +499,8 @@ class Controls extends Component { field={field} index={index} neuronTextfieldModified={this.neuronTextfieldModified} - resultSelectedChanged={this.resultSelectedChanged} + getLatestNeuronFields={this.getUpdatedNeuronFields} + resultSelectedChanged={(event, value) => this.resultSelectedChanged(event, value, index)} ref={this.autocompleteRef[index.toString()]} /> @@ -535,6 +568,7 @@ class Controls extends Component { valueLabelDisplay="auto" min={configuration.minPaths} max={configuration.maxPaths} + classes={{ root : classes.slider }} /> @@ -547,18 +581,18 @@ class Controls extends Component { + onClick={() => this.props.updateGraph(this.neuronFields, this.paths, this.weight)} + >Run Query diff --git a/components/interface/VFBCircuitBrowser/QueryParser.js b/components/interface/VFBCircuitBrowser/QueryParser.js index 3b657930c..6e2df5988 100644 --- a/components/interface/VFBCircuitBrowser/QueryParser.js +++ b/components/interface/VFBCircuitBrowser/QueryParser.js @@ -5,6 +5,7 @@ export function queryParser (e) { // The nodes and links arrays used for the graph let nodes = [], links = []; let graphData = e.data.params.results; + let weight = e.data.params.weight; console.log("Results ", e); // Reads graph data let data = graphData?.results[0]?.data; @@ -51,7 +52,9 @@ export function queryParser (e) { if (allRelationships.get(parseInt(startNode)) === undefined) { allRelationships.set(parseInt(startNode), new Array()); } - allRelationships?.get(parseInt(startNode))?.push( { target : parseInt(endNode), label : properties[e.data.params.configuration.resultsMapping.link.label], weight : properties[e.data.params.configuration.resultsMapping.link.weight] }); + if ( data[0]?.row[3].includes(parseInt(id)) ) { + allRelationships?.get(parseInt(startNode))?.push( { target : parseInt(endNode), label : properties[e.data.params.configuration.resultsMapping.link.label], weight : properties[e.data.params.configuration.resultsMapping.link.weight] }); + } }); }); @@ -129,6 +132,7 @@ export function queryParser (e) { let hopsMap = hopAssignment(sourceNodeID, targetNodeID, allRelationships, 0, {}); maxHops = Math.max.apply(null, Object.keys(hopsMap)?.map( key => parseInt(hopsMap[key]))); + let positionArray = []; // Loop through nodes and assign X position based on hops nodes.forEach( sourceNode => { let id = sourceNode.id; @@ -152,24 +156,27 @@ export function queryParser (e) { positionX = (maxHops * (-1 * spaceBetween) ) + space sourceNode.positionX = positionX; } + + + if ( positionArray.some(row => row[0] == sourceNode.level && row[1] == sourceNode.hop ) ) { + sourceNode.level = sourceNode.level + 1 ; + } else { + positionArray.push([sourceNode.level, sourceNode.hop]); + } }); + // Creates links map from Relationships, avoid duplicates data.forEach(({ graph, row }) => { graph.relationships.forEach(({ startNode, endNode, properties, id }) => { let matchingStartNode = nodes.find(node => node.id === parseInt(startNode)); let matchingEndNode = nodes.find(node => node.id === parseInt(endNode)); - let reverseLink = false; - if ( matchingStartNode.positionX >= matchingEndNode.positionX ){ - reverseLink = true - } - - if ( !reverseLink ) { + if ( row[3].includes(parseInt(id)) ) { if (linksMap.get(startNode) === undefined) { linksMap.set(startNode, new Array()); } - + let newLink = true; linksMap.get(startNode).find( ele => { if ( ele.target !== endNode ) { @@ -178,7 +185,7 @@ export function queryParser (e) { newLink = false; } }); - + // Only keep track of new links, avoid duplicates if ( newLink ) { linksMap.get(startNode).push( { target : endNode, label : properties[e.data.params.configuration.resultsMapping.link.label], weight : properties[e.data.params.configuration.resultsMapping.link.weight] }); @@ -188,30 +195,11 @@ export function queryParser (e) { if (reverseMap.get(startNode) === undefined) { reverseMap.set(startNode, new Array()); } - reverseMap.get(startNode).push( { target : endNode, label : properties[e.data.params.configuration.resultsMapping.link.label], weight : properties[e.data.params.configuration.resultsMapping.link.weight] }); - } + } }); }); - // Loop through reverse map and find reverse links - reverseMap.forEach( (value, key) => { - let linkInMap = value.find( valueNode => { - let found = linksMap?.get(valueNode.target)?.find( ele => { - if ( ele.target === key ) { - return ele; - } - }); - - if ( !found ) { - if (linksMap.get(key) === undefined) { - linksMap.set(key, new Array()); - } - linksMap?.get(key)?.push( { target : valueNode?.target, label : valueNode?.label, weight : valueNode?.weight }); - } - }) - }); - // Creates Links array with nodes, assign node neighbors that are connected by links nodes.forEach( sourceNode => { let id = sourceNode.id; @@ -227,7 +215,8 @@ export function queryParser (e) { let reverse = reverseMap.get(targetNode.id.toString())?.find( node => node.target === sourceNode.id.toString()); if ( !match ) { // Create tooltip label for link and weight - const tooltip = "Label : " + n[i].label + '
' + const labelWeight = n[i].weight >= weight ? n[i].weight : 0; + const tooltip = "Label : " + n[i].label + '
' + "Weight : " + (reverse ? n[i].weight + " [" + reverse.weight + "]" : n[i].weight + "[0]"); const weightLabel = reverse ? n[i].weight + " [" + reverse.weight + "]" : n[i].weight + "[0]"; // Create new link for graph diff --git a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js index 7e5cdb005..04bd523ba 100644 --- a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js +++ b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js @@ -57,7 +57,7 @@ class VFBCircuitBrowser extends Component { queryLoaded : false, dropDownAnchorEl : null, neurons : [{ id : "", label : "" } , { id : "", label : "" }], - hops : Math.ceil((configuration.maxHops - configuration.minHops) / 2), + paths : Math.ceil((configuration.maxPaths - configuration.minPaths) / 2), weight : 0, reload : false } @@ -249,7 +249,8 @@ class VFBCircuitBrowser extends Component { results: response.data, configuration : configuration, styling : stylingConfiguration, - hops : self.state.hops, + paths : self.state.paths, + weight : self.state.weight, NODE_WIDTH : NODE_WIDTH, NODE_HEIGHT : NODE_HEIGHT } @@ -299,7 +300,7 @@ class VFBCircuitBrowser extends Component { this.circuitQuerySelected = circuitQuerySelected; let errorMessage = "Not enough input queries to create a graph, needs 2."; - if ( this.state.neurons?.[0].id != "" && this.state.neurons?.[1].id != "" ){ + 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 ( @@ -321,11 +322,12 @@ class VFBCircuitBrowser extends Component { resetCamera={self.resetCamera} zoomIn={self.zoomIn} zoomOut={self.zoomOut} - circuitQuerySelected={this.circuitQuerySelected} + circuitQuerySelected={circuitQuerySelected} datasource="SOLR" legend = {self.state.legend} ref={self.controlsRef} clearGraph={self.clearGraph} + key="controls" /> : { node.fx = node.positionX; - node.fy = node.level > 0 ? -100 * node.level : node.fy ? node.fy : 0 ; + node.fy = -100 * node.level }} dagLevelDistance = {25} onDagError={loopNodeIds => {}} diff --git a/components/interface/VFBTermInfo/VFBTermInfo.js b/components/interface/VFBTermInfo/VFBTermInfo.js index e9221f535..e5ff6ea32 100644 --- a/components/interface/VFBTermInfo/VFBTermInfo.js +++ b/components/interface/VFBTermInfo/VFBTermInfo.js @@ -336,8 +336,8 @@ class VFBTermInfo extends React.Component { let graphs = new Array(); for (var j = 0; j < values.length; j++) { graphs.push( @@ -787,6 +787,14 @@ class VFBTermInfoWidget extends React.Component { $('#add-new-query-container')[0].hidden = true; $('#query-builder-items-container')[0].hidden = true; } + + /** + * Fire event to set the Shift key as not pressed, this is needed since the presence of the + * confirm() dialog prevents the DOM to un-set the 'shift' key. + */ + var e = new KeyboardEvent('keyup', { bubbles : true, cancelable : true, shiftKey : false }); + document.querySelector("body").dispatchEvent(e); + $("body").css("cursor", "progress"); diff --git a/components/interface/VFBUploader/VFBUploader.js b/components/interface/VFBUploader/VFBUploader.js new file mode 100644 index 000000000..020486f0d --- /dev/null +++ b/components/interface/VFBUploader/VFBUploader.js @@ -0,0 +1,172 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import FormControl from '@material-ui/core/FormControl'; +import Typography from '@material-ui/core/Typography'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import InputLabel from '@material-ui/core/InputLabel'; +import Select from '@material-ui/core/Select'; +import ChevronRightIcon from "@material-ui/icons/ChevronRight"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import Checkbox from '@material-ui/core/Checkbox'; +import { withStyles } from '@material-ui/styles'; +import axios from 'axios'; +import { DropzoneArea } from 'material-ui-dropzone' + +const PERMISSIONS = "Permissions to store URL in browser history"; +const styles = theme => ({ + listItemText: { fontSize:'1em' }, + templateSelection: { + width : "30% !important", + height : "5rem" + }, + templateContent : { fontSize : "14px" }, + dialogActions : { justifyContent : "space-evenly" } +}); + +class VFBUploader extends React.Component { + constructor (props) { + super(props); + + this.state = { + open: false, + uploading : false, + nblastEnabled : false, + files : [], + permissionsChecked : false, + templateSelected : "" + } + + this.configuration = require('../../configuration/VFBUploader/configuration'); + this.handleCloseDialog = this.handleCloseDialog.bind(this); + this.openDialog = this.openDialog.bind(this); + this.handlePermissionsCheck = this.handlePermissionsCheck.bind(this); + this.handleNBLASTAction = this.handleNBLASTAction.bind(this); + this.requestUpload = this.requestUpload.bind(this); + } + + handleCloseDialog () { + this.setState({ open : false }); + } + + handleDropZoneChange (files){ + this.setState({ files: files }); + } + + handleTemplateChange (event) { + this.setState({ templateSelected : event.target.value }) + } + + openDialog () { + this.setState({ open : true }); + } + + handleNBLASTAction () { + this.requestUpload({}); + } + + /** + * Make axios call to download the zip + */ + requestUpload (jsonRequest) { + let self = this; + + this.setState( { uploading : true } ); + // Axios HTTP Post request with post query + axios({ + method: 'post', + url: this.configuration.nblastURL, + headers: { 'content-type': this.configuration.contentType }, + data: jsonRequest, + responseType: "arraybuffer" + }).then( function (response) { + const url = window.URL.createObjectURL(new Blob([response.data])); + setTimeout( () => self.setState( { nblastEnabled : true, uploading : false } ), 500); + }).catch( function (error) { + self.downloadErrorMessage = error?.message; + self.setState( { nblastEnabled : true, uploading : false } ); + }) + } + + /** + * Handle checkbox selection of different types to download + */ + handlePermissionsCheck (event) { + this.setState({ permissionsChecked : event.target.checked }); + } + + render () { + let self = this; + const { classes } = this.props; + + return ( + +

NBLAST Uploader

+ + + Choose Template: + + + + + +
+ --- NBLAST Text Updates --- + self.handlePermissionsCheck(event)} + /> + } + label={{PERMISSIONS}} + /> +
+ { + self.state.uploading ? : null + } + +
+
+ ) + } +} + +export default (withStyles(styles)(VFBUploader)); \ No newline at end of file diff --git a/dockerFiles/startup.sh b/dockerFiles/startup.sh index b3bc4f5da..218201639 100755 --- a/dockerFiles/startup.sh +++ b/dockerFiles/startup.sh @@ -17,8 +17,8 @@ then grep -rls url $HOME/workspace/org.geppetto.frontend/src/main/webapp/components/configuration/VFBGraph/graphConfiguration.js grep -rls url $HOME/workspace/org.geppetto.frontend/src/main/webapp/components/configuration/VFBGraph/graphConfiguration.js | xargs sed -i "s@https://pdb.*virtualflybrain.org@$VFB_TREE_PDB_SERVER@g" echo "Server PDB: $VFB_PDB_SERVER" - grep -rls http://pdb.virtualflybrain.org $HOME/workspace/org.geppetto.frontend/src/main/webapp/model/vfb.xmi - grep -rls http://pdb.virtualflybrain.org $HOME/workspace/org.geppetto.frontend/src/main/webapp/model/vfb.xmi | xargs sed -i "s@http://pdb.*virtualflybrain.org@$VFB_PDB_SERVER@g" + grep -rls "http://pdb.*virtualflybrain.org" $HOME/workspace/org.geppetto.frontend/src/main/webapp/model/vfb.xmi + grep -rls "http://pdb.*virtualflybrain.org" $HOME/workspace/org.geppetto.frontend/src/main/webapp/model/vfb.xmi | xargs sed -i "s@http://pdb.*virtualflybrain.org@$VFB_PDB_SERVER@g" echo "Server OWL: $VFB_OWL_SERVER" grep -rls http://owl.virtualflybrain.org/kbs/vfb/ $HOME/workspace/org.geppetto.frontend/src/main/webapp/model/vfb.xmi grep -rls http://owl.virtualflybrain.org/kbs/vfb/ $HOME/workspace/org.geppetto.frontend/src/main/webapp/model/vfb.xmi | xargs sed -i "s@http://owl.*virtualflybrain.org/kbs/vfb/@$VFB_OWL_SERVER@g" @@ -26,8 +26,8 @@ then grep -rls http://r.virtualflybrain.org/ocpu/library/vfbr/R/vfb_nblast $HOME/workspace/org.geppetto.frontend/src/main/webapp/model/vfb.xmi grep -rls http://r.virtualflybrain.org/ocpu/library/vfbr/R/vfb_nblast $HOME/workspace/org.geppetto.frontend/src/main/webapp/model/vfb.xmi | xargs sed -i "s@http://r.virtualflybrain.org/ocpu/library/vfbr/R/vfb_nblast@$VFB_R_SERVER@g" echo "Client SOLR Server: $SOLR_SERVER" - grep -rls https://solr.virtualflybrain.org/solr/ontology/select $HOME/workspace/org.geppetto.frontend/src/main/webapp/components/configuration/VFBMain/ - grep -rls https://solr.virtualflybrain.org/solr/ontology/select $HOME/workspace/org.geppetto.frontend/src/main/webapp/components/configuration/VFBMain/ | xargs sed -i "s@https://solr.*virtualflybrain.org/solr/ontology/select@$SOLR_SERVER@g" + grep -rls https://solr.*virtualflybrain.org/solr/ontology/select $HOME/workspace/org.geppetto.frontend/src/main/webapp/components/configuration/ + grep -rls https://solr.*virtualflybrain.org/solr/ontology/select $HOME/workspace/org.geppetto.frontend/src/main/webapp/components/configuration/ | xargs sed -i "s@https://solr.*virtualflybrain.org/solr/ontology/select@$SOLR_SERVER@g" echo "Google Analytics code: ${googleAnalyticsSiteCode}" grep -rls "ga('create', 'UA-" $HOME/workspace/org.geppetto.frontend/ grep -rls "ga('create', 'UA-" $HOME/workspace/org.geppetto.frontend/ | xargs sed -i "s@ga('create', 'UA-[0-9]*-[0-9]'@ga('create', '${googleAnalyticsSiteCode}'@g" diff --git a/package.json b/package.json index a64b64f55..eb74472e6 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "jest-image-snapshot": "^4.1.0", "jest-puppeteer": "^4.3.0", "less-loader": "^5.0.0", + "material-ui-dropzone": "^3.5.0", "mini-css-extract-plugin": "^0.7.0", "puppeteer": "^1.17.0", "react-collapsible": "^2.3.1", diff --git a/reducers/generals.js b/reducers/generals.js index f9b5806bf..97299f6b9 100644 --- a/reducers/generals.js +++ b/reducers/generals.js @@ -17,6 +17,7 @@ import { } from '../actions/generals'; const componentsMap = require('../components/configuration/VFBLoader/VFBLoaderConfiguration').componentsMap; +const configuration = require('../components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration').configuration; export const GENERAL_DEFAULT_STATE = { error: undefined, @@ -42,7 +43,7 @@ export const GENERAL_DEFAULT_STATE = { termInfo : { termInfoVisible : false }, layers : { listViewerInfoVisible : true }, circuitBrowser : { - circuitQuerySelected : [], + circuitQuerySelected : [{ id : "", label : "" } , { id : "", label : "" }], visible : true }, layout: { @@ -248,7 +249,21 @@ function generalReducer (state, action) { newQueryMap = action.data.instance; } else { // Instance is object - !state.ui.circuitBrowser.circuitQuerySelected.includes(action.data.instance) ? newQueryMap = [...state.ui.circuitBrowser.circuitQuerySelected, action.data.instance] : newQueryMap = [...state.ui.circuitBrowser.circuitQuerySelected]; + let match = state.ui.circuitBrowser.circuitQuerySelected?.find( query => query.id === action.data.instance.id ); + if ( match ) { + newQueryMap = [...state.ui.circuitBrowser.circuitQuerySelected] + } else { + const maxedOut = state.ui.circuitBrowser?.circuitQuerySelected?.find( query => query.id === "" ); + const emptyIndex = state.ui.circuitBrowser?.circuitQuerySelected?.findIndex( field => field.id === ""); + if ( emptyIndex >= 0 ) { + newQueryMap = [...state.ui.circuitBrowser.circuitQuerySelected] + newQueryMap[emptyIndex] = action.data.instance; + } else { + newQueryMap = [...state.ui.circuitBrowser.circuitQuerySelected]; + newQueryMap.pop(); + newQueryMap.push(action.data.instance); + } + } } ui.circuitBrowser.circuitQuerySelected = newQueryMap; diff --git a/tests/jest/vfb/batch2/slice-viewer-tests.js b/tests/jest/vfb/batch2/slice-viewer-tests.js index 726e153aa..56180c0e1 100644 --- a/tests/jest/vfb/batch2/slice-viewer-tests.js +++ b/tests/jest/vfb/batch2/slice-viewer-tests.js @@ -69,7 +69,8 @@ describe('VFB Slice Viewer Component Tests', () => { it('Typing medu in the query builder search bar', async () => { await page.focus('input#query-typeahead'); - await page.keyboard.type('medu'); + await page.keyboard.type('med'); + await page.keyboard.type('ulla'); await page.keyboard.press(String.fromCharCode(13)) await wait4selector(page, 'div.tt-suggestion', { visible: true , timeout : 10000}) diff --git a/tests/jest/vfb/review/circuit-browser-tests.js b/tests/jest/vfb/review/circuit-browser-tests.js index 5bcc709ad..4ae1be1ab 100644 --- a/tests/jest/vfb/review/circuit-browser-tests.js +++ b/tests/jest/vfb/review/circuit-browser-tests.js @@ -6,7 +6,7 @@ import { wait4selector, click, testLandingPage, flexWindowClick, findElementByTe import * as ST from '../selectors'; const baseURL = process.env.url || 'http://localhost:8080/org.geppetto.frontend'; -const projectURL = baseURL + "/geppetto"; +const projectURL = baseURL + "/geppetto?id=VFB_jrchjrch"; const ONE_SECOND = 1000; @@ -19,14 +19,13 @@ describe('VFB Circuit Browser Tests', () => { //increases timeout to ~8 minutes jest.setTimeout(60 * ONE_SECOND); await page.goto(projectURL); - }); //Tests opening term context and clicking on row buttons describe('Test Circuit Browser Component', () => { //Tests components in landing page are present it('Test Landing Page', async () => { - await testLandingPage(page, 'VFB_00101567'); + await testLandingPage(page, 'VFB_jrchjrch'); }) it('Open Circuit Browser', async () => { @@ -36,13 +35,22 @@ describe('VFB Circuit Browser Tests', () => { await wait4selector(page, 'div#VFBCircuitBrowser', { visible: true, timeout : 90 * ONE_SECOND }); }) - it('Set Neuron 1 , VFB_jrchjrch', async () => { - await page.waitFor(5 * ONE_SECOND); - await setTextFieldValue(".neuron1 input", "VFB_jrchjrch") - - await wait4selector(page, 'ul.MuiAutocomplete-listbox', { visible: true, timeout : 90 * ONE_SECOND }); + it('Open Term Info', async () => { + await selectTab(page, "Term Info"); + + // Check that the Tree Browser is visible + await wait4selector(page, '#circuitBrowserLink', { visible: true, timeout : 150 * ONE_SECOND }); + }) + + it('Open Circuit Browser from Term Info with ID : VFB_jrchjrch', async () => { + await page.click("#circuitBrowserLink"); + + // Check that the Tree Browser is visible + await wait4selector(page, 'div#VFBCircuitBrowser', { visible: true, timeout : 90 * ONE_SECOND }); - await page.click('ul.MuiAutocomplete-listbox li'); + await page.waitFor(ONE_SECOND); + const neuron1Input = await page.evaluate( () => document.querySelector(".neuron1 input").value) + expect(neuron1Input).toBe("5-HTPLP01_R (FlyEM-HB:1324365879) (VFB_jrchjrch)"); }) it('Set Neuron 2, VFB_jrchjsfu', async () => { @@ -69,6 +77,7 @@ describe('VFB Circuit Browser Tests', () => { await page.waitFor(ONE_SECOND); await page.click('#refreshCircuitBrowser'); + await wait4selector(page, '.MuiCircularProgress-svg', { visible: true, timeout : 10 * ONE_SECOND }); await page.waitFor(10 * ONE_SECOND); await wait4selector(page, '#circuitBrowserLegend', { visible: true, timeout : 240 * ONE_SECOND }); @@ -82,6 +91,7 @@ describe('VFB Circuit Browser Tests', () => { await page.waitFor(ONE_SECOND); await page.click('#refreshCircuitBrowser'); + await wait4selector(page, '.MuiCircularProgress-svg', { visible: true, timeout : 10 * ONE_SECOND }); await page.waitFor(10 * ONE_SECOND); await wait4selector(page, '#circuitBrowserLegend', { visible: true, timeout : 240 * ONE_SECOND }); diff --git a/tests/jest/vfb/review/query-builder-tests.js b/tests/jest/vfb/review/query-builder-tests.js new file mode 100644 index 000000000..8066d5498 --- /dev/null +++ b/tests/jest/vfb/review/query-builder-tests.js @@ -0,0 +1,53 @@ +const puppeteer = require('puppeteer'); +const { TimeoutError } = require('puppeteer/Errors'); + +import { getUrlFromProjectId } from '../cmdline.js'; +import { wait4selector, click, closeModalWindow, findElementByText } from '../utils'; +import * as ST from '../selectors'; + +const baseURL = process.env.url || 'http://localhost:8080/org.geppetto.frontend'; +const PROJECT_URL = baseURL + "/geppetto?id=VFB_00017894&q=VFB_00000001,SimilarMorphologyTo; VFBexp_FBal0276838,epFrag"; +const COMPOUND_QUERY_MATCHER = "53 Neurons with similar morphology to fru-M-200266 [NBLAST mean score] AND Images of fragments of Scer\\GAL4[fru.P1.D] expression pattern"; + +/** + * Query Builder component tests + */ +describe('VFB Query Builder Tests', () => { + beforeAll(async () => { + jest.setTimeout(1800000); + await page.goto(PROJECT_URL); + }); + + describe('Test landing page', () => { + it('Loading spinner goes away', async () => { + await wait4selector(page, ST.SPINNER_SELECTOR, { hidden: true, timeout : 120000 }) + // Close tutorial window + closeModalWindow(page); + }) + + it('VFB Title shows up', async () => { + const title = await page.title(); + expect(title).toMatch("Virtual Fly Brain"); + }) + + it('Zoom button for VFB_00017894 appears in button bar inside the term info component', async () => { + await wait4selector(page, 'button[id=VFB_00017894_zoom_buttonBar_btn]', { visible: true , timeout : 120000 }) + }) + + it('Term info component created after load', async () => { + await wait4selector(page, 'div#bar-div-vfbterminfowidget', { visible: true }) + }) + }) + + describe('Tests Compound Queries from URL', () => { + it('Wait for First Query', async () => { + await wait4selector(page, '#queryitem-fru-M-200266_0', { visible: true, timeout : 150000 }); + }) + + it('Griddle Results Loads 53 Results for Compounds QUeries', async () => { + await wait4selector(page, '#query-results-container', { visible: true, timeout : 150000 }); + const griddleResults = await page.evaluate(async () => document.getElementsByClassName("result-verbose-label")[0].textContent); + expect(griddleResults.trim()).toEqual(COMPOUND_QUERY_MATCHER); + }) + }) +})