From d7c2bb2af9546405deacc7c732af090cda461b1a Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Wed, 29 Sep 2021 22:18:19 -0400 Subject: [PATCH 1/3] Modularizes and refactors various parts of App Moves lengthy autocompletion logic into its own file. Moves main toolbar initialization data into AppToolbar under Toolbar.js. Moves settings modal into SettingsModal.js. Moves example queries used by help modal into static file. --- src/tranql/web/src/App.js | 1012 +---------------- src/tranql/web/src/SettingsModal.js | 183 +++ src/tranql/web/src/Toolbar.js | 73 ++ src/tranql/web/src/autocomplete.js | 548 +++++++++ .../src/static/app_data/example_queries.js | 108 ++ 5 files changed, 959 insertions(+), 965 deletions(-) create mode 100644 src/tranql/web/src/SettingsModal.js create mode 100644 src/tranql/web/src/autocomplete.js create mode 100644 src/tranql/web/src/static/app_data/example_queries.js diff --git a/src/tranql/web/src/App.js b/src/tranql/web/src/App.js index 6638e73..17c78e2 100644 --- a/src/tranql/web/src/App.js +++ b/src/tranql/web/src/App.js @@ -25,7 +25,6 @@ import DefaultTooltipContent from 'recharts/lib/component/DefaultTooltipContent' import ReactTooltip from 'react-tooltip'; import { NotificationContainer , NotificationManager } from 'react-notifications'; import 'react-notifications/lib/notifications.css'; -import { Range } from 'rc-slider'; import { GridLoader } from 'react-spinners'; import SplitPane from 'react-split-pane'; import Cache from './Cache.js'; @@ -37,15 +36,17 @@ import Legend from './Legend.js'; import TableViewer from './TableViewer.js'; import HelpModal, { ToolbarHelpModal } from './HelpModal.js'; import ImportExportModal from './ImportExportModal.js'; +import SettingsModal from './SettingsModal.js'; import confirmAlert from './confirmAlert.js'; import highlightTypes from './highlightTypes.js'; import { shadeColor, adjustTitle, hydrateState, formatBytes } from './Util.js'; -import { Toolbar, Tool, /*ToolGroup*/ } from './Toolbar.js'; +import { AppToolbar, Tool, /*ToolGroup*/ } from './Toolbar.js'; import LinkExaminer from './LinkExaminer.js'; // import FindTool from './FindTool.js'; import FindTool2 from './FindTool2.js'; import Message from './Message.js'; import Chain from './Chain.js'; +import autoComplete from './autocomplete.js'; import ContextMenu from './ContextMenu.js'; import GraphSerializer from './GraphSerializer.js'; import { RenderInit, RenderSchemaInit, IdFilter, LegendFilter, LinkFilter, NodeFilter, ReasonerFilter, SourceDatabaseFilter, CurvatureAdjuster } from './Render.js'; @@ -100,7 +101,7 @@ class App extends Component { this._getModelConcepts = this._getModelConcepts.bind (this); this._getModelRelations = this._getModelRelations.bind (this); this._getReasonerURLs = this._getReasonerURLs.bind (this); - this._codeAutoComplete = this._codeAutoComplete.bind(this); + this._codeAutoComplete = autoComplete.bind(this); this._updateCode = this._updateCode.bind (this); this._executeQuery = this._executeQuery.bind(this); this._abortQuery = this._abortQuery.bind(this); @@ -111,9 +112,6 @@ class App extends Component { this._setNavMode = this._setNavMode.bind(this); this._setSelectMode = this._setSelectMode.bind(this); - this._getTools = this._getTools.bind(this); - this._getButtons = this._getButtons.bind(this); - this._setHighlightTypesMode = this._setHighlightTypesMode.bind(this); this._highlightType = this._highlightType.bind(this); this.__highlightTypes = highlightTypes.bind(this); @@ -168,7 +166,6 @@ class App extends Component { // Settings management this._handleUpdateSettings = this._handleUpdateSettings.bind (this); this._toggleCheckbox = this._toggleCheckbox.bind (this); - this._renderCheckboxes = this._renderCheckboxes.bind (this); this._hydrateState = hydrateState.bind (this); this._handleQueryString = this._handleQueryString.bind (this); @@ -376,114 +373,7 @@ class App extends Component { activeModal : null, - exampleQueries : [ - { - title: 'Protein-Metabolite Interaction', - query: -`-- What proteins are targetted by the metabolite KEGG:C00017? - -set metabolite = "KEGG:C00017" - -select metabolite->protein - from "/graph/rtx" - where metabolite=$metabolite - -` - }, - { - title: 'Chemical substances target genes that target asthma', - query: -`-- Which chemical substances target genes that target asthma? -select chemical_substance->gene->disease - from "/graph/gamma/quick" - where disease="asthma" -` - }, - { - title: 'Usage of predicates to narrow results', - query: -`-- Which chemical substances decrease activity of genes that contribute to asthma? -select chemical_substance-[decreases_activity_of]->gene-[contributes_to]->disease - from "/graph/gamma/quick" - where disease="asthma" -` - }, - { - title: 'Phenotypic Feature-Disease Association', - query: -`-- What diseases are associated with the phenotypic feature HP:0005978? - -select phenotypic_feature->disease - from "/graph/rtx" - where phenotypic_feature="HP:0005978" -` - }, - { - title: 'Drug-Disease Pair', - query: -`-- --- Produce clinial outcome pathways for this drug disease pair. --- - -set drug = 'PUBCHEM:2083' -set disease = 'MONDO:0004979' - -select chemical_substance->gene->anatomical_entity->phenotypic_feature<-disease - from '/graph/gamma/quick' - where chemical_substance = $drug - and disease = $disease` - }, - { - title: 'Drug Targets Gene', - query: -`-- --- What drug targets some gene? --- - -set target_gene = 'HGNC:6871' --mapk1 -select chemical_substance->gene - from '/graph/gamma/quick' - where gene = $target_gene` - }, - { - title: 'Tissue-Disease Association', - query: -`-- --- What tissue types are associated with [disease]? --- -set disease = 'asthma' -select disease->anatomical_feature->cell - from '/graph/gamma/quick' - where disease = $disease -` - }, - { - title: 'Workflow 5 v3', - query: -`-- --- Workflow 5 --- --- Modules 1-4: Chemical Exposures by Clinical Clusters --- For ICEES cohorts, eg, defined by differential population --- density, which chemicals are associated with these --- cohorts with a p_value lower than some threshold? --- --- Modules 5-*: Knowledge Graph Phenotypic Associations --- For chemicals produced by steps 1-4, what phenotypes are --- associated with exposure to these chemicals? --- - -SELECT population_of_individual_organisms->chemical_substance->gene->biological_process_or_activity<-phenotypic_feature - FROM "/schema" - WHERE icees.table = 'patient' - AND icees.year = 2010 - AND icees.cohort_features.AgeStudyStart = '0-2' - AND icees.feature.EstResidentialDensity < 1 - AND icees.maximum_p_value = 1 - AND chemical_substance !=~ '^(SCTID.*|rxcui.*|CAS.*|SMILES.*|umlscui.*)$' - AND icees.regex = "(MONDO|HP):.*""` - } - ] + exampleQueries : require("./static/app_data/example_queries.js") //showAnswerViewer : true }; @@ -620,549 +510,7 @@ SELECT population_of_individual_organisms->chemical_substance->gene->biological_ code: newCode }); } - /** - * Callback for handling autocompletion within the query editor. - * - * @param {object} cm - The CodeMirror object. - * @private - */ - _codeAutoComplete () { - // https://github.com/JedWatson/react-codemirror/issues/52 - var codeMirror = this._codemirror; - - // hint options for specific plugin & general show-hint - // 'tables' is sql-hint specific - // 'disableKeywords' is also sql-hint specific, and undocumented but referenced in sql-hint plugin - // Other general hint config, like 'completeSingle' and 'completeOnSingleClick' - // should be specified here and will be honored - - // Shallow copy it. - const pos = Object.assign({}, codeMirror.getCursor()); - const untrimmedPos = codeMirror.getCursor(); - const textToCursorPositionUntrimmed = codeMirror.getRange({ line : 0, ch : 0 }, { line : pos.line, ch : pos.ch }); - const textToCursorPosition = textToCursorPositionUntrimmed.trimRight(); - const entireText = codeMirror.getValue(); - - // const splitLines = textToCursorPosition.split(/\r\n|\r|\n/); - // // Adjust the position after trimming to be on the correct line. - // pos.line = splitLines.length - 1; - // // Adjust the position after trimming to be on the correct char. - // pos.ch = splitLines[splitLines.length-1].length; - - const setHint = function(options, noResultsTip) { - if (typeof noResultsTip === 'undefined') noResultsTip = true; - if (noResultsTip && options.length === 0) { - options.push({ - text: String(''), - displayText:'No valid results' - }); - } - const hintOptions = { - // tables: tables, - hint: function() { - return { - from: pos, - to: untrimmedPos, - list: options.map((option) => { - // Process custom options - `replaceText` - if (option.hasOwnProperty('replaceText')) { - let replaceText = option.replaceText; - let from = option.hasOwnProperty('from') ? option.from : pos; - let to = option.hasOwnProperty('to') ? option.to : untrimmedPos; - - option.from = { line : from.line, ch : from.ch - replaceText.length }; - option.to = { line : to.line, ch : to.ch}; - - if (replaceText.length > 0) { - const trimmedLines = textToCursorPositionUntrimmed.trimRight().split(/\r\n|\r|\n/); - const lastLine = trimmedLines[trimmedLines.length-1]; - option.from.line = trimmedLines.length - 1; - option.from.ch = lastLine.length - replaceText.length; - } - - - delete option.replaceText; - } - - return option; - }) - }; - }, - disableKeywords: true, - completeSingle: false, - completeOnSingleClick: false - }; - - codeMirror.showHint(hintOptions); - // codeMirror.state.completionActive.pick = () => { - // codeMirror.showHint({ - // hint: function() { - // return { - // from: pos, - // to: pos, - // list: [{ - // text: String(''), - // displayText: 'foobar', - // className: 'testing' - // }] - // }; - // }, - // disableKeywords: true, - // completeSingle: false, - // }); - // } - } - - const setError = (resultText, status, errors, resultOptions) => { - if (typeof resultOptions === "undefined") resultOptions = {}; - codeMirror.showHint({ - hint: function() { - return { - from: pos, - to: pos, - list: [{ - text: String(''), - displayText: resultText, - className: 'autocomplete-result-error', - ...resultOptions, - }] - }; - }, - disableKeywords: true, - completeSingle: false, - }); - if (typeof status !== "undefined" && typeof errors !== "undefined") { - codeMirror.state.completionActive.pick = () => { - this._handleMessageDialog (status, errors); - } - } - } - - const setLoading = function(loading) { - if (loading) { - // text property has to be String('') because when it is '' (falsey) it refuses to display it. - codeMirror.showHint({ - hint: function() { - return { - from: pos, - to: pos, - list: [{ - text: String(''), - displayText: 'Loading', - className: 'loading-animation' - }] - }; - }, - disableKeywords: true, - completeSingle: false, - }); - } - else { - codeMirror.closeHint(); - } - } - - /** - * TODO: - * could try to see if its possible to have two select menus for predicates that also show concepts from the predicates - * would look something like this image, when, for example, you pressed the right arrow or left clicked or something on a predicate: - * https://i.imgur.com/LBsdrcq.png - * could somehow see if there's a way to have predicate suggestion work properly when there's a concept already following the predicate - * Ex: 'select foo-[what_can_I_put_here?]->baz' - * Would involve sending more of the query instead of cutting it off at cursor. - * Then would somehow have to backtrack and locate which token the cursor's position translates to. - */ - - this._autoCompleteController.abort(); - this._autoCompleteController = new window.AbortController(); - - setLoading(true); - - fetch(this.tranqlURL + '/tranql/parse_incomplete', { - signal: this._autoCompleteController.signal, - method: "POST", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify([textToCursorPositionUntrimmed, entireText]) - }).then(res => res.json()) - .then(async (parsedTree) => { - setLoading(false) - - if (parsedTree.errors) { - // this._handleMessageDialog (parsedTree.status, parsedTree.errors); - setError("Failed to parse", parsedTree.status, parsedTree.errors); - } - else { - setLoading(true); - await this.schemaPromise; - setLoading(false); - const graph = this.state.schemaMessage.knowledge_graph; - - // Recursviely removes any tokens that are linebreaks from a parsed tree. - const stripLinebreaks = function(tree) { - if (Array.isArray(tree)) { - return tree.filter((token) => stripLinebreaks(token)); - } - else { - return tree.toString().match(/\r\n|\r|\n/) === null; - } - } - - const incompleteTree = parsedTree[0]; - const completeTree = parsedTree[1]; - - // Filter whitespace from the statements - const block = incompleteTree[incompleteTree.length-1].map((statement) => { - return stripLinebreaks(statement); - }); - const completeBlock = completeTree[completeTree.length-1].map((statement) => { - return stripLinebreaks(statement); - }); - - const lastStatement = block[block.length-1]; - const lastStatementComplete = completeBlock[block.length-1]; - - const statementType = lastStatement[0]; - - setLoading(true); - const fromOptions = await this.reasonerURLs; - setLoading(false); - - fromOptions["/schema"] = "/schema"; - - const whereOptions = [ - 'testing', - 'foobar' - ]; - - const concept_arrows = [ - '->', - '<-' - ]; - - const all_arrows = [ - '->', - '<-', - '-[', - '<-[' - ]; - - const arrow_to_pred_arrow = (arrow) => { - return { - '->' : [ - '-[', - '', - ']->' - ], - '<-' : [ - '<-[', - '', - ']-' - ] - }[arrow]; - } - - const arrowToEmptyPredicate = (arrow) => { - return arrow_to_pred_arrow(arrow); - } - - const isBackwardsPredicate = (predicate) => { - return predicate[0] === '<-['; - } - - const toForwardPredicate = (predicate) => { - predicate[0] = '-['; - predicate[2] = ']->'; - return predicate; - } - - const completePredicate = (predicate) => { - if (isBackwardsPredicate (predicate)) { - predicate[2] = arrow_to_pred_arrow("<-")[2]; - } - else { - predicate[2] = arrow_to_pred_arrow("->")[2]; - } - return predicate; - } - - const concept = (old_concept) => { - // Concept identifiers aren't actually parsed by the lexer, but rather the ast in Query::add. - // This just copies the methods that the ast uses to parse concept identifiers. - if (old_concept.indexOf(":") !== -1) { - const split = old_concept.split(":"); - if (split.length - 1 > 1) { - throw new Error(`Invalid concept identifier "${old_concept}"`); - } - const [name, type_name] = split; - return type_name; - } - else { - return old_concept; - } - } - - const lastToken = lastStatement[lastStatement.length-1]; - const secondLastToken = lastStatement[lastStatement.length-2]; - const thirdLastToken = lastStatement[lastStatement.length-3]; - - console.log(statementType, lastStatement, lastToken); - - // Try/catch the entirety of the logic - try { - if (statementType === 'select') { - let validConcepts; - if (lastToken === "-") { - // Arrow suggestion - // "select foo-" - validConcepts = all_arrows.map((arrow) => { - return { - displayText: arrow, - text: arrow, - replaceText: "-" - }; - }); - } - else if (Array.isArray(lastToken) && lastToken.length < 3) { - // If the last token is an array and not length 3 then it is an incomplete predicate. - // "select foo-[" or "select foo-[bar" - let currentPredicate = completePredicate([ - lastToken[0], - lastToken[1] !== undefined ? lastToken[1] : "" - ]); - let previousConcept = concept(secondLastToken); - // May be undefined if there is no next concept - let has_no_next_concept = (lastStatementComplete.length - lastStatement.length) == 0; - let nextConcept = has_no_next_concept? undefined : concept(lastStatementComplete[lastStatement.length]) ; - // See https://github.com/frostyfan109/tranql/issues/117 for why this approach doesn't work - - - - const backwards = isBackwardsPredicate (currentPredicate); - - console.log ([previousConcept, currentPredicate, nextConcept]); - - // Should replace this method with reduce - - const allEdges = graph.edges.filter((edge) => { - if (backwards) { - return edge.target_id === previousConcept && - (nextConcept === undefined || edge.source_id === nextConcept) && - edge.type.startsWith(currentPredicate[1]); - } - else { - return ( - edge.source_id === previousConcept && - (nextConcept === undefined || edge.target_id === nextConcept) && - edge.type.startsWith(currentPredicate[1]) - ); - } - }); - const uniqueEdgeMap = {}; - allEdges.forEach((edge) => { - if (!uniqueEdgeMap.hasOwnProperty(edge.type)) { - uniqueEdgeMap[edge.type] = edge; - } - }); - const uniqueEdges = Object.values(uniqueEdgeMap); - validConcepts = uniqueEdges.map((edge) => { - const replaceText = currentPredicate[1]; - // const actualText = type + currentPredicate[2]; - const conceptHint = " (" + (backwards ? edge.source_id : edge.target_id) + ")"; - const actualText = edge.type; - const displayText = edge.type + conceptHint; - return { - displayText: displayText, - text: actualText, - replaceText : replaceText - }; - }); - } - else { - // Otherwise, we are handling autocompletion of a concept. - let currentConcept = ""; - let predicate = null; - let previousConcept = null; - - if (lastToken === statementType) { - // "select" - } - else if (secondLastToken === statementType) { - // "select foo" - currentConcept = concept(lastToken); - } - else if (concept_arrows.includes(lastToken) || Array.isArray(lastToken)) { - // "select foo->" or "select foo-[bar]->" - predicate = lastToken; - previousConcept = concept(secondLastToken); - } - else { - previousConcept = concept(thirdLastToken); - predicate = secondLastToken; - currentConcept = concept(lastToken); - } - - - if (predicate === null) { - // Predicate will only be null if there are no arrows, and therefore the previousConcept is also null. - // Single concept - just "select" or "select foo" where the concept is either "" or "foo" - validConcepts = graph.nodes.filter((node) => node.type.startsWith(currentConcept)).map(node => node.type); - } - else { - // If there is a predicate, we have to factor in the previous concept, the predicate, and the current concept. - if (!Array.isArray(predicate)) { - // We want to assign an empty predicate - predicate = arrowToEmptyPredicate (predicate); - } - - const backwards = isBackwardsPredicate (predicate); - - console.log ([previousConcept, predicate, currentConcept]); - // Concepts could be named like select f1:foo->f2:bar - // we need to split them and grab the actual types - let previousConceptSplit = previousConcept.split(':'); - let currentConceptSplit = currentConcept.split(':'); - previousConcept = previousConceptSplit[previousConceptSplit.length - 1]; - currentConcept = currentConceptSplit[currentConceptSplit.length - 1]; - validConcepts = graph.edges.filter((edge) => { - if (backwards) { - return ( - edge.source_id.startsWith(currentConcept) && - edge.target_id === previousConcept && - (predicate[1] === "" || edge.type === predicate[1]) - ); - } - else { - return ( - edge.source_id === previousConcept && - edge.target_id.startsWith(currentConcept) && - (predicate[1] === "" || edge.type === predicate[1]) - ); - } - }).map((edge) => { - if (backwards) { - return edge.source_id; - } - else { - return edge.target_id - } - }) - } - validConcepts = validConcepts.unique().map((concept) => { - return { - displayText: concept, - text: concept, - replaceText: currentConcept - }; - }); - } - setHint(validConcepts); - - } - else if (statementType === 'from') { - let currentReasonerArray = lastStatement[1]; - let startingQuote = ""; - if (currentReasonerArray === undefined) { - // Adds an apostrophe to the start of the string if it doesn't have one ("from") - startingQuote = "'"; - currentReasonerArray = [[ - "'", - "" - ]]; - } - const endingQuote = currentReasonerArray[currentReasonerArray.length - 1][0]; - const currentReasoner = currentReasonerArray[currentReasonerArray.length - 1][1]; - // The select statement must be the first statement in the block, but thorough just in case. - // We also want to filter out whitespace that would be detected as a token. - const selectStatement = block.filter((statement) => statement[0] === "select")[0].filter((token) => { - return typeof token !== "string" || token.match(/\s/) === null; - }); - // Don't want the first token ("select") - const tokens = selectStatement.slice(1); - - let validReasoners = []; - - Object.keys(fromOptions).forEach((reasoner) => { - let valid = true; - if (tokens.length === 1) { - // Handles if there's only one concept ("select foo") - const currentConcept = concept(tokens[0]); - graph.nodes.filter((node) => node.type.startsWith(currentConcept)).forEach(node => node.reasoner.forEach((reasoner) => { - !validReasoners.includes(reasoner) && validReasoners.push(reasoner); - })); - } - else { - for (let i=0;i { - if (backwards) { - return ( - edge.source_id.startsWith(currentConcept) && - edge.target_id === previousConcept && - (predicate[1] === "" || edge.type === predicate[1]) && - (reasoner === "/schema" || edge.reasoner.includes(reasoner)) - ); - } - else { - return ( - edge.source_id === previousConcept && - edge.target_id.startsWith(currentConcept) && - (predicate[1] === "" || edge.type === predicate[1]) && - (reasoner === "/schema" || edge.reasoner.includes(reasoner)) - ); - } - }).length > 0; - if (!isTransitionValid) { - valid = false; - break; - } - } - if (valid) { - validReasoners.push(reasoner); - } - } - }); - - const validReasonerValues = validReasoners.map((reasoner) => { - return fromOptions[reasoner]; - }).filter((reasonerValue) => { - return reasonerValue.startsWith(currentReasoner); - }).map((reasonerValue) => { - return { - displayText: reasonerValue, - text: startingQuote + reasonerValue, - // text: startingQuote + reasonerValue + endingQuote, - replaceText: currentReasoner - }; - }); - - setHint(validReasonerValues); - } - else if (statementType === 'where') { - - } - } - catch (e) { - setError('Failed to parse', 'Failed to parse', [{message: e.message, details: e.stack}]); - } - } - }) - .catch((error) => { - if (error.name !== "AbortError") { - setError('Error', 'Error', [{message: error.message, details: error.stack}]); - } - }); - } + /** * Sets the active force graph * @@ -2098,33 +1446,6 @@ SELECT population_of_individual_organisms->chemical_substance->gene->biological_ this._translateGraph(newMessage,false,false); }); } - - // If the selected node/link is hidden we want to deselect it. - // this.setState({},() => { - // if (this.state.selectedNode !== null) { - // if (this.state.selectedNode.hasOwnProperty('node') && newMessage.graph.nodes.filter((node) => node.id === this.state.selectedNode.node.id).length === 0) { - // delete this.state.selectedNode.node; - // this._updateDimensions(); - // } - // if ( - // this.state.selectedNode.hasOwnProperty('link') && - // ( - // newMessage.graph.nodes.filter((node) => ( - // node.id === this.state.selectedNode.link.source_id || - // node.id === this.state.selectedNode.link.target_id - // )).length < 2 || - // newMessage.graph.links.filter((link) => ( - // link.origin.source_id === this.state.selectedNode.link.source_id && - // link.origin.target_id === this.state.selectedNode.link.target_id && - // JSON.stringify(link.origin.type) === JSON.stringify(this.state.selectedNode.link.type) - // )).length === 0 - // ) - // ) { - // delete this.state.selectedNode.link; - // this._updateDimensions(); - // } - // } - // }) } /** * Handle a click on a graph node. @@ -2271,77 +1592,6 @@ SELECT population_of_individual_organisms->chemical_substance->gene->biological_ _renderForceGraphVR (data, props) { return } - /** - * Render the toolbar buttons - * - * @private - */ - _getButtons() { - return ( - <> - - this._findTool.current.toggleShow()}/> - this._setActiveModal('HelpModal')}/> - this._setActiveModal('CachedQueriesModal')}/> - this._setActiveModal('ImportExportModal')}/> - this._setActiveModal('SettingsModal')} /> - { - this.state.tableViewerComponents.tableViewerCompActive ? this._closeTableViewer() : this._openTableViewer("tableViewerCompActive"); - }}/> - { - // Perfectly functional but does not provide enough functionality as of now to warrant its presence - /* this.setState ({ showTypeChart : true })} />*/ - // The tool works as intended but the annotator does not yet. - /* this._annotateGraph ()}/>*/ - } - - ); - } - /** - * Render the toolbar tools - * - * @private - */ - _getTools() { - return ( - <> - this._setNavMode(bool)}> - - - this._setSelectMode(bool)}> - - - this._setHighlightTypesMode(bool)}> - - - this._setConnectionExaminerActive(bool)}> - - - { - this.setState({ browseNodeActive : bool }); - if (!bool) { - this._browseNodeInterface.current.hide(); - } - }}> - - - - ); - } _handleShowAnswerViewer () { console.log (this._answerViewer); if (this.state.message) { @@ -2464,20 +1714,6 @@ SELECT population_of_individual_organisms->chemical_substance->gene->biological_ this._translateGraph(msg,false,schemaActive); } - _renderCheckboxes(stateKey) { - return this.state[stateKey].map((checkbox, index) => -
- -
- ); - } /** * * @private @@ -2838,164 +2074,7 @@ SELECT population_of_individual_organisms->chemical_substance->gene->biological_ _renderSettingsModal () { return ( <> - this._setActiveModal(null)}> - - Settings - - - - -
-
- Visualization Mode and Graph Colorization -
-
- 3D   -
-
- 2D   -
-
- VR    -
-
- Color the graph. -
-
-
- -
- -
-
- Use Cache -
- Use cached responses. -
-
-
- -
-
- - { - /* Really *bad* feature... -
- -
- Cursor -
- Use active tool as cursor. -
-
- */ - } - -
- -
- Node Drag -
- Allow node dragging in the force graph (requires refresh). -
-
- -
- -
- Dynamic ID Resolution -
- Enables dynamic id lookup of curies. -
-
-
- -
- Link Weight Range Min: [{this.state.linkWeightRange[0] / 100}] Max: [{this.state.linkWeightRange[1] / 100}]
- Include only links with a weight in this range. - - - Node Connectivity Range Min: [{this.state.nodeDegreeRange[0]}] Max: [{this.state.nodeDegreeRange[1]}] (reset on load)
- Include only nodes with a number of connections in this range. - -
- Force Graph Charge
- Set the charge force on the active graph
-
- {if (e.keyCode === 13) e.preventDefault();}} - /> - -
- - Legend Display Limit ({this.state.schemaViewerActive && this.state.schemaViewerEnabled ? "schema" : "graph"})
-
- Set the number of nodes that the legend displays: - (this._onLegendDisplayLimitChange('nodes',e))} - onKeyDown={(e) => {if (e.keyCode === 13) e.preventDefault();}} - /> - Set the number of links that the legend displays: - (this._onLegendDisplayLimitChange('links',e))} - onKeyDown={(e) => {if (e.keyCode === 13) e.preventDefault();}} - /> - - - - {/*
*/} -
- - -
- Database Sources Filter graph edges by source database. Deselecting a database deletes all associations from that source. -
{this._renderCheckboxes('dataSources')}
-
- Reasoner Sources Filter graph elements by source reasoner. Deselecting a reasoner deletes all associations from that source. -
{this._renderCheckboxes('reasonerSources')}
-
- - - - - - + ); } @@ -3086,14 +2165,27 @@ SELECT population_of_individual_organisms->chemical_substance->gene->biological_ // Render it. return (
+ this._setActiveModal(null)} + appState={this.state} + + toggleCheckbox={this._toggleCheckbox} + + handleUpdateSettings={this._handleUpdateSettings} + clearCache={this._clearCache} + onLinkWeightRangeChange={this._onLinkWeightRangeChange} + onNodeDegreeRangeChange={this._onNodeDegreeRangeChange} + onChargeChange={this._onChargeChange} + onLegendNodeLimitChange={(e) => (this._onLegendDisplayLimitChange('nodes',e))} + onLegendLinkLimitChange={(e) => (this._onLegendDisplayLimitChange('links',e))}/> {this._renderSettingsModal () } {this._renderTypeChart ()} - + buttons={this._toolbar.current._getButtons()} + tools={this._toolbar.current._getTools()} + />} chemical_substance->gene->biological_
{ this.state.toolbarEnabled && ( - + this._findTool.current.toggleShow()} + helpToolCallback={() => this._setActiveModal('HelpModal')} + cachedQueriesToolCallback={() => this._setActiveModal('CachedQueriesModal')} + importExportToolCallback={() => this._setActiveModal('ImportExportModal')} + settingsToolCallback={() => this._setActiveModal('SettingsModal')} + tableViewerToolCallback={() => { + this.state.tableViewerComponents.tableViewerCompActive ? this._closeTableViewer() : this._openTableViewer("tableViewerCompActive"); + }} + // Tool callbacks + navigateToolCallback={this._setNavMode} + selectToolCallback={this._setSelectMode} + highlightTypesToolCallback={this._setHighlightTypesMode} + examineConnectionToolCallback={this._setConnectionExaminerActive} + browseNodeToolCallback={(bool) => { + this.setState({ browseNodeActive : bool }); + if (!bool) { + this._browseNodeInterface.current.hide(); + } + }}/> ) }
@@ -3360,32 +2468,6 @@ SELECT population_of_individual_organisms->chemical_substance->gene->biological_ resultMouseLeave={(result) => { this._highlightType(result.id, false, false, undefined, "id") }}/> - {/*{ - const isNode = function(element) { - return !element.origin.hasOwnProperty('source_id') && !element.origin.hasOwnProperty('target_id'); - } - if (values.length > 1) { - // Grouped syntax which isn't really compatible - just use the link. - values = values.filter((element) => !isNode(element)); - } - values.forEach((element) => { - if (isNode(element)) { - this._handleNodeClick(element); - } - else { - this._handleLinkClick(element, true); - } - }); - }} - resultMouseEnter={(values)=>{ - values.forEach((element) => this._highlightType(element.id,0xff0000,false,undefined,'id'))} - } - resultMouseLeave={(values)=>{ - values.forEach((element) => this._highlightType(element.id,false,false,undefined,'id'))} - } - ref={this._findTool}/>*/} -
{/*
*/} diff --git a/src/tranql/web/src/SettingsModal.js b/src/tranql/web/src/SettingsModal.js new file mode 100644 index 0000000..291f78c --- /dev/null +++ b/src/tranql/web/src/SettingsModal.js @@ -0,0 +1,183 @@ +import React, { Component } from 'react'; +import { Modal, Tabs, Tab, Form } from 'react-bootstrap'; +import { Button } from 'reactstrap'; +import { Range } from 'rc-slider'; + +export default class SettingsModal extends Component { + _renderCheckboxes(stateKey) { + return this.props.appState[stateKey].map((checkbox, index) => ( +
+ +
+ )); + } + render() { + return ( + + + Settings + + + + +
+
+ Visualization Mode and Graph Colorization +
+
+ 3D   +
+
+ 2D   +
+
+ VR    +
+
+ Color the graph. +
+
+
+ +
+ +
+
+ Use Cache +
+ Use cached responses. +
+
+
+ +
+
+ + { + /* Really *bad* feature... +
+ +
+ Cursor +
+ Use active tool as cursor. +
+
+ */ + } + +
+ +
+ Node Drag +
+ Allow node dragging in the force graph (requires refresh). +
+
+ +
+ +
+ Dynamic ID Resolution +
+ Enables dynamic id lookup of curies. +
+
+
+ +
+ Link Weight Range Min: [{this.props.appState.linkWeightRange[0] / 100}] Max: [{this.props.appState.linkWeightRange[1] / 100}]
+ Include only links with a weight in this range. + + + Node Connectivity Range Min: [{this.props.appState.nodeDegreeRange[0]}] Max: [{this.props.appState.nodeDegreeRange[1]}] (reset on load)
+ Include only nodes with a number of connections in this range. + +
+ Force Graph Charge
+ Set the charge force on the active graph
+
+ {if (e.keyCode === 13) e.preventDefault();}} + /> + +
+ + Legend Display Limit ({this.props.appState.schemaViewerActive && this.props.appState.schemaViewerEnabled ? "schema" : "graph"})
+
+ Set the number of nodes that the legend displays: + {if (e.keyCode === 13) e.preventDefault();}} + /> + Set the number of links that the legend displays: + {if (e.keyCode === 13) e.preventDefault();}} + /> + + + + {/*
*/} +
+ + +
+ Database Sources Filter graph edges by source database. Deselecting a database deletes all associations from that source. +
{this._renderCheckboxes('dataSources')}
+
+ Reasoner Sources Filter graph elements by source reasoner. Deselecting a reasoner deletes all associations from that source. +
{this._renderCheckboxes('reasonerSources')}
+
+ + + + + + + ); + } +} \ No newline at end of file diff --git a/src/tranql/web/src/Toolbar.js b/src/tranql/web/src/Toolbar.js index 4f7f497..bd6443b 100644 --- a/src/tranql/web/src/Toolbar.js +++ b/src/tranql/web/src/Toolbar.js @@ -1,5 +1,11 @@ import React, { Component } from 'react'; import ReactTooltip from 'react-tooltip'; +import { + FaArrowsAlt, FaMousePointer, FaHighlighter, FaEye, FaPlayCircle, + FaSearch, FaQuestionCircle, FaDatabase, FaFolderOpen, FaCog, + FaTable +} from 'react-icons/fa'; +import { IoMdBrowsers } from 'react-icons/io'; import { CSSStringtoRGB } from './Util.js'; import './Toolbar.css'; @@ -487,3 +493,70 @@ export class Toolbar extends Component { } } + +export class AppToolbar extends Component { + _getTools() { + return ( + <> + + + + + + + + + + + + + + + + + ); + } + _getButtons() { + return ( + <> + + + + + + + + { + // Perfectly functional but does not provide enough functionality as of now to warrant its presence + /* this.setState ({ showTypeChart : true })} />*/ + // The tool works as intended but the annotator does not yet. + /* this._annotateGraph ()}/>*/ + } + + ); + } + render() { + return ( + + ); + } +} \ No newline at end of file diff --git a/src/tranql/web/src/autocomplete.js b/src/tranql/web/src/autocomplete.js new file mode 100644 index 0000000..5edabc1 --- /dev/null +++ b/src/tranql/web/src/autocomplete.js @@ -0,0 +1,548 @@ +/** +* Callback for handling autocompletion within the query editor. +* +* Note: Since this function is so long, it's been moved into its own file. +* App.js imports this function and binds it to the App context. It will not +* function outside of the App context. If this ever becomes problematic, +* just the parsing logic can be moved into its own function. +* +* @param {object} cm - The CodeMirror object. +* @private +*/ +export default function autoComplete () { + // https://github.com/JedWatson/react-codemirror/issues/52 + var codeMirror = this._codemirror; + + // hint options for specific plugin & general show-hint + // 'tables' is sql-hint specific + // 'disableKeywords' is also sql-hint specific, and undocumented but referenced in sql-hint plugin + // Other general hint config, like 'completeSingle' and 'completeOnSingleClick' + // should be specified here and will be honored + + // Shallow copy it. + const pos = Object.assign({}, codeMirror.getCursor()); + const untrimmedPos = codeMirror.getCursor(); + const textToCursorPositionUntrimmed = codeMirror.getRange({ line : 0, ch : 0 }, { line : pos.line, ch : pos.ch }); + const textToCursorPosition = textToCursorPositionUntrimmed.trimRight(); + const entireText = codeMirror.getValue(); + + // const splitLines = textToCursorPosition.split(/\r\n|\r|\n/); + // // Adjust the position after trimming to be on the correct line. + // pos.line = splitLines.length - 1; + // // Adjust the position after trimming to be on the correct char. + // pos.ch = splitLines[splitLines.length-1].length; + + const setHint = function(options, noResultsTip) { + if (typeof noResultsTip === 'undefined') noResultsTip = true; + if (noResultsTip && options.length === 0) { + options.push({ + text: String(''), + displayText:'No valid results' + }); + } + const hintOptions = { + // tables: tables, + hint: function() { + return { + from: pos, + to: untrimmedPos, + list: options.map((option) => { + // Process custom options - `replaceText` + if (option.hasOwnProperty('replaceText')) { + let replaceText = option.replaceText; + let from = option.hasOwnProperty('from') ? option.from : pos; + let to = option.hasOwnProperty('to') ? option.to : untrimmedPos; + + option.from = { line : from.line, ch : from.ch - replaceText.length }; + option.to = { line : to.line, ch : to.ch}; + + if (replaceText.length > 0) { + const trimmedLines = textToCursorPositionUntrimmed.trimRight().split(/\r\n|\r|\n/); + const lastLine = trimmedLines[trimmedLines.length-1]; + option.from.line = trimmedLines.length - 1; + option.from.ch = lastLine.length - replaceText.length; + } + + + delete option.replaceText; + } + + return option; + }) + }; + }, + disableKeywords: true, + completeSingle: false, + completeOnSingleClick: false + }; + + codeMirror.showHint(hintOptions); + // codeMirror.state.completionActive.pick = () => { + // codeMirror.showHint({ + // hint: function() { + // return { + // from: pos, + // to: pos, + // list: [{ + // text: String(''), + // displayText: 'foobar', + // className: 'testing' + // }] + // }; + // }, + // disableKeywords: true, + // completeSingle: false, + // }); + // } + } + + const setError = (resultText, status, errors, resultOptions) => { + if (typeof resultOptions === "undefined") resultOptions = {}; + codeMirror.showHint({ + hint: function() { + return { + from: pos, + to: pos, + list: [{ + text: String(''), + displayText: resultText, + className: 'autocomplete-result-error', + ...resultOptions, + }] + }; + }, + disableKeywords: true, + completeSingle: false, + }); + if (typeof status !== "undefined" && typeof errors !== "undefined") { + codeMirror.state.completionActive.pick = () => { + this._handleMessageDialog (status, errors); + } + } + } + + const setLoading = function(loading) { + if (loading) { + // text property has to be String('') because when it is '' (falsey) it refuses to display it. + codeMirror.showHint({ + hint: function() { + return { + from: pos, + to: pos, + list: [{ + text: String(''), + displayText: 'Loading', + className: 'loading-animation' + }] + }; + }, + disableKeywords: true, + completeSingle: false, + }); + } + else { + codeMirror.closeHint(); + } + } + + /** + * TODO: + * could try to see if its possible to have two select menus for predicates that also show concepts from the predicates + * would look something like this image, when, for example, you pressed the right arrow or left clicked or something on a predicate: + * https://i.imgur.com/LBsdrcq.png + * could somehow see if there's a way to have predicate suggestion work properly when there's a concept already following the predicate + * Ex: 'select foo-[what_can_I_put_here?]->baz' + * Would involve sending more of the query instead of cutting it off at cursor. + * Then would somehow have to backtrack and locate which token the cursor's position translates to. + */ + + this._autoCompleteController.abort(); + this._autoCompleteController = new window.AbortController(); + + setLoading(true); + + fetch(this.tranqlURL + '/tranql/parse_incomplete', { + signal: this._autoCompleteController.signal, + method: "POST", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify([textToCursorPositionUntrimmed, entireText]) + }).then(res => res.json()) + .then(async (parsedTree) => { + setLoading(false) + + if (parsedTree.errors) { + // this._handleMessageDialog (parsedTree.status, parsedTree.errors); + setError("Failed to parse", parsedTree.status, parsedTree.errors); + } + else { + setLoading(true); + await this.schemaPromise; + setLoading(false); + const graph = this.state.schemaMessage.knowledge_graph; + + // Recursviely removes any tokens that are linebreaks from a parsed tree. + const stripLinebreaks = function(tree) { + if (Array.isArray(tree)) { + return tree.filter((token) => stripLinebreaks(token)); + } + else { + return tree.toString().match(/\r\n|\r|\n/) === null; + } + } + + const incompleteTree = parsedTree[0]; + const completeTree = parsedTree[1]; + + // Filter whitespace from the statements + const block = incompleteTree[incompleteTree.length-1].map((statement) => { + return stripLinebreaks(statement); + }); + const completeBlock = completeTree[completeTree.length-1].map((statement) => { + return stripLinebreaks(statement); + }); + + const lastStatement = block[block.length-1]; + const lastStatementComplete = completeBlock[block.length-1]; + + const statementType = lastStatement[0]; + + setLoading(true); + const fromOptions = await this.reasonerURLs; + setLoading(false); + + fromOptions["/schema"] = "/schema"; + + const whereOptions = [ + 'testing', + 'foobar' + ]; + + const concept_arrows = [ + '->', + '<-' + ]; + + const all_arrows = [ + '->', + '<-', + '-[', + '<-[' + ]; + + const arrow_to_pred_arrow = (arrow) => { + return { + '->' : [ + '-[', + '', + ']->' + ], + '<-' : [ + '<-[', + '', + ']-' + ] + }[arrow]; + } + + const arrowToEmptyPredicate = (arrow) => { + return arrow_to_pred_arrow(arrow); + } + + const isBackwardsPredicate = (predicate) => { + return predicate[0] === '<-['; + } + + const toForwardPredicate = (predicate) => { + predicate[0] = '-['; + predicate[2] = ']->'; + return predicate; + } + + const completePredicate = (predicate) => { + if (isBackwardsPredicate (predicate)) { + predicate[2] = arrow_to_pred_arrow("<-")[2]; + } + else { + predicate[2] = arrow_to_pred_arrow("->")[2]; + } + return predicate; + } + + const concept = (old_concept) => { + // Concept identifiers aren't actually parsed by the lexer, but rather the ast in Query::add. + // This just copies the methods that the ast uses to parse concept identifiers. + if (old_concept.indexOf(":") !== -1) { + const split = old_concept.split(":"); + if (split.length - 1 > 1) { + throw new Error(`Invalid concept identifier "${old_concept}"`); + } + const [name, type_name] = split; + return type_name; + } + else { + return old_concept; + } + } + + const lastToken = lastStatement[lastStatement.length-1]; + const secondLastToken = lastStatement[lastStatement.length-2]; + const thirdLastToken = lastStatement[lastStatement.length-3]; + + console.log(statementType, lastStatement, lastToken); + + // Try/catch the entirety of the logic + try { + if (statementType === 'select') { + let validConcepts; + if (lastToken === "-") { + // Arrow suggestion + // "select foo-" + validConcepts = all_arrows.map((arrow) => { + return { + displayText: arrow, + text: arrow, + replaceText: "-" + }; + }); + } + else if (Array.isArray(lastToken) && lastToken.length < 3) { + // If the last token is an array and not length 3 then it is an incomplete predicate. + // "select foo-[" or "select foo-[bar" + let currentPredicate = completePredicate([ + lastToken[0], + lastToken[1] !== undefined ? lastToken[1] : "" + ]); + let previousConcept = concept(secondLastToken); + // May be undefined if there is no next concept + let has_no_next_concept = (lastStatementComplete.length - lastStatement.length) == 0; + let nextConcept = has_no_next_concept? undefined : concept(lastStatementComplete[lastStatement.length]) ; + // See https://github.com/frostyfan109/tranql/issues/117 for why this approach doesn't work + + + + const backwards = isBackwardsPredicate (currentPredicate); + + console.log ([previousConcept, currentPredicate, nextConcept]); + + // Should replace this method with reduce + + const allEdges = graph.edges.filter((edge) => { + if (backwards) { + return edge.target_id === previousConcept && + (nextConcept === undefined || edge.source_id === nextConcept) && + edge.type.startsWith(currentPredicate[1]); + } + else { + return ( + edge.source_id === previousConcept && + (nextConcept === undefined || edge.target_id === nextConcept) && + edge.type.startsWith(currentPredicate[1]) + ); + } + }); + const uniqueEdgeMap = {}; + allEdges.forEach((edge) => { + if (!uniqueEdgeMap.hasOwnProperty(edge.type)) { + uniqueEdgeMap[edge.type] = edge; + } + }); + const uniqueEdges = Object.values(uniqueEdgeMap); + validConcepts = uniqueEdges.map((edge) => { + const replaceText = currentPredicate[1]; + // const actualText = type + currentPredicate[2]; + const conceptHint = " (" + (backwards ? edge.source_id : edge.target_id) + ")"; + const actualText = edge.type; + const displayText = edge.type + conceptHint; + return { + displayText: displayText, + text: actualText, + replaceText : replaceText + }; + }); + } + else { + // Otherwise, we are handling autocompletion of a concept. + let currentConcept = ""; + let predicate = null; + let previousConcept = null; + + if (lastToken === statementType) { + // "select" + } + else if (secondLastToken === statementType) { + // "select foo" + currentConcept = concept(lastToken); + } + else if (concept_arrows.includes(lastToken) || Array.isArray(lastToken)) { + // "select foo->" or "select foo-[bar]->" + predicate = lastToken; + previousConcept = concept(secondLastToken); + } + else { + previousConcept = concept(thirdLastToken); + predicate = secondLastToken; + currentConcept = concept(lastToken); + } + + + if (predicate === null) { + // Predicate will only be null if there are no arrows, and therefore the previousConcept is also null. + // Single concept - just "select" or "select foo" where the concept is either "" or "foo" + validConcepts = graph.nodes.filter((node) => node.type.startsWith(currentConcept)).map(node => node.type); + } + else { + // If there is a predicate, we have to factor in the previous concept, the predicate, and the current concept. + if (!Array.isArray(predicate)) { + // We want to assign an empty predicate + predicate = arrowToEmptyPredicate (predicate); + } + + const backwards = isBackwardsPredicate (predicate); + + console.log ([previousConcept, predicate, currentConcept]); + // Concepts could be named like select f1:foo->f2:bar + // we need to split them and grab the actual types + let previousConceptSplit = previousConcept.split(':'); + let currentConceptSplit = currentConcept.split(':'); + previousConcept = previousConceptSplit[previousConceptSplit.length - 1]; + currentConcept = currentConceptSplit[currentConceptSplit.length - 1]; + validConcepts = graph.edges.filter((edge) => { + if (backwards) { + return ( + edge.source_id.startsWith(currentConcept) && + edge.target_id === previousConcept && + (predicate[1] === "" || edge.type === predicate[1]) + ); + } + else { + return ( + edge.source_id === previousConcept && + edge.target_id.startsWith(currentConcept) && + (predicate[1] === "" || edge.type === predicate[1]) + ); + } + }).map((edge) => { + if (backwards) { + return edge.source_id; + } + else { + return edge.target_id + } + }) + } + validConcepts = validConcepts.unique().map((concept) => { + return { + displayText: concept, + text: concept, + replaceText: currentConcept + }; + }); + } + setHint(validConcepts); + + } + else if (statementType === 'from') { + let currentReasonerArray = lastStatement[1]; + let startingQuote = ""; + if (currentReasonerArray === undefined) { + // Adds an apostrophe to the start of the string if it doesn't have one ("from") + startingQuote = "'"; + currentReasonerArray = [[ + "'", + "" + ]]; + } + const endingQuote = currentReasonerArray[currentReasonerArray.length - 1][0]; + const currentReasoner = currentReasonerArray[currentReasonerArray.length - 1][1]; + // The select statement must be the first statement in the block, but thorough just in case. + // We also want to filter out whitespace that would be detected as a token. + const selectStatement = block.filter((statement) => statement[0] === "select")[0].filter((token) => { + return typeof token !== "string" || token.match(/\s/) === null; + }); + // Don't want the first token ("select") + const tokens = selectStatement.slice(1); + + let validReasoners = []; + + Object.keys(fromOptions).forEach((reasoner) => { + let valid = true; + if (tokens.length === 1) { + // Handles if there's only one concept ("select foo") + const currentConcept = concept(tokens[0]); + graph.nodes.filter((node) => node.type.startsWith(currentConcept)).forEach(node => node.reasoner.forEach((reasoner) => { + !validReasoners.includes(reasoner) && validReasoners.push(reasoner); + })); + } + else { + for (let i=0;i { + if (backwards) { + return ( + edge.source_id.startsWith(currentConcept) && + edge.target_id === previousConcept && + (predicate[1] === "" || edge.type === predicate[1]) && + (reasoner === "/schema" || edge.reasoner.includes(reasoner)) + ); + } + else { + return ( + edge.source_id === previousConcept && + edge.target_id.startsWith(currentConcept) && + (predicate[1] === "" || edge.type === predicate[1]) && + (reasoner === "/schema" || edge.reasoner.includes(reasoner)) + ); + } + }).length > 0; + if (!isTransitionValid) { + valid = false; + break; + } + } + if (valid) { + validReasoners.push(reasoner); + } + } + }); + + const validReasonerValues = validReasoners.map((reasoner) => { + return fromOptions[reasoner]; + }).filter((reasonerValue) => { + return reasonerValue.startsWith(currentReasoner); + }).map((reasonerValue) => { + return { + displayText: reasonerValue, + text: startingQuote + reasonerValue, + // text: startingQuote + reasonerValue + endingQuote, + replaceText: currentReasoner + }; + }); + + setHint(validReasonerValues); + } + else if (statementType === 'where') { + + } + } + catch (e) { + setError('Failed to parse', 'Failed to parse', [{message: e.message, details: e.stack}]); + } + } + }) + .catch((error) => { + if (error.name !== "AbortError") { + setError('Error', 'Error', [{message: error.message, details: error.stack}]); + } + }); +} \ No newline at end of file diff --git a/src/tranql/web/src/static/app_data/example_queries.js b/src/tranql/web/src/static/app_data/example_queries.js new file mode 100644 index 0000000..00699d2 --- /dev/null +++ b/src/tranql/web/src/static/app_data/example_queries.js @@ -0,0 +1,108 @@ +module.exports = [ + { + title: 'Protein-Metabolite Interaction', + query: +`-- What proteins are targetted by the metabolite KEGG:C00017? + +set metabolite = "KEGG:C00017" + +select metabolite->protein +from "/graph/rtx" +where metabolite=$metabolite + +` + }, + { + title: 'Chemical substances target genes that target asthma', + query: +`-- Which chemical substances target genes that target asthma? +select chemical_substance->gene->disease +from "/graph/gamma/quick" +where disease="asthma" +` + }, + { + title: 'Usage of predicates to narrow results', + query: +`-- Which chemical substances decrease activity of genes that contribute to asthma? +select chemical_substance-[decreases_activity_of]->gene-[contributes_to]->disease +from "/graph/gamma/quick" +where disease="asthma" +` + }, + { + title: 'Phenotypic Feature-Disease Association', + query: +`-- What diseases are associated with the phenotypic feature HP:0005978? + +select phenotypic_feature->disease +from "/graph/rtx" +where phenotypic_feature="HP:0005978" +` + }, + { + title: 'Drug-Disease Pair', + query: +`-- +-- Produce clinial outcome pathways for this drug disease pair. +-- + +set drug = 'PUBCHEM:2083' +set disease = 'MONDO:0004979' + +select chemical_substance->gene->anatomical_entity->phenotypic_feature<-disease +from '/graph/gamma/quick' +where chemical_substance = $drug +and disease = $disease` + }, + { + title: 'Drug Targets Gene', + query: +`-- +-- What drug targets some gene? +-- + +set target_gene = 'HGNC:6871' --mapk1 +select chemical_substance->gene +from '/graph/gamma/quick' +where gene = $target_gene` + }, + { + title: 'Tissue-Disease Association', + query: +`-- +-- What tissue types are associated with [disease]? +-- +set disease = 'asthma' +select disease->anatomical_feature->cell +from '/graph/gamma/quick' +where disease = $disease +` + }, + { + title: 'Workflow 5 v3', + query: +`-- +-- Workflow 5 +-- +-- Modules 1-4: Chemical Exposures by Clinical Clusters +-- For ICEES cohorts, eg, defined by differential population +-- density, which chemicals are associated with these +-- cohorts with a p_value lower than some threshold? +-- +-- Modules 5-*: Knowledge Graph Phenotypic Associations +-- For chemicals produced by steps 1-4, what phenotypes are +-- associated with exposure to these chemicals? +-- + +SELECT population_of_individual_organisms->chemical_substance->gene->biological_process_or_activity<-phenotypic_feature +FROM "/schema" +WHERE icees.table = 'patient' +AND icees.year = 2010 +AND icees.cohort_features.AgeStudyStart = '0-2' +AND icees.feature.EstResidentialDensity < 1 +AND icees.maximum_p_value = 1 +AND chemical_substance !=~ '^(SCTID.*|rxcui.*|CAS.*|SMILES.*|umlscui.*)$' +AND icees.regex = "(MONDO|HP):.*""` + } +]; \ No newline at end of file From 89b130869e8c23142424a8289ecdcfbfd7601e54 Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Thu, 30 Sep 2021 20:45:11 -0400 Subject: [PATCH 2/3] Moves bulky table viewer initialization into AppTableViewer under TableViewer.js --- src/tranql/web/src/App.js | 78 ++++++------------------------- src/tranql/web/src/TableViewer.js | 70 +++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 69 deletions(-) diff --git a/src/tranql/web/src/App.js b/src/tranql/web/src/App.js index 17c78e2..a79643b 100644 --- a/src/tranql/web/src/App.js +++ b/src/tranql/web/src/App.js @@ -33,7 +33,7 @@ import QueriesModal from './QueriesModal.js'; import HistoryViewer from './HistoryViewer.js'; import BrowseNodeInterface from './BrowseNodeInterface.js'; import Legend from './Legend.js'; -import TableViewer from './TableViewer.js'; +import { AppTableViewer } from './TableViewer.js'; import HelpModal, { ToolbarHelpModal } from './HelpModal.js'; import ImportExportModal from './ImportExportModal.js'; import SettingsModal from './SettingsModal.js'; @@ -2066,18 +2066,6 @@ class App extends Component { ); } - /** - * Render the modal settings dialog. - * - * @private - */ - _renderSettingsModal () { - return ( - <> - - - ); - } /** * Sets the active modal * @@ -2178,7 +2166,6 @@ class App extends Component { onChargeChange={this._onChargeChange} onLegendNodeLimitChange={(e) => (this._onLegendDisplayLimitChange('nodes',e))} onLegendLinkLimitChange={(e) => (this._onLegendDisplayLimitChange('links',e))}/> - {this._renderSettingsModal () } {this._renderTypeChart ()} {this._toolbar.current &&
- { - const graph = this.state.schemaViewerActive && this.state.schemaViewerEnabled ? this.state.schema : this.state.graph; - // Table viewer generates a tab for every property in the object provided, but we only want nodes and links as our tabs. - return { - nodes : graph.nodes.map((node) => node.origin), - links : graph.links.map((link) => link.origin) - }; - })()} - defaultTableAttributes={{ - "nodes" : [ - "name", - "id", - "type" - ], - "links" : [ - "source_id", - "target_id", - "type", - "source_database" - ] - }} - tableProps={{ - getTdProps: (tableState, rowInfo, columnInfo, tableInstance) => { - const get_element = () => { - const is_node = this._tableViewer.current._tabs.current.props.activeKey === "0"; - const graph = this.state.schemaViewerActive && this.state.schemaViewerEnabled ? this.state.schema : this.state.graph; - const elements = is_node ? graph.nodes : graph.links; - const origin = rowInfo.original; - - const element = elements.filter((element) => element.origin.id === origin.id)[0]; - - const click_method = (is_node ? () => { - this._handleNodeClick(element); - } : () => { - this._handleLinkClick(element, true); - }); - - return { - click : click_method - }; - } - return { - onClick: () => { - get_element().click(); - } - }; - } - }}/> +
diff --git a/src/tranql/web/src/TableViewer.js b/src/tranql/web/src/TableViewer.js index d89c110..56da7be 100644 --- a/src/tranql/web/src/TableViewer.js +++ b/src/tranql/web/src/TableViewer.js @@ -38,10 +38,10 @@ export default class TableViewer extends Component { this.state = { tableFilterView : false, tableAttributes : this.props.defaultTableAttributes, - filterFilter : '' + filterFilter : '', + activeTab: undefined }; - this._tabs = React.createRef(); this._filterInputFilter = React.createRef(); this.resetAttributes = this.resetAttributes.bind(this); @@ -51,7 +51,11 @@ export default class TableViewer extends Component { this.setState({ tableAttributes : this.props.defaultTableAttributes }); } _getActiveTabName() { - return this._tabs.current.props.activeKey; + // return this._tabs.current.props.activeKey; + return this.state.activeTab; + } + componentDidMount() { + this.setState({ activeTab: Object.keys(this.props.data)[0] }); } render() { if (!this.props.tableView) return null; @@ -82,7 +86,7 @@ export default class TableViewer extends Component {
- + this.setState({activeTab: key})}> { (() => { const elementTypes = Object.keys(this.props.data); @@ -188,3 +192,61 @@ export default class TableViewer extends Component { ); } } + +export class AppTableViewer extends Component { + render() { + return ( + { + const graph = this.props.appState.schemaViewerActive && this.props.appState.schemaViewerEnabled ? this.props.appState.schema : this.props.appState.graph; + // Table viewer generates a tab for every property in the object provided, but we only want nodes and links as our tabs. + return { + nodes : graph.nodes.map((node) => node.origin), + links : graph.links.map((link) => link.origin) + }; + })()} + defaultTableAttributes={{ + "nodes" : [ + "name", + "id", + "type" + ], + "links" : [ + "source_id", + "target_id", + "type", + "source_database" + ] + }} + tableProps={{ + getTdProps: (tableState, rowInfo, columnInfo, tableInstance) => { + const getElement = () => { + const isNode = this.props.tableViewer.current._getActiveTabName() === "nodes"; + const graph = this.props.appState.schemaViewerActive && this.props.appState.schemaViewerEnabled ? this.props.appState.schema : this.props.appState.graph; + const elements = isNode ? graph.nodes : graph.links; + const origin = rowInfo.original; + + const element = elements.filter((element) => element.origin.id === origin.id)[0]; + + const clickMethod = (isNode ? () => { + this.props.handleNodeClick(element); + } : () => { + this.props.handleLinkClick(element, true); + }); + + return { + click : clickMethod + }; + } + return { + onClick: () => { + getElement().click(); + } + }; + } + }}/> + ) + } +} \ No newline at end of file From fd55500a4c85bdab4d9cc4d218b86ae960a9ce34 Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Tue, 9 Nov 2021 14:57:47 -0500 Subject: [PATCH 3/3] Fixes repo-wide dependency conflict bug --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c61e77a..ecb04bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ openapi==1.1.0 pytest==5.4.1 pytest-cov==2.7.1 python-coveralls==2.9.2 -python-dateutil==2.8.0 +python-dateutil==2.8.1 pyyaml==5.1 redisgraph==2.2.4 requests==2.25.1