diff --git a/Dockerfile b/Dockerfile index a82f3095b..b389561f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ ARG geppettoSimulationRelease=vfb_20200604_a ARG geppettoDatasourceRelease=vfb_20200604_a ARG geppettoModelSwcRelease=v1.0.1 ARG geppettoFrontendRelease=development -ARG geppettoClientRelease=VFBv2.2.0.7 +ARG geppettoClientRelease=VFBv2.2.2 ARG ukAcVfbGeppettoRelease=download ARG mvnOpt="-Dhttps.protocols=TLSv1.2 -DskipTests --quiet -Pmaster" @@ -38,6 +38,7 @@ ENV VFB_OWL_SERVER=${VFB_OWL_SERVER_ARG} ENV VFB_R_SERVER=${VFB_R_SERVER_ARG} ENV SOLR_SERVER=${SOLR_SERVER_ARG} ENV googleAnalyticsSiteCode=${googleAnalyticsSiteCode_ARG} +ENV LOG4J_FORMAT_MSG_NO_LOOKUPS=true RUN /bin/echo -e "\e[1;35mORIGIN BRANCH ------------ $originBranch\e[0m" &&\ /bin/echo -e "\e[1;35mTARGET BRANCH ------------ $targetBranch\e[0m" &&\ diff --git a/components/VFBMain.js b/components/VFBMain.js index e16e44f06..aefbb6272 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 VFBDownloadContents from './interface/VFBDownloadContents/VFBDownloadContents'; import VFBUploader from './interface/VFBUploader/VFBUploader'; import HTMLViewer from '@geppettoengine/geppetto-ui/html-viewer/HTMLViewer'; import VFBListViewer from './interface/VFBListViewer/VFBListViewer'; @@ -52,6 +53,7 @@ class VFBMain extends React.Component { quickHelpVisible: undefined, UIUpdated: true, wireframeVisible: false, + downloadContentsVisible : true, uploaderContentsVisible : true }; @@ -488,6 +490,12 @@ class VFBMain extends React.Component { [buttonState]: !this.state[buttonState] }); break; + case 'downloadContentsVisible': + this.refs.downloadContentsRef?.openDialog(); + break; + case 'uploaderContentsVisible': + this.refs.uploaderContentsRef?.openDialog(); + break; case 'quickHelpVisible': if (this.state[buttonState] === undefined) { this.setState({ @@ -527,6 +535,9 @@ class VFBMain extends React.Component { case 'triggerSetTermInfo': this.handlerInstanceUpdate(click.value[0]); break; + case 'downloadContentsVisible': + this.refs.downloadContentsRef?.openDialog(); + break; case 'uploaderContentsVisible': this.refs.uploaderContentsRef?.openDialog(); break; @@ -1286,63 +1297,6 @@ class VFBMain extends React.Component { idsList += ","; } idsList += this.idFromURL; - // populate page meta for this term for indexing - try { - window.ga('vfb.send', 'pageview', window.location.href ); - if ( window.XMLHttpRequest ) { - var xhr = new XMLHttpRequest(); - xhr.onload = function () { - try { - if (this.responseXML.title.indexOf("404 Not Found") < 0) { - document.title = 'Virtual Fly Brain (' + this.responseXML.title + ')'; - document.body.style.font = "x-large"; - document.querySelector('meta[property="og:title"]').setAttribute("content",this.responseXML.title); - document.querySelector('meta[name="description"]').setAttribute("content",this.responseXML.getElementById('json').innerText.substring(0, 4900)); - document.querySelector('meta[property="og:description"]').setAttribute("content",this.responseXML.getElementById('json').innerText.substring(0, 4900)); - if (document.getElementById('metaDesc') != null) { - if (this.responseXML.head != undefined && this.responseXML.head.getElementsByTagName('script') != undefined && this.responseXML.head.getElementsByTagName('script') != null && this.responseXML.head.getElementsByTagName('script')[1] != undefined) { - document.getElementById('metaDesc').innerHTML = this.responseXML.head.getElementsByTagName('script')[1].innerHTML; - } - } else { - if (this.responseXML.head != undefined && this.responseXML.head.getElementsByTagName('script') != undefined && this.responseXML.head.getElementsByTagName('script') != null && this.responseXML.head.getElementsByTagName('script')[1] != undefined) { - var script = document.createElement('script'); - script.type = 'application/ld+json'; - script.id = 'metaDesc'; - script.innerHTML = this.responseXML.head.getElementsByTagName('script')[1].innerHTML; - document.getElementsByTagName('head')[0].appendChild(script); - } - } - var viewport = !!document.querySelector("meta[name='viewport']"); - viewport = viewport ? document.querySelector("meta[name='viewport']") : document.createElement('meta'); - viewport.setAttribute('name', 'viewport'); - viewport.setAttribute('content', 'width=device-width, initial-scale=1'); - document.head.appendChild(viewport); - } - } catch (err) { - console.log(err); - } - } - xhr.open( 'GET', 'https://virtualflybrain.org/data/VFB/json/' + this.idFromURL + '.html') - xhr.responseType = 'document'; - xhr.send(); - } - } catch (err) { - console.error(err); - } - try { - var link = !!document.querySelector("link[rel='amphtml']"); - link = link ? document.querySelector("link[rel='amphtml']") : document.createElement('link'); - link.setAttribute('rel', 'amphtml'); - link.setAttribute('href', 'https://virtualflybrain.org/data/VFB/json/' + this.idFromURL + '.html'); - document.head.appendChild(link); - var conlink = !!document.querySelector("link[rel='canonical']"); - conlink = conlink ? document.querySelector("link[rel='canonical']") : document.createElement('link'); - conlink.setAttribute('rel', 'canonical'); - conlink.setAttribute('href', 'https://virtualflybrain.org/reports/' + this.idFromURL); - document.head.appendChild(conlink); - } catch (err) { - console.error(err); - } } else if (idList[list].indexOf("i=") > -1) { if (idsList.length > 0) { idsList = "," + idsList; @@ -1351,7 +1305,7 @@ class VFBMain extends React.Component { } else if (idList[list].indexOf("q=") > -1) { const multipleQueries = idList[list].replace("q=","").replace("%20", " ").split(";"); let that = this; - multipleQueries?.forEach( query => { + multipleQueries?.forEach( query => { const querySplit = query.split(","); that.urlQueryLoader.push({ id : querySplit[0].trim(), selection : querySplit[1].trim() }); }); @@ -1392,7 +1346,7 @@ 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) ) { @@ -1529,6 +1483,10 @@ class VFBMain extends React.Component { GEPPETTO.on(GEPPETTO.Events.Websocket_disconnected, function () { window.ga('vfb.send', 'event', 'disconnected', 'websocket-disconnect', (window.location.pathname + window.location.search)); + if (GEPPETTO.MessageSocket.protocol == 'wss://' && location.protocol !== 'https:') { + console.log("%c Unsecure connection used reloading with HTTPS connection... ", 'background: #444; color: #bada55'); + location.replace(`https:${location.href.substring(location.protocol.length)}`); + } if (GEPPETTO.MessageSocket.socketStatus == GEPPETTO.Resources.SocketStatus.CLOSE) { if (GEPPETTO.MessageSocket.attempts < 10) { window.ga('vfb.send', 'event', 'reconnect-attempt:' + GEPPETTO.MessageSocket.attempts, 'websocket-disconnect', (window.location.pathname + window.location.search)); @@ -1752,7 +1710,10 @@ 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 66339c535..d73bcc55f 100644 --- a/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js +++ b/components/configuration/VFBCircuitBrowser/circuitBrowserConfiguration.js @@ -19,11 +19,20 @@ var locationCypherQuery = ( instances, paths, weight ) => ({ + " WITH * ORDER BY index DESC" + " UNWIND relationships(path) as sr" + " 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 ", + + " OPTIONAL MATCH (x)-[xio:INSTANCEOF]->(xpc:Class) OPTIONAL MATCH (y)-[yio:INSTANCEOF]->(ypc:Class) WITH *,'\"'+ x.short_form+'\":{\"'+xpc.short_form+'\":\"' + xpc.label + '\"},\"'+ y.short_form+'\":{\"'+ypc.short_form+'\":\"' + ypc.label + '\"}' as Class" + + " 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, " + + " apoc.convert.fromJsonMap('{' + apoc.text.join(collect(Class),',') + '}') as class ", "resultDataContents": ["row", "graph"] } ] }); + +var Neo4jLabels = { + FAFB : "FAFB", + L1EM : "L1EM", + FlyEM_HB : "FlyEM_HB" +} + // See query explanation on https://github.com/VirtualFlyBrain/graph_queries/blob/main/weighted_path.md var configuration = { @@ -63,7 +72,7 @@ var styling = { // Color apply to target and source nodes when hovering over a link or a node. defaultNeighborNodesHoverColor : "orange", // Font used for text in nodes - defaultNodeFont : "5px sans-serif", + defaultNodeFont : "8px sans-serif", // Color of font in node's text defaultNodeFontColor : "black", // Node border color @@ -71,38 +80,35 @@ var styling = { // When hovering over a node, the node's border color changes to create a halo effect defaultNodeHoverBoderColor : "red", // Title bar (in node) background color - defaultNodeTitleBackgroundColor : "#11bffe", + defaultNodeTitleBackgroundColor : "grey", // Description area (in node) background color defaultNodeDescriptionBackgroundColor : "white", nodeColorsByLabel : { - "Template" : "#ff6cc8", - "GABAergic" : "#9551ff", - "Dopaminergic" : "#3551ff", - "Cholinergic" : "#95515f", - "Glutamatergic" : "#95f1ff", - "Octopaminergic" : "#f3511f", - "Serotonergic" : "#9501f0", - "Motor_neuron" : "#fffa30", - "Sensory_neuron" : "#ff3a3a", - "Peptidergic_neuron" : "#5f6a3a", - "Glial_cell" : "#ff3a6a", - "Clone" : "#d6007d", - "Synaptic_neuropil" : "#00a2aa", - "License" : "#0164d8", - "Person" : "#023f00", - "Neuron" : "#7f2100", - "Neuron_projection_bundle" : "#d6327d", - "Resource" : "#005f1d", - "Site" : "#005f1d", - "Expression_pattern" : "#534700", - "Split" : "#e012e3", - "DataSet" : "#b700b5", - "Ganglion" : "#d6007d", - "Neuromere" : "#d6507d", - "Cell" : "#ff6a3a", - "Property" : "#005f1d", - "Anatomy" : "#00a2aa", - "_Class" : "#0164d8" + "GABAergic" : "#377eb8", + "Neuron_projection_bundle" : "#ff7f00", + "Nervous_system" : "#4daf4a", + "Dopaminergic" : "#f781bf", + "Motor_neuron" : "#a65628", + "Cholinergic" : "#984ea3", + "Neuromere" : "#999999", + "Expression_pattern_fragment" : "#e41a1c", + "Peptidergic_neuron" : "#dede00", + "Ganglion" : "#377eb8", + "Muscle" : "#ff7f00", + "Glutamatergic" : "#4daf4a", + "Cluster" : "#f781bf", + "Clone" : "#a65628", + "Octopaminergic" : "#984ea3", + "Anatomy" : "#999999", + "Adult" : "#e41a1c", + "Synaptic_neuropil_subdomain" : "#dede00", + "Expression_pattern" : "#377eb8", + "Synaptic_neuropil_block" : "#ff7f00", + "Synaptic_neuropil_domain" : "#4daf4a", + "Serotonergic" : "#f781bf", + "Larva" : "#a65628", + "Neuron" : "#984ea3", + "Sensory_neuron" : "#999999" }, controlIcons : { home : "fa fa-home", @@ -120,5 +126,6 @@ module.exports = { configuration, styling, restPostConfig, - locationCypherQuery + locationCypherQuery, + Neo4jLabels }; diff --git a/components/configuration/VFBDownloadContents/configuration.json b/components/configuration/VFBDownloadContents/configuration.json new file mode 100644 index 000000000..55ad5f5ce --- /dev/null +++ b/components/configuration/VFBDownloadContents/configuration.json @@ -0,0 +1,34 @@ +{ + "postURL":"https://zip.virtualflybrain.org/download", + "contentType": "application/json", + "zipName" : "VFB Files.zip", + "options" :{ + "obj": { + "label" : "OBJ", + "tooltip" : "Download OBJ" + }, + "swc": { + "label" : "SWC", + "tooltip" : "Download SWC" + }, + "nrrd": { + "label" : "NRRD", + "tooltip" : "Download NRRD" + }, + "reference": { + "label" : "References", + "tooltip" : "Download References" + } + }, + "text" : { + "title" : "Download Data", + "typesSubtitle" : "Please select the desired types", + "variablesSubtitle" : "Please select Variables:", + "noVariablesSubtitle" : "No loaded variables", + "errorMessage" : "Something went wrong... We were not able to download the data. Please try again.", + "noEntriesFound" : "No entries found for the types and variables selected.", + "cancelButton" : "Cancel", + "downloadButton" : "Download", + "tryAgainButton" : "Try Again" + } +} \ No newline at end of file diff --git a/components/configuration/VFBDownloadContents/nrrd.png b/components/configuration/VFBDownloadContents/nrrd.png new file mode 100644 index 000000000..beee12a5c Binary files /dev/null and b/components/configuration/VFBDownloadContents/nrrd.png differ diff --git a/components/configuration/VFBDownloadContents/obj.png b/components/configuration/VFBDownloadContents/obj.png new file mode 100644 index 000000000..fbcc914c7 Binary files /dev/null and b/components/configuration/VFBDownloadContents/obj.png differ diff --git a/components/configuration/VFBDownloadContents/reference.png b/components/configuration/VFBDownloadContents/reference.png new file mode 100644 index 000000000..b821fe7e1 Binary files /dev/null and b/components/configuration/VFBDownloadContents/reference.png differ diff --git a/components/configuration/VFBDownloadContents/swc.png b/components/configuration/VFBDownloadContents/swc.png new file mode 100644 index 000000000..3eb5d02b6 Binary files /dev/null and b/components/configuration/VFBDownloadContents/swc.png differ diff --git a/components/configuration/VFBMain/searchConfiguration.js b/components/configuration/VFBMain/searchConfiguration.js index 859ef3327..b2c8c1037 100644 --- a/components/configuration/VFBMain/searchConfiguration.js +++ b/components/configuration/VFBMain/searchConfiguration.js @@ -60,7 +60,7 @@ var searchStyle = { singleResult: { "color": "white", "fontSize": "18px", - + "whiteSpace" : "normal", ":hover": { "color": "#11bffe", "background-color": "#252323", @@ -91,7 +91,7 @@ var datasourceConfiguration = { "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", + "fl": "short_form,label,synonym,id,facets_annotation,unique_facets", "start": "0", "pf":"true", "fq": [ @@ -99,7 +99,7 @@ var datasourceConfiguration = { ], "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" + "bq": "shortform_autosuggest:VFBexp*^10.0 shortform_autosuggest:VFB*^100.0 shortform_autosuggest:FBbt*^100.0 label_s:\"\"^2 synonym_s:\"\" short_form:FBbt_00003982^2 facets_annotation:Deprecated^0.001" } }; @@ -107,9 +107,13 @@ var searchConfiguration = { "resultsMapping": { "name": "label", - "id": "short_form" + "id": "short_form", + "labels" : "unique_facets" }, + "label_manipulation" : label => label, "filters_expanded": true, + "filter_positive" : "^100", + "filter_negative" : "^0.001", "filters": [ { "key": "facets_annotation", @@ -305,6 +309,13 @@ var searchConfiguration = { if (b.label.toLowerCase().indexOf(InputString.toLowerCase()) > -1 && b.label.toLowerCase().indexOf(InputString.toLowerCase()) < a.label.toLowerCase().indexOf(InputString.toLowerCase())) { return 1; } + // move up expression (VFBexp) terms + if (a.id.indexOf("VFBexp") > -1 && b.id.indexOf("VFBexp") < 0) { + return -1; + } + if (b.id.indexOf("VFBexp") > -1 && a.id.indexOf("VFBexp") < 0) { + 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; @@ -323,11 +334,16 @@ var searchConfiguration = { }, "clickHandler": function (id) { window.addVfbId(id); + }, + "Neo4jLabels" : { + "FAFB" : "FAFB", + "L1EM" : "L1EM", + "FlyEM_HB" : "FlyEM_HB" } }; module.exports = { searchStyle, searchConfiguration, - datasourceConfiguration, + datasourceConfiguration }; diff --git a/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js b/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js index 68c59639d..423a6fe34 100644 --- a/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js +++ b/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.js @@ -110,16 +110,16 @@ var toolbarMenu = { label: "About", icon: "", action: { - handlerAction: "clickAbout", - parameters: [] + handlerAction: "openNewTab", + parameters: ["https://www.virtualflybrain.org/about/"] } }, { label: "Contribute", icon: "", action: { - handlerAction: "clickContribute", - parameters: [] + handlerAction: "openNewTab", + parameters: ["http://www.virtualflybrain.org/docs/contribution-guidelines/"] } }, { @@ -156,19 +156,11 @@ var toolbarMenu = { } }, { - label: "Blog", + label: "Latest News & Releases", icon: "", action: { handlerAction: "openNewTab", - parameters: ["https://virtualflybrain.tumblr.com/"] - } - }, - { - label: "Rss", - icon: "fa fa-rss", - action: { - handlerAction: "openNewTab", - parameters: ["http://blog.virtualflybrain.org/rss"] + parameters: ["https://www.virtualflybrain.org/about/blog"] } } ] @@ -253,6 +245,14 @@ var toolbarMenu = { parameters: ["circuitBrowserVisible"] } }, + { + label: "Download Contents", + icon: "fa fa-download", + action: { + handlerAction: "downloadContentsVisible", + parameters: [] + } + }, { label: "NBLAST Uploader", icon: "fa fa-upload", @@ -299,7 +299,7 @@ var toolbarMenu = { trailerIcon: "fa fa-external-link", action: { handlerAction: "openNewTab", - parameters: ["http://catmaid.readthedocs.io/"] + parameters: ["https://www.virtualflybrain.org/blog/releases/catmaid/"] } }, { @@ -760,11 +760,11 @@ var toolbarMenu = { position: "bottom-start", list: [ { - label: "F.A.Q.", + label: "Documentation", icon: "fa fa-comments-o", action: { handlerAction: "openNewTab", - parameters: ["https://groups.google.com/g/vfb-suport"] + parameters: ["https://www.virtualflybrain.org/docs/"] } }, { @@ -775,6 +775,14 @@ var toolbarMenu = { parameters: ["https://groups.google.com/g/vfb-suport"] } }, + { + label: "Contribute", + icon: "", + action: { + handlerAction: "openNewTab", + parameters: ["http://www.virtualflybrain.org/docs/contribution-guidelines/"] + } + }, { label: "Circuit Browser Query (Shortest Weighted Path Algorithm)", icon: "fa fa-connectdevelop", diff --git a/components/configuration/VFBTree/VFBTreeConfiguration.js b/components/configuration/VFBTree/VFBTreeConfiguration.js index c30f27754..abd9f9a69 100644 --- a/components/configuration/VFBTree/VFBTreeConfiguration.js +++ b/components/configuration/VFBTree/VFBTreeConfiguration.js @@ -8,7 +8,7 @@ var treeCypherQuery = instance => ({ { "statement": "MATCH (root:Class)<-[:INSTANCEOF]-(t:Template {short_form:'" + instance + "'})" + "<-[:depicts]-(tc:Template)<-[ie:in_register_with]-(c:Individual)-[:depicts]->(image:" - + "Individual)-[r:INSTANCEOF]->(anat:Class:Nervous_system) WHERE exists(ie.index) WITH root, anat,r,image" + + "Individual)-[r:INSTANCEOF]->(anat:Class:Anatomy) WHERE exists(ie.index) WITH root, anat,r,image" + " MATCH p=allshortestpaths((root)<-[:SUBCLASSOF|part_of*..]-(anat)) " + "UNWIND nodes(p) as n UNWIND nodes(p) as m WITH * WHERE id(n) < id(m) " + "MATCH path = allShortestPaths( (n)-[:SUBCLASSOF|part_of*..1]-(m) ) " diff --git a/components/configuration/VFBUploader/configuration.json b/components/configuration/VFBUploader/configuration.json index a6d1a1091..45126dc53 100644 --- a/components/configuration/VFBUploader/configuration.json +++ b/components/configuration/VFBUploader/configuration.json @@ -1,12 +1,27 @@ { - "nblastURL" : "https://zip.virtualflybrain.org/download", - "contentType" : "", - "templates" : [ - { "VFB_00101567" : "VFB_00101567 Template" }, - { "VFB_00000001" : "VFB_00000001 Template" } - ], - "acceptedFiles" : [".swc"], - "filesLimit" : 10, + "nblastURL" : "http://upload.virtualflybrain.org/files/UNIQUE_ID?token=bec3a40f0ab377c39103", + "contentType" : "multipart/form-data", + "templates" : [{"short_form":"VFB_00101567","label":"JRC2018Unisex"},{"short_form":"VFB_00200000","label":"JRC2018UnisexVNC"}] , + "acceptedFiles" : [".swc", ".nrrd"], + "filesLimit" : 1, "maxFileSize" : 3000000, - "dropZoneMessage" : "Drag and drop a SWC file here or click" + "queryType": "uploaderQuery", + "cookieStorageDays" : 100, + "cookiesLearnLink" : "https://en.wikipedia.org/wiki/HTTP_cookie", + "text" : { + "dialogTitle" : "Upload Data File", + "dialogSubtitle" : "Generate a nblast query from your own data.", + "selectTemplate" : "1. Select a Template", + "select" : "Select", + "addYourFile" : "2. Add your file (Please use a .swc)", + "dropZoneMessage" : "Click or Drag & Drop your file here", + "agreeTerms" : "I agree to the use of cookies to store the NBLAST QUERY URL. ", + "learnMore" : "Learn More", + "blastButtonText" : "Generate a nblast query link", + "restartButtonText" : "Restart", + "copyButtonText" : "Copy", + "infoMessage" : "Generating a nblast query can take up to 30 min. Save this link to find the result of your query.", + "errorDialog" : "Something went wrong... We were not able to generate a nblast query link from your data. Please try again.", + "errorButtonText" : "Try Again" + } } \ No newline at end of file diff --git a/components/configuration/VFBUploader/file-icon.png b/components/configuration/VFBUploader/file-icon.png new file mode 100644 index 000000000..7ac1b6ff8 Binary files /dev/null and b/components/configuration/VFBUploader/file-icon.png differ diff --git a/components/configuration/VFBUploader/upload-icon.png b/components/configuration/VFBUploader/upload-icon.png new file mode 100644 index 000000000..d10bcbbb2 Binary files /dev/null and b/components/configuration/VFBUploader/upload-icon.png differ diff --git a/components/interface/ErrorCatcher.js b/components/interface/ErrorCatcher.js index 63f6fcf64..181793148 100644 --- a/components/interface/ErrorCatcher.js +++ b/components/interface/ErrorCatcher.js @@ -38,22 +38,44 @@ const styles = { class ErrorCatcher extends React.Component { constructor (props) { super(props); - this.state = { - hasError: false, + this.state = { + hasError: false, open: true, error: undefined }; } - handleClose = () => { var url = "https://github.com/VirtualFlyBrain/VFB2/issues/new"; var customMessage = "Steps to reproduce the problem: \n\nPlease fill the below with the necessary steps to reproduce the problem\n\n\n\nError Information:\n\n" - var body = customMessage + this.state.error.message + "\n\n" + this.state.error.stack.replace("#",escape("#")) + "\n\n```diff\n" + window.console.logs.slice(-50).join('\n').replace("#",escape("#")) + "\n```\n"; + // return as much of the log up to the last 10 events < 1000 characters: + var logLength = -1; + 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 > -50) { + logLength -= 1; + limitedLog = window.console.logs.slice(logLength).join('%0A').replace( + /\&/g,escape('&') + ).replace( + /\#/g,escape('#') + ).replace( + /\-/g,'%2D' + ).replace( + /\+/g,'%2B' + ); + } + var body = customMessage + this.state.error.message + "\n\n" + this.state.error.stack.replace("#",escape("#")) + "\n\n```diff\n" + limitedLog + "\n```\n"; var form = document.createElement("form"); form.setAttribute("method", "get"); form.setAttribute("action", url); form.setAttribute("target", "view"); - var hiddenField = document.createElement("input"); + var hiddenField = document.createElement("input"); hiddenField.setAttribute("type", "hidden"); hiddenField.setAttribute("name", "body"); hiddenField.setAttribute("value", body); @@ -62,7 +84,7 @@ class ErrorCatcher extends React.Component { window.open('', 'view'); form.submit(); }; - + componentDidCatch (error, info) { // Report error to GA window.ga('vfb.send', 'event', 'error', 'react', error.message + " - " + error.stack.replace("#",escape("#"))); diff --git a/components/interface/VFBCircuitBrowser/Controls.js b/components/interface/VFBCircuitBrowser/Controls.js index a6c6e1838..32eb45cc9 100644 --- a/components/interface/VFBCircuitBrowser/Controls.js +++ b/components/interface/VFBCircuitBrowser/Controls.js @@ -37,23 +37,10 @@ const theme = createMuiTheme({ overrides : { MuiSlider: { markLabelActive: { color: 'white' }, - markLabel: { color: 'white' }, - markActive: { color: 'red' } - }, - MuiButton: { - label: { - fontSize: '12px', - fontFamily: ['Barlow Condensed', 'Khand', "sans-serif"] - }, - button : { padding : "" } + markLabel: { color: 'white' } } }, - typography: { - body1: { - fontSize: 15, - fontFamily : ['Barlow Condensed', 'Khand', "sans-serif"] - } - } + typography: { body1: { fontFamily : ['Barlow Condensed', 'Khand', "sans-serif"] } } }); /** @@ -97,20 +84,26 @@ const styles = theme => ({ height : "20px", border : "none !important", backgroundColor: "#80808040 !important", - paddingLeft : "10px !important" + paddingLeft : "10px !important", + fontSize : "15px !important" }, weightInputDiv : { width : "100% !important" }, refreshButton : { backgroundColor : "#0AB7FE", flexBasis: "100%", fontWeight : 600, + fontSize: '12px', + fontFamily: ['Barlow Condensed', 'Khand', "sans-serif"] }, clearButton : { backgroundColor : "#E53935", flexBasis: "100%", fontWeight : 600, + fontSize: '12px', + fontFamily: ['Barlow Condensed', 'Khand', "sans-serif"] }, - slider : { color: '#0AB7FE' } + slider : { color: '#0AB7FE' }, + typography : { fontSize : "15px" } }); /** @@ -120,9 +113,11 @@ const configuration = require('../../configuration/VFBCircuitBrowser/circuitBrow const restPostConfig = require('../../configuration/VFBCircuitBrowser/circuitBrowserConfiguration').restPostConfig; const cypherQuery = require('../../configuration/VFBCircuitBrowser/circuitBrowserConfiguration').locationCypherQuery; const stylingConfiguration = require('../../configuration/VFBCircuitBrowser/circuitBrowserConfiguration').styling; +const Neo4jLabels = require('../../configuration/VFBCircuitBrowser/circuitBrowserConfiguration').Neo4jLabels; const searchConfiguration = require('./../../configuration/VFBCircuitBrowser/datasources/SOLRclient').searchConfiguration; -const datasourceConfiguration = require('./../../configuration/VFBCircuitBrowser/datasources/SOLRclient').datasourceConfiguration; +const defaultDatasourceConfiguration = require('./../../configuration/VFBCircuitBrowser/datasources/SOLRclient').datasourceConfiguration; +const datasourceConfiguration = JSON.parse(JSON.stringify(defaultDatasourceConfiguration)); /** * Create custom marks for Paths slider. @@ -161,11 +156,15 @@ class AutocompleteResults extends Component { this.setState({ filteredResults : results }); } + clearResults () { + this.setState({ filteredResults : {} }); + } + getFilteredResults (){ return this.state.filteredResults; } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate (nextProps, nextState) { this.fieldLabel = nextProps.getLatestNeuronFields()[this.props.index].label; return true; } @@ -179,11 +178,13 @@ class AutocompleteResults extends Component { fullWidth freeSolo disableClearable + clearOnEscape disablePortal autoHighlight + clearOnBlur value={this.fieldLabel} id={this.props.index.toString()} - ListboxProps={{ style: { maxHeight: "10rem" } }} + ListboxProps={{ style: { maxHeight: "10rem", fontSize: "15px" } }} onChange={this.props.resultSelectedChanged} options={Object.keys(this.state.filteredResults).map(option => this.state.filteredResults[option].label)} renderInput={params => ( @@ -194,8 +195,8 @@ class AutocompleteResults extends Component { className={label.replace(/ +/g, "").toLowerCase()} onChange={this.props.neuronTextfieldModified} onDelete={this.props.neuronTextfieldModified} - inputProps={{ ...params.inputProps, id: this.props.index, style: { height : "20px", color: "white" ,paddingLeft : "10px", border : "none", backgroundColor: "#80808040" } }} - InputLabelProps={{ ...params.inputProps,style: { color: "white", paddingLeft : "10px" } }} + inputProps={{ ...params.inputProps, id: this.props.index, style: { height : "20px", color: "white" ,paddingLeft : "10px", fontSize: "15px", border : "none", backgroundColor: "#80808040" } }} + InputLabelProps={{ ...params.inputProps,style: { color: "white", paddingLeft : "10px", fontSize: "15px" } }} /> )} /> @@ -267,6 +268,11 @@ class Controls extends Component { this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, neurons); delete this.autocompleteRef[id.toString()]; this.neuronFields = neurons; + if ( !this.state.neurons.find( neuron => neuron.id != "") ) { + // reset configuration of fq to default + datasourceConfiguration.query_settings.fq = defaultDatasourceConfiguration.query_settings.fq; + } + this.forceUpdate(); } @@ -292,6 +298,7 @@ class Controls extends Component { // User has added the maximum number of neurons allowed in query search this.neuronFields = neuronFields; this.autocompleteRef[(neuronFields.length - 1).toString()] = React.createRef(); + datasourceConfiguration.query_settings.fq = defaultDatasourceConfiguration.query_settings.fq; this.forceUpdate(); } @@ -360,6 +367,12 @@ class Controls extends Component { getResultsSOLR( event.target.value, this.autocompleteRef[this.setInputValue].current.handleResults,searchConfiguration.sorter,datasourceConfiguration ); } this.neuronFields = neurons; + + if ( !this.neuronFields.find( neuron => neuron.id != "") ) { + // reset configuration of fq to default + this.autocompleteRef[this.setInputValue].current.clearResults(); + datasourceConfiguration.query_settings.fq = defaultDatasourceConfiguration.query_settings.fq; + } } /** @@ -369,9 +382,17 @@ class Controls extends Component { // 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 result = this.autocompleteRef[textFieldId].current.getFilteredResults()[value]; + let shortForm = result && result.short_form; neurons[index] = { id : shortForm, label : value }; + result.facets_annotation.forEach( annotation => { + let facet = "facets_annotation:" + annotation; + if ( Object.values(Neo4jLabels).includes(annotation) && !datasourceConfiguration.query_settings.fq.includes(facet) ) { + datasourceConfiguration.query_settings.fq.push(facet); + } + }); + // 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); @@ -401,6 +422,12 @@ class Controls extends Component { this.props.vfbCircuitBrowser(UPDATE_CIRCUIT_QUERY, []) this.setState({ key: Math.random() }); } + + clearGraph () { + datasourceConfiguration.query_settings.fq = defaultDatasourceConfiguration.query_settings.fq; + this.props.clearGraph() + } + /** * Update neuron fields if there's a query preselected. */ @@ -479,7 +506,7 @@ class Controls extends Component { IconButtonProps={{ style: { padding : "0px", margin : "0px" } }} >
- Configure circuit + Configure circuit
@@ -545,7 +572,7 @@ class Controls extends Component { - Add Neuron + Add Neuron } @@ -556,7 +583,7 @@ class Controls extends Component { - # Paths + # Paths - Min Weight + Min Weight @@ -594,7 +621,7 @@ class Controls extends Component { color="secondary" classes={{ root : classes.clearButton }} id="clearCircuitBrowser" - onClick={() => this.props.clearGraph()} + onClick={this.clearGraph.bind(this)} >Clear diff --git a/components/interface/VFBCircuitBrowser/QueryParser.js b/components/interface/VFBCircuitBrowser/QueryParser.js index 6e2df5988..ff13c9310 100644 --- a/components/interface/VFBCircuitBrowser/QueryParser.js +++ b/components/interface/VFBCircuitBrowser/QueryParser.js @@ -21,6 +21,7 @@ export function queryParser (e) { let maxHops = Math.ceil(data[0]?.row[6] / 2) + 1; // Read relationship max hops let relationshipsHops = data[0]?.row[7]; + const idClassLabels = data[0]?.row[8]; // Keeps track of links let linksMap = new Map(); @@ -31,7 +32,7 @@ export function queryParser (e) { let nodesMap = new Map(); // Colors for labels - let presentColorLabels = new Array(); + let legendLabels = new Array(); // maps of links with their max hop let linksMaxHops = {}; // Keeps track of what level nodes belong @@ -60,24 +61,27 @@ export function queryParser (e) { // Loop through nodes from query and create nodes for graph data.forEach(({ graph }) => { - graph.nodes.forEach(({ id, labels, properties }) => { + console.log("Results ", graph.nodes); + graph.nodes.forEach(({ id, properties }) => { let label = properties[e.data.params.configuration.resultsMapping.node.label]; let title = properties[e.data.params.configuration.resultsMapping.node.title]; let color = e.data.params.styling.defaultNodeDescriptionBackgroundColor; + let nodeColorLabels = new Array(); + const labels = properties.uniqueFacets; // Retrieve list of Label colors from configuration const colorLabels = Object.entries(e.data.params.styling.nodeColorsByLabel); - + console.log("Labels ", labels); + console.log("Color labels, ", colorLabels); // Loop through color labels for (var i = 0; i < colorLabels.length ; i++ ) { let index = labels.indexOf(colorLabels[i][0]); if ( index > -1 ) { - color = colorLabels[i][1]; + nodeColorLabels.push(colorLabels[i][1]); // Add to array of present colors only if array doesn't have it already - if ( !presentColorLabels.includes(labels[index]) ) { - presentColorLabels.push(labels[index]); + if ( !legendLabels.includes(labels[index]) ) { + legendLabels.push(labels[index]); } - break; } } let n = null; @@ -96,15 +100,20 @@ export function queryParser (e) { hop = 0; } + const classLabel = idClassLabels[title]; + console.log("nodeColorLabels ", nodeColorLabels); + console.log(" className ", Object.values(classLabel).join()); + n = { - path : label, + name : label, id : parseID, title : title, level : level, hop : hop, width : e.data.params.NODE_WIDTH, height : e.data.params.NODE_HEIGHT, - color : color, + nodeColorLabels : nodeColorLabels, + classLabel : Object.values(classLabel).join() }; nodesMap.set(id, n); @@ -143,7 +152,7 @@ export function queryParser (e) { // Set the X position of each node, this will place them on their corresponding column depending on hops let positionX = 0; - let spaceBetween = maxHops > 2 ? 100 : 200; + let spaceBetween = maxHops > 2 ? 300 : 400; if ( sourceNode.level === 0 ){ if ( sourceNode.id == targetNodeID ){ sourceNode.positionX = maxHops > 0 ? maxHops * spaceBetween : spaceBetween; @@ -244,5 +253,5 @@ export function queryParser (e) { console.log("Links ", links); // Worker is done, notify main thread - this.postMessage({ resultMessage: "OK", params: { results: { nodes, links }, colorLabels : presentColorLabels } }); + this.postMessage({ resultMessage: "OK", params: { results: { nodes, links }, colorLabels : legendLabels } }); } \ No newline at end of file diff --git a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js index 04bd523ba..b13233754 100644 --- a/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js +++ b/components/interface/VFBCircuitBrowser/VFBCircuitBrowser.js @@ -39,8 +39,8 @@ const defaultHTTPConfiguration = { } const COMPONENT_ID = "VFBCircuitBrowser"; -const NODE_WIDTH = 55; -const NODE_HEIGHT = 40; +const NODE_WIDTH = 120; +const NODE_HEIGHT = 80; const NODE_BORDER_THICKNESS = 2; /** @@ -72,6 +72,7 @@ class VFBCircuitBrowser extends Component { this.updatePaths = this.updatePaths.bind(this); this.updateWeight = this.updateWeight.bind(this); this.resize = this.resize.bind(this); + this.nodeRendering = this.nodeRendering.bind(this); this.highlightNodes = new Set(); this.highlightLinks = new Set(); @@ -261,27 +262,47 @@ class VFBCircuitBrowser extends Component { self.setState( { loading : false } ); }) } + + getFontSize (context, maxWidth, text, textLength) { + let baseSize = 8; + let width = context.measureText(text).width; + while (width > maxWidth) { + baseSize--; + context.font = `${baseSize}px sans-serif` + width = context.measureText(text).width; + } + + return baseSize; + } /** * Breaks Description texts into lines to fit within a certain width value. */ - wrapText (context, text, x, y, maxWidth, lineHeight) { - var words = text.split(' '); - var line = ''; - - for (var n = 0; n < words.length; n++) { - var testLine = line + words[n] + ' '; - var metrics = context.measureText(testLine); - var testWidth = metrics.width; - if (testWidth > maxWidth && n > 0) { - context.fillText(line, x, y); - line = words[n] + ' '; - y += lineHeight; + wrapText (context, text, x, y, maxWidth, maxHeight) { + let words = text.split(' '); + let lines = []; + let line = ''; + + let maxTextLength = text.length < 20 ? text.length / 2 : text.length / 3; + + words.forEach( word => { + let testLine = line + word + ' '; + if ( line.length >= maxTextLength ) { + lines.push(line); + line = word + ' '; } else { line = testLine; } - } - context.fillText(line, x, y); + }); + + lines.push(line); + + const lineHeight = this.getFontSize(context, maxWidth, lines[0], maxTextLength); + + lines.forEach( line => { + context.fillText(line, x, y); + y += lineHeight; + }); } // Calculate link middle point @@ -291,7 +312,58 @@ class VFBCircuitBrowser extends Component { y: (1 - t) * (1 - t) * sy + 2 * (1 - t) * t * cp1y + t * t * ey, }; } + + nodeRendering (node, ctx, globalScale) { + const cardWidth = NODE_WIDTH; + const cardHeight = NODE_HEIGHT; + const classnameHeight = cardHeight * .45; + const idHeight = cardHeight * .45; + let borderThickness = this.highlightNodes.has(node) ? NODE_BORDER_THICKNESS : 1; + + // Node border color + ctx.fillStyle = self.hoverNode == node ? stylingConfiguration.defaultNodeHoverBoderColor : (this.highlightNodes.has(node) ? stylingConfiguration.defaultNeighborNodesHoverColor : stylingConfiguration.defaultNodeBorderColor) ; + // Create Border + ctx.fillRect(node.x - cardWidth / 2 - (borderThickness), node.y - cardHeight / 2 - (borderThickness), cardWidth , cardHeight ); + + // Assign color to Description Area background in Node + ctx.fillStyle = stylingConfiguration.defaultNodeDescriptionBackgroundColor; + // Create Description Area in Node + ctx.fillRect(node.x - cardWidth / 2,node.y - cardHeight / 2, cardWidth - (borderThickness * 2 ), cardHeight - (borderThickness * 2 )); + + ctx.fillStyle = stylingConfiguration.defaultNodeTitleBackgroundColor; + ctx.fillRect(node.x - cardWidth / 2,node.y - cardHeight / 2, cardWidth - (borderThickness * 2 ), cardHeight / 2 ); + + const lastIndex = node.nodeColorLabels.length; + node.nodeColorLabels.forEach( (color, index) => { + // Assign color to Title Bar background in Node + ctx.fillStyle = color; + const x = (node.x - cardWidth / 2) + (index * (cardWidth / lastIndex)); + const y = node.y; + // Create Title Bar in Node + ctx.fillRect(x,y, (cardWidth / lastIndex) - ( index == lastIndex - 1 ? borderThickness * 2 : 0 ) , cardHeight / 10); + }) + // Assign font to text in Node + ctx.font = stylingConfiguration.defaultNodeFont; + // Assign color to text in Node + ctx.fillStyle = stylingConfiguration.defaultNodeFontColor; + // Text in font to be centered + ctx.textAlign = "center"; + ctx.textBaseline = 'middle'; + + // Create Title in Node + this.wrapText(ctx, node.classLabel, node.x, node.y - (cardHeight / 2) + 10, cardWidth * .8 , classnameHeight); + + ctx.font = stylingConfiguration.defaultNodeFont; + /* + * Add Description text to Nodes + * node.name = text to display + * node.x = x coordinate of text + * node.y + 20 = y coordinate, adds 20 pixels for padding from upper element + */ + this.wrapText(ctx, node.name, node.x, node.y + 20, cardWidth * .8 , classnameHeight); + } + render () { let self = this; @@ -336,7 +408,7 @@ class VFBCircuitBrowser extends Component { data={this.state.graph} // Create the Graph as 2 Dimensional d2={true} - nodeLabel={node => node.path} + nodeLabel={node => node.title} // Relationship label, placed in Link linkLabel={link => link.label} // Width of links, log(weight) @@ -347,7 +419,7 @@ class VFBCircuitBrowser extends Component { // Node label, used in tooltip when hovering over Node linkCanvasObjectMode={() => "after"} linkCanvasObject={(link, ctx) => { - const MAX_FONT_SIZE = 8; + const MAX_FONT_SIZE = 16; const LABEL_NODE_MARGIN = 1 * 1.5; const start = link.source; @@ -419,46 +491,16 @@ class VFBCircuitBrowser extends Component { return color; }} - nodeCanvasObject={(node, ctx, globalScale) => { - let cardWidth = NODE_WIDTH; - let cardHeight = NODE_HEIGHT; - let borderThickness = self.highlightNodes.has(node) ? NODE_BORDER_THICKNESS : 1; - - // Node border color - ctx.fillStyle = self.hoverNode == node ? stylingConfiguration.defaultNodeHoverBoderColor : (self.highlightNodes.has(node) ? stylingConfiguration.defaultNeighborNodesHoverColor : stylingConfiguration.defaultNodeBorderColor) ; - // Create Border - ctx.fillRect(node.x - cardWidth / 2 - (borderThickness), node.y - cardHeight / 2 - (borderThickness), cardWidth , cardHeight ); - - // Assign color to Description Area background in Node - ctx.fillStyle = stylingConfiguration.defaultNodeDescriptionBackgroundColor; - // Create Description Area in Node - ctx.fillRect(node.x - cardWidth / 2,node.y - cardHeight / 2, cardWidth - (borderThickness * 2 ), cardHeight - ( borderThickness * 2) ); - // Assign color to Title Bar background in Node - ctx.fillStyle = node.color; - // Create Title Bar in Node - ctx.fillRect(node.x - cardWidth / 2 ,node.y - cardHeight / 2, cardWidth - ( borderThickness * 2 ), cardHeight / 3); - - // Assign font to text in Node - ctx.font = stylingConfiguration.defaultNodeFont; - // Assign color to text in Node - ctx.fillStyle = stylingConfiguration.defaultNodeFontColor; - // Text in font to be centered - ctx.textAlign = "center"; - ctx.textBaseline = 'middle'; - // Create Title in Node - ctx.fillText(node.title, node.x, node.y - 15); - // Add Description text to Node - this.wrapText(ctx, node.path, node.x, node.y, cardWidth - (borderThickness * 2) , 5); - }} + nodeCanvasObject={this.nodeRendering} // Overwrite Node Canvas Object nodeCanvasObjectMode={node => 'replace'} // bu = Bottom Up, creates Graph with root at bottom dagMode="lr" nodeVal = { node => { node.fx = node.positionX; - node.fy = -100 * node.level + node.fy = -150 * node.level }} - dagLevelDistance = {25} + dagLevelDistance = {75} onDagError={loopNodeIds => {}} // Handles clicking event on an individual node onNodeClick = { (node,event) => this.handleNodeLeftClick(node,event) } diff --git a/components/interface/VFBDownloadContents/VFBDownloadContents.js b/components/interface/VFBDownloadContents/VFBDownloadContents.js new file mode 100644 index 000000000..6b86cf888 --- /dev/null +++ b/components/interface/VFBDownloadContents/VFBDownloadContents.js @@ -0,0 +1,571 @@ +import React from "react"; +import Button from "@material-ui/core/Button"; +import Grid from "@material-ui/core/Grid"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import Tooltip from "@material-ui/core/Tooltip"; +import Typography from "@material-ui/core/Typography"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import CircularProgress from '@material-ui/core/CircularProgress'; +import ChevronRightIcon from "@material-ui/icons/ChevronRight"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import { Checkbox, Divider, IconButton } from "@material-ui/core"; +import Box from "@material-ui/core/Box"; +import { createMuiTheme, ThemeProvider } from "@material-ui/core/styles"; +import { withStyles } from "@material-ui/styles"; +import axios from "axios"; +import TreeView from "@material-ui/lab/TreeView"; +import TreeItem from "@material-ui/lab/TreeItem"; +import NRRDIcon from "../../configuration/VFBDownloadContents/nrrd.png"; +import OBJIcon from "../../configuration/VFBDownloadContents/obj.png"; +import SWCIcon from "../../configuration/VFBDownloadContents/swc.png"; +import ReferenceIcon from "../../configuration/VFBDownloadContents/reference.png"; +import CloseIcon from "@material-ui/icons/Close"; +import { connect } from "react-redux"; + +const iconsMap = { + obj: OBJIcon, + swc: SWCIcon, + reference: ReferenceIcon, + nrrd: NRRDIcon, +}; + +const ALL_INSTANCES = { id: "ALL_INSTANCES", name: "All Instances" }; + +const styles = theme => ({ + downloadButton: { backgroundColor: "#0AB7FE", color: "white !important" }, + downloadErrorButton: { backgroundColor: "#FCE7E7", color: "#E53935", border : "1px solid #E53935" }, + error: { color: "#E53935" }, + errorMessage: { wordWrap: "break-word" }, + downloadButtonText: { color: "white !important" }, + checkedBox: { borderColor: "#0AB7FE" }, + footer: { backgroundColor: "#EEF9FF" }, + errorFooter: { backgroundColor: "#FCE7E7" }, + listItemText: { fontSize: "1em" }, + customizedButton: { + position: "absolute", + left: "95%", + top: "2%", + backgroundColor: "#F5F5F5", + color: "gray", + }, + dialog: { + overflow: "unset", + margin: "0 auto", + }, + dialogContent: { overflow: "hidden" }, + checked: { "&$checked": { color: "#0AB7FE" } }, + "@global": { + ".MuiTreeItem-root.Mui-selected > .MuiTreeItem-content .MuiTreeItem-label": { backgroundColor: "white" }, + ".MuiTreeItem-root.Mui-selected > .MuiTreeItem-content .MuiTreeItem-label:hover, .MuiTreeItem-root.Mui-selected:focus > .MuiTreeItem-content .MuiTreeItem-label": { backgroundColor: "white" } + }, +}); + +const theme = createMuiTheme({ + typography: { + h2: { + fontSize: 22, + fontWeight: 400, + fontStyle: "normal", + lineHeight: "26.4px", + color: "#181818", + fontFamily: "Barlow", + }, + h5: { + fontSize: 11, + fontWeight: 500, + fontStyle: "normal", + lineHeight: "13.2px", + fontFamily: "Barlow", + color: "rgba(0, 0, 0, 0.54)", + }, + subtitle2: { + fontSize: 11, + fontWeight: 500, + fontStyle: "normal", + lineHeight: "13.2px", + fontFamily: "Barlow", + color: "rgba(0, 0, 0, 0.24)", + }, + error: { + fontSize: 11, + fontWeight: 500, + fontStyle: "normal", + lineHeight: "13.2px", + fontFamily: "Barlow", + color: "#E53935", + }, + button: { + fontSize: 11, + fontWeight: 600, + fontStyle: "normal", + lineHeight: "13.2px", + fontFamily: "Barlow", + color: "#0AB7FE", + }, + }, + Button: { + "&:hover": { + backgroundColor: "#0AB7FE", + boxShadow: "none", + }, + "&:active": { + boxShadow: "none", + backgroundColor: "#0AB7FE", + }, + }, +}); + +/** + * Component to download files contents + */ +class VFBDownloadContents extends React.Component { + constructor (props) { + super(props); + + this.state = { + open: false, + typesChecked: [], + downloadError: false, + downloading: false, + selectedVariables: [], + allVariablesSelectedFlag: false, + errorMessage : "" + }; + + this.configuration = require("../../configuration/VFBDownloadContents/configuration"); + this.configurationOptions = this.configuration.options; + this.handleCloseDialog = this.handleCloseDialog.bind(this); + this.openDialog = this.openDialog.bind(this); + this.handleTypeSelection = this.handleTypeSelection.bind(this); + this.handleDownload = this.handleDownload.bind(this); + this.extractVariableFileMeta = this.extractVariableFileMeta.bind(this); + this.getAllLoadedVariables = this.getAllLoadedVariables.bind(this); + this.requestZipDownload = this.requestZipDownload.bind(this); + this.getVariableById = this.getVariableById.bind(this); + this.toggleVariable = this.toggleVariable.bind(this); + this.variables = [ALL_INSTANCES]; + } + + handleCloseDialog () { + this.setState({ open: false }); + } + + openDialog () { + this.variables = this.getAllLoadedVariables(); + this.setState({ + open: true, + downloadError : false, + downloading : false, + downloadEnabled : this.state.typesChecked.length > 0 && this.state.selectedVariables.length > 0 + }); + } + + handleDownload () { + if ( this.state.downloading ) { + return; + } + + let json = { entries: [] }; + + this.state.selectedVariables.map( variable => { + const filemeta = this.extractVariableFileMeta(variable); + json.entries = json.entries.concat(filemeta); + }); + + json.entries.length > 0 ? this.requestZipDownload(json) : this.setState({ downloadError : true, errorMessage : this.configuration.text.noEntriesFound }); + } + + /** + * Extract filemeta from geppetto model, using variable id to find it + */ + extractVariableFileMeta (variable) { + let filemetaText = variable.filemeta?.values[0]?.value?.text; + filemetaText = filemetaText?.replace(/'/g, '"'); + + const filemetaObject = JSON.parse(filemetaText); + let filesArray = []; + + this.state.typesChecked.map( check => { + filemetaObject[check] + && filesArray.push({ + Url: filemetaObject[check]?.url, + ZipPath: filemetaObject[check]?.local, + }); + }); + + return filesArray; + } + + /** + * Get array of all loaded variables in application + */ + getAllLoadedVariables () { + let entities = GEPPETTO.ModelFactory.allPaths; + var visuals = []; + + for (var i = 0; i < entities.length; i++) { + if ( entities[i].metaType === "VisualType" || entities[i].metaType === "CompositeVisualType" ) { + const variable = entities[i]?.path?.split(".")[0]; + const instance = window.Instances[variable]; + const filemeta = instance[variable + "_meta"]?.variable?.types[0]?.filemeta; + visuals.push({ id: variable, name: instance?.name, filemeta: filemeta }); + } + } + + return visuals; + } + + /** + * Make axios call to download the zip + */ + requestZipDownload (jsonRequest) { + let self = this; + + this.setState({ downloading: true, downloadEnabled : false }); + // Axios HTTP Post request with post query + axios({ + method: "post", + url: this.configuration.postURL, + headers: { "content-type": this.configuration.contentType }, + data: jsonRequest, + responseType: "arraybuffer", + }) + .then(function (response) { + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", self.configuration.zipName); + document.body.appendChild(link); + link.click(); + setTimeout( + () => + self.setState({ + downloading: false, + open: false, + downloadEnabled : true + }), + 500 + ); + }) + .catch(function (error) { + self.setState({ + downloadError: true, + downloading: false, + errorMessage : this.props.classes.errorMessage + }); + }); + } + + /** + * Handle checkbox selection of different types to download + */ + handleTypeSelection (value) { + const currentIndex = this.state.typesChecked.indexOf(value); + const newTypesChecked = [...this.state.typesChecked]; + + if (currentIndex === -1) { + newTypesChecked.push(value); + } else { + newTypesChecked.splice(currentIndex, 1); + } + + this.setState({ typesChecked: newTypesChecked, downloadEnabled : newTypesChecked.length > 0 && this.state.selectedVariables.length > 0 }); + } + + /** + * Get variable by id, trigger by checkbox selection of variables + */ + getVariableById (nodes, id) { + let variablesMatched = []; + + if (id === ALL_INSTANCES.id) { + variablesMatched = nodes; + } else { + nodes.forEach(node => { + if (node.id === id) { + variablesMatched.push(node); + } + }); + } + + return variablesMatched; + } + + /** + * Toggle variable selection from checklist + */ + toggleVariable (checked, node) { + const allNode = this.getVariableById(this.variables, node.id); + let updatedVariables = checked + ? [...this.state.selectedVariables, ...allNode] + : this.state.selectedVariables.filter( + value => !allNode.find( node => node.id === value.id ) + ); + + updatedVariables = updatedVariables.filter((v, i) => updatedVariables.indexOf(v) === i); + + this.setState({ + selectedVariables: updatedVariables, + allVariablesSelectedFlag: updatedVariables.length > 0, + downloadEnabled : this.state.typesChecked.length > 0 && updatedVariables.length > 0 + }); + } + + render () { + let self = this; + const { idsMap } = this.props; + this.variables = this.getAllLoadedVariables(); + + return ( + + + + {this.configuration.text.title} + + + { !this.state.downloadError ? ( + + + + {this.configuration.text.typesSubtitle} + + + + {Object.keys(this.configurationOptions).map(key => { + const option = this.configurationOptions[key]; + const labelId = `checkbox-list-secondary-label-${key}`; + return ( + + {`${option.tooltip}`} } classes={ { popper: "light" } } placement="top-start" arrow> + + + + {`${option.label}`} + + self.handleTypeSelection(key)} + checked={this.state.typesChecked.indexOf(key) !== -1} + inputProps={{ "aria-labelledby": labelId }} + disabled={this.state.downloading} + disableRipple + className={self.props.classes.checked} + id={option.id} + /> + + + + ); + })} + + {this.variables.length > 0 ? ( + <> + + + {this.configuration.text.variablesSubtitle} + + + + } + defaultExpanded={[ALL_INSTANCES.id]} + defaultExpandIcon={} + > + e.stopPropagation()} + checked={self.state.allVariablesSelectedFlag} + onChange={event => + self.toggleVariable( + event.currentTarget.checked, + ALL_INSTANCES + ) + } + disableRipple + className={self.props.classes.checked} + id={ALL_INSTANCES.id} + /> + } + label={ + + {ALL_INSTANCES.name} + + } + key={ALL_INSTANCES.id} + /> + } + > + {this.variables.map(node => ( + e.stopPropagation()} + checked={self.state.selectedVariables.some( + item => item.id === node.id + )} + onChange={event => + self.toggleVariable( + event.currentTarget.checked, + node + ) + } + className={self.props.classes.checked} + id={"Download_" + node.id} + /> + } + label={ + + {node.name} + + } + key={node.id} + /> + } + /> + ))} + + + + + ) : ( + + {this.configuration.text.noVariablesSubtitle} + + )} + + + ) + : ( + + + + + + + {this.state.errorMessage} + + + + ) + } + + + + + + { !this.state.downloadError ? ( + + + + + + + + + ) + : ( + + + + + + ) + } + + + + ); + } +} + +function mapStateToProps (state) { + return { + instanceDeleted : state.generals.ui.canvas.instanceDeleted, + instanceOnFocus : state.generals.instanceOnFocus, + idsMap : state.generals.idsMap, + idsList : state.generals.idsList + } +} + +export default connect(mapStateToProps, null, null, { forwardRef : true } )(withStyles(styles)(VFBDownloadContents)); \ No newline at end of file diff --git a/components/interface/VFBFocusTerm/VFBFocusTerm.js b/components/interface/VFBFocusTerm/VFBFocusTerm.js index 482347f9b..28d41946b 100644 --- a/components/interface/VFBFocusTerm/VFBFocusTerm.js +++ b/components/interface/VFBFocusTerm/VFBFocusTerm.js @@ -568,6 +568,20 @@ class VFBFocusTerm extends React.Component { : } + + { + this.props.UIUpdateManager("uploaderContentsVisible"); + }} /> + + + { + this.props.UIUpdateManager("downloadContentsVisible"); + }} /> + { + stylingConfiguration?.dropDownQueries?.map((item, index) => { if ( parseInt(self.props.graphQueryIndex) === index ) { self.props.vfbGraph(UPDATE_GRAPH, null, -1, true, false); self.instanceFocusChange(self.props.instanceOnFocus); @@ -314,7 +316,7 @@ class VFBGraph extends Component { selectedNodeLoaded (instance) { var loadedId = instance.id; if (instance.getParent() !== null) { - loadedId = instance.getParent().id; + loadedId = instance.getParent()?.id; } if ( this.nodeSselected ) { @@ -347,9 +349,9 @@ class VFBGraph extends Component { * 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; - instanceName = this.focusedInstance.getParent().name; + if (this.focusedInstance?.getParent() !== null) { + idToSearch = this.focusedInstance?.getParent()?.id; + instanceName = this.focusedInstance?.getParent()?.name; } if (this.__isMounted){ diff --git a/components/interface/VFBTermInfo/ButtonBarComponent.js b/components/interface/VFBTermInfo/ButtonBarComponent.js index eec3845d2..ee07fd6cd 100644 --- a/components/interface/VFBTermInfo/ButtonBarComponent.js +++ b/components/interface/VFBTermInfo/ButtonBarComponent.js @@ -184,7 +184,7 @@ export default class ButtonBarComponent extends React.Component { return (
- {ctrlButtons.map(function (control, id) { + {ctrlButtons?.map(function (control, id) { // grab attributes to init button attributes var controlConfig = that.resolveCondition(control, path); var idVal = path.replace(/\./g, '_').replace(/\[/g, '_').replace(/\]/g, '_') + "_" + controlConfig.id + "_buttonBar_btn"; @@ -233,7 +233,7 @@ export default class ButtonBarComponent extends React.Component { that.colorPickerActionFn = actionFn; // set style val to color tint icon if (entity !== undefined) { - var colorVal = String(entity.getColor().replace(/0X/i, "#") + "0000").slice(0, 7); + var colorVal = String(entity?.getColor()?.replace(/0X/i, "#") + "0000")?.slice(0, 7); styleVal = { color: colorVal.startsWith('#') ? colorVal : ('#' + colorVal) }; classVal += " color-picker-button"; } @@ -268,7 +268,7 @@ export default class ButtonBarComponent extends React.Component { ref={ref => that.colorPickerContainer = ref} style={{ left: that.state.pickerPosition }}> { Instances[path].setColor(color.hex); that.setState({ displayColorPicker: true }); diff --git a/components/interface/VFBTermInfo/VFBTermInfo.js b/components/interface/VFBTermInfo/VFBTermInfo.js index e5ff6ea32..a23091531 100644 --- a/components/interface/VFBTermInfo/VFBTermInfo.js +++ b/components/interface/VFBTermInfo/VFBTermInfo.js @@ -133,8 +133,12 @@ class VFBTermInfo extends React.Component { instanceType = anyInstance.getType(); } + if ( typeof instanceType?.getVariables !== "function" ) { + return; + } + // If there are no variables, we have an empty composite node, don't add any links - if ( instanceType.getVariables().length == 0 ){ + if ( instanceType?.getVariables()?.length == 0 ){ return; } diff --git a/components/interface/VFBTree/VFBTree.js b/components/interface/VFBTree/VFBTree.js index 19ef9b601..99c33a2f0 100644 --- a/components/interface/VFBTree/VFBTree.js +++ b/components/interface/VFBTree/VFBTree.js @@ -90,9 +90,9 @@ class VFBTree extends React.Component { }); } - findChildren (parent, key, familyList, label) { + findChildren (parent, key, familyList, labels) { var childrenList = []; - var childKey = this.searchChildren(familyList, key, parent, label); + var childKey = this.searchChildren(familyList, key, parent, labels); if (childKey !== undefined) { childrenList.push(childKey); var i = childKey - 1; @@ -110,7 +110,8 @@ class VFBTree extends React.Component { } insertChildren (nodes, edges, child, imagesMap) { - var childrenList = this.findChildren({ from: child.id }, "from", edges, "part of"); + // Extend the array of relationships from here + var childrenList = this.findChildren({ from: child.id }, "from", edges, ["part of", "SUBCLASSOF"]); // child.images = this.findChildren({ from: child.id }, "from", edges, "INSTANCEOF"); var nodesList = []; for ( var i = 0; i < childrenList.length; i++) { @@ -190,16 +191,16 @@ class VFBTree extends React.Component { * this will reflect and move to the node (if it exists) that we have on focus. */ var innerInstance = undefined; - if (instance.getParent() !== null) { + if (instance?.getParent() !== null) { innerInstance = instance.getParent(); } else { innerInstance = instance; } - var idToSearch = innerInstance.getId(); + var idToSearch = innerInstance?.getId(); if (this.state.nodeSelected !== undefined - && idToSearch !== this.state.nodeSelected.instanceId - && idToSearch !== this.state.nodeSelected.classId) { + && idToSearch !== this.state.nodeSelected?.instanceId + && idToSearch !== this.state.nodeSelected?.classId) { if (idToSearch === window.templateID) { this.selectNode(this.state.dataTree[0]) return; @@ -257,9 +258,9 @@ class VFBTree extends React.Component { nodes: nodes, nodeSelected: (this.props.instance === undefined ? treeData[0] - : (this.props.instance.getParent() === null - ? { subtitle: this.props.instance.getId() } - : { subtitle: this.props.instance.getParent().getId() })) + : (this.props.instance?.getParent() === null + ? { subtitle: this.props.instance?.getId() } + : { subtitle: this.props.instance?.getParent()?.getId() })) }); } else { var treeData = [{ @@ -351,10 +352,10 @@ class VFBTree extends React.Component { aria-hidden="true" onClick={ e => { e.stopPropagation(); - if (Instances[rowInfo.node.instanceId].getParent() !== null) { - Instances[rowInfo.node.instanceId].getParent().show(); + if (Instances[rowInfo.node.instanceId]?.getParent() !== null) { + Instances[rowInfo.node.instanceId]?.getParent().show(); } else { - Instances[rowInfo.node.instanceId].show(); + Instances[rowInfo.node.instanceId]?.show(); } this.setState({ nodeSelected: rowInfo.node }); }} />); @@ -365,8 +366,8 @@ class VFBTree extends React.Component { aria-hidden="true" onClick={ e => { e.stopPropagation(); - if (Instances[rowInfo.node.instanceId].getParent() !== null) { - Instances[rowInfo.node.instanceId].getParent().hide(); + if (Instances[rowInfo.node.instanceId]?.getParent() !== null) { + Instances[rowInfo.node.instanceId]?.getParent().hide(); } else { Instances[rowInfo.node.instanceId].hide(); } @@ -438,7 +439,7 @@ class VFBTree extends React.Component { this.colorPickerContainer = undefined; let instanceFound = false; for (let i = 0; i < Instances.length; i++) { - if (Instances[i].getId() === rowInfo.node.instanceId) { + if (Instances[i]?.getId() === rowInfo.node.instanceId) { instanceFound = true; break; } @@ -548,11 +549,11 @@ class VFBTree extends React.Component { treeData={treeData} activateParentsNodeOnClick={true} handleClick={this.nodeClick} - style={{ width: this.props.size.width - 10, height: this.props.size.height, float: 'left', clear: 'both' }} + style={{ width: this.props.size?.width - 10, height: this.props.size?.height, float: 'left', clear: 'both' }} rowHeight={this.styles.row_height} getButtons={this.getButtons} getNodesProps={this.getNodes} - searchQuery={this.state.nodeSelected === undefined ? this.props.instance.getParent().getId() : this.state.nodeSelected.subtitle} + searchQuery={this.state.nodeSelected === undefined ? this.props.instance?.getParent()?.getId() : this.state.nodeSelected?.subtitle} onlyExpandSearchedNodes={false} /> } diff --git a/components/interface/VFBTree/helper.js b/components/interface/VFBTree/helper.js index 084051717..26adb81e0 100644 --- a/components/interface/VFBTree/helper.js +++ b/components/interface/VFBTree/helper.js @@ -48,7 +48,7 @@ const buildDictClassToIndividual = data => { return dictionaryIndividuals; }; -const searchChildren = (array, key, target, label) => { +const searchChildren = (array, key, target, labels) => { // Define Start and End Index let startIndex = 0; let endIndex = array.length - 1; @@ -60,7 +60,7 @@ const searchChildren = (array, key, target, label) => { // Compare Middle Index with Target for match if (isNumber(array[middleIndex][key]) === isNumber(target[key])) { // check for target relationship (label) - if (array[middleIndex].label === label){ + if ((labels === undefined) || labels.includes(array[middleIndex].label)){ return middleIndex; } else { // move on if not matching target relationship (label) diff --git a/components/interface/VFBUploader/VFBUploader.js b/components/interface/VFBUploader/VFBUploader.js index 020486f0d..146f9f68b 100644 --- a/components/interface/VFBUploader/VFBUploader.js +++ b/components/interface/VFBUploader/VFBUploader.js @@ -1,172 +1,359 @@ -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" } -}); +import React from "react"; +import { + Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, + FormGroup, InputLabel, Select, Typography, IconButton, Divider, Box, TextField, + ListItemIcon, ListItemText, Checkbox, MenuItem, Button, LinearProgress, CircularProgress, Grid +} from "@material-ui/core"; +import FileCopyIcon from "@material-ui/icons/FileCopy"; +import CloseIcon from "@material-ui/icons/Close"; +import DeleteIcon from "@material-ui/icons/Delete"; +import CheckIcon from '@material-ui/icons/Check'; +import InfoIcon from '@material-ui/icons/Info'; +import ReplayIcon from '@material-ui/icons/Replay'; +import { createMuiTheme, ThemeProvider } from "@material-ui/core/styles"; +import { withStyles } from "@material-ui/styles"; +import axios from "axios"; +import { DropzoneArea } from "material-ui-dropzone"; +import UploadIcon from "../../configuration/VFBUploader/upload-icon.png"; +import { nanoid } from 'nanoid'; +import FileIcon from "../../configuration/VFBUploader/file-icon.png"; +import { CustomStyle, CustomTheme } from "./styles"; +const UNIQUE_ID = "UNIQUE_ID"; 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'); + fileNBLASTURL: "", + nblastEnabled: false, + files: [], + templateSelected: "", + progress: 100, + cookies : false, + error : false, + uploading : false + }; + + 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.handleFileDelete = this.handleFileDelete.bind(this); this.requestUpload = this.requestUpload.bind(this); + this.getTitleHead = this.getTitleHead.bind(this); + this.getUploaderComponents = this.getUploaderComponents.bind(this); + this.getErrorDialog = this.getErrorDialog.bind(this); + this.getUploadActions = this.getUploadActions.bind(this); + this.handleCookieEvent = this.handleCookieEvent.bind(this); + } + + handleCookieEvent (event) { + this.setState({ cookies : event.target.checked }) } handleCloseDialog () { - this.setState({ open : false }); + this.setState({ open: false }); } - - handleDropZoneChange (files){ + + handleDropZoneChange (files) { this.setState({ files: files }); } - + + handleFileDelete () { + this.setState({ files: [], nblastEnabled: false }); + } + handleTemplateChange (event) { - this.setState({ templateSelected : event.target.value }) + this.setState({ templateSelected: event.target.value }); } - + openDialog () { - this.setState({ open : true }); + this.setState({ open: true }); } - + handleNBLASTAction () { - this.requestUpload({}); + let newId = "VFBu_" + nanoid(8); + let url = this.configuration.nblastURL.replace(UNIQUE_ID, this.state.templateSelected + "&" + newId); + var formData = new FormData(); + formData.append("file", this.state.files[0]); + formData.append("vfbID", newId); + formData.append("templateID", this.state.templateSelected); + this.requestUpload(formData, url); } - + /** - * Make axios call to download the zip + * Make axios call to upload to server */ - requestUpload (jsonRequest) { + requestUpload (formData, url) { let self = this; + let _id = formData.get("vfbID"); + let newURL = window.location.origin + window.location.pathname + "&q=" + _id + "," + this.configuration.queryType; - 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 } ); + this.setState({ fileNBLASTURL: newURL, uploading : true }); + window.setCookie(_id, newURL, this.configuration.cookieStorageDays); + + axios.put(url, + formData, { headers: { 'Content-Type': this.configuration.contentType } } + ).then(function (response) { + console.log('SUCCESS!!', response); + self.setState({ uploading : false, nblastEnabled: true }); }) + .catch(function (error) { + console.log('FAILURE!!', error); + self.setState({ error : true, uploading : false }); + }); } - /** - * Handle checkbox selection of different types to download - */ - handlePermissionsCheck (event) { - this.setState({ permissionsChecked : event.target.checked }); + getTitleHead () { + return ( + + {this.configuration.text.dialogTitle} + + + {this.configuration.text.dialogSubtitle} + + ); } - render () { - let self = this; + getUploaderComponents () { const { classes } = this.props; - + let self = this; return ( - -

NBLAST Uploader

- - - Choose Template: + + + {self.configuration.text.selectTemplate} + - - - -
- --- NBLAST Text Updates --- - self.handlePermissionsCheck(event)} - /> - } - label={{PERMISSIONS}} + + + {self.configuration.text.addYourFile} + {self.state.files.length > 0 ? ( + + + + + + + + + {self.state.files[0].name} + + + + + + + + + + ) + : upload} + showAlerts={["error"]} + fullWidth + showPreviewsInDropzone={false} + dropzoneClass={classes.dropzoneArea} /> -
- { - self.state.uploading ? : null } - -
-
- ) + + + + + + + + {this.configuration.text.infoMessage} + + + + + ); + } + + getErrorDialog () { + const { classes } = this.props; + + return ( + + + + + + + + {this.configuration.text.errorDialog} + + + + + ); + } + + getUploadActions () { + const { classes } = this.props; + let self = this; + + return ( + <> + + + + + { !this.state.error + ? !this.state.nblastEnabled ? ( + + + + ) : ( + + + + ) + : + + + } + + + ); + } + + render () { + let self = this; + const { classes } = this.props; + + return ( + + + + {this.getTitleHead()} + + + { !self.state.error + ? !self.state.nblastEnabled ? this.getUploaderComponents() : this.getSuccessComponent() + : this.getErrorDialog() + } + + + + {self.getUploadActions()} + + + + ); } } -export default (withStyles(styles)(VFBUploader)); \ No newline at end of file +export default withStyles(CustomStyle)(VFBUploader); \ No newline at end of file diff --git a/components/interface/VFBUploader/styles.js b/components/interface/VFBUploader/styles.js new file mode 100644 index 000000000..a4a796f76 --- /dev/null +++ b/components/interface/VFBUploader/styles.js @@ -0,0 +1,107 @@ +import { createMuiTheme } from "@material-ui/core/styles"; + +export const CustomStyle = theme => ({ + dropzoneArea: { minHeight: "20vh !important" }, + marginTop: { marginTop: "2vh !important" }, + checked: { "&$checked": { color: "#0AB7FE" } }, + dialog: { + overflowY: 'unset', + maxWidth: "60vh", + width: "60vh", + margin: "0 auto" + }, + customizedButton: { + position: 'absolute', + left: '90%', + top: '2%', + backgroundColor: '#F5F5F5', + color: 'gray', + }, + errorButton : { + backgroundColor : "rgba(252, 231, 231, 1)", + color : "red", + borderColor : "red", + "&:hover": { + backgroundColor: "rgba(252, 231, 231, 1)", + color: "red" + } + }, + vfbColor : { backgroundColor : "#EEF9FF" }, + cookiesBox : { + width : "100%", + display : "contents" + } +}) + +export const CustomTheme = createMuiTheme({ + typography: { + h2: { + fontSize: 22, + fontWeight: 400, + fontStyle: "normal", + color : "#181818", + lineHeight: "26.4px", + fontFamily: "Barlow Condensed", + }, + caption: { + fontSize: 11, + fontWeight: 500, + fontStyle: "normal", + color : "#181818", + lineHeight: "13.2px", + fontFamily: "Barlow Condensed", + }, + h5: { + fontSize: 11, + fontWeight: 500, + fontStyle: "normal", + color : "rgba(0, 0, 0, 0.4)", + lineHeight: "13.2px", + fontFamily: "Barlow Condensed", + } + }, + palette: { primary: { main: '#0AB7FE' }, secondary : { main : "#fff" }, error : { main : "#ff0000" } }, + overrides: { + MuiButton: { + contained: { + color: "#f1f1f1", + backgroundColor : "#0AB7FE", + "&:hover": { + backgroundColor: "#0AB7FE", + color: "#f1f1f1" + }, + "&:disabled": { + backgroundColor: "rgba(10, 183, 254, .4)", + color: "#f1f1f1" + } + }, + outlined: { + color: "#0AB7FE", + borderColor : "#0AB7FE", + "&:hover": { + backgroundColor: "#0AB7FE", + color: "#f1f1f1" + } + }, + }, + MuiFilledInput : { + root : { backgroundColor : "#EEF9FF" }, + input : { + color : "#0AB7FE !important", + borderColor : "#0AB7FE !important" + } + }, + MuiSelect : { + root : { + textAlign : "start", + display : "flex" + } + }, + MuiListItemIcon : { + root : { + color : "#0AB7FE", + padding : "4px" + } + } + } +}); diff --git a/model/vfb.xmi b/model/vfb.xmi index 4590f5d37..987c0c371 100644 --- a/model/vfb.xmi +++ b/model/vfb.xmi @@ -186,7 +186,7 @@ name="anatomy_query" description="fetch Individual instances from Class ID list" runForCount="false" - query=""statement": "MATCH (primary:Class) WHERE primary.short_form in $ids WITH primary CALL apoc.cypher.run('WITH primary OPTIONAL MATCH (primary)<- [:has_source|SUBCLASSOF|INSTANCEOF*]-(i:Individual)<-[:depicts]- (channel:Individual)-[irw:in_register_with] ->(template:Individual)-[:depicts]-> (template_anat:Individual) RETURN template, channel, template_anat, i, irw limit 5', {primary:primary}) yield value with value.template as template, value.channel as channel,value.template_anat as template_anat, value.i as i, value.irw as irw, primary OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i), symbol: coalesce(i.symbol[0], '')} , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }} }) END AS anatomy_channel_image ,primary RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'bac066c' AS version, 'anatomy_query' AS query, anatomy_channel_image", "parameters" : { "ids" : $ARRAY_ID_RESULTS }" + query=""statement": "MATCH (primary:Class) WHERE primary.short_form in $ids WITH primary CALL apoc.cypher.run('WITH primary OPTIONAL MATCH (primary)<- [:has_source|SUBCLASSOF|INSTANCEOF*]-(i:Individual)<-[:depicts]- (channel:Individual)-[irw:in_register_with] ->(template:Individual)-[:depicts]-> (template_anat:Individual) RETURN template, channel, template_anat, i, irw limit 5', {primary:primary}) yield value with value.template as template, value.channel as channel,value.template_anat as template_anat, value.i as i, value.irw as irw, primary OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i), symbol: coalesce(i.symbol[0], '')} , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), unique_facets: coalesce(channel.uniqueFacets,[]), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), unique_facets: coalesce(technique.uniqueFacets,[]), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), unique_facets: coalesce(template.uniqueFacets,[]), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }} }) END AS anatomy_channel_image ,primary RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), unique_facets: coalesce(primary.uniqueFacets,[]), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'bac066c' AS version, 'anatomy_query' AS query, anatomy_channel_image", "parameters" : { "ids" : $ARRAY_ID_RESULTS }" countQuery=""statement": "MATCH (n:Class) WHERE n.short_form IN $ids RETURN count(n) AS count", "parameters" : { "ids" : $ARRAY_ID_RESULTS }"/> + query=""statement": "MATCH (n:Template {short_form:$id})<-[:depicts]-(:Template)<-[r:in_register_with]-(dc:Individual)-[:depicts]->(di:Individual) OPTIONAL MATCH (di)-[:INSTANCEOF]->(d:Class) RETURN distinct di.short_form as id, di.label as name, coalesce(di.description[0],d.description[0]) as def, COLLECT(DISTINCT d.label) as type, replace(r.folder[0],'http:','https:') + '/thumbnailT.png' as file", "parameters" : { "id" : "$ID" }" + countQuery=""statement": "MATCH (n:Template {short_form:$id})<-[:depicts]-(:Template)<-[r:in_register_with]-(dc:Individual)-[:depicts]->(di:Individual) RETURN count(di) as count", "parameters" : { "id" : "$ID" }"/> @@ -597,7 +597,7 @@ name="Get JSON for Neuron Class" description="Get JSON for Neuron Class" runForCount="false" - query=""statement": "MATCH (primary:Class) WHERE primary.short_form in [$id] WITH primary OPTIONAL MATCH (o:Class)<-[r:SUBCLASSOF|INSTANCEOF]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} ) END AS parents ,primary OPTIONAL MATCH (o:Class)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} }) END AS relationships ,primary,parents OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, parents, relationships OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,parents,relationships CALL apoc.cypher.run('WITH primary OPTIONAL MATCH (primary)<-[:has_source|SUBCLASSOF|INSTANCEOF*]-(i:Individual)<-[:depicts]-(channel:Individual)-[irw:in_register_with] ->(template:Individual)-[:depicts]->(template_anat:Individual) RETURN template, channel, template_anat, i, irw limit 10', {primary:primary}) yield value with value.template as template, value.channel as channel,value.template_anat as template_anat, value.i as i, value.irw as irw, primary, parents, relationships, xrefs OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i), symbol: coalesce(i.symbol[0], '')} , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }} }) END AS anatomy_channel_image ,primary,parents,relationships,xrefs OPTIONAL MATCH (primary)-[rp:has_reference]->(p:pub) where rp.typ = 'syn' WITH CASE WHEN p is null THEN [] ELSE collect({ pub: { core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } , synonym: { label: coalesce(rp.value[0], ''), scope: coalesce(rp.scope, ''), type: coalesce(rp.has_synonym_type[0],'') } }) END AS pub_syn,primary,parents,relationships,xrefs,anatomy_channel_image OPTIONAL MATCH (primary)-[rp:has_reference]->(p:pub) WHERE rp.typ = 'def' WITH CASE WHEN p is null THEN [] ELSE collect({ core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } ) END AS def_pubs,primary,parents,relationships,xrefs,anatomy_channel_image,pub_syn OPTIONAL MATCH (:Class { label: 'intersectional expression pattern'})<-[:SUBCLASSOF]-(ep:Class)<-[ar:part_of]-(anoni:Individual)-[:INSTANCEOF]->(primary) WITH CASE WHEN ep IS NULL THEN [] ELSE COLLECT({ short_form: ep.short_form, label: coalesce(ep.label,''), iri: ep.iri, types: labels(ep), symbol: coalesce(ep.symbol[0], '')} ) END AS targeting_splits,primary,parents,relationships,xrefs,anatomy_channel_image,pub_syn,def_pubs RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for Neuron Class' AS query, '4025897' AS version , parents, relationships, xrefs, anatomy_channel_image, pub_syn, def_pubs, targeting_splits", "parameters" : { "id" : "$ID" }" + query=""statement": "MATCH (primary:Class) WHERE primary.short_form in [$id] WITH primary OPTIONAL MATCH (o:Class)<-[r:SUBCLASSOF|INSTANCEOF]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), unique_facets: coalesce(o.uniqueFacets,[]), symbol: coalesce(o.symbol[0], '')} ) END AS parents ,primary OPTIONAL MATCH (o:Class)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), unique_facets: coalesce(o.uniqueFacets,[]), symbol: coalesce(o.symbol[0], '')} }) END AS relationships ,primary,parents OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), unique_facets: coalesce(s.uniqueFacets,[]), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, parents, relationships OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), unique_facets: coalesce(s.uniqueFacets,[]), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,parents,relationships CALL apoc.cypher.run('WITH primary OPTIONAL MATCH (primary)<-[:has_source|SUBCLASSOF|INSTANCEOF*]-(i:Individual)<-[:depicts]-(channel:Individual)-[irw:in_register_with] ->(template:Individual)-[:depicts]->(template_anat:Individual) RETURN template, channel, template_anat, i, irw limit 10', {primary:primary}) yield value with value.template as template, value.channel as channel,value.template_anat as template_anat, value.i as i, value.irw as irw, primary, parents, relationships, xrefs OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i), symbol: coalesce(i.symbol[0], '')} , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), unique_facets: coalesce(channel.uniqueFacets,[]), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), unique_facets: coalesce(technique.uniqueFacets,[]), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), unique_facets: coalesce(template.uniqueFacets,[]), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }} }) END AS anatomy_channel_image ,primary,parents,relationships,xrefs OPTIONAL MATCH (primary)-[rp:has_reference]->(p:pub) where rp.typ = 'syn' WITH CASE WHEN p is null THEN [] ELSE collect({ pub: { core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } , synonym: { label: coalesce(rp.value[0], ''), scope: coalesce(rp.scope, ''), type: coalesce(rp.has_synonym_type[0],'') } }) END AS pub_syn,primary,parents,relationships,xrefs,anatomy_channel_image OPTIONAL MATCH (primary)-[rp:has_reference]->(p:pub) WHERE rp.typ = 'def' WITH CASE WHEN p is null THEN [] ELSE collect({ core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } ) END AS def_pubs,primary,parents,relationships,xrefs,anatomy_channel_image,pub_syn OPTIONAL MATCH (:Class { label: 'intersectional expression pattern'})<-[:SUBCLASSOF]-(ep:Class)<-[ar:part_of]-(anoni:Individual)-[:INSTANCEOF]->(primary) WITH CASE WHEN ep IS NULL THEN [] ELSE COLLECT({ short_form: ep.short_form, label: coalesce(ep.label,''), iri: ep.iri, types: labels(ep), symbol: coalesce(ep.symbol[0], '')} ) END AS targeting_splits,primary,parents,relationships,xrefs,anatomy_channel_image,pub_syn,def_pubs RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), unique_facets: coalesce(primary.uniqueFacets,[]), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for Neuron Class' AS query, '4025897' AS version , parents, relationships, xrefs, anatomy_channel_image, pub_syn, def_pubs, targeting_splits", "parameters" : { "id" : "$ID" }" countQuery=""statement": "MATCH (primary:Class {short_form: $id} ) RETURN count(primary) as count", "parameters" : { "id" : "$ID" }"> @@ -607,7 +607,7 @@ name="Get JSON for Split Class" description="Get JSON for Split Class" runForCount="false" - query=""statement": "MATCH (primary:Class) WHERE primary.short_form in [$id] WITH primary OPTIONAL MATCH (o:Class)<-[r:SUBCLASSOF|INSTANCEOF]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} ) END AS parents ,primary OPTIONAL MATCH (o:Class)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} }) END AS relationships ,primary,parents OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, parents, relationships OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,parents,relationships CALL apoc.cypher.run('WITH primary OPTIONAL MATCH (primary)<-[:has_source|SUBCLASSOF|INSTANCEOF*]-(i:Individual)<-[:depicts]-(channel:Individual)-[irw:in_register_with] ->(template:Individual)-[:depicts]->(template_anat:Individual) RETURN template, channel, template_anat, i, irw limit 10', {primary:primary}) yield value with value.template as template, value.channel as channel,value.template_anat as template_anat, value.i as i, value.irw as irw, primary, parents, relationships, xrefs OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i), symbol: coalesce(i.symbol[0], '')} , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }} }) END AS anatomy_channel_image ,primary,parents,relationships,xrefs OPTIONAL MATCH (primary)-[rp:has_reference]->(p:pub) where rp.typ = 'syn' WITH CASE WHEN p is null THEN [] ELSE collect({ pub: { core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } , synonym: { label: coalesce(rp.value[0], ''), scope: coalesce(rp.scope, ''), type: coalesce(rp.has_synonym_type[0],'') } }) END AS pub_syn,primary,parents,relationships,xrefs,anatomy_channel_image OPTIONAL MATCH (primary)-[rp:has_reference]->(p:pub) WHERE rp.typ = 'def' WITH CASE WHEN p is null THEN [] ELSE collect({ core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } ) END AS def_pubs,primary,parents,relationships,xrefs,anatomy_channel_image,pub_syn OPTIONAL MATCH (:Class { label: 'intersectional expression pattern'})<-[:SUBCLASSOF]-(primary)<-[ar:part_of]-(anoni:Individual)-[:INSTANCEOF]->(n:Neuron) WITH CASE WHEN n IS NULL THEN [] ELSE COLLECT({ short_form: n.short_form, label: coalesce(n.label,''), iri: n.iri, types: labels(n), symbol: coalesce(n.symbol[0], '')} ) END AS target_neurons,primary,parents,relationships,xrefs,anatomy_channel_image,pub_syn,def_pubs RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for Split Class' AS query, '4025897' AS version , parents, relationships, xrefs, anatomy_channel_image, pub_syn, def_pubs, target_neurons", "parameters" : { "id" : "$ID" }" + query=""statement": "MATCH (primary:Class) WHERE primary.short_form in [$id] WITH primary OPTIONAL MATCH (o:Class)<-[r:SUBCLASSOF|INSTANCEOF]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), unique_facets: coalesce(o.uniqueFacets,[]), symbol: coalesce(o.symbol[0], '')} ) END AS parents ,primary OPTIONAL MATCH (o:Class)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), unique_facets: coalesce(o.uniqueFacets,[]), symbol: coalesce(o.symbol[0], '')} }) END AS relationships ,primary,parents OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), unique_facets: coalesce(s.uniqueFacets,[]), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, parents, relationships OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), unique_facets: coalesce(s.uniqueFacets,[]), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,parents,relationships CALL apoc.cypher.run('WITH primary OPTIONAL MATCH (primary)<-[:has_source|SUBCLASSOF|INSTANCEOF*]-(i:Individual)<-[:depicts]-(channel:Individual)-[irw:in_register_with] ->(template:Individual)-[:depicts]->(template_anat:Individual) RETURN template, channel, template_anat, i, irw limit 10', {primary:primary}) yield value with value.template as template, value.channel as channel,value.template_anat as template_anat, value.i as i, value.irw as irw, primary, parents, relationships, xrefs OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i), symbol: coalesce(i.symbol[0], '')} , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), unique_facets: coalesce(channel.uniqueFacets,[]), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), unique_facets: coalesce(technique.uniqueFacets,[]), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), unique_facets: coalesce(template.uniqueFacets,[]), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }} }) END AS anatomy_channel_image ,primary,parents,relationships,xrefs OPTIONAL MATCH (primary)-[rp:has_reference]->(p:pub) where rp.typ = 'syn' WITH CASE WHEN p is null THEN [] ELSE collect({ pub: { core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } , synonym: { label: coalesce(rp.value[0], ''), scope: coalesce(rp.scope, ''), type: coalesce(rp.has_synonym_type[0],'') } }) END AS pub_syn,primary,parents,relationships,xrefs,anatomy_channel_image OPTIONAL MATCH (primary)-[rp:has_reference]->(p:pub) WHERE rp.typ = 'def' WITH CASE WHEN p is null THEN [] ELSE collect({ core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } ) END AS def_pubs,primary,parents,relationships,xrefs,anatomy_channel_image,pub_syn OPTIONAL MATCH (:Class { label: 'intersectional expression pattern'})<-[:SUBCLASSOF]-(primary)<-[ar:part_of]-(anoni:Individual)-[:INSTANCEOF]->(n:Neuron) WITH CASE WHEN n IS NULL THEN [] ELSE COLLECT({ short_form: n.short_form, label: coalesce(n.label,''), iri: n.iri, types: labels(n), symbol: coalesce(n.symbol[0], '')} ) END AS target_neurons,primary,parents,relationships,xrefs,anatomy_channel_image,pub_syn,def_pubs RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), unique_facets: coalesce(primary.uniqueFacets,[]), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for Split Class' AS query, '4025897' AS version , parents, relationships, xrefs, anatomy_channel_image, pub_syn, def_pubs, target_neurons", "parameters" : { "id" : "$ID" }" countQuery=""statement": "MATCH (primary:Class {short_form: $id} ) RETURN count(primary) as count", "parameters" : { "id" : "$ID" }"> @@ -617,7 +617,7 @@ name="Get JSON for Individual" description="Get JSON for Individual:Anatomy" runForCount="false" - query=""statement": "MATCH (primary:Individual) WHERE primary.short_form in [$id] WITH primary OPTIONAL MATCH (primary)-[:has_source]-(ds:DataSet)-[:has_license|license]->(l:License) WITH COLLECT ({ dataset: { link : coalesce(ds.dataset_link[0], ''), core : { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds), symbol: coalesce(ds.symbol[0], '')} }, license: { icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), symbol: coalesce(l.symbol[0], '')} }}) AS dataset_license,primary OPTIONAL MATCH (o:Class)<-[r:SUBCLASSOF|INSTANCEOF]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} ) END AS parents ,primary,dataset_license OPTIONAL MATCH (o:Class)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} }) END AS relationships ,primary,dataset_license,parents OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, dataset_license, parents, relationships OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,dataset_license,parents,relationships OPTIONAL MATCH (primary)<-[:depicts]-(channel:Individual)-[irw:in_register_with]->(template:Individual)-[:depicts]->(template_anat:Individual) WITH template, channel, template_anat, irw, primary, dataset_license, parents, relationships, xrefs OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE collect ({ channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }}) END AS channel_image,primary,dataset_license,parents,relationships,xrefs RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for Individual:Anatomy' AS query, '4025897' AS version , dataset_license, parents, relationships, xrefs, channel_image", "parameters" : { "id" : "$ID" }" + query=""statement": "MATCH (primary:Individual) WHERE primary.short_form in [$id] WITH primary OPTIONAL MATCH (primary)-[:has_source]-(ds:DataSet)-[:has_license|license]->(l:License) WITH COLLECT ({ dataset: { link : coalesce(ds.dataset_link[0], ''), core : { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds), symbol: coalesce(ds.symbol[0], '')} }, license: { icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), unique_facets: coalesce(l.uniqueFacets,[]), symbol: coalesce(l.symbol[0], '')} }}) AS dataset_license,primary OPTIONAL MATCH (o:Class)<-[r:SUBCLASSOF|INSTANCEOF]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), unique_facets: coalesce(o.uniqueFacets,[]), symbol: coalesce(o.symbol[0], '')} ) END AS parents ,primary,dataset_license OPTIONAL MATCH (o:Class)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), unique_facets: coalesce(o.uniqueFacets,[]), symbol: coalesce(o.symbol[0], '')} }) END AS relationships ,primary,dataset_license,parents OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), unique_facets: coalesce(s.uniqueFacets,[]), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, dataset_license, parents, relationships OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), unique_facets: coalesce(s.uniqueFacets,[]), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,dataset_license,parents,relationships OPTIONAL MATCH (primary)<-[:depicts]-(channel:Individual)-[irw:in_register_with]->(template:Individual)-[:depicts]->(template_anat:Individual) WITH template, channel, template_anat, irw, primary, dataset_license, parents, relationships, xrefs OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE collect ({ channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), unique_facets: coalesce(channel.uniqueFacets,[]), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), unique_facets: coalesce(technique.uniqueFacets,[]), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), unique_facets: coalesce(template.uniqueFacets,[]), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }}) END AS channel_image,primary,dataset_license,parents,relationships,xrefs RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), unique_facets: coalesce(primary.uniqueFacets,[]), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for Individual:Anatomy' AS query, '4025897' AS version , dataset_license, parents, relationships, xrefs, channel_image", "parameters" : { "id" : "$ID" }" countQuery=""statement": "MATCH (primary:Individual {short_form: $id} ) RETURN count(primary) as count", "parameters" : { "id" : "$ID" }"> @@ -637,7 +637,7 @@ name="Get JSON for Template" description="Get JSON for Template" runForCount="false" - query=""statement": "MATCH (primary:Template) WHERE primary.short_form in [$id] WITH primary MATCH (channel:Individual)<-[irw:in_register_with]-(channel:Individual)-[:depicts]->(primary) WITH { index: coalesce(apoc.convert.toInteger(irw.index), []) + [], extent: irw.extent[0], center: irw.center[0], voxel: irw.voxel[0], orientation: coalesce(irw.orientation[0], ''), image_folder: coalesce(irw.folder[0],''), channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), symbol: coalesce(channel.symbol[0], '')} } as template_channel,primary OPTIONAL MATCH (technique:Class)<-[:is_specified_output_of]-(channel:Individual)-[irw:in_register_with]->(template:Individual)-[:depicts]->(primary) WHERE technique.short_form IN ['FBbi_00000224','FBbi_00000251'] AND exists(irw.index) WITH primary, template_channel, collect ({ channel: channel, irw: irw}) AS painted_domains UNWIND painted_domains AS pd OPTIONAL MATCH (channel:Individual { short_form: pd.channel.short_form})-[:depicts]-(ai:Individual)-[:INSTANCEOF]->(c:Class) WITH collect({ anatomical_type: { short_form: c.short_form, label: coalesce(c.label,''), iri: c.iri, types: labels(c), symbol: coalesce(c.symbol[0], '')} , anatomical_individual: { short_form: ai.short_form, label: coalesce(ai.label,''), iri: ai.iri, types: labels(ai), symbol: coalesce(ai.symbol[0], '')} , folder: pd.irw.folder[0], center: coalesce (pd.irw.center, []), index: [] + coalesce (pd.irw.index, []) }) AS template_domains,primary,template_channel OPTIONAL MATCH (primary)-[:has_source]-(ds:DataSet)-[:has_license|license]->(l:License) WITH COLLECT ({ dataset: { link : coalesce(ds.dataset_link[0], ''), core : { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds), symbol: coalesce(ds.symbol[0], '')} }, license: { icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), symbol: coalesce(l.symbol[0], '')} }}) AS dataset_license,primary,template_channel,template_domains OPTIONAL MATCH (o:Class)<-[r:SUBCLASSOF|INSTANCEOF]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} ) END AS parents ,primary,template_channel,template_domains,dataset_license OPTIONAL MATCH (o:Class)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} }) END AS relationships ,primary,template_channel,template_domains,dataset_license,parents OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, template_channel, template_domains, dataset_license, parents, relationships OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,template_channel,template_domains,dataset_license,parents,relationships OPTIONAL MATCH (o:Individual)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), symbol: coalesce(o.symbol[0], '')} }) END AS related_individuals ,primary,template_channel,template_domains,dataset_license,parents,relationships,xrefs RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for Template with expanded domain techniques' AS query, 'm20211015' AS version , template_channel, template_domains, dataset_license, parents, relationships, xrefs, related_individuals", "parameters" : { "id" : "$ID" }" + query=""statement": "MATCH (primary:Template) WHERE primary.short_form in [$id] WITH primary MATCH (channel:Individual)<-[irw:in_register_with]-(channel:Individual)-[:depicts]->(primary) WITH { index: coalesce(apoc.convert.toInteger(irw.index), []) + [], extent: irw.extent[0], center: irw.center[0], voxel: irw.voxel[0], orientation: coalesce(irw.orientation[0], ''), image_folder: coalesce(irw.folder[0],''), channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), unique_facets: coalesce(channel.uniqueFacets,[]), symbol: coalesce(channel.symbol[0], '')} } as template_channel,primary OPTIONAL MATCH (technique:Class)<-[:is_specified_output_of]-(channel:Individual)-[irw:in_register_with]->(template:Individual)-[:depicts]->(primary) WHERE technique.short_form IN ['FBbi_00000224','FBbi_00000251'] AND exists(irw.index) WITH primary, template_channel, collect ({ channel: channel, irw: irw}) AS painted_domains UNWIND painted_domains AS pd OPTIONAL MATCH (channel:Individual { short_form: pd.channel.short_form})-[:depicts]-(ai:Individual)-[:INSTANCEOF]->(c:Class) WITH collect({ anatomical_type: { short_form: c.short_form, label: coalesce(c.label,''), iri: c.iri, types: labels(c), symbol: coalesce(c.symbol[0], '')} , anatomical_individual: { short_form: ai.short_form, label: coalesce(ai.label,''), iri: ai.iri, types: labels(ai), symbol: coalesce(ai.symbol[0], '')} , folder: pd.irw.folder[0], center: coalesce (pd.irw.center, []), index: [] + coalesce (pd.irw.index, []) }) AS template_domains,primary,template_channel OPTIONAL MATCH (primary)-[:has_source]-(ds:DataSet)-[:has_license|license]->(l:License) WITH COLLECT ({ dataset: { link : coalesce(ds.dataset_link[0], ''), core : { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds), symbol: coalesce(ds.symbol[0], '')} }, license: { icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), unique_facets: coalesce(l.uniqueFacets,[]), symbol: coalesce(l.symbol[0], '')} }}) AS dataset_license,primary,template_channel,template_domains OPTIONAL MATCH (o:Class)<-[r:SUBCLASSOF|INSTANCEOF]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), unique_facets: coalesce(o.uniqueFacets,[]), symbol: coalesce(o.symbol[0], '')} ) END AS parents ,primary,template_channel,template_domains,dataset_license OPTIONAL MATCH (o:Class)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), unique_facets: coalesce(o.uniqueFacets,[]), symbol: coalesce(o.symbol[0], '')} }) END AS relationships ,primary,template_channel,template_domains,dataset_license,parents OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), unique_facets: coalesce(s.uniqueFacets,[]), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, template_channel, template_domains, dataset_license, parents, relationships OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), unique_facets: coalesce(s.uniqueFacets,[]), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,template_channel,template_domains,dataset_license,parents,relationships OPTIONAL MATCH (o:Individual)<-[r {type:'Related'}]-(primary) WITH CASE WHEN o IS NULL THEN [] ELSE COLLECT ({ relation: { label: r.label, iri: r.iri, type: type(r) } , object: { short_form: o.short_form, label: coalesce(o.label,''), iri: o.iri, types: labels(o), unique_facets: coalesce(o.uniqueFacets,[]), symbol: coalesce(o.symbol[0], '')} }) END AS related_individuals ,primary,template_channel,template_domains,dataset_license,parents,relationships,xrefs RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), unique_facets: coalesce(primary.uniqueFacets,[]), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for Template with expanded domain techniques' AS query, 'm20211015' AS version , template_channel, template_domains, dataset_license, parents, relationships, xrefs, related_individuals", "parameters" : { "id" : "$ID" }" countQuery=""statement": "MATCH (primary:Template {short_form: $id} ) RETURN count(primary) as count", "parameters" : { "id" : "$ID" }"> @@ -647,7 +647,7 @@ name="Get JSON for pub" description="Fetches JSON for pub." runForCount="false" - query=""statement": "MATCH (primary:Individual:pub) WHERE primary.short_form in [$id] WITH primary OPTIONAL MATCH (primary)-[:has_reference]-(ds:DataSet)-[:has_license|license]->(l:License) WITH COLLECT ({ dataset: { link : coalesce(ds.dataset_link[0], ''), core : { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds), symbol: coalesce(ds.symbol[0], '')} }, license: { icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), symbol: coalesce(l.symbol[0], '')} }}) AS dataset_license,primary RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'bac066c' AS version , dataset_license, {title: coalesce(primary.title[0], '') ,PubMed: coalesce(primary.PMID[0], ''), FlyBase: coalesce(primary.FlyBase[0], ''), DOI: coalesce(primary.DOI[0], '') }AS pub_specific_content", "parameters" : { "id" : "$ID" }" + query=""statement": "MATCH (primary:Individual:pub) WHERE primary.short_form in [$id] WITH primary OPTIONAL MATCH (primary)-[:has_reference]-(ds:DataSet)-[:has_license|license]->(l:License) WITH COLLECT ({ dataset: { link : coalesce(ds.dataset_link[0], ''), core : { short_form: ds.short_form, label: coalesce(ds.label,''), iri: ds.iri, types: labels(ds), symbol: coalesce(ds.symbol[0], '')} }, license: { icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), unique_facets: coalesce(l.uniqueFacets,[]), symbol: coalesce(l.symbol[0], '')} }}) AS dataset_license,primary RETURN { core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), unique_facets: coalesce(primary.uniqueFacets,[]), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'bac066c' AS version , dataset_license, {title: coalesce(primary.title[0], '') ,PubMed: coalesce(primary.PMID[0], ''), FlyBase: coalesce(primary.FlyBase[0], ''), DOI: coalesce(primary.DOI[0], '') }AS pub_specific_content", "parameters" : { "id" : "$ID" }" countQuery=""statement": "MATCH (primary:pub:Individual {short_form: $id} ) RETURN count(primary) as count", "parameters" : { "id" : "$ID" }"> @@ -657,7 +657,7 @@ name="Get JSON for DataSet" description="Get JSON for DataSet" runForCount="false" - query=""statement": "MATCH (primary:DataSet) WHERE primary.short_form in [$id] WITH primary CALL apoc.cypher.run('WITH primary OPTIONAL MATCH (primary)<-[:has_source|SUBCLASSOF|INSTANCEOF*]-(i:Individual)<-[:depicts]-(channel:Individual)-[irw:in_register_with] ->(template:Individual)-[:depicts]->(template_anat:Individual) RETURN template, channel, template_anat, i, irw limit 10', {primary:primary}) yield value with value.template as template, value.channel as channel,value.template_anat as template_anat, value.i as i, value.irw as irw, primary OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i), symbol: coalesce(i.symbol[0], '')} , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }} }) END AS anatomy_channel_image ,primary OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, anatomy_channel_image OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,anatomy_channel_image OPTIONAL MATCH (primary)-[:has_license|license]->(l:License) WITH collect ({ icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), symbol: coalesce(l.symbol[0], '')} }) as license,primary,anatomy_channel_image,xrefs OPTIONAL MATCH (primary)-[rp:has_reference]->(p:pub) WITH CASE WHEN p is null THEN [] ELSE collect({ core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } ) END AS pubs,primary,anatomy_channel_image,xrefs,license OPTIONAL MATCH (primary)<-[:has_source]-(i:Individual) WITH i, primary, anatomy_channel_image, xrefs, license, pubs OPTIONAL MATCH (i)-[:INSTANCEOF]-(c:Class) WITH DISTINCT { images: count(distinct i),types: count(distinct c) } as dataset_counts,primary,anatomy_channel_image,xrefs,license,pubs RETURN { link : coalesce(primary.dataset_link[0], ''), core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for DataSet' AS query, 'bac066c' AS version , anatomy_channel_image, xrefs, license, pubs, dataset_counts", "parameters" : { "id" : "$ID" }" + query=""statement": "MATCH (primary:DataSet) WHERE primary.short_form in [$id] WITH primary CALL apoc.cypher.run('WITH primary OPTIONAL MATCH (primary)<-[:has_source|SUBCLASSOF|INSTANCEOF*]-(i:Individual)<-[:depicts]-(channel:Individual)-[irw:in_register_with] ->(template:Individual)-[:depicts]->(template_anat:Individual) RETURN template, channel, template_anat, i, irw limit 10', {primary:primary}) yield value with value.template as template, value.channel as channel,value.template_anat as template_anat, value.i as i, value.irw as irw, primary OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) WITH CASE WHEN channel IS NULL THEN [] ELSE COLLECT({ anatomy: { short_form: i.short_form, label: coalesce(i.label,''), iri: i.iri, types: labels(i), symbol: coalesce(i.symbol[0], '')} , channel_image: { channel: { short_form: channel.short_form, label: coalesce(channel.label,''), iri: channel.iri, types: labels(channel), unique_facets: coalesce(channel.uniqueFacets,[]), symbol: coalesce(channel.symbol[0], '')} , imaging_technique: { short_form: technique.short_form, label: coalesce(technique.label,''), iri: technique.iri, types: labels(technique), unique_facets: coalesce(technique.uniqueFacets,[]), symbol: coalesce(technique.symbol[0], '')} ,image: { template_channel : { short_form: template.short_form, label: coalesce(template.label,''), iri: template.iri, types: labels(template), unique_facets: coalesce(template.uniqueFacets,[]), symbol: coalesce(template.symbol[0], '')} , template_anatomy: { short_form: template_anat.short_form, label: coalesce(template_anat.label,''), iri: template_anat.iri, types: labels(template_anat), symbol: coalesce(template_anat.symbol[0], '')} ,image_folder: COALESCE(irw.folder[0], ''), index: coalesce(apoc.convert.toInteger(irw.index[0]), []) + [] }} }) END AS anatomy_channel_image ,primary OPTIONAL MATCH (s:Site { short_form: apoc.convert.toList(primary.self_xref)[0]}) WITH CASE WHEN s IS NULL THEN [] ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(primary.short_form, ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), unique_facets: coalesce(s.uniqueFacets,[]), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) END AS self_xref, primary, anatomy_channel_image OPTIONAL MATCH (s:Site)<-[dbx:database_cross_reference]-(primary) WITH CASE WHEN s IS NULL THEN self_xref ELSE COLLECT({ link_base: coalesce(s.link_base[0], ''), accession: coalesce(dbx.accession[0], ''), link_text: primary.label + ' on ' + s.label, homepage: coalesce(s.homepage[0], ''), site: { short_form: s.short_form, label: coalesce(s.label,''), iri: s.iri, types: labels(s), unique_facets: coalesce(s.uniqueFacets,[]), symbol: coalesce(s.symbol[0], '')} , icon: coalesce(s.link_icon_url[0], ''), link_postfix: coalesce(s.link_postfix[0], '')}) + self_xref END AS xrefs,primary,anatomy_channel_image OPTIONAL MATCH (primary)-[:has_license|license]->(l:License) WITH collect ({ icon : coalesce(l.license_logo[0], ''), link : coalesce(l.license_url[0], ''), core : { short_form: l.short_form, label: coalesce(l.label,''), iri: l.iri, types: labels(l), unique_facets: coalesce(l.uniqueFacets,[]), symbol: coalesce(l.symbol[0], '')} }) as license,primary,anatomy_channel_image,xrefs OPTIONAL MATCH (primary)-[rp:has_reference]->(p:pub) WITH CASE WHEN p is null THEN [] ELSE collect({ core: { short_form: p.short_form, label: coalesce(p.label,''), iri: p.iri, types: labels(p), symbol: coalesce(p.symbol[0], '')} , PubMed: coalesce(p.PMID[0], ''), FlyBase: coalesce(p.FlyBase[0], ''), DOI: coalesce(p.DOI[0], '') } ) END AS pubs,primary,anatomy_channel_image,xrefs,license OPTIONAL MATCH (primary)<-[:has_source]-(i:Individual) WITH i, primary, anatomy_channel_image, xrefs, license, pubs OPTIONAL MATCH (i)-[:INSTANCEOF]-(c:Class) WITH DISTINCT { images: count(distinct i),types: count(distinct c) } as dataset_counts,primary,anatomy_channel_image,xrefs,license,pubs RETURN { link : coalesce(primary.dataset_link[0], ''), core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), unique_facets: coalesce(primary.uniqueFacets,[]), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for DataSet' AS query, 'bac066c' AS version , anatomy_channel_image, xrefs, license, pubs, dataset_counts", "parameters" : { "id" : "$ID" }" countQuery=""statement": "MATCH (primary:DataSet {short_form: $id} ) RETURN count(primary) as count", "parameters" : { "id" : "$ID" }"> @@ -667,7 +667,7 @@ name="Get JSON for License" description="Get JSON for License" runForCount="false" - query=""statement": "MATCH (primary:License) WHERE primary.short_form in [$id] WITH primary RETURN { icon : coalesce(primary.license_logo[0], ''), link : coalesce(primary.license_url[0], ''), core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for License' AS query, 'bac066c' AS version", "parameters" : { "id" : "$ID" }" + query=""statement": "MATCH (primary:License) WHERE primary.short_form in [$id] WITH primary RETURN { icon : coalesce(primary.license_logo[0], ''), link : coalesce(primary.license_url[0], ''), core : { short_form: primary.short_form, label: coalesce(primary.label,''), iri: primary.iri, types: labels(primary), unique_facets: coalesce(primary.uniqueFacets,[]), symbol: coalesce(primary.symbol[0], '')} , description : coalesce(primary.description, []), comment : coalesce(primary.comment, []) } AS term, 'Get JSON for License' AS query, 'bac066c' AS version", "parameters" : { "id" : "$ID" }" countQuery=""statement": "MATCH (primary:License {short_form: $id} ) RETURN count(primary) as count", "parameters" : { "id" : "$ID" }"> diff --git a/package.json b/package.json index eb74472e6..3d7a8389a 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "license": "MIT", "scripts": { "test": "jest --verbose", + "update-tests-snapshots": "jest --updateSnapshot", "prebuild": "eslint . --color", "build": "webpack -p --progress", "prebuild-dev": "eslint . --color", @@ -25,7 +26,7 @@ "@babel/runtime": "^7.4.5", "@babel/plugin-transform-runtime": "^7.4.5", "@geppettoengine/geppetto-client": "file:./geppetto-client", - "@material-ui/icons": "3.0.1", + "@material-ui/icons": "^4.0.0", "@material-ui/lab": "^4.0.0-alpha.57", "@types/react-rnd": "^8.0.0", "axios": "^0.19.2", @@ -45,7 +46,7 @@ "puppeteer": "^1.17.0", "react-collapsible": "^2.3.1", "react-color": "^2.17.3", - "react-tabs": "3.0.0", + "react-tabs": "^3.2.3", "react-redux": "^7.0.3", "redux": "^4.0.1", "style-loader": "^0.13.2", diff --git a/tests/jest/resources/volume.nrrd b/tests/jest/resources/volume.nrrd new file mode 100644 index 000000000..2191e6b26 Binary files /dev/null and b/tests/jest/resources/volume.nrrd differ diff --git a/tests/jest/vfb/batch1/focus-term-tests.js b/tests/jest/vfb/batch1/focus-term-tests.js index 3c103529e..04e8a8ff8 100644 --- a/tests/jest/vfb/batch1/focus-term-tests.js +++ b/tests/jest/vfb/batch1/focus-term-tests.js @@ -50,7 +50,7 @@ describe('VFB Focus Term Tests', () => { await page.evaluate(async () => { let tabs = document.getElementsByClassName('MuiListItem-root '); for ( var i = 0; i < tabs.length ; i ++ ) { - if ( tabs[i].innerText === "medulla (FBbt_00003748)" ) { + if ( tabs[i].innerText.split('\n')[0] === "medulla (FBbt_00003748)" ) { tabs[i].click(); } } diff --git a/tests/jest/vfb/batch1/spotlight-tests.js b/tests/jest/vfb/batch1/spotlight-tests.js index dd13c25a6..a4e81778a 100644 --- a/tests/jest/vfb/batch1/spotlight-tests.js +++ b/tests/jest/vfb/batch1/spotlight-tests.js @@ -81,10 +81,10 @@ describe('VFB Spotlight Tests', () => { await page.evaluate(async () => { let tabs = document.getElementsByClassName('MuiListItem-root '); for ( var i = 0; i < tabs.length ; i ++ ) { - if ( tabs[i].innerText === "fru-M-200266 (VFB_00000001)" ) { + if ( tabs[i].innerText.split('\n')[0] === "fru-M-200266 (VFB_00000001)" ) { tabs[i].click(); } - } + } }); await wait4selector(page, ST.SPOT_LIGHT_SELECTOR, { hidden: true, timeout : 50000 }); }) diff --git a/tests/jest/vfb/batch2/tree-browser-tests.js b/tests/jest/vfb/batch2/tree-browser-tests.js index 51fe67ea5..8fb9d3556 100644 --- a/tests/jest/vfb/batch2/tree-browser-tests.js +++ b/tests/jest/vfb/batch2/tree-browser-tests.js @@ -184,7 +184,7 @@ describe('VFB Tree Browser Component Tests', () => { await page.evaluate(async () => { let tabs = document.getElementsByClassName('MuiListItem-root '); for ( var i = 0; i < tabs.length ; i ++ ) { - if ( tabs[i].innerText === "medulla (FBbt_00003748)" ) { + if ( tabs[i].innerText.split('\n')[0] === "medulla (FBbt_00003748)" ) { tabs[i].click(); } } @@ -192,8 +192,15 @@ describe('VFB Tree Browser Component Tests', () => { await wait4selector(page, ST.SPOT_LIGHT_SELECTOR, { hidden: true, timeout : 50000 }); }) + // Check Medulla is focus term + it('Medulla loaded as the focus term', async () => { + await page.waitFor(5000); + // Check Medulla actually loaded + let element = await findElementByText(page, "medulla"); + expect(element).toBe("medulla"); + }) + it('Open Tree Browser', async () => { - await page.waitFor(2000); await selectTab(page, "Template ROI Browser"); // Check that the Tree Browser is visible diff --git a/tests/jest/vfb/batch3/term-context-tests.js b/tests/jest/vfb/batch3/term-context-tests.js index c1bd76d25..5ef780ec7 100644 --- a/tests/jest/vfb/batch3/term-context-tests.js +++ b/tests/jest/vfb/batch3/term-context-tests.js @@ -13,7 +13,7 @@ const SNAPSHOT_OPTIONS = { customSnapshotsDir : "./tests/jest/vfb/snapshots", comparisonMethod: 'ssim', failureThresholdType: 'percent', - failureThreshold: 0.05 // This means a 5% difference is allowed between compared snapshots during tests + failureThreshold: 0.20 // This means a 20% difference is allowed between compared snapshots during tests }; //Import snapshot module @@ -73,7 +73,7 @@ describe('VFB Term Context Component Tests', () => { await page.evaluate(async () => { let tabs = document.getElementsByClassName('MuiListItem-root '); for ( var i = 0; i < tabs.length ; i ++ ) { - if ( tabs[i].innerText === "medulla (FBbt_00003748)" ) { + if ( tabs[i].innerText.split('\n')[0] === "medulla (FBbt_00003748)" ) { tabs[i].click(); } } @@ -123,6 +123,7 @@ describe('VFB Term Context Component Tests', () => { // Take screenshot, and compared to stored image of page. const image = await page.screenshot(); // This will fail if Medulla didn't load in Term Context, since snapshot comparison will show differences + SNAPSHOT_OPTIONS.failureThreshold = 0.20 // allowing for minor graph layout changes expect(image).toMatchImageSnapshot( { ...SNAPSHOT_OPTIONS, customSnapshotsDir : "./tests/jest/vfb/snapshots/term-context/medulla" }); }) }) diff --git a/tests/jest/vfb/review/downloader-tests.js b/tests/jest/vfb/review/downloader-tests.js new file mode 100644 index 000000000..d9b35ea05 --- /dev/null +++ b/tests/jest/vfb/review/downloader-tests.js @@ -0,0 +1,82 @@ +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:8081/org.geppetto.frontend'; +const PROJECT_URL = baseURL + "/geppetto?id=VFB_00102107&i=VFB_00101567,VFB_00102271,VFB_00102107"; + +/** + * Query Builder component tests + */ +describe('VFB Downloader 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_00102107 appears in button bar inside the term info component', async () => { + await wait4selector(page, 'button[id=VFB_00102107_zoom_buttonBar_btn]', { visible: true , timeout : 120000 }) + }) + + it('Term info component created after load', async () => { + await wait4selector(page, 'div#bar-div-vfbterminfowidget', { visible: true }) + }) + + //Tests canvas has 5 meshes rendered + it('Canvas container component has 3 meshes rendered', async () => { + expect( + await page.evaluate(async () => Object.keys(CanvasContainer.engine.meshes).length) + ).toBe(3) + }) + }) + + describe('Tests Download Contents', () => { + it('Open Download Component Files', async () => { + await page.click('i.fa-download'); + + await page.waitForSelector('#downloadContents'); + }) + + it('Download disabled by default', async () => { + expect( + await page.evaluate(async () => document.querySelector('#downloadContentsButton').disabled ) + ).toBe(true) + }) + + it('Download all files for all instances', async () => { + await page.evaluate(async selector => { + let inputs = document.querySelectorAll(".MuiDialogContent-root input"); + inputs = Array.prototype.slice.call(inputs).filter(input => input.id != "ALL_INSTANCES"); + inputs.forEach( input => { + input.click(); + }); + }); + + expect( + await page.evaluate(async () => document.querySelector('#downloadContentsButton').disabled ) + ).toBe(false) + }) + + + it('Download Contents Dialog goes away', async () => { + await page.click('#downloadContentsButton'); + await wait4selector(page, '#downloadContents', { hidden: true , timeout : 120000 }) + }) + + }) +}) diff --git a/tests/jest/vfb/review/nblast-uploader-tests.js b/tests/jest/vfb/review/nblast-uploader-tests.js new file mode 100644 index 000000000..3a81c62a7 --- /dev/null +++ b/tests/jest/vfb/review/nblast-uploader-tests.js @@ -0,0 +1,75 @@ +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:8081/org.geppetto.frontend'; +const PROJECT_URL = baseURL + "/geppetto?id=VFB_00017894"; + +/** + * Query Builder component tests + */ +describe('VFB Uploader 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 NBLAST Uploader', () => { + it('Open Uploader', async () => { + await page.click('#fa-upload'); + + await page.waitForSelector('div.MuiDialog-root'); + }) + + it('Templates Populated', async () => { + await page.evaluate(async () => { + var dropdown = document.getElementById('mui-component-select-template'); + var event = document.createEvent('MouseEvents'); + event.initMouseEvent('mousedown', true, true, window); + dropdown.dispatchEvent(event); + }); + + await page.waitForSelector('li.MuiListItem-root'); + + const list = await page.evaluate(async () => { + document.querySelectorAll('.MuiListItem-root')[1].click(); + return document.querySelectorAll('.MuiListItem-root').length; + }); + + expect(list).toBe(3); + }) + + it('Template Selected', async () => { + const selection = await page.evaluate(async () => { + return document.getElementById("template-selection").value; + }); + + expect(selection).toBe("JRC2018Unisex"); + }) + + }) +}) diff --git a/tests/jest/vfb/snapshots/term-context/adult-brain/term-context-tests-js-vfb-term-context-component-tests-add-medulla-snapshot-comparison-of-term-context-after-medulla-loaded-graph-remains-the-same-1-snap.png b/tests/jest/vfb/snapshots/term-context/adult-brain/term-context-tests-js-vfb-term-context-component-tests-add-medulla-snapshot-comparison-of-term-context-after-medulla-loaded-graph-remains-the-same-1-snap.png index cab033cce..3a5593dc3 100644 Binary files a/tests/jest/vfb/snapshots/term-context/adult-brain/term-context-tests-js-vfb-term-context-component-tests-add-medulla-snapshot-comparison-of-term-context-after-medulla-loaded-graph-remains-the-same-1-snap.png and b/tests/jest/vfb/snapshots/term-context/adult-brain/term-context-tests-js-vfb-term-context-component-tests-add-medulla-snapshot-comparison-of-term-context-after-medulla-loaded-graph-remains-the-same-1-snap.png differ diff --git a/tests/jest/vfb/snapshots/term-context/adult-brain/term-context-tests-js-vfb-term-context-component-tests-test-term-context-component-snapshot-comparison-of-term-context-1-snap.png b/tests/jest/vfb/snapshots/term-context/adult-brain/term-context-tests-js-vfb-term-context-component-tests-test-term-context-component-snapshot-comparison-of-term-context-1-snap.png index 682c069a8..9291d978d 100644 Binary files a/tests/jest/vfb/snapshots/term-context/adult-brain/term-context-tests-js-vfb-term-context-component-tests-test-term-context-component-snapshot-comparison-of-term-context-1-snap.png and b/tests/jest/vfb/snapshots/term-context/adult-brain/term-context-tests-js-vfb-term-context-component-tests-test-term-context-component-snapshot-comparison-of-term-context-1-snap.png differ diff --git a/tests/jest/vfb/snapshots/term-context/medulla/term-context-tests-js-vfb-term-context-component-tests-add-medulla-snapshot-comparison-of-term-context-after-sync-trigger-graph-displays-medulla-1-snap.png b/tests/jest/vfb/snapshots/term-context/medulla/term-context-tests-js-vfb-term-context-component-tests-add-medulla-snapshot-comparison-of-term-context-after-sync-trigger-graph-displays-medulla-1-snap.png index 41a04136b..d475d20e9 100644 Binary files a/tests/jest/vfb/snapshots/term-context/medulla/term-context-tests-js-vfb-term-context-component-tests-add-medulla-snapshot-comparison-of-term-context-after-sync-trigger-graph-displays-medulla-1-snap.png and b/tests/jest/vfb/snapshots/term-context/medulla/term-context-tests-js-vfb-term-context-component-tests-add-medulla-snapshot-comparison-of-term-context-after-sync-trigger-graph-displays-medulla-1-snap.png differ