diff --git a/.gitignore b/.gitignore index 3a51f4a..03dd994 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # See http://help.github.com/ignore-files/ for more about ignoring files. # ignore static files generated during build process /static/app.html.fragment +/static/static.html.fragment /static/blogFeed.json # deploy key diff --git a/.travis.yml b/.travis.yml index f53b5f8..f9b97dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: -- 6.3.1 +- 7.9.0 before_script: - npm install -g grunt-cli script: grunt package diff --git a/Gruntfile.js b/Gruntfile.js index ecc3736..c93eda3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -5,6 +5,8 @@ var moment = require('moment'); var fs = require('fs'); var grameneRelease = require('./package.json').gramene.dbRelease; +var reactomeURL = require('./package.json').gramene.reactomeURL; + var webserviceVersion = 'v' + grameneRelease; module.exports = function (grunt) { @@ -29,11 +31,11 @@ module.exports = function (grunt) { }, exec: { - generateStaticApp: { - cmd: 'node scripts/babel.js' + generateStaticWelcome: { + cmd: 'node scripts/babel.js -c app' }, - blogFeed: { - cmd: 'node scripts/getBlogFeed.js' + generateStaticApp: { + cmd: 'node scripts/babel.js -c static' } }, @@ -96,7 +98,7 @@ module.exports = function (grunt) { }, html: { files: ['*.template.html'], - tasks: ['packageIndexHtml'] + tasks: ['packageIndexHtml','packageStaticHtml'] }, styles: { files: ['styles/*.less'], @@ -121,7 +123,7 @@ module.exports = function (grunt) { {expand: true, cwd: 'assets/', src: ['**'], dest: 'build/assets/'}, { expand: true, - cwd: 'node_modules/gramene-dalliance', + cwd: 'node_modules/dalliance', src: [ 'css/*-scoped.css', 'css/font-awesome.min.css', @@ -131,8 +133,12 @@ module.exports = function (grunt) { dest: 'build/assets/gramene-dalliance/' }, { - src: 'node_modules/gramene-dalliance/prebuilt/worker-all.js', + src: 'node_modules/dalliance/prebuilt/worker-all.js', dest: 'build/assets/gramene-dalliance/worker-all.js' + }, + { + src: 'node_modules/dalliance/prebuilt/dalliance-all.js', + dest: 'build/assets/gramene-dalliance/dalliance-all.js' } ] }, @@ -195,7 +201,8 @@ module.exports = function (grunt) { footer: footer, content: content, loadingMessage: loadingMessage, - hideIntro: hideIntro + hideIntro: hideIntro, + reactomeURL: reactomeURL }; return template(props); @@ -205,9 +212,77 @@ module.exports = function (grunt) { var atlas = grunt.file.read('./static/atlasWidget.template.html') grunt.file.write('build/atlasWidget.html', atlas); + + // var htaccess = "RewriteBase /\n" + // + "RewriteRule ^index\.html$ - [L]\n" + // + "RewriteCond %{REQUEST_FILENAME} !-f\n" + // + "RewriteCond %{REQUEST_FILENAME} !-d\n" + // + "RewriteRule . /index.html [L]\n"; + // grunt.file.write('build/.htaccess', htaccess); }); - - grunt.registerTask('generateStaticFiles', ['exec:blogFeed', 'copy:assets', 'copy:icons', 'exec:generateStaticApp', 'packageIndexHtml']); + + grunt.registerTask('packageStaticHtml', 'Build static.html for distribution.', function () { + var footer = (function compileFooterTemplate() { + function defaultServer() { + const PROD_SERVER = 'http://data.gramene.org/'; + const DEV_SERVER = 'http://devdata.gramene.org/'; + var defaultServer; + + if (process.env.GRAMENE_SERVER) { + defaultServer = process.env.GRAMENE_SERVER; + } + else if (props.tag || props.branch === 'master') { + defaultServer = PROD_SERVER; + } + else { + defaultServer = DEV_SERVER; + } + + defaultServer += webserviceVersion + '/swagger'; + + return defaultServer; + } + + var template = _.template(grunt.file.read('./static/footer.template.html')); + + var props = { + jobId: process.env.TRAVIS_JOB_ID, + jobNumber: process.env.TRAVIS_JOB_NUMBER, + branch: process.env.TRAVIS_BRANCH, + tag: process.env.TRAVIS_TAG, + user: process.env.USER, + date: moment().format('MMMM Do YYYY [at] h:mm:ss a'), + isDev: process.env.isDev, + grameneRelease: grameneRelease + }; + + props.defaultServer = defaultServer(); + console.log("This build will use " + props.defaultServer + " as default web service server"); + + return template(props); + })(); + + var index = (function compileIndexTemplate() { + var template = _.template(grunt.file.read('./static/index.template.html')); + var content = grunt.file.read('./static/static.html.fragment'); + var loadingMessage = grunt.file.read('./static/loading-message.html.fragment'); + var hideIntro = grunt.file.read('./static/hide-intro.html.fragment'); + + var props = { + footer: footer, + content: content, + loadingMessage: loadingMessage, + hideIntro: hideIntro, + reactomeURL: reactomeURL + }; + + return template(props); + })(); + + grunt.file.write('build/static.html', index); + }); + + grunt.registerTask('generateStaticFiles', ['copy:assets', 'copy:icons', 'exec:generateStaticApp', 'exec:generateStaticWelcome', 'packageStaticHtml', 'packageIndexHtml']); grunt.registerTask('test', ['jasmine_node']); grunt.registerTask('default', ['env:dev', 'generateStaticFiles', 'less:dev', 'browserify:dev', 'watch']); grunt.registerTask('package', ['env:prod', 'generateStaticFiles', 'less:production', 'browserify:production', 'test']); diff --git a/assets/images/welcome/TrackHub.png b/assets/images/welcome/TrackHub.png new file mode 100644 index 0000000..39abd85 Binary files /dev/null and b/assets/images/welcome/TrackHub.png differ diff --git a/assets/images/welcome/ensemblgramene.png b/assets/images/welcome/ensemblgramene.png new file mode 100644 index 0000000..28c68af Binary files /dev/null and b/assets/images/welcome/ensemblgramene.png differ diff --git a/assets/images/welcome/plantReactome.svg b/assets/images/welcome/plantReactome.svg new file mode 100644 index 0000000..8422f10 --- /dev/null +++ b/assets/images/welcome/plantReactome.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/welcome/tools.png b/assets/images/welcome/tools.png new file mode 100644 index 0000000..7a10cfd Binary files /dev/null and b/assets/images/welcome/tools.png differ diff --git a/package.json b/package.json index 4b0fa68..901521f 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,12 @@ { "name": "gramoogle", - "version": "2.5.1", + "version": "3.0.0", "description": "Gramene search interface", "main": "index.js", "gramene": { - "dbRelease": 52 + "dbRelease": 53, + "reactomeURL": "//plantreactome.gramene.org", + "ensemblURL": "//ensembl.gramene.org" }, "scripts": { "build": "grunt package", @@ -23,25 +25,44 @@ "homepage": "https://github.com/warelab/gramoogle", "dependencies": { "axios": "^0.9.1", + "body-parser": "^1.16.0", + "component-closest": "^1.0.1", + "compression": "^1.6.2", + "cors": "^2.8.1", + "dalliance": "git://github.com/ajo2995/dalliance.git#cran", + "email": "^0.2.6", + "express": "^4.14.0", + "flat-to-nested": "^1.0.2", "gramene-bins-client": "^2.2.9", "gramene-client-cache": "1.0.0", - "gramene-dalliance": "^0.13.2", "gramene-dbxrefs": "^3.0.2", - "gramene-genetree-vis": "3.1.4", + "gramene-genetree-vis": "git://github.com/warelab/gramene-genetree-vis.git", "gramene-search-client": "^3.0.4", "gramene-search-vis": "^4.1.2", "gramene-taxonomy-with-genomes": "^3.0.8", - "gramene-trees-client": "2.4.7", + "gramene-trees-client": "git://github.com/warelab/gramene-trees-client.git#consensus", + "history": "^4.5.1", + "is-email": "^1.0.0", "lodash": "^4.3.0", + "mysql": "^2.13.0", + "node-schedule": "^1.2.0", "normalize.less": "^1.0.0", "numeral": "^1.5.3", + "path": "^0.12.7", "q": "^1.2.0", "query-string": "^4.2.2", - "react": "^15.0.1", - "react-bootstrap": "^0.30.0", - "react-dom": "^15.0.1", + "react": "^15.4.2", + "react-bootstrap": "^0.30.7", + "react-dom": "^15.4.2", + "react-ga": "^2.2.0", + "react-recaptcha": "^2.2.6", + "react-router": "^3.0.2", + "react-tree-menu": "^1.5.0", + "reactify": "^1.1.1", "reflux": "^0.4.1", "rss-parser": "latest", + "soap": "^0.18.0", + "tree-model": "^1.0.6", "xml2js": "^0.4.16" }, "devDependencies": { diff --git a/scripts/actions/docActions.js b/scripts/actions/docActions.js index f93f831..cd35c41 100644 --- a/scripts/actions/docActions.js +++ b/scripts/actions/docActions.js @@ -30,7 +30,7 @@ function getClientPromise(collection) { return client[collection]; } -DocActions.needDocs.listen(function (collection, id, postprocessFn) { +DocActions.needDocs.listen(function (collection, id, postprocessFn, callbackFn) { var cacheKey, clientCall; cacheKey = [collection,id]; console.log('DocActions.needDocs', collection, id); @@ -43,7 +43,13 @@ DocActions.needDocs.listen(function (collection, id, postprocessFn) { var promise, cached; if ((cached = docCache.get(cacheKey))) { - promise = Q(cached); + promise = Q(cached) + .then(function callbackMaybe(result) { + if (callbackFn) { + callbackFn(result.docs); + } + return result; + }); } else { docCache.set(id, 'loading…'); @@ -71,6 +77,12 @@ DocActions.needDocs.listen(function (collection, id, postprocessFn) { } return result; }) + .then(function callbackMaybe(result) { + if (callbackFn) { + callbackFn(result.docs); + } + return result; + }) .then(function addToCache(result) { docCache.set(cacheKey, result); return result; diff --git a/scripts/actions/drupalActions.js b/scripts/actions/drupalActions.js new file mode 100644 index 0000000..d705c23 --- /dev/null +++ b/scripts/actions/drupalActions.js @@ -0,0 +1,26 @@ +import Reflux from "reflux"; +import {getBlogFeed} from "../welcome/getDrupalContent.js"; +// import Q from 'q'; + +const DrupalActions = Reflux.createActions([ + {'refreshBlogFeed': {asyncResult: true}}, + // {'fetchDrupalPage': {asyncResult: true}} +]); + +// console.log(DrupalActions); + +DrupalActions.refreshBlogFeed.listen(function () { + console.log("Will refreshBlog Feed"); + getBlogFeed() + .then(this.completed) + .catch(this.failed); +}); + +// DrupalActions.fetchDrupalPage.listen(function (url) { +// console.log("Will fetchDrupalPage from", url); +// getDrupalPage(url) +// .then(this.completed) +// .catch(this.failed); +// }); + +export default DrupalActions; \ No newline at end of file diff --git a/scripts/actions/welcomeActions.js b/scripts/actions/welcomeActions.js index 52ccb79..9c8d1e4 100644 --- a/scripts/actions/welcomeActions.js +++ b/scripts/actions/welcomeActions.js @@ -1,21 +1,12 @@ import Reflux from "reflux"; -import getBlogFeed from "../welcome/getBlogFeed.js"; import Q from 'q'; const WelcomeActions = Reflux.createActions([ - {'refreshBlogFeed': {asyncResult: true}}, {'flashSearchBox': {asyncResult: true}} ]); console.log(WelcomeActions); -WelcomeActions.refreshBlogFeed.listen(function () { - console.log("Will refreshBlog Feed"); - getBlogFeed() - .then(this.completed) - .catch(this.failed); -}); - WelcomeActions.flashSearchBox.listen(function (delayMs = 500) { const deferred = Q.defer(); setTimeout(()=>deferred.resolve("finish"), delayMs); diff --git a/scripts/babel.js b/scripts/babel.js index 2e6e1f3..a7ef933 100644 --- a/scripts/babel.js +++ b/scripts/babel.js @@ -3,6 +3,9 @@ 'use strict'; var fs = require('fs'); +var argv = require('minimist')(process.argv.slice(2)); + +var serverRenderMode = argv.c; require('babel-register')({ presets: ['es2015', 'react'] @@ -16,4 +19,4 @@ var ReactDOMServer = require('react-dom/server'); var App = React.createFactory(require('./components/appStatic.jsx').default); -fs.writeFileSync('static/app.html.fragment', ReactDOMServer.renderToString(new App({context: 'compile'}))); \ No newline at end of file +fs.writeFileSync(`static/${serverRenderMode}.html.fragment`, ReactDOMServer.renderToString(new App({serverRenderMode}))); \ No newline at end of file diff --git a/scripts/components/DrupalPage.jsx b/scripts/components/DrupalPage.jsx new file mode 100644 index 0000000..6d3496f --- /dev/null +++ b/scripts/components/DrupalPage.jsx @@ -0,0 +1,120 @@ +import React from 'react'; +import axios from "axios"; +import { browserHistory } from 'react-router'; +import _ from 'lodash'; +import closest from 'component-closest'; +import ReactGA from "react-ga"; + + +export default class DrupalPage extends React.Component { + constructor(props) { + super(props); + this.state = { + aliases: {} + }; + } + + componentWillMount() { + // populate this.state.aliases + this.fetchAliases(); + } + + getNid(params) { + if (params.drupalPath) { + if (this.state.aliases && this.state.aliases[params.drupalPath]) { + return this.state.aliases[params.drupalPath]; + } + } + else if (params.drupalNode) { + return params.drupalNode; + } + return null; + } + + componentDidMount() { + window.scrollTo(0, 0); + } + + componentDidUpdate(prevProps, prevState) { + window.scrollTo(0, 0); + } + + initListener() { + let iframeDoc = this.iframe.contentDocument || this.iframe.contentWindow.document; + if (typeof iframeDoc.addEventListener !== "undefined") { + iframeDoc.addEventListener("click", this.iframeClickHandler.bind(this), true); + } else if (typeof iframeDoc.attachEvent !== "undefined") { + iframeDoc.attachEvent("onclick", this.iframeClickHandler.bind(this)); + } + } + + componentWillReceiveProps(nextProps) { + let nextNid = this.getNid(nextProps.params); + if (nextNid && this.nid !== nextNid) { + this.nid = nextNid; + this.iframe.src = `/ww/${this.nid}`; + } + } + + iframeClickHandler(e) { + let target = e.target || e.srcElement; + target = closest(target, 'a'); + let href = target.getAttribute('href'); + let drupalLink; + let matches = href.match(/(node\/\d+)$/); + if (!matches) { + matches = href.match(/gramene\.org\/(.+)/); + if (matches && this.state.aliases[matches[1]]) { + drupalLink = matches[1]; + } + } + else { + drupalLink = matches[1]; + } + if (drupalLink) { + e.preventDefault(); + browserHistory.push('/'+drupalLink); + } + else { + ReactGA.outboundLink({ + label: href + }, function () { + console.log('have fun at',href); + }); + } + } + + componentWillUnmount () { + this.ignoreLastFetch = true; + } + + fetchAliases () { + let url = `/aliases`; + axios.get(url).then(response => { + if (!this.ignoreLastFetch) { + this.setState({ aliases: response.data }); + } + }) + } + + render() { + let src; + this.nid = this.getNid(this.props.params); + if (this.nid) { + src = `/ww/${this.nid}`; + } + const setIframeRef = (elem) => { + // TODO ignoring null here is probably a bad idea. + if(!_.isNull(elem)) { + this.iframe = elem; + } + }; + + return ( + + ); + + } +} diff --git a/scripts/components/Feedback.jsx b/scripts/components/Feedback.jsx new file mode 100644 index 0000000..c91fc9e --- /dev/null +++ b/scripts/components/Feedback.jsx @@ -0,0 +1,214 @@ +import React from 'react'; +import Recaptcha from 'react-recaptcha'; +import { FormGroup, FormControl, Form, ControlLabel, InputGroup, Col, Button } from 'react-bootstrap'; +import isEmail from 'is-email'; +import _ from 'lodash'; +import axios from 'axios'; + +export default class Feedback extends React.Component { + constructor(props) { + super(props); + this.state = { + referrer: 'No referrer', + subject: '', + content: '', + name: '', + email: '', + org: '' + }; + } + + componentWillMount() { + if (document.referrer) { + this.setState({referrer : document.referrer}); + } + } + + handleChange(e) { + let nextState = _.cloneDeep(this.state); + nextState[e.target.id] = e.target.value; + this.setState(nextState); + } + + validateField(fieldName) { + if (fieldName === 'subject') { + const length = this.state.subject.length; + if (length > 10) return 'success'; + else if (length > 5) return 'warning'; + else if (length > 0) return 'error'; + } + if (fieldName === 'name') { + const length = this.state.name.length; + if (length > 4) return 'success'; + else if (length > 2) return 'warning'; + else if (length > 0) return 'error'; + } + if (fieldName === 'email') { + const length = this.state.email.length; + if (isEmail(this.state.email)) + return 'success'; + else if (length > 0) + return 'error'; + } + if (fieldName === 'org') { + const length = this.state.org.length; + if (length > 2) return 'success'; + else if (length > 1) return 'warning'; + else if (length > 0) return 'error'; + } + } + + submitForm() { + let that = this; + axios.post('/feedback',this.state) + .then(function (response) { + that.setState({submittedForm: true, ticket: response.data.ticket}); + }) + .catch(function (error) { + console.log(error); + }); + } + + verifyRecaptcha(response) { + this.setState({recaptcha: response}); + } + + loadRecaptcha() { + console.log('loaded recaptcha'); + } + + formIsValid() { + return (this.validateField('subject') === 'success' + && this.validateField('name') === 'success' + && this.validateField('email') === 'success' + && this.validateField('org') === 'success' + && this.state.recaptcha + ) + } + + renderForm() { + let submit = this.formIsValid() ? ( + + + + + + ) : ''; + return ( +
+

+ Questions? Comments? Please let us know. +

+
+ + + Refer to + + + + {this.state.referrer} + + + + + + + Subject + + + + + + + + + + Questions/Comments + + + + + + + + + Your Name + + + + + + + + + + Your Email + + + + + + + + + + Organization + + + + + + + + + + + + {submit} +
+
+ ); + } + + renderThanks() { + return ( +
Thank You.

Your issue has been assigned the ticket number {this.state.ticket}.

+ ); + } + + render() { + if (this.state.submittedForm) { + return this.renderThanks(); + } + else { + return this.renderForm(); + } + } +} diff --git a/scripts/components/app.jsx b/scripts/components/app.jsx index 4b320b5..9c88ab6 100644 --- a/scripts/components/app.jsx +++ b/scripts/components/app.jsx @@ -2,8 +2,8 @@ import React from 'react'; import Header from './header.jsx'; +import Footer from './footer/Footer.jsx'; import Results from './results/results.jsx'; -import Welcome from './welcome/WelcomePage.jsx'; import searchStore from '../stores/searchStore'; import _ from 'lodash'; @@ -11,9 +11,7 @@ export default class App extends React.Component { constructor(props) { super(props); - this.state = { - search: searchStore.state - }; + this.state = {}; } componentWillMount() { @@ -27,21 +25,21 @@ export default class App extends React.Component { } dontShowResults() { - // don't show the results if there are no user-specified filters - return _.isEmpty(_.get(this.state.search, 'query.filters')); + // don't show the results if there are no user-specified filters + return _.isEmpty(_.get(this.state, 'search.query.filters')); } render() { - var search = this.state.search, - content = this.dontShowResults() ? - : - + let content = this.dontShowResults() ? + this.props.children : + ; return (
-
+
{content} +
); } diff --git a/scripts/components/appStatic.jsx b/scripts/components/appStatic.jsx index cf5be03..e3e83a6 100644 --- a/scripts/components/appStatic.jsx +++ b/scripts/components/appStatic.jsx @@ -17,12 +17,22 @@ import React from 'react'; import Header from './header.jsx'; +import Footer from './footer/Footer.jsx'; import Welcome from "./welcome/WelcomePage.jsx"; -const App = () => -
-
- -
; +export default class App extends React.Component { + + constructor(props) { + super(props); + } -export default App; \ No newline at end of file + render() { + return ( +
+
+ +
+
+ ); + } +}; diff --git a/scripts/components/footer/EmbeddedDrupalPageLink.jsx b/scripts/components/footer/EmbeddedDrupalPageLink.jsx new file mode 100644 index 0000000..4cad8aa --- /dev/null +++ b/scripts/components/footer/EmbeddedDrupalPageLink.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { browserHistory } from 'react-router'; +import QueryActions from '../../actions/queryActions'; + +const EmbeddedDrupalPageLink = ({text, path, onClick}) => + { + QueryActions.removeAllFilters(); + browserHistory.push(path); + }}>{text}; + +EmbeddedDrupalPageLink.propTypes = { + text: React.PropTypes.string.isRequired, + path: React.PropTypes.string.isRequired +}; + +export default EmbeddedDrupalPageLink; \ No newline at end of file diff --git a/scripts/components/footer/Footer.jsx b/scripts/components/footer/Footer.jsx new file mode 100644 index 0000000..109a2ce --- /dev/null +++ b/scripts/components/footer/Footer.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import EmbeddedDrupalPageLink from './EmbeddedDrupalPageLink.jsx'; +import StaticSocialButtons from './StaticSocialButtons.jsx'; +var grameneRelease = require('../../../package.json').gramene.dbRelease; + + +const Footer = ({noSocial}) => { + const releaseUrl = `/release-notes-${grameneRelease}`; + const releaseLabel = `Release Notes (${grameneRelease})`; + const socialMaybe = noSocial ? undefined : ; + return ( + + ); +}; + +Footer.propTypes = { + noSocial: React.PropTypes.bool +}; + +export default Footer; \ No newline at end of file diff --git a/scripts/components/footer/StaticSocialButtons.jsx b/scripts/components/footer/StaticSocialButtons.jsx new file mode 100644 index 0000000..f0708f1 --- /dev/null +++ b/scripts/components/footer/StaticSocialButtons.jsx @@ -0,0 +1,34 @@ +import React from 'react' + +export default class StaticSocialButtons extends React.Component { + componentDidMount() { + + if(twttr && this.twttrEl) { + twttr.widgets.createFollowButton( + "GrameneDatabase", + this.twttrEl, + ); + } + } + + shouldComponentUpdate() { + return false; + } + + render() { + return ( +
    +
  • + +
  • +
  • this.twttrEl = el}/> +
+ ); + } +} diff --git a/scripts/components/header.jsx b/scripts/components/header.jsx index 786e94a..f0c2822 100644 --- a/scripts/components/header.jsx +++ b/scripts/components/header.jsx @@ -1,23 +1,18 @@ 'use strict'; -var React = require('react'); -var _ = require('lodash'); - +import React from 'react'; +import _ from 'lodash'; import Search from './search/search.jsx'; -var QueryActions = require('../actions/queryActions'); - -var bs = require('react-bootstrap'); -var Navbar = bs.Navbar; +import QueryActions from '../actions/queryActions'; +import { browserHistory } from 'react-router'; +import { Navbar, SplitButton, MenuItem } from 'react-bootstrap'; -var Header = React.createClass({ - propTypes: { - search: React.PropTypes.object.isRequired - }, +const Header = React.createClass({ removeAllFilters: function() { QueryActions.removeAllFilters(); + browserHistory.push('/'); }, render: function() { - var search = this.props.search; var logo = (
@@ -28,9 +23,11 @@ var Header = React.createClass({ return ( - {logo} + + {logo} + - + ); } diff --git a/scripts/components/result/ResultBody.jsx b/scripts/components/result/ResultBody.jsx index 58516d6..38af818 100644 --- a/scripts/components/result/ResultBody.jsx +++ b/scripts/components/result/ResultBody.jsx @@ -1,5 +1,6 @@ import React from "react"; import ClosestOrtholog from "./closestOrtholog.jsx"; +var ensemblURL = require('../../../package.json').gramene.ensemblURL; const ResultBody = (props) =>
@@ -35,7 +36,7 @@ function renderTitle({searchResult}) { {geneId} {synonyms} - {species} + {species} ); } diff --git a/scripts/components/result/ResultDetails.jsx b/scripts/components/result/ResultDetails.jsx index e5f05f6..edbaa64 100644 --- a/scripts/components/result/ResultDetails.jsx +++ b/scripts/components/result/ResultDetails.jsx @@ -1,32 +1,82 @@ import React from "react"; import _ from "lodash"; import ResultDetailLink from './ResultDetailLink.jsx'; +import {Button, Modal} from 'react-bootstrap'; -const ResultDetails = (props) =>
-
    - {renderDetailLinks(props)} -
- {renderVisibleDetailElement(props)} -
; +export default class ResultDetails extends React.Component { + constructor(props) { + super(props); + this.state = {fullScreen: false}; + } -function renderVisibleDetailElement({visibleDetail, geneDoc, docs}) { - if(visibleDetail) { - const visibleDetailComponent = visibleDetail.reactClass; - return ( -
- {React.createElement(visibleDetailComponent, {gene: geneDoc, docs})} -
- ) + toggleModal() { + this.setState({fullScreen: !this.state.fullScreen}); + } + + renderVisibleDetailElement({visibleDetail, geneDoc, docs, speciesName}) { + if (visibleDetail) { + const visibleDetailComponent = visibleDetail.reactClass; + let resizeStyle = {'ariaHidden': "true", float: "right"}; + let resizeClass = this.state.fullScreen ? "glyphicon glyphicon-resize-small" : "glyphicon glyphicon-resize-full"; + if (this.state.fullScreen) + return ( +
+ this.toggleModal()} + dialogClassName="detail-modal" + > + + +
+

+ {geneDoc.name}  + {geneDoc._id}  + {speciesName} +

+

{geneDoc.description}

+
+
+
+ + {React.createElement(visibleDetailComponent, {gene: geneDoc, docs, closeModal: this.toggleModal.bind(this)})} + +
+
+ ) + else + return ( +
+ + {React.createElement(visibleDetailComponent, {gene: geneDoc, docs})} +
+ ) + } } -}; -function renderDetailLinks(props) { + renderDetailLinks(props) { - return _.map(props.details, (resultDetail, key) => + return _.map(props.details, (resultDetail, key) => ); + } + + render() { + return ( +
+
    + {this.renderDetailLinks(this.props)} +
+ {this.renderVisibleDetailElement(this.props)} +
+ ); + } } ResultDetails.propTypes = { @@ -38,5 +88,3 @@ ResultDetails.propTypes = { geneDoc: React.PropTypes.object, docs: React.PropTypes.object }; - -export default ResultDetails; \ No newline at end of file diff --git a/scripts/components/result/details/_inventory.js b/scripts/components/result/details/_inventory.js index 30bbb22..d6073f8 100644 --- a/scripts/components/result/details/_inventory.js +++ b/scripts/components/result/details/_inventory.js @@ -32,11 +32,11 @@ const inventory = [ reactClass: Homology }, - // { - // name: 'Pathways', // for display - // capability: 'pathways', - // reactClass: require('./pathways.jsx') - // }, + { + name: 'Pathways', // for display + capability: 'pathways', + reactClass: require('./pathways.jsx') + }, { diff --git a/scripts/components/result/details/domains.jsx b/scripts/components/result/details/domains.jsx index 9b2b0ab..510e802 100644 --- a/scripts/components/result/details/domains.jsx +++ b/scripts/components/result/details/domains.jsx @@ -31,7 +31,7 @@ var Domains = React.createClass({ return { category: 'Domain Structure', fq: qString, - id: qString, + exclude: false, display_name: 'Domain structure like ' + this.props.gene.name } } diff --git a/scripts/components/result/details/expression.jsx b/scripts/components/result/details/expression.jsx index dd45b02..6195c1f 100644 --- a/scripts/components/result/details/expression.jsx +++ b/scripts/components/result/details/expression.jsx @@ -9,8 +9,9 @@ export default class Atlas extends React.Component { render() { const url = `./atlasWidget.html?geneQuery=${this.props.gene._id}&species=${this.props.gene.system_name.replace(/_/,'%20')}`; + const height = (!!this.props.closeModal) ? window.innerHeight : '500px'; return ( - ); diff --git a/scripts/components/result/details/generic/detail.jsx b/scripts/components/result/details/generic/detail.jsx index 2f332bb..cf693ac 100644 --- a/scripts/components/result/details/generic/detail.jsx +++ b/scripts/components/result/details/generic/detail.jsx @@ -5,7 +5,6 @@ import { Grid, Row, Col } from 'react-bootstrap'; export class Detail extends React.Component { render() { var subComponents = keyBy(this.props.children, 'key'); - console.log(subComponents, this.props.children); return ( diff --git a/scripts/components/result/details/generic/links.jsx b/scripts/components/result/details/generic/links.jsx index 8801d9d..8319921 100644 --- a/scripts/components/result/details/generic/links.jsx +++ b/scripts/components/result/details/generic/links.jsx @@ -1,10 +1,16 @@ import React from "react"; +import ReactGA from "react-ga"; export default class Links extends React.Component { renderLinks() { return this.props.links.map((link, idx) =>
  • - {link.name} + + {link.name} +
  • ) } diff --git a/scripts/components/result/details/homology.jsx b/scripts/components/result/details/homology.jsx index 9b027cc..1c10610 100644 --- a/scripts/components/result/details/homology.jsx +++ b/scripts/components/result/details/homology.jsx @@ -6,6 +6,9 @@ import DocActions from "../../../actions/docActions"; import searchStore from "../../../stores/searchStore"; import _ from "lodash"; import treesClient from "gramene-trees-client"; +import Spinner from "../../Spinner.jsx"; +var ensemblURL = require('../../../../package.json').gramene.ensemblURL; + const processGenetreeDoc = treesClient.genetree.tree; export default class Homology extends React.Component { @@ -122,30 +125,40 @@ export default class Homology extends React.Component { filterAllHomologs() { queryActions.setAllFilters(this.createAllHomologsFilters()); + if (this.props.closeModal) this.props.closeModal(); } filterOrthologs() { queryActions.setAllFilters(this.createOrthologFilters()); + if (this.props.closeModal) this.props.closeModal(); } filterParalogs() { queryActions.setAllFilters(this.createParalogFilters()); + if (this.props.closeModal) this.props.closeModal(); } renderTreeVis() { if (this.genetree && this.state.taxonomy) { return (
    -
    Gene Tree
    ); } + else { + return ( +
    + Loading +
    + ) + } } explorations() { @@ -189,7 +202,7 @@ export default class Homology extends React.Component { var gene, ensemblGenetreeUrl; gene = this.props.gene; - ensemblGenetreeUrl = '//ensembl.gramene.org/' + gene.system_name + '/Gene/Compara_Tree?g=' + gene._id; + ensemblGenetreeUrl = `//${ensemblURL}/${gene.system_name}/Gene/Compara_Tree?g=${gene._id}`; return [ {name: 'Ensembl Gene Tree view', url: ensemblGenetreeUrl} @@ -212,5 +225,6 @@ export default class Homology extends React.Component { Homology.propTypes = { gene: React.PropTypes.object.isRequired, - docs: React.PropTypes.object.isRequired + docs: React.PropTypes.object.isRequired, + closeModal: React.PropTypes.func }; \ No newline at end of file diff --git a/scripts/components/result/details/location.jsx b/scripts/components/result/details/location.jsx index ace6f3b..73a3f44 100644 --- a/scripts/components/result/details/location.jsx +++ b/scripts/components/result/details/location.jsx @@ -1,10 +1,11 @@ import React from "react"; import getProp from "lodash/get"; import isEqual from "lodash/isEqual"; -import {Col} from "react-bootstrap"; +import {Button} from "react-bootstrap"; import Browser from "./location/browser.jsx"; import QueryActions from "../../../actions/queryActions"; import {Detail, Title, Description, Content, Explore, Links} from "./generic/detail.jsx"; +var ensemblURL = require('../../../../package.json').gramene.ensemblURL; export default class Location extends React.Component { @@ -14,7 +15,6 @@ export default class Location extends React.Component { this.state = { visibleRange: this.initVisibleRange(props) }; - // this.handleViewChange = _.debounce(this._undebounced_handleViewChange, 50).bind(this); } initVisibleRange(props) { @@ -60,12 +60,8 @@ export default class Location extends React.Component { } } - // handleSelection(selection) { - // this.setState({ selection: selection }); - // } handleViewChange(chr, start, end) { - // console.log('view changed:', arguments); var visibleRange = { chr: chr, start: start, @@ -108,6 +104,7 @@ export default class Location extends React.Component { QueryActions.removeAllFilters(); QueryActions.setFilter(filter); + if (this.props.closeModal) this.props.closeModal(); } explorations() { @@ -130,18 +127,22 @@ export default class Location extends React.Component { links() { var gene = this.props.gene; - return [ - {name: 'Gramene Ensembl', url: `//ensembl.gramene.org/${gene.system_name}/Gene/Summary?g=${gene._id}`}, + let links = [ + {name: 'Gramene Ensembl', url: `${ensemblURL}/${gene.system_name}/Gene/Summary?g=${gene._id}`}, {name: 'PhytoMine', url: `https://phytozome.jgi.doe.gov/phytomine/keywordSearchResults.do?searchTerm=${gene._id}&searchSubmit=Search`}, - {name: 'Araport', url: `https://www.araport.org/search/thalemine/${gene._id}`} - ] + ]; + if (gene.taxon_id === 3702) + links.push({name: 'Araport', url: `https://www.araport.org/search/thalemine/${gene._id}`}); + if (gene.taxon_id === 4577) + links.push({name: 'MaizeGDB', url: `http://www.maizegdb.org/gene_center/gene/${gene._id}`}); + return links; } render() { return ( Genome location: {this.renderGenePosition()} - Currently viewing: {this.renderLocation()} + Currently viewing: {this.renderLocation()} {this.renderResetButton()} {this.renderBrowser()} @@ -176,6 +177,21 @@ export default class Location extends React.Component { ); } + + resetVisibleRange() { + const {chr, start, end} = this.initVisibleRange(this.props); + this.handleViewChange(chr, start, end); + } + + renderResetButton() { + // let active=isEqual(this.props.visibleRange, this.state.visibleRange); + return ( + + ) + } } Location.propTypes = { diff --git a/scripts/components/result/details/location/browser.jsx b/scripts/components/result/details/location/browser.jsx index 86d8566..956fc9c 100644 --- a/scripts/components/result/details/location/browser.jsx +++ b/scripts/components/result/details/location/browser.jsx @@ -1,47 +1,19 @@ import React from "react"; import DallianceBrowser from "./dallianceBrowser.jsx"; -import isEqual from "lodash/isEqual"; -import {Col, Button} from "react-bootstrap"; export default class Browser extends React.Component { constructor(props) { super(props); - this.initialVisibleRange = props.visibleRange; - } - - resetVisibleRange() { - var {chr, start, end} = this.initialVisibleRange; - this.props.onViewChange(chr, start, end) } render() { return (
    - - {this.renderBiodalliance()} - - - {this.renderResetButton()} - +
    ) } - renderBiodalliance() { - return ( - - ); - } - - renderResetButton() { - // active={!isEqual(this.props.visibleRange, this.initialVisibleRange)} - return ( - - ) - } } Browser.propTypes = { diff --git a/scripts/components/result/details/location/dallianceBrowser.jsx b/scripts/components/result/details/location/dallianceBrowser.jsx index 370928e..1612ac2 100644 --- a/scripts/components/result/details/location/dallianceBrowser.jsx +++ b/scripts/components/result/details/location/dallianceBrowser.jsx @@ -1,10 +1,10 @@ import React from "react"; import isEqual from "lodash/isEqual"; -import {browser as Dalliance} from "gramene-dalliance"; +var grameneRelease = require('../../../../../package.json').gramene.dbRelease; // calculate this once. -const PREFIX = (global.location ? global.location.origin + global.location.pathname : '') - + 'assets/gramene-dalliance/'; +const PREFIX = (global.location ? global.location.origin : '') + + '/assets/gramene-dalliance/'; export default class DallianceBrowser extends React.Component { @@ -16,7 +16,7 @@ export default class DallianceBrowser extends React.Component { shouldComponentUpdate(newProps) { // should we reset the view to initial state? if(isEqual(newProps.visibleRange, this.initialVisibleRange)) { - this.browser.setLocation(newProps.visibleRange.chr, newProps.visibleRange.start, newProps.visibleRange.end); + // this.browser.setLocation(newProps.visibleRange.chr, newProps.visibleRange.start, newProps.visibleRange.end); } return false; @@ -33,7 +33,7 @@ export default class DallianceBrowser extends React.Component { start = view.start; end = view.end; - this.browser = browser = new Dalliance( + this.browser = browser = new Browser( { pageName: this.biodallianceElementId(), chr: g.location.region, @@ -46,38 +46,41 @@ export default class DallianceBrowser extends React.Component { speciesName: g.system_name, taxon: g.taxon_id, auth: 'Gramene', - version: '3' + version: '3', + ucscName: 'IRGSP-1.0' }, sources: [ { name: 'DNA', - ensemblURI: 'http://data.gramene.org/ensembl', + ensemblURI: 'http://data.gramene.org/ensembl'+ grameneRelease, species: g.system_name, tier_type: 'sequence' }, { name: 'Genes', - uri: 'http://data.gramene.org/ensembl', + uri: 'http://data.gramene.org/ensembl'+ grameneRelease, tier_type: 'ensembl', species: g.system_name, type: ['gene', 'transcript', 'exon', 'cds'] } ], + + hubs: ['/Track_Hubs/DRP000315/hub.txt'], disablePoweredBy: true, setDocumentTitle: false, - noDefaultLabels: true, + noDefaultLabels: !this.props.expanded, noPersist: true, noPersistView: true, maxWorkers: 2, noTitle: true, - noLocationField: true, //!this.props.expanded, - noLeapButtons: true, //!this.props.expanded, + noLocationField: true, + noLeapButtons: !this.props.expanded, noZoomSlider: false, //!this.props.expanded, - noTrackAdder: true, //!this.props.expanded, - noTrackEditor: true, - noExport: true, - noOptions: true , // !this.props.expanded, + noTrackAdder: !this.props.expanded, + noTrackEditor: !this.props.expanded, + noExport: !this.props.expanded, + noOptions: !this.props.expanded, noHelp: true, maxViewWidth: 1000000 } diff --git a/scripts/components/result/details/pathways.jsx b/scripts/components/result/details/pathways.jsx index b35c23f..b05475c 100644 --- a/scripts/components/result/details/pathways.jsx +++ b/scripts/components/result/details/pathways.jsx @@ -1,22 +1,71 @@ 'use strict'; var React = require('react'); -var Reflux = require('reflux'); +const Reflux = require('reflux'); var _ = require('lodash'); +import QueryActions from "../../../actions/queryActions"; var DocActions = require('../../../actions/docActions'); var docStore = require('../../../stores/docStore'); +var reactomeURL = require('../../../../package.json').gramene.reactomeURL; +import searchStore from "../../../stores/searchStore"; +var FlatToNested = require('flat-to-nested'); +import TreeMenu from 'react-tree-menu'; +import {Explore, Links} from "./generic/detail.jsx"; + -var ReactomeItem = require('./pathways/reactomeItem.jsx'); -var ReactomeImg = require('./pathways/reactomeImg.jsx'); var Pathways = React.createClass({ propTypes: { gene: React.PropTypes.object.isRequired, docs: React.PropTypes.object // all documents requested by the page. }, + mixins: [ + Reflux.connect(docStore, 'docs') + ], getInitialState: function() { - return {}; + this.holderId = 'displayHolder' + this.props.gene._id; + return { + taxonomy: searchStore.taxonomy + }; + }, + + initDiagram: function() { + this.diagram = Reactome.Diagram.create({ + proxyPrefix: reactomeURL, //'//plantreactome.gramene.org', //'//plantreactomedev.oicr.on.ca', ////cord3084-pc7.science.oregonstate.edu', // reactomedev.oicr.on.ca + placeHolder: this.holderId, + width: this.divWrapper.clientWidth, + height: (!!this.props.closeModal) ? window.innerHeight : 500 + }); + }, + + stableId: function(dbId) { + let prefix = this.state.taxonomy.indices.id[this.props.gene.taxon_id].model.reactomePrefix; + return `R-${prefix}-${dbId}`; + }, + + loadDiagram: function(pathwayId, reactionId) { + if (!this.diagram) this.initDiagram(); + this.diagram.loadDiagram(pathwayId); + + this.diagram.onDiagramLoaded(function (loaded) { + this.loadedDiagram = loaded; + if (reactionId) { + this.diagram.selectItem(reactionId); + } + // this.diagram.flagItems(this.props.gene._id); + }.bind(this)); + }, + + componentDidMount: function() { + if (Reactome && Reactome.Diagram) { + // this.initDiagram(); + } + else { + window.addEventListener('launchDiagram', function (e) { + // this.initDiagram() + }.bind(this)); + } }, componentWillMount: function() { @@ -26,64 +75,158 @@ var Pathways = React.createClass({ throw new Error("No pathway annotation present for " + _.get(this.props, 'gene._id')); } - ancestorIds = pathways.ancestors; - if(!ancestorIds) { - throw new Error("Reactome ancestors are required because that's where the Pathway is"); - } - - this.pathwayId = _.head(ancestorIds); + this.pathwayIds = _.clone(pathways.ancestors); + pathways.entries.forEach(function(reaction) { + this.pathwayIds.push(reaction.id);
 + }.bind(this)); - if(_.get(pathways, 'entries.length') != 1) { - console.error("Number of reactions is not 1"); - } - - reactionId = _.get(pathways, 'entries[0].id'); - this.pathwayIds = [+reactionId].concat(ancestorIds); - - DocActions.needDocs('pathways', this.pathwayIds); + DocActions.needDocs('pathways', this.pathwayIds, undefined, this.getHierarchy); }, + componentWillUnmount: function() { DocActions.noLongerNeedDocs('pathways', this.pathwayIds); + if (this.diagram) this.diagram.detach(); }, - getReaction: function() { - var rxnId, rxn; - if(!this.reaction) { - rxnId = _.head(this.pathwayIds); - rxn = _.get(this.props.docs, ['pathways', rxnId]); - if(rxn) { - this.reaction = rxn; + getHierarchy: function (docs) { + let pathways = _.keyBy(docs,'_id'); + let nodes = []; + this.pathwayIds.forEach(function (pwyId) { + if (pathways[pwyId]) { + let pwy = pathways[pwyId]; + pwy.lineage.forEach(function(line) { + let parentOffset = line.length - 2; + nodes.push({ + id: pwyId, + label: pwy.name, + type: pwy.type, + checkbox: false, + selected: false, + parent: parentOffset >=0 ? line[parentOffset] : undefined + }); + }); } - } - return this.reaction; + }); + + let nested = new FlatToNested().convert(nodes); + + this.setState({hierarchy: [nested], selectedNode: undefined}); }, + renderHierarchy: function() { - var reactionData, currentNodeId, currentNode, els; - reactionData = this.getReaction(); - if(reactionData) { - currentNodeId = reactionData._id; - els = []; - while(currentNodeId) { - currentNode = this.props.docs.pathways[currentNodeId]; - els.push( ); - currentNodeId = _.get(currentNode, 'parents[0]'); - } + if(this.state.hierarchy) { + + var onClick = function(nodes) { + let hierarchy = this.state.hierarchy; + let selectedNode = this.state.selectedNode; + let offset = nodes.shift(); + let nodeRef = hierarchy[offset]; + let lineage = [nodeRef]; + nodes.forEach(function(n) { + nodeRef = nodeRef.children[n]; + lineage.unshift(nodeRef); + }); + if (lineage[0].id !== 2894885) { + let pathway = this.stableId(lineage[0].id); + let reaction = undefined; + if (lineage[0].type === "Reaction") { + reaction = pathway; + pathway = this.stableId(lineage[1].id); + } + if (lineage[0].selected) { + selectedNode = undefined; + lineage[0].selected = false; + if (this.loadedDiagram === pathway) { + this.diagram.resetSelection(); + } + else { + if (this.diagram) this.diagram.resetSelection(); + this.loadDiagram(pathway); + } + } + else { + if (selectedNode) { + selectedNode.selected = false; + } + selectedNode = lineage[0]; + lineage[0].selected = true; + if (this.loadedDiagram === pathway) { + if (reaction) { + this.diagram.selectItem(reaction); + } + else { + this.diagram.resetSelection(); + } + } + else { + if (this.diagram) this.diagram.resetSelection(); + if (reaction) { + this.loadDiagram(pathway, reaction); + } + else { + this.loadDiagram(pathway); + } + } + } + this.setState({hierarchy: hierarchy, selectedNode: selectedNode}); + } + }; + + return ( + + ); } else { - els =
  • Nothing yet.
  • + return
    Nothing yet.
    } - return els; + }, + + updateQuery() { + let fq = `pathways__ancestors:${this.state.selectedNode.id}`; + let filterDisplayName = `${this.state.selectedNode.label}`; + console.log("User asked to filter by "+ this.state.selectedNode.type); + + let filter = { + category: 'Plant Reactome', + id: fq, + fq: fq, + display_name: filterDisplayName + }; + + QueryActions.removeAllFilters(); + QueryActions.setFilter(filter); + if (this.props.closeModal) this.props.closeModal(); }, render: function () { + let reactomeLink,searchFilter; + if (this.state.selectedNode) { + let links = [ + {name: 'Plant Reactome', url: `${reactomeURL}/content/detail/${this.stableId(this.state.selectedNode.id)}`} + ]; + reactomeLink = ; + let filters = [ + { + name: `All genes in this ${this.state.selectedNode.type}`, + handleClick: ()=>this.updateQuery() + } + ]; + searchFilter = ; + } return ( -
    - -
      - {this.renderHierarchy()} -
    +
    {this.divWrapper = div;}}> +
    + {this.renderHierarchy()} + {searchFilter} + {reactomeLink}
    ); } diff --git a/scripts/components/result/details/xrefs/Xref.jsx b/scripts/components/result/details/xrefs/Xref.jsx index 07f7f3d..91c93d5 100644 --- a/scripts/components/result/details/xrefs/Xref.jsx +++ b/scripts/components/result/details/xrefs/Xref.jsx @@ -1,4 +1,5 @@ import React from "react"; +import ReactGA from "react-ga"; import _ from "lodash"; const HOW_MANY_TO_SHOW_BY_DEFAULT = 10; @@ -41,7 +42,8 @@ export default class Xref extends React.Component { } render() { - var members, vals; + var members, vals, db; + db = this.props.displayName; members = this.props.members; @@ -56,7 +58,13 @@ export default class Xref extends React.Component { var url = members[0].url(item), liClass = idx < HOW_MANY_TO_SHOW_BY_DEFAULT ? "default" : "extra"; return ( -
  • {item}
  • +
  • + + {item} + +
  • ) }) .value(); diff --git a/scripts/components/result/result.jsx b/scripts/components/result/result.jsx index e8a0ab9..79ce7b1 100644 --- a/scripts/components/result/result.jsx +++ b/scripts/components/result/result.jsx @@ -65,7 +65,7 @@ export default class Result extends React.Component { hoverDetailCapability={_.get(this.state.hoverDetail, 'capability')} geneDoc={this.props.geneDoc} docs={this.props.docs} - + speciesName={this.props.searchResult.species_name} onDetailSelect={this.updateVisibleDetail.bind(this)} /> diff --git a/scripts/components/results/Fireworks.jsx b/scripts/components/results/Fireworks.jsx new file mode 100644 index 0000000..7006221 --- /dev/null +++ b/scripts/components/results/Fireworks.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import {Button} from 'react-bootstrap'; +var reactomeURL = require('../../../package.json').gramene.reactomeURL; + +const TEST_TOKEN = 'MjAxNzAzMDkxNTE4MjBfMjI%3D'; + +export default class Fireworks extends React.Component { + constructor(props) { + super(props); + this.state = { + isTokenSet: false + }; + } + + initFireworks() { + this.fireworks = Reactome.Fireworks.create({ + proxyPrefix: reactomeURL, + placeHolder: 'fireworksHolder', + width: 1140, + height: 500 + }); + + this.fireworks.onNodeSelected(function (obj) { + console.log('selected node', obj); + }); + + this.fireworks.onNodeHovered(function (obj) { + if (obj) { + console.log('hovered node', obj); + } + }); + + // this.fireworks.onAnalysisReset(function () { + // console.log('onAnalysisReset callback'); + // this.setState({isTokenSet: false}); + // }.bind(this)); + + this.fireworks.onFireworksLoaded(function(id) { + console.log('FireworksLoaded',id); + // this.fireworks.setAnalysisToken(TEST_TOKEN,'TOTAL'); + this.setState({isTokenSet: true}); + }.bind(this)); + } + + componentDidMount() { + if (Reactome && Reactome.Fireworks) { + this.initFireworks(); + } + else { + window.addEventListener('launchFireworks', function (e) { + this.initFireworks(); + }, false); + } + } + + handleClick() { + if (this.state.isTokenSet) + this.fireworks.resetAnalysis(); + else + this.fireworks.setAnalysisToken(TEST_TOKEN, 'TOTAL'); + this.setState({isTokenSet: !this.state.isTokenSet}); + } + + render() { + const setTokenButton = (this.state.isTokenSet) ? null : + (); + + return ( +
    +
    + {setTokenButton} +
    + ); + } +} diff --git a/scripts/components/results/results.jsx b/scripts/components/results/results.jsx index f3329ce..db357f2 100644 --- a/scripts/components/results/results.jsx +++ b/scripts/components/results/results.jsx @@ -1,56 +1,41 @@ 'use strict'; -var React = require('react'); +import React from 'react'; +import {Tabs, Tab} from 'react-bootstrap'; import ResultsList from './resultsList.jsx'; -var ResultsVisualization = require('./resultsVisualization.jsx'); -var mq = require('../../config/mq'); +import ResultsVisualization from './resultsVisualization.jsx'; +import Fireworks from './Fireworks.jsx'; -var bs = require('react-bootstrap'); - -var Results = React.createClass({ - getInitialState: function () { - return { - viz: this.shouldShowVis(), +export default class Results extends React.Component { + constructor(props) { + super(props); + this.state = { + summary: 'taxagenomic', list: true }; - }, - shouldShowVis: function(props) { - return true; - }, - toggleViz: function() { - var newState = { - viz: !this.state.viz - }; - this.setState(newState); - }, - toggleList: function() { - var newState = { - list: !this.state.list - }; - this.setState(newState); - }, - componentWillReceiveProps: function(newProps) { - this.setState({ - viz: this.shouldShowVis(newProps) - }); - }, - render: function () { - var theViz, theList; - if(this.state.viz) { - theViz = (); - } - if(this.state.list) { - theList = (); - } + } + render() { + let viz, pathways; + if (this.state.summary === 'taxagenomic') + viz = (); + if (this.state.summary === 'pathways') + pathways = (); return (
    - {theViz} - {theList} + this.setState({summary})} + id="results-summary-tabs"> + {viz} + {/*{pathways}*/} + +
    ); } -}); -module.exports = Results; \ No newline at end of file +} diff --git a/scripts/components/results/resultsList.jsx b/scripts/components/results/resultsList.jsx index cac2565..51937a3 100644 --- a/scripts/components/results/resultsList.jsx +++ b/scripts/components/results/resultsList.jsx @@ -5,6 +5,7 @@ import _ from "lodash"; import {resultTypes} from "gramene-search-client"; import QueryActions from "../../actions/queryActions"; import docStore from "../../stores/docStore"; +import searchStore from "../../stores/searchStore"; import Result from "./../result/result.jsx"; @@ -24,6 +25,13 @@ export default class ResultsList extends React.Component { componentWillMount() { QueryActions.setResultType('list', this.getResultType()); + this.unsubscribeFromSearchStore = searchStore.listen((searchState) => + { + if (searchState.results) { + this.setState({results: searchState.results}) + } + } + ); this.unsubDocs = docStore.listen( (docs) => this.setState({docs}) ); @@ -32,6 +40,9 @@ export default class ResultsList extends React.Component { componentWillUnmount() { QueryActions.removeResultType('list'); + if (this.unsubscribeFromSearchStore) { + this.unsubscribeFromSearchStore(); + } if (this.unsubDocs) { this.unsubDocs(); } @@ -44,7 +55,7 @@ export default class ResultsList extends React.Component { render() { const docs = this.state.docs; const geneDocs = _.get(docs, 'genes') || {}; - const list = this.props.results.list; + const list = this.state.results ? this.state.results.list : undefined; if (list && list.length) { var searchResults = list.map(function (searchResult) { @@ -73,8 +84,8 @@ export default class ResultsList extends React.Component { } moreButton() { - const list = this.props.results.list; - const totalResults = this.props.results.metadata.count; + const list = this.state.results.list; + const totalResults = this.state.results.metadata.count; if (list.length < totalResults) { return (
      @@ -87,6 +98,3 @@ export default class ResultsList extends React.Component { } } -ResultsList.propTypes = { - results: React.PropTypes.object.isRequired, -}; \ No newline at end of file diff --git a/scripts/components/search/search.jsx b/scripts/components/search/search.jsx index c0820f8..d59278e 100644 --- a/scripts/components/search/search.jsx +++ b/scripts/components/search/search.jsx @@ -7,7 +7,7 @@ import SearchBox from "./searchBox.jsx"; import QueryActions from "../../actions/queryActions"; import Suggest from "../suggest/suggest.jsx"; import Filters from "./filters.jsx"; - +import searchStore from "../../stores/searchStore"; export default class Search extends React.Component { constructor(props) { @@ -27,6 +27,16 @@ export default class Search extends React.Component { ]); } + componentWillMount() { + this.unsubscribeFromSearchStore = searchStore.listen((searchState) => + this.setState({search: searchState}) + ); + } + + componentWillUnmount() { + this.unsubscribeFromSearchStore(); + } + componentDidMount() { // listen directly to an action method. @@ -39,6 +49,12 @@ export default class Search extends React.Component { // of app state and we must manually clear it here if the query string is // removed (e.g. when a suggestion is picked) QueryActions.removeQueryString.listen(this.clearInputString); + QueryActions.setFilter.listen(this.clearInputString); + QueryActions.setAllFilters.listen(this.clearInputString); + QueryActions.removeFilter.listen(this.clearInputString); + QueryActions.removeFilters.listen(this.clearInputString); + QueryActions.removeAllFilters.listen(this.clearInputString); + QueryActions.toggleFilter.listen(this.clearInputString); } handleQueryChange(queryString) { @@ -55,6 +71,8 @@ export default class Search extends React.Component { clearInputString() { this.refs.searchBox.clearSearchString(); + this.refs.searchBox.focus(); + window.scrollTo(0,0); this.setState({ suggestionsVisible: false }); @@ -75,13 +93,11 @@ export default class Search extends React.Component { } render() { - var search = this.props.search; return (
    ); } -}); - -module.exports = Summary; \ No newline at end of file +} diff --git a/scripts/components/suggest/suggest.jsx b/scripts/components/suggest/suggest.jsx index d538ef8..d43739d 100644 --- a/scripts/components/suggest/suggest.jsx +++ b/scripts/components/suggest/suggest.jsx @@ -39,7 +39,7 @@ var Suggest = React.createClass({ No suggestions found. You may still attempt a full text search, though it is unlikely to find any genes for you.
      - +
    ) diff --git a/scripts/components/welcome/GrameneTools.jsx b/scripts/components/welcome/GrameneTools.jsx index b85cb22..df85e02 100644 --- a/scripts/components/welcome/GrameneTools.jsx +++ b/scripts/components/welcome/GrameneTools.jsx @@ -1,6 +1,9 @@ import React from "react"; +import ReactGA from "react-ga"; import {ListGroup, ListGroupItem, Media, Glyphicon} from "react-bootstrap"; import WelcomeActions from "../../actions/welcomeActions"; +import { browserHistory } from 'react-router'; +var ensemblURL = require('../../../package.json').gramene.ensemblURL; const GrameneTool = ({title, description, imgSrc, link, isExternal}) => { let external; @@ -26,47 +29,76 @@ function focusSearch() { WelcomeActions.flashSearchBox(250); } +function drupalLink(path) { + return { + onClick: () => browserHistory.push(path), + href: "javascript:void(0);" + }; +} + +function externalLink(path) { + return { + onClick: () => { + ReactGA.outboundLink({ + label: path + }, function () { + window.location.href = path; + }); + }, + href: "javascript:void(0);" + } +} + const GrameneTools = () =>

    Gramene Portals

    - + {/**/} + link={externalLink(`${ensemblURL}/genome_browser/index.html`)} + imgSrc="/assets/images/welcome/ensemblgramene.png"/> + link={externalLink("http://plantreactome.gramene.org/")} + imgSrc="/assets/images/welcome/plantReactome.svg"/> + + link={externalLink(`${ensemblURL}/Tools/Blast?db=core`)} + imgSrc="/assets/images/welcome/BLAST.png"/> + link={externalLink(`${ensemblURL}/biomart/martview`)} + imgSrc="/assets/images/welcome/Biomart250.png"/> + + link={drupalLink('/outreach')} + imgSrc="/assets/images/welcome/noun_553934.png"/> + link={drupalLink('/ftp-download')} + imgSrc="/assets/images/welcome/download.png"/> + link={drupalLink('/archive')} + imgSrc="/assets/images/welcome/archive.jpg"/>
    ; export default GrameneTools; \ No newline at end of file diff --git a/scripts/components/welcome/Intro.jsx b/scripts/components/welcome/Intro.jsx index 4622ebd..b380dea 100644 --- a/scripts/components/welcome/Intro.jsx +++ b/scripts/components/welcome/Intro.jsx @@ -8,7 +8,6 @@ const Intro = ({onClose}) => integrated data resource for comparative functional genomics in crops and model plant species.

    -
    ; Intro.propTypes = { diff --git a/scripts/components/welcome/Posts.jsx b/scripts/components/welcome/Posts.jsx index ac8c928..8337f94 100644 --- a/scripts/components/welcome/Posts.jsx +++ b/scripts/components/welcome/Posts.jsx @@ -1,61 +1,47 @@ import React from 'react'; -// -// const Posts = ({posts}) =>
    -//

    Latest News

    -// -//
    ; -// -// Posts.propTypes = { -// posts: React.PropTypes.array.isRequired -// }; -// -// export default Posts; - -import BlogModal from "./BlogModal.jsx"; +import { browserHistory } from 'react-router'; +import _ from "lodash"; +import Spinner from "../Spinner.jsx"; export default class Posts extends React.Component { constructor(props) { super(props); - this.state = { - showModal: false - }; - } - - show(post) { - this.selectedPost = post; - this.setState({showModal: true}) } - closeModal() { - this.setState({showModal: false}); - } - render() { - let modal; - - if (this.state.showModal) { - modal = + let content; + if(_.isEmpty(this.props.feed)) { + content = + } + else { + content = ( + + ); } return (

    Latest News

    - - {modal} + {content}
    ); } } Posts.propTypes = { - posts: React.PropTypes.array.isRequired + feed: React.PropTypes.array.isRequired }; diff --git a/scripts/components/welcome/WelcomePage.jsx b/scripts/components/welcome/WelcomePage.jsx index c5449be..4285c06 100644 --- a/scripts/components/welcome/WelcomePage.jsx +++ b/scripts/components/welcome/WelcomePage.jsx @@ -1,38 +1,60 @@ import React from "react"; -import WelcomeStore from "../../stores/welcomeStore"; +import DrupalStore from "../../stores/drupalStore"; import Intro from "./Intro.jsx"; import Posts from "./Posts.jsx"; import GrameneTools from "./GrameneTools.jsx"; +import Spinner from "../Spinner.jsx"; import {shouldShowIntro, setIntroVisibility} from "../../welcome/intro"; - import {Grid, Row, Col} from "react-bootstrap"; +import WelcomeActions from "../../actions/welcomeActions"; export default class Welcome extends React.Component { constructor(props) { super(props); this.state = { - posts: require('../../../static/blogFeed.json'), + drupal: DrupalStore.state, showIntro: shouldShowIntro() }; } componentWillMount() { - if (this.props.context === 'client') { - this.unsubscribe = WelcomeStore.listen( - (posts) => this.setState({posts: posts}) + console.log("WelcomePAge componentWillMount", this); + if (!this.props.serverRenderMode) { + this.unsubscribe = DrupalStore.listen( + (state) => this.setState({drupal: state}) ); } } + componentDidUpdate(prevProps, prevState) { + window.scrollTo(0, 0); + if (!this.props.children) { + let ms = this.state.showIntro ? 600 : 300; + WelcomeActions.flashSearchBox(ms); + } + } + componentWillUnmount() { if (this.unsubscribe) this.unsubscribe(); } - + hideIntro() { this.setState({showIntro: false}); setIntroVisibility(false); } + bodyContent() { + if (this.props.children) { + return this.props.children; + } + else if (this.props.serverRenderMode === 'static') { + return ; + } + else { + return ; + } + } + render() { return (
    @@ -44,10 +66,10 @@ export default class Welcome extends React.Component { - + {this.bodyContent()} - + @@ -63,5 +85,5 @@ export default class Welcome extends React.Component { }; Welcome.propTypes = { - context: React.PropTypes.string + serverRenderMode: React.PropTypes.string }; \ No newline at end of file diff --git a/scripts/getBlogFeed.js b/scripts/getBlogFeed.js deleted file mode 100644 index 16d1718..0000000 --- a/scripts/getBlogFeed.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -require('babel-register')({ - presets: ['es2015'] -}); - -var fs = require('q-io/fs'); -var getBlogFeed = require('./welcome/getBlogFeed').default; -var done; - -getBlogFeed() - .then((feed) => {console.log(feed); return feed}) - .then((feed) => - fs.write('static/blogFeed.json', JSON.stringify(feed)) - ) - .then(() => {console.log("we are done")}) - .then(()=>done = true) - .catch((e)=>console.log("ERROR", e.message, e.stack)); - -(function wait () { - if (!done) { - console.log('waiting'); - setTimeout(wait, 1000); - } - else { - console.log('done'); - } -})(); \ No newline at end of file diff --git a/scripts/gramoogle.js b/scripts/gramoogle.js index 1c0c6b8..3d220d8 100644 --- a/scripts/gramoogle.js +++ b/scripts/gramoogle.js @@ -3,19 +3,34 @@ import React from 'react'; import ReactDOM from 'react-dom'; - -// Instantiate searchStore now so it's ready -// to listen to taxonomyActions.getTaxonomy -import './stores/searchStore'; -import TaxonomyActions from './actions/taxonomyActions'; -import WelcomeActions from './actions/welcomeActions'; - +import { Router, Route, browserHistory, IndexRoute } from 'react-router'; +import ReactGA from 'react-ga'; import App from './components/app.jsx'; +import Welcome from './components/welcome/WelcomePage.jsx'; +import DrupalPage from './components/DrupalPage.jsx'; +import Feedback from './components/Feedback.jsx'; +import './stores/searchStore'; // Instantiate searchStore now so it's ready +import TaxonomyActions from './actions/taxonomyActions'; // to listen to taxonomyActions.getTaxonomy +import DrupalActions from './actions/drupalActions'; // and drupalActions.refreshBlogFeed +ReactGA.initialize('UA-1624628-5'); TaxonomyActions.getTaxonomy(); -WelcomeActions.refreshBlogFeed(); +DrupalActions.refreshBlogFeed(); -const AppFactory = React.createFactory(App); -const app = new AppFactory({context: 'client'}); +function logPageView() { + ReactGA.set({ page: window.location.pathname }); + ReactGA.pageview(window.location.pathname); +} -ReactDOM.render(app, document.getElementById('content')); \ No newline at end of file +ReactDOM.render(( + + + + + + + + + + +), document.getElementById('content')); \ No newline at end of file diff --git a/scripts/search/getidListFromURLParams.js b/scripts/search/getidListFromURLParams.js new file mode 100644 index 0000000..bb8329c --- /dev/null +++ b/scripts/search/getidListFromURLParams.js @@ -0,0 +1,13 @@ +import _ from "lodash"; + +function trimVersion(id) { + return id.replace(/\.\d+$/,''); +} + +export default function getidListFromURLParams() { + const params = _.get(global.gramene, 'searchParams'); + if (params && params.idList) { + return _.uniq(_.map(params.idList.split(','), trimVersion)); + } + return []; +} \ No newline at end of file diff --git a/scripts/search/persist.js b/scripts/search/persist.js index 507a6de..1c9cca8 100644 --- a/scripts/search/persist.js +++ b/scripts/search/persist.js @@ -12,15 +12,19 @@ This module is likely to be replaced if/when we refactor the codebase to use Rea */ import _ from 'lodash'; +import getidListFromURLParams from './getidListFromURLParams'; let expectedSerializedHashState = ''; let expectedSerializedLocalStorageState = ''; const loc = global.location || {hash: expectedSerializedHashState}; const localStore = global.localStorage || {}; +const maxLengthToShow = 3; + export function initUrlHashPersistence(callback) { var hashChangeHandler = handleHashChangeFactory(callback); possiblyCopyStateFromLocalStorage(); + possiblyHandleIdList(); global.onhashchange = hashChangeHandler; hashChangeHandler(); } @@ -32,6 +36,22 @@ function possiblyCopyStateFromLocalStorage() { } } +// if there is an idList query parameter, clear everything and create a filter +function possiblyHandleIdList() { + var idList = getidListFromURLParams(); + if (idList.length > 0) { + var state = {filters: {}, taxa: {}}; + var fqString = 'id:(' + idList.join(' ') + ')'; + state.filters[fqString] = { + category: "Gene", + display_name: idList.length <= maxLengthToShow ? idList.join(', ') + : idList.slice(0,maxLengthToShow).join(', ') + ' and ' + (idList.length - maxLengthToShow) + ' more', + exclude: false, + fq: fqString + }; + loc.hash = '#' + encodeURI(JSON.stringify(state)); + } +} function handleHashChangeFactory(callback) { return function handleHashChange() { if (hashDidChange()) { diff --git a/scripts/search/search.js b/scripts/search/search.js index 34085af..f6f23fd 100644 --- a/scripts/search/search.js +++ b/scripts/search/search.js @@ -31,7 +31,7 @@ function prepFilters(filters: map): map { if (fqMetadata.exclude || // we always AND queries where we exclude things newFq.indexOf('{!surround}') === 0) // surround queries cannot be wrapped in parens { - newFilters[newFq] = {}; + newFilters[fq] = {}; } else { if (!filterCategories.hasOwnProperty(category)) { diff --git a/scripts/stores/drupalStore.js b/scripts/stores/drupalStore.js new file mode 100644 index 0000000..e2a8ca2 --- /dev/null +++ b/scripts/stores/drupalStore.js @@ -0,0 +1,31 @@ +import _ from 'lodash'; +import Reflux from "reflux"; +import DrupalActions from "../actions/drupalActions"; + +const DrupalStore = Reflux.createStore( + { + listenables: DrupalActions, + init: function () { + this.state = { + feed: [] + } + }, + refreshBlogFeedCompleted: function (results) { + // console.log('DrupalActions.refreshBlogFeedCompleted', results); + this.state = _.assign({}, this.state, {feed: results}); + this.trigger(this.state); + }, + refreshBlogFeedFailed: function (error) { + console.log('DrupalActions.refreshBlogFeedFailed', error); + // }, + // fetchDrupalPageCompleted: function (results) { + // console.log('DrupalActions.fetchDrupalPageCompleted', results); + // this.state = _.assign({}, this.state, results); + // this.trigger(this.state); + // }, + // fetchDrupalPageFailed: function (error) { + // console.log('DrupalActions.fetchDrupalPageFailed', error); + } + }); + +export default DrupalStore; \ No newline at end of file diff --git a/scripts/stores/searchStore.js b/scripts/stores/searchStore.js index c5b7ee7..18f3bae 100644 --- a/scripts/stores/searchStore.js +++ b/scripts/stores/searchStore.js @@ -3,6 +3,7 @@ /* @flow */ var Reflux = require('reflux'); +import ReactGA from "react-ga"; var _ = require('lodash'); var QueryActions = require('../actions/queryActions'); import TaxonomyActions from '../actions/taxonomyActions'; @@ -83,12 +84,22 @@ module.exports = Reflux.createStore({ console.log('setFilter', arguments); if (!this.state.query.filters.hasOwnProperty(filter.fq)) { this.state.query.filters[filter.fq] = filter; + ReactGA.event({ + category: 'Search', + action: 'setFilter', + label: JSON.stringify(arguments) + }); this.search(); } }, toggleFilter: function (filter) { - console.log('toggleFilter', arguments); + console.log('toggleFilter', filter); + ReactGA.event({ + category: 'Search', + action: 'toggleFilter', + label: JSON.stringify(filter) + }); filter = _.clone(filter); delete this.state.query.filters[filter.fq]; if (filter.exclude) { @@ -104,13 +115,23 @@ module.exports = Reflux.createStore({ }, setAllFilters: function (filters) { - console.log('setAllFilters', arguments); + console.log('setAllFilters', filters); + ReactGA.event({ + category: 'Search', + action: 'setAllFilters', + label: JSON.stringify(filters) + }); this.state.query.filters = filters; this.search(); }, removeFilter: function (filter) { - console.log('removeFilter', arguments); + console.log('removeFilter', filter); + ReactGA.event({ + category: 'Search', + action: 'removeFilter', + label: JSON.stringify(filter) + }); if (this.state.query.filters.hasOwnProperty(filter.fq)) { delete this.state.query.filters[filter.fq]; this.search(); @@ -118,13 +139,24 @@ module.exports = Reflux.createStore({ }, removeFilters: function (predicate) { + ReactGA.event({ + category: 'Search', + action: 'removeFilters', + label: JSON.stringify(predicate) + }); this.state.query.filters = _.omitBy(this.state.query.filters, predicate); this.search(); }, removeAllFilters: function () { console.log('removeAllFilters'); - this.setAllFilters({}); + ReactGA.event({ + category: 'Search', + action: 'removeAllFilters', + label: 'remove all filters' + }); + if(!_.isEmpty(this.state.query.filters)) + this.setAllFilters({}); }, moreResults: function (howManyMore) { diff --git a/scripts/stores/suggestStore.js b/scripts/stores/suggestStore.js index e5849b5..96cdcfe 100644 --- a/scripts/stores/suggestStore.js +++ b/scripts/stores/suggestStore.js @@ -159,8 +159,8 @@ module.exports = Reflux.createStore({ } result = []; - arrA = top.slice(); - arrB = category.suggestions.slice(); + arrA = _.filter(top.slice(),'num_genes'); + arrB = _.filter(category.suggestions.slice(),'num_genes'); // while we don't have NUM_TOP and there are still suggestions available while (result.length < NUM_TOP && (arrA.length || arrB.length)) { diff --git a/scripts/stores/welcomeStore.js b/scripts/stores/welcomeStore.js deleted file mode 100644 index 19f1572..0000000 --- a/scripts/stores/welcomeStore.js +++ /dev/null @@ -1,20 +0,0 @@ -import Reflux from "reflux"; -import WelcomeActions from "../actions/welcomeActions"; - -const WelcomeStore = Reflux.createStore( - { - listenables: WelcomeActions, - refreshBlogFeed: function () { - console.log('refresh blog feed in the store'); - }, - refreshBlogFeedCompleted: function (results) { - console.log('WelcomeActions.refreshBlogFeedCompleted', results); - this.posts = results; - this.trigger(this.posts); - }, - refreshBlogFeedFailed: function (error) { - console.log('WelcomeActions.refreshBlogFeedFailed', error); - } - }); - -export default WelcomeStore; \ No newline at end of file diff --git a/scripts/welcome/getBlogFeed.js b/scripts/welcome/getDrupalContent.js similarity index 64% rename from scripts/welcome/getBlogFeed.js rename to scripts/welcome/getDrupalContent.js index f684b4b..a814d6f 100644 --- a/scripts/welcome/getBlogFeed.js +++ b/scripts/welcome/getDrupalContent.js @@ -8,11 +8,12 @@ export const parseFeed = (response) => { return Q.nfcall(RSSParser.parseString, response.data); }; -const getBlogFeed = () => axios.get("http://gramene.org/blog/feed") +export const getBlogFeed = () => axios.get("http://news.gramene.org/blog/feed") .then(parseFeed) .then((rss)=> { console.log(rss); return _.get(rss, 'feed.entries'); }); -export default getBlogFeed; \ No newline at end of file +// export const getDrupalPage = (path) => axios.get('http://data.gramene.org/drupal/'+path) +// .then(response => {return {path:path,page:response.data}}); diff --git a/server.js b/server.js new file mode 100644 index 0000000..fe8614d --- /dev/null +++ b/server.js @@ -0,0 +1,141 @@ +var express = require('express'); +var cors = require('cors'); +var bodyParser = require('body-parser'); +var request = require('request'); +var path = require('path'); +var compression = require('compression'); +var schedule = require('node-schedule'); +var mysql = require('mysql'); +var soap = require('soap'); +var Email = require('email').Email; +var argv = require('minimist')(process.argv.slice(2)); + +const drupalArgs = { + host: argv.h, + user: argv.u, + password: argv.p, + database: argv.d +}; +const recaptchaSecret = argv.s; +const mantisUser = argv.m; +const mantisPass = argv.n; + +var app = express(); +app.use(cors()); +app.use(compression()); +app.use(bodyParser.urlencoded({ extended: false})); +app.use(bodyParser.json()); + +var aliasLUT = {}; + +function updateLUT() { + var drupalDb = mysql.createConnection(drupalArgs); + drupalDb.query("select source, alias from url_alias", function(err, rows, fields) { + if (err) { + console.log('error connecting to drupal db',err); + } + else { + rows.forEach(function(row) { + aliasLUT[row.alias] = row.source.replace(/node\//,''); + }) + } + }); + drupalDb.end(); +} + +updateLUT(); +schedule.scheduleJob({minute:[0,5,10,15,20,25,30,35,40,45,50,55]}, updateLUT); + + +// serve our static stuff like index.css +app.use(express.static(path.join(__dirname, 'build'))) + +app.get('/aliases', function (req, res) { + res.json(aliasLUT) +}); + +app.get('/ww/:nid', function (req, res) { + let url = `http://news.gramene.org/ww?nid=${req.params.nid}`; + request.get(url).pipe(res); // just proxying +}); + +app.post('/feedback', function (req, res) { + let comments = req.body.content; + if (comments.length > 10000) { + comments = comments.substr(0,10000); + comments += "\n[MESSAGE TRUNCATED]"; + } + let message = [ + `URL : ${req.body.referrer}`, + `Subject : ${req.body.subject}`, + `Name : ${req.body.name}`, + `Email : ${req.body.email}`, + `Organization: ${req.body.org}`, + `Comments : ${comments}` + ].join("\n\n"); + let subject = `Site Feedback: ${req.body.subject}`; + request.post({ + url: 'https://www.google.com/recaptcha/api/siteverify', + formData: {secret: recaptchaSecret, response: req.body.recaptcha} + },function(err, response, body) { + let check = JSON.parse(body); + if (err) { + res.json({error: err}); + } + if (check.success) { + // submit the ticket + const url = 'http://warelab.org/bugs/api/soap/mantisconnect.php?wsdl'; + soap.createClient(url, function(err, client) { + if (err) { + res.json({error: 'soap error'}); + } + client.mc_issue_add({ + username: mantisUser, + password: mantisPass, + issue: { + project: { + id: 2 + }, + category: 'Uncategorized', + summary: subject, + description: message + } + }, function(err, result) { + if (err) { + res.json({error: 'error adding issue: ' + err}); + } + else { + const ticket = result.return.$value; + var myMsg = new Email( + { from: "feedback@gramene.org" + , to: "feedback@gramene.org" + , cc: req.body.email + , 'reply-to' : `${req.body.email}, feedback@gramene.org` + , subject: subject + , body: `${message}\n\nhttp://www.warelab.org/bugs/view.php?id=${ticket}\n` + }); + + myMsg.send(); + + res.json({ticket:ticket}); + } + }); + }); + } + else { + res.json({error: check}); + } + }); +}); + +// send all requests to static.html so browserHistory works +app.get('*', function (req, res) { + res.sendFile(path.join(__dirname, 'build', 'static.html')) +}); + +var PORT = process.env.PORT || 8080; + +app.listen(PORT, function() { + console.log('Production Express server running at localhost:' + PORT) +}); + diff --git a/static/atlasWidget.template.html b/static/atlasWidget.template.html index 72f4172..1cd5c1d 100644 --- a/static/atlasWidget.template.html +++ b/static/atlasWidget.template.html @@ -4,7 +4,7 @@ + href="/style.css"/> diff --git a/static/footer.template.html b/static/footer.template.html index 79358b4..e540d53 100644 --- a/static/footer.template.html +++ b/static/footer.template.html @@ -1,33 +1,33 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +