├── search ├── client │ ├── static │ │ ├── CNAME │ │ ├── tile.png │ │ ├── favicon.ico │ │ ├── robots.txt │ │ ├── tile-wide.png │ │ ├── apple-touch-icon.png │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ ├── humans.txt │ │ ├── browserconfig.xml │ │ ├── crossdomain.xml │ │ └── images │ │ │ ├── search-icon.svg │ │ │ └── nci-logo-full.svg │ ├── .babelrc │ ├── .gitignore │ ├── styles │ │ ├── components │ │ │ ├── _all.scss │ │ │ ├── _card.scss │ │ │ └── _button.scss │ │ ├── pages │ │ │ ├── _all.scss │ │ │ ├── _clinical-trial.scss │ │ │ ├── _clinical-trials.scss │ │ │ └── _index.scss │ │ ├── modules │ │ │ ├── _toggle-more.scss │ │ │ ├── _filter-gender.scss │ │ │ ├── _all.scss │ │ │ ├── _filter-suggest.scss │ │ │ ├── _filter-age.scss │ │ │ ├── _navigation.scss │ │ │ ├── _filter-text.scss │ │ │ └── _customer-service-widget.scss │ │ ├── base │ │ │ ├── _container.scss │ │ │ ├── _all.scss │ │ │ ├── _forms.scss │ │ │ ├── _typography.scss │ │ │ └── _variables.scss │ │ ├── vendor │ │ │ └── font-awesome │ │ │ │ ├── _fixed-width.scss │ │ │ │ ├── _screen-reader.scss │ │ │ │ ├── _larger.scss │ │ │ │ ├── _list.scss │ │ │ │ ├── _core.scss │ │ │ │ ├── font-awesome.scss │ │ │ │ ├── _stacked.scss │ │ │ │ ├── _bordered-pulled.scss │ │ │ │ ├── _rotated-flipped.scss │ │ │ │ ├── _path.scss │ │ │ │ ├── _animated.scss │ │ │ │ └── _mixins.scss │ │ └── main.scss │ ├── tools │ │ ├── .eslintrc │ │ ├── lib │ │ │ ├── copy.js │ │ │ ├── fs.js │ │ │ ├── task.js │ │ │ └── routes-loader.js │ │ ├── build.js │ │ ├── clean.js │ │ ├── copy.js │ │ ├── bundle.js │ │ ├── deploy.js │ │ ├── start.js │ │ └── render.js │ ├── components │ │ ├── Html │ │ │ ├── package.json │ │ │ └── Html.js │ │ ├── Link │ │ │ ├── package.json │ │ │ └── Link.js │ │ ├── Layout │ │ │ ├── package.json │ │ │ └── Layout.js │ │ ├── Search │ │ │ ├── Filter │ │ │ │ ├── Age │ │ │ │ │ ├── package.json │ │ │ │ │ └── Age.js │ │ │ │ ├── Date │ │ │ │ │ ├── package.json │ │ │ │ │ └── Date.js │ │ │ │ ├── Text │ │ │ │ │ ├── package.json │ │ │ │ │ └── Text.js │ │ │ │ ├── package.json │ │ │ │ ├── Gender │ │ │ │ │ ├── package.json │ │ │ │ │ └── Gender.js │ │ │ │ ├── Select │ │ │ │ │ ├── package.json │ │ │ │ │ └── Select.js │ │ │ │ └── Suggest │ │ │ │ │ ├── package.json │ │ │ │ │ └── Suggest.js │ │ │ ├── Status │ │ │ │ ├── package.json │ │ │ │ ├── StatusItem │ │ │ │ │ ├── package.json │ │ │ │ │ └── StatusItem.js │ │ │ │ └── Status.js │ │ │ └── PromptFiltering │ │ │ │ ├── package.json │ │ │ │ └── PromptFiltering.js │ │ ├── ClinicalTrial │ │ │ ├── Detail │ │ │ │ ├── package.json │ │ │ │ └── Detail.js │ │ │ ├── Result │ │ │ │ ├── package.json │ │ │ │ └── Result.js │ │ │ └── Results │ │ │ │ ├── package.json │ │ │ │ └── Results.js │ │ ├── Navigation │ │ │ ├── package.json │ │ │ └── Navigation.js │ │ ├── OmniSuggest │ │ │ └── package.json │ │ ├── ToggleMore │ │ │ ├── package.json │ │ │ └── ToggleMore.js │ │ ├── GoogleAnalytics │ │ │ ├── package.json │ │ │ └── GoogleAnalytics.js │ │ ├── CustomerServiceWidget │ │ │ ├── package.json │ │ │ └── CustomerServiceWidget.js │ │ └── variables.scss │ ├── config.js │ ├── .travis.yml │ ├── .eslintrc │ ├── lib │ │ ├── Location.js │ │ ├── ApiFetch.js │ │ ├── Url.js │ │ └── ValidParams.js │ ├── pages │ │ ├── 404.js │ │ ├── clinical-trial.js │ │ ├── 500.js │ │ ├── clinical-trials.js │ │ └── index.js │ ├── test │ │ └── routes-loader-spec.js │ ├── .editorconfig │ ├── .gitattributes │ ├── app.js │ └── package.json ├── api │ ├── .babelrc │ ├── views │ │ ├── index.jade │ │ ├── error.jade │ │ └── layout.jade │ ├── public │ │ └── stylesheets │ │ │ └── style.css │ ├── package.json │ ├── bin │ │ └── www │ ├── app.js │ └── test │ │ └── search │ │ └── searcher.spec.js ├── config.json ├── common │ ├── .babelrc │ ├── search_adapters │ │ ├── abstract_search_adapter.js │ │ ├── testable_elasticsearch_adapter.js │ │ ├── base_elasticsearch_adapter.js │ │ └── elasticsearch_adapter.js │ └── package.json ├── index │ ├── .babelrc │ ├── indexer │ │ ├── term │ │ │ ├── mapping.json │ │ │ ├── settings.json │ │ │ └── indexer.js │ │ ├── trial │ │ │ ├── settings.json │ │ │ └── indexer.js │ │ ├── index_optimizer.js │ │ ├── abstract_index_tool.js │ │ ├── abstract_indexer.js │ │ └── alias_swapper.js │ ├── package.json │ ├── test │ │ └── integration │ │ │ └── indexer │ │ │ ├── alias_swapper.spec.js │ │ │ └── index_cleaner.spec.js │ ├── index.js │ └── sample_queries └── INSTALL.md ├── devops ├── scripts │ ├── es │ │ ├── stop_elasticsearch.sh │ │ └── start_elasticsearch.sh │ ├── update.sh │ └── install.sh ├── datawarehouse-cron │ ├── psql.sh │ ├── dw_to_s3.sh │ └── copy_preamend.sql ├── api.monit.config ├── clinicaltrialsapi-prod │ ├── ctapi-build │ ├── ctapi.monit.centos.conf │ ├── apache.ctsapi.conf │ ├── cron-index │ ├── apache.ctsapi-ssl.conf │ ├── ctapi.initd │ ├── ctapi-deploy-data │ ├── ctapi-config-web │ ├── ctapi-deploy-code │ ├── ctapi-install-backend │ └── ctapi-install-frontend ├── elasticsearch.monit.config ├── api.upstart.config └── nginx_site.config ├── data └── .gitignore ├── common ├── package.json ├── logger.js └── utils.js ├── todos.txt ├── import └── transform │ ├── package.json │ └── stream │ ├── special_chars.js │ ├── geo_coding.js │ ├── csv.js │ └── cleanse.js ├── .gitignore ├── LICENSE └── examples └── full-text-search.htm /search/client/static/CNAME: -------------------------------------------------------------------------------- 1 | www.example.com 2 | -------------------------------------------------------------------------------- /search/client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /devops/scripts/es/stop_elasticsearch.sh: -------------------------------------------------------------------------------- 1 | kill `cat es_pid` 2 | -------------------------------------------------------------------------------- /devops/scripts/es/start_elasticsearch.sh: -------------------------------------------------------------------------------- 1 | elasticsearch -d -p es_pid 2 | -------------------------------------------------------------------------------- /search/client/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /search/client/styles/components/_all.scss: -------------------------------------------------------------------------------- 1 | @import "button"; 2 | @import "card"; 3 | -------------------------------------------------------------------------------- /search/api/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["lodash"], 3 | "presets": ["es2015"] 4 | } -------------------------------------------------------------------------------- /search/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ES_HOST": "localhost", 3 | "ES_PORT": "9200" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/tools/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": 0 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /search/common/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["lodash"], 3 | "presets": ["es2015"] 4 | } -------------------------------------------------------------------------------- /search/index/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["lodash"], 3 | "presets": ["es2015"] 4 | } -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /search/api/views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | include:md ../README.md 5 | -------------------------------------------------------------------------------- /search/client/components/Html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Html", 3 | "main": "./Html.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/Link/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Link", 3 | "main": "./Link.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/Layout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Layout", 3 | "main": "./Layout.js" 4 | } 5 | -------------------------------------------------------------------------------- /devops/datawarehouse-cron/psql.sh: -------------------------------------------------------------------------------- 1 | psql -d ctrp-data-warehouse -f trial_query.sql -o ../../data/trials.out 2 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/Age/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Age", 3 | "main": "./Age.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/Date/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Date", 3 | "main": "./Date.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/Text/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Text", 3 | "main": "./Text.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Filter", 3 | "main": "./Filter.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/Search/Status/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Status", 3 | "main": "./Status.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/styles/pages/_all.scss: -------------------------------------------------------------------------------- 1 | @import "clinical-trial"; 2 | @import "clinical-trials"; 3 | @import "index"; 4 | -------------------------------------------------------------------------------- /search/client/components/ClinicalTrial/Detail/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Detail", 3 | "main": "./Detail.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/ClinicalTrial/Result/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Result", 3 | "main": "./Result.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/Navigation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Navigation", 3 | "main": "./Navigation.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/OmniSuggest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OmniSuggest", 3 | "main": "./OmniSuggest.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/Gender/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gender", 3 | "main": "./Gender.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/Select/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Select", 3 | "main": "./Select.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/ToggleMore/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ToggleMore", 3 | "main": "./ToggleMore.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/static/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCIP/clinical-trials-search/HEAD/search/client/static/tile.png -------------------------------------------------------------------------------- /search/client/components/ClinicalTrial/Results/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Results", 3 | "main": "./Results.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/Suggest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Suggest", 3 | "main": "./Suggest.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCIP/clinical-trials-search/HEAD/search/client/static/favicon.ico -------------------------------------------------------------------------------- /search/client/static/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /search/client/static/tile-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCIP/clinical-trials-search/HEAD/search/client/static/tile-wide.png -------------------------------------------------------------------------------- /search/api/views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /search/client/components/GoogleAnalytics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GoogleAnalytics", 3 | "main": "./GoogleAnalytics.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/Search/Status/StatusItem/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "StatusItem", 3 | "main": "./StatusItem.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/components/Search/PromptFiltering/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PromptFiltering", 3 | "main": "./PromptFiltering.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCIP/clinical-trials-search/HEAD/search/client/static/apple-touch-icon.png -------------------------------------------------------------------------------- /search/client/static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCIP/clinical-trials-search/HEAD/search/client/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /search/client/styles/modules/_toggle-more.scss: -------------------------------------------------------------------------------- 1 | .toggle-more { 2 | cursor: pointer; 3 | color: $primary-color; 4 | font-size: 14px; 5 | } 6 | -------------------------------------------------------------------------------- /search/client/components/CustomerServiceWidget/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CustomerServiceWidget", 3 | "main": "./CustomerServiceWidget.js" 4 | } 5 | -------------------------------------------------------------------------------- /search/client/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'NCI - Clinical Trials Search', 3 | description: '', 4 | googleAnalyticsId: 'UA-XXXXX-X', 5 | }; 6 | -------------------------------------------------------------------------------- /search/client/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCIP/clinical-trials-search/HEAD/search/client/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /search/client/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCIP/clinical-trials-search/HEAD/search/client/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /search/client/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCIP/clinical-trials-search/HEAD/search/client/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /search/client/static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCIP/clinical-trials-search/HEAD/search/client/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /search/client/styles/components/_card.scss: -------------------------------------------------------------------------------- 1 | %card { 2 | background-color: $white; 3 | box-shadow: 4px 4px 4px rgba($black, 0.1); 4 | padding: 0.9em 1em; 5 | } 6 | -------------------------------------------------------------------------------- /search/api/views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /search/client/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - iojs 5 | - '0.12' 6 | - '0.10' 7 | matrix: 8 | allow_failures: 9 | - node_js: iojs 10 | -------------------------------------------------------------------------------- /search/client/styles/modules/_filter-gender.scss: -------------------------------------------------------------------------------- 1 | .filter-gender { 2 | margin-bottom: 23px; 3 | 4 | .checkbox-label { 5 | display: block; 6 | margin-bottom: 4px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /search/client/styles/base/_container.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | @include outer-container; 3 | padding: 0 1em; 4 | 5 | @include media($medium-screen) { 6 | padding: 0 2.5em; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /search/client/styles/vendor/font-awesome/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /search/client/tools/lib/copy.js: -------------------------------------------------------------------------------- 1 | import ncp from 'ncp'; 2 | 3 | export default (source, dest) => new Promise((resolve, reject) => { 4 | ncp(source, dest, err => err ? reject(err) : resolve()); 5 | }); 6 | -------------------------------------------------------------------------------- /search/client/styles/vendor/font-awesome/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /search/client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "globals": { 4 | "__DEV__": true 5 | }, 6 | "rules": { 7 | "react/jsx-quotes": 0, 8 | "jsx-quotes": [2, "prefer-double"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /devops/scripts/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # transform trials 5 | cd import/transform 6 | npm run transform-trials 7 | cd ../.. 8 | 9 | # index trials and terms in es 10 | cd search/index 11 | npm run index 12 | cd ../../ 13 | -------------------------------------------------------------------------------- /common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utils", 3 | "version": "0.0.1", 4 | "private": true, 5 | "dependencies": { 6 | "JSONStream": "1.1.1", 7 | "bunyan": "1.8.1", 8 | "latinize": "^0.2.0", 9 | "lodash": "4.13.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /search/client/tools/build.js: -------------------------------------------------------------------------------- 1 | import task from './lib/task'; 2 | 3 | export default task(async function build() { 4 | await require('./clean')(); 5 | await require('./copy')(); 6 | await require('./bundle')(); 7 | await require('./render')(); 8 | }); 9 | -------------------------------------------------------------------------------- /search/client/styles/modules/_all.scss: -------------------------------------------------------------------------------- 1 | @import "customer-service-widget"; 2 | @import "filter-search"; 3 | @import "filter-suggest"; 4 | @import "filter-text"; 5 | @import "filter-gender"; 6 | @import "filter-age"; 7 | @import "toggle-more"; 8 | @import "navigation"; 9 | -------------------------------------------------------------------------------- /search/client/styles/base/_all.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | @import "container"; 4 | @import "forms"; 5 | @import "typography"; 6 | 7 | 8 | body { 9 | background-color: $base-background-color 10 | } 11 | 12 | html, 13 | body { 14 | margin: 0; 15 | } 16 | -------------------------------------------------------------------------------- /search/client/tools/clean.js: -------------------------------------------------------------------------------- 1 | import del from 'del'; 2 | import task from './lib/task'; 3 | import fs from './lib/fs'; 4 | 5 | export default task(async function clean() { 6 | await del(['build/*', '!build/.git'], { dot: true }); 7 | await fs.mkdir('build'); 8 | }); 9 | -------------------------------------------------------------------------------- /search/client/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "bourbon"; 2 | @import "vendor/font-awesome/font-awesome"; 3 | @import "neat"; 4 | 5 | @import "base/all"; 6 | @import "components/all"; 7 | @import "modules/all"; 8 | @import "pages/all"; 9 | 10 | .layout { 11 | margin: 0 auto; 12 | } 13 | -------------------------------------------------------------------------------- /search/client/static/humans.txt: -------------------------------------------------------------------------------- 1 | # humanstxt.org/ 2 | # The humans responsible & technology colophon 3 | 4 | # TEAM 5 | 6 | -- -- 7 | 8 | # THANKS 9 | 10 | 11 | 12 | # TECHNOLOGY COLOPHON 13 | 14 | ES2015, HTML5, CSS3 15 | React, Normalize.css 16 | -------------------------------------------------------------------------------- /devops/api.monit.config: -------------------------------------------------------------------------------- 1 | #!monit 2 | 3 | check process api with pidfile "/var/run/api.pid" 4 | start program = "/sbin/start api" 5 | stop program = "/sbin/stop api" 6 | if failed port 3000 protocol HTTP 7 | request /terms 8 | with timeout 10 seconds 9 | then restart 10 | -------------------------------------------------------------------------------- /search/client/lib/Location.js: -------------------------------------------------------------------------------- 1 | import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; 2 | import createHistory from 'history/lib/createBrowserHistory'; 3 | import useQueries from 'history/lib/useQueries'; 4 | 5 | const Location = canUseDOM ? useQueries(createHistory)() : {}; 6 | 7 | export default Location; 8 | -------------------------------------------------------------------------------- /search/client/tools/copy.js: -------------------------------------------------------------------------------- 1 | import task from './lib/task'; 2 | import cp from './lib/copy'; 3 | 4 | /** 5 | * Copies static files such as robots.txt, favicon.ico to the 6 | * output (build) folder. 7 | */ 8 | export default task(async function copy() { 9 | await cp('static', 'build'); 10 | }); 11 | -------------------------------------------------------------------------------- /search/client/styles/modules/_filter-suggest.scss: -------------------------------------------------------------------------------- 1 | .filter-suggest { 2 | margin-bottom: 10px; 3 | max-width: 340px; 4 | 5 | label { 6 | display: block; 7 | font-weight: bold; 8 | margin-bottom: 4px; 9 | } 10 | 11 | .filter-select-highlight { 12 | font-weight: bold; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /devops/clinicaltrialsapi-prod/ctapi-build: -------------------------------------------------------------------------------- 1 | #Add Version Info 2 | GHASH=$(tail -1 .git/logs/HEAD | cut -f 2 -d " ") 3 | echo "{" > search/api/git_hash.json 4 | echo " \"git_hash\": \"$GHASH\" " >> search/api/git_hash.json 5 | echo "}" >> search/api/git_hash.json 6 | 7 | #Packaging 8 | tar cf cts.tar search common import 9 | -------------------------------------------------------------------------------- /devops/datawarehouse-cron/dw_to_s3.sh: -------------------------------------------------------------------------------- 1 | psql -d ctrp-data-warehouse -f trial_query.sql -o /opt/api/data/trials.out 2 | aws s3 cp /opt/api/data/trials.out s3://datawarehouse-development/ 3 | aws s3 cp /opt/api/data/trials.out s3://datawarehouse-staging/ 4 | aws s3 cp /opt/api/data/trials.out s3://datawarehouse-production/ 5 | -------------------------------------------------------------------------------- /search/client/pages/404.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class extends Component { 4 | 5 | render() { 6 | return ( 7 |
8 |

Not Found

9 |

The page you're looking for was not found.

10 |
11 | ); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /search/client/lib/ApiFetch.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | function ApiFetch(endpointQuery) { 4 | 5 | // const host = "https://clinicaltrialsapi.cancer.gov"; 6 | const host = "http://localhost:3000"; 7 | 8 | return fetch(`${host}/${endpointQuery}`); 9 | 10 | }; 11 | 12 | export default ApiFetch 13 | -------------------------------------------------------------------------------- /todos.txt: -------------------------------------------------------------------------------- 1 | =========================================================================== 2 | TODOs 3 | =========================================================================== 4 | 5 | + Add alerting that checks transform and index process (check counts in 6 | each part of the pipeline) 7 | + Create transformer tests 8 | -------------------------------------------------------------------------------- /search/client/styles/modules/_filter-age.scss: -------------------------------------------------------------------------------- 1 | .filter-age { 2 | margin-bottom: 10px; 3 | 4 | label { 5 | display: block; 6 | font-weight: bold; 7 | margin-bottom: 4px; 8 | } 9 | 10 | input { 11 | border: none; 12 | height: 36px; 13 | width: 70px; 14 | padding: 0 15px; 15 | font-size: 0.9em; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /search/api/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | max-width: 600px; 5 | } 6 | 7 | a { 8 | color: #2b7bba; 9 | } 10 | 11 | h1 { 12 | margin-top: 0; 13 | } 14 | 15 | hr { 16 | border: 0; 17 | border-bottom: 1px dashed #ccc; 18 | background: #999; 19 | } 20 | -------------------------------------------------------------------------------- /devops/clinicaltrialsapi-prod/ctapi.monit.centos.conf: -------------------------------------------------------------------------------- 1 | #!monit 2 | 3 | check process api with pidfile "/var/run/ctapi.pid" 4 | start program = "/sbin/service ctapi start" 5 | stop program = "/sbin/service ctapi stop" 6 | if failed port 3000 protocol HTTP 7 | request /terms 8 | with timeout 10 seconds 9 | then restart 10 | -------------------------------------------------------------------------------- /devops/elasticsearch.monit.config: -------------------------------------------------------------------------------- 1 | #!monit 2 | 3 | check process elasticsearch with pidfile "/var/run/elasticsearch.pid" 4 | start program = "/etc/init.d/elasticsearch start" 5 | stop program = "/etc/init.d/elasticsearch stop" 6 | if failed port 9200 protocol HTTP 7 | request /cancer-terms 8 | with timeout 10 seconds 9 | then restart 10 | -------------------------------------------------------------------------------- /search/client/pages/clinical-trial.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ClinicalTrialDetail from '../components/ClinicalTrial/Detail' 3 | 4 | export default class extends Component { 5 | 6 | render() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /search/common/search_adapters/abstract_search_adapter.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Defines an abstract adapter for getting search clients 4 | * 5 | * @class AbstractSearchAdapter 6 | */ 7 | class AbstractSearchAdapter { 8 | 9 | /** 10 | * Gets an instance of a client 11 | * 12 | * @returns 13 | */ 14 | getClient() { 15 | return this.client; 16 | } 17 | } 18 | 19 | module.exports = AbstractSearchAdapter; -------------------------------------------------------------------------------- /search/client/tools/lib/fs.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import mkdirp from 'mkdirp'; 3 | 4 | const writeFile = (filename, contents) => new Promise((resolve, reject) => { 5 | fs.writeFile(filename, contents, 'utf8', err => 6 | err ? reject(err) : resolve()); 7 | }); 8 | 9 | const mkdir = (name) => new Promise((resolve, reject) => { 10 | mkdirp(name, err => err ? reject(err) : resolve()); 11 | }); 12 | 13 | export default { writeFile, mkdir }; 14 | -------------------------------------------------------------------------------- /search/client/tools/lib/task.js: -------------------------------------------------------------------------------- 1 | function format(time) { 2 | return time.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1'); 3 | } 4 | 5 | export default (fn) => async () => { 6 | const start = new Date(); 7 | console.log(`[${format(start)}] Starting '${fn.name}'...`); 8 | await fn(); 9 | const end = new Date(); 10 | const time = end.getTime() - start.getTime(); 11 | console.log(`[${format(end)}] Finished '${fn.name}' after ${time}ms`); 12 | }; 13 | -------------------------------------------------------------------------------- /search/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ctsearch-common", 3 | "version": "0.0.1", 4 | "private": true, 5 | "dependencies": { 6 | "elasticsearch": "11.0.1", 7 | "lodash": "4.12.0" 8 | }, 9 | "devDependencies": { 10 | "babel-cli": "6.5.1", 11 | "babel-core": "6.5.1", 12 | "babel-plugin-lodash": "2.0.1", 13 | "babel-preset-es2015": "6.5.0", 14 | "chai": "3.2.0", 15 | "eslint": "1.10.2", 16 | "mocha": "2.4.5" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /search/client/styles/vendor/font-awesome/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /search/client/static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /search/client/styles/vendor/font-awesome/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /search/client/test/routes-loader-spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | import { expect } from 'chai'; 3 | 4 | describe('routes-loader', () => { 5 | it('Should load a list of routes', done => { 6 | this.cacheable = () => {}; 7 | this.async = () => (err, result) => { 8 | expect(err).to.be.null; 9 | expect(result).to.not.to.be.empty.and.have.all.keys('/', '/404', '/500'); 10 | done(); 11 | }; 12 | 13 | require('../tools/lib/routes-loader').call(this); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /search/client/tools/bundle.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import task from './lib/task'; 3 | import config from './config'; 4 | 5 | export default task(function bundle() { 6 | return new Promise((resolve, reject) => { 7 | const bundler = webpack(config); 8 | const run = (err, stats) => { 9 | if (err) { 10 | reject(err); 11 | } else { 12 | console.log(stats.toString(config[0].stats)); 13 | resolve(); 14 | } 15 | }; 16 | bundler.run(run); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /search/client/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /search/client/pages/500.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class extends Component { 4 | 5 | static propTypes = { 6 | error: PropTypes.instanceOf(Error), 7 | }; 8 | 9 | render() { 10 | return ( 11 |
12 |

Error

13 |
{
14 |           this.props.error ?
15 |             this.props.error.message + '\n\n' + this.props.error.stack :
16 |             'A critical error occurred.'
17 |         }
18 |
19 | ); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /import/transform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transform", 3 | "version": "0.0.1", 4 | "description": "", 5 | "scripts": { 6 | "transform-trials": "node transform_trials.js | ../../common/node_modules/bunyan/bin/bunyan" 7 | }, 8 | "author": "Michael Balint (https://presidentialinnovationfellows.gov/)", 9 | "dependencies": { 10 | "JSONStream": "1.1.4", 11 | "async": "2.0.1", 12 | "babyparse": "0.4.6", 13 | "byline": "^5.0.0", 14 | "lodash": "4.13.1", 15 | "moment": "^2.14.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /search/client/styles/vendor/font-awesome/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /search/client/components/Navigation/Navigation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Link from '../Link'; 3 | 4 | export default class extends Component { 5 | 6 | render() { 7 | return ( 8 |
9 |
10 |
11 | 12 | 13 | 14 |
15 |
16 |
17 | ); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /search/client/styles/vendor/font-awesome/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /search/client/styles/components/_button.scss: -------------------------------------------------------------------------------- 1 | @mixin button($color: $primary-color) { 2 | -webkit-font-smoothing: antialiased; 3 | background-color: $color; 4 | border-radius: $base-border-radius; 5 | color: $white; 6 | display: inline-block; 7 | font-size: $base-font-size; 8 | font-weight: bold; 9 | line-height: 1; 10 | padding: 0.75em 1em; 11 | text-decoration: none; 12 | 13 | &:hover { 14 | background-color: darken($color, 10); 15 | color: $white; 16 | } 17 | 18 | &:disabled { 19 | cursor: not-allowed; 20 | opacity: 0.5; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /devops/clinicaltrialsapi-prod/apache.ctsapi.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | ServerName clinicaltrialsapi.cancer.gov 4 | ServerAlias clinicaltrialsapi-prod.cancer.gov 5 | ServerAlias clinicaltrialsapi-stage.cancer.gov 6 | ServerAlias clinicaltrialsapi-dev.cancer.gov 7 | 8 | CustomLog /local/content/apache/logs/ctapi-access.log combined env=!dontlog 9 | ErrorLog /local/content/apache/logs/ctapi-error.log 10 | 11 | RewriteEngine On 12 | RewriteRule ^/(.*) https://%{HTTP_HOST}/$1 [R=301,L] 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/Date/Date.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Fetch from 'isomorphic-fetch'; 3 | 4 | import Url from '../../../../lib/Url'; 5 | 6 | class Date extends Component { 7 | 8 | static propTypes = { 9 | paramField: PropTypes.string.isRequired, 10 | displayName: PropTypes.string.isRequired 11 | }; 12 | 13 | render() { 14 | let { displayName } = this.props; 15 | 16 | return ( 17 |
18 | filter {displayName} using dates 19 |
20 | ); 21 | } 22 | 23 | } 24 | 25 | export default Date; 26 | -------------------------------------------------------------------------------- /search/client/styles/vendor/font-awesome/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /search/client/components/Layout/Layout.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Navigation from '../Navigation'; 3 | import CustomerServiceWidget from '../CustomerServiceWidget'; 4 | 5 | require('../../styles/main.scss'); 6 | 7 | class Layout extends Component { 8 | 9 | static propTypes = { 10 | children: PropTypes.element.isRequired, 11 | }; 12 | 13 | render() { 14 | return ( 15 |
16 | 17 | {this.props.children} 18 | 19 |
20 | ); 21 | } 22 | 23 | } 24 | 25 | export default Layout; 26 | -------------------------------------------------------------------------------- /devops/clinicaltrialsapi-prod/cron-index: -------------------------------------------------------------------------------- 1 | # daily downloading trials.out, and indexing the elasticsearch at 7:30am 2 | 30 07 * * * ( date > /tmp/ctapi-get-trialsout.log; /local/content/trialsout/get_trialsout.sh >> /tmp/ctapi-get-trialsout.log 2>&1; chown -R ctapi_user.ctapi_user /local/content/trialsout; mv /local/content/trialsout/trials.out /local/content/ctapi_data >> /tmp/ctapi-get-trialsout.log 2>&1; su - ctapi_user -c "cd /local/content/deployment/ctapi/import/transform && npm run transform-trials > /tmp/ctapi-run-transform.log 2>&1"; su - ctapi_user -c "cd /local/content/deployment/ctapi/search/index && npm run index > /tmp/ctapi-run-index.log 2>&1" ) 3 | -------------------------------------------------------------------------------- /devops/api.upstart.config: -------------------------------------------------------------------------------- 1 | #!upstart 2 | description "node.js api server" 3 | author "Michael Balint" 4 | 5 | start on startup 6 | stop on shutdown 7 | 8 | script 9 | echo $$ > /var/run/api.pid 10 | cd /opt/clinical-trials-search/search/api 11 | npm start >> /var/log/api.sys.log 2>&1 12 | end script 13 | 14 | pre-start script 15 | # Date format same as (new Date()).toISOString() for consistency 16 | echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Starting" >> /var/log/api.sys.log 17 | end script 18 | 19 | pre-stop script 20 | rm /var/run/api.pid 21 | echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Stopping" >> /var/log/api.sys.log 22 | end script 23 | -------------------------------------------------------------------------------- /search/client/styles/modules/_navigation.scss: -------------------------------------------------------------------------------- 1 | .navigation { 2 | background-color: $primary-color; 3 | height: 80px; 4 | margin: 0; 5 | padding: 20px 0; 6 | 7 | .nci-logo { 8 | float: left; 9 | width: 320px; 10 | 11 | @include media($small-screen) { 12 | width: 400px; 13 | } 14 | 15 | img { 16 | height: auto; 17 | width: 100%; 18 | } 19 | } 20 | 21 | .nci-home-link { 22 | float: right; 23 | 24 | a { 25 | color: darken($primary-color, 25%); 26 | font-weight: bold; 27 | text-decoration: none; 28 | 29 | &:visited { 30 | color: darken($primary-color, 25%); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /search/client/static/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /search/client/.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.html text eol=lf 12 | *.jade text eol=lf 13 | *.js text eol=lf 14 | *.json text eol=lf 15 | *.less text eol=lf 16 | *.md text eol=lf 17 | *.sh text eol=lf 18 | *.txt text eol=lf 19 | *.xml text eol=lf 20 | -------------------------------------------------------------------------------- /search/client/components/CustomerServiceWidget/CustomerServiceWidget.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class extends Component { 4 | 5 | render() { 6 | return ( 7 |
8 |
9 | Get Help 10 |
11 |
12 |

Have a Question?

13 |

We’re here to help.

14 |

15 | 1-800-4-CANCER

16 | (1-800-422-6237) 17 |

18 |
19 |
20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /search/client/styles/vendor/font-awesome/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/Suggest/Suggest.js: -------------------------------------------------------------------------------- 1 | import Select from '../Select'; 2 | 3 | import ApiFetch from '../../../../lib/ApiFetch.js'; 4 | 5 | class Suggest extends Select { 6 | 7 | constructor() { 8 | super(); 9 | 10 | this.className = "filter-suggest"; 11 | 12 | this.state.minimumInput = 1; 13 | // this.state.placeholderText = "search..."; 14 | } 15 | 16 | getOptions(input, callback) { 17 | let { paramField } = this.props; 18 | ApiFetch(`terms?term=${input}&term_type=${paramField}&size=5`) 19 | .then((response) => { 20 | return response.json(); 21 | }).then((json) => { 22 | return callback(null, { 23 | options: json.terms 24 | }); 25 | }); 26 | } 27 | 28 | } 29 | 30 | export default Suggest; 31 | -------------------------------------------------------------------------------- /search/client/components/GoogleAnalytics/GoogleAnalytics.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { googleAnalyticsId } from '../../config'; 3 | 4 | const trackingCode = { __html: 5 | `(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=` + 6 | `function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;` + 7 | `e=o.createElement(i);r=o.getElementsByTagName(i)[0];` + 8 | `e.src='https://www.google-analytics.com/analytics.js';` + 9 | `r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));` + 10 | `ga('create','${googleAnalyticsId}','auto');`, 11 | }; 12 | 13 | class GoogleAnalytics extends Component { 14 | 15 | render() { 16 | return 27 | 28 | 29 |
30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | } 37 | 38 | export default Html; 39 | -------------------------------------------------------------------------------- /search/client/tools/start.js: -------------------------------------------------------------------------------- 1 | import browserSync from 'browser-sync'; 2 | import webpack from 'webpack'; 3 | import hygienistMiddleware from 'hygienist-middleware'; 4 | import webpackDevMiddleware from 'webpack-dev-middleware'; 5 | import webpackHotMiddleware from 'webpack-hot-middleware'; 6 | 7 | global.watch = true; 8 | const webpackConfig = require('./config')[0]; 9 | const bundler = webpack(webpackConfig); 10 | 11 | export default async () => { 12 | await require('./build')(); 13 | 14 | browserSync({ 15 | server: { 16 | baseDir: 'build', 17 | 18 | middleware: [ 19 | hygienistMiddleware('build'), 20 | 21 | webpackDevMiddleware(bundler, { 22 | // IMPORTANT: dev middleware can't access config, so we should 23 | // provide publicPath by ourselves 24 | publicPath: webpackConfig.output.publicPath, 25 | 26 | // pretty colored output 27 | stats: webpackConfig.stats, 28 | 29 | // for other settings see 30 | // http://webpack.github.io/docs/webpack-dev-middleware.html 31 | }), 32 | 33 | // bundler should be the same as above 34 | webpackHotMiddleware(bundler), 35 | ], 36 | }, 37 | 38 | // no need to watch '*.js' here, webpack will take care of it for us, 39 | // including full page reloads if HMR won't work 40 | files: [ 41 | 'build/**/*.css', 42 | 'build/**/*.html', 43 | ], 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /search/client/pages/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import OmniSuggest from '../components/OmniSuggest'; 3 | 4 | export default class extends Component { 5 | 6 | render() { 7 | return ( 8 |
9 |
10 |
11 |
12 |

13 | 16 |

17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |

What are Clinical Trials?

26 |

Clinical trials are research studies that involve people. They are the final step in a long process that begins with research in the lab. Most treatments we use today are the results of past clinical trials.

27 |

28 | Cancer clinical trials are designed to test new ways to treat cancer, find and diagnose cancer, and explore ways to prevent cancer. 29 |

30 |
31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /search/client/components/Link/Link.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Location from '../../lib/Location'; 3 | 4 | function isLeftClickEvent(event) { 5 | return event.button === 0; 6 | } 7 | 8 | function isModifiedEvent(event) { 9 | return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); 10 | } 11 | 12 | class Link extends Component { 13 | 14 | static propTypes = { 15 | to: PropTypes.string.isRequired, 16 | children: PropTypes.element.isRequired, 17 | state: PropTypes.object, 18 | onClick: PropTypes.func, 19 | }; 20 | 21 | static handleClick = event => { 22 | let allowTransition = true; 23 | let clickResult; 24 | 25 | if (this.props && this.props.onClick) { 26 | clickResult = this.props.onClick(event); 27 | } 28 | 29 | if (isModifiedEvent(event) || !isLeftClickEvent(event)) { 30 | return; 31 | } 32 | 33 | if (clickResult === false || event.defaultPrevented === true) { 34 | allowTransition = false; 35 | } 36 | 37 | event.preventDefault(); 38 | 39 | if (allowTransition) { 40 | const link = event.currentTarget; 41 | Location.push({ 42 | pathname: link.pathname, 43 | search: link.search 44 | }); 45 | } 46 | }; 47 | 48 | render() { 49 | const { to, children, ...props } = this.props; 50 | return {children}; 51 | } 52 | 53 | } 54 | 55 | export default Link; 56 | -------------------------------------------------------------------------------- /import/transform/stream/csv.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | const babyparse = require("babyparse"); 3 | const Transform = require("stream").Transform; 4 | const Logger = require("../../../common/logger"); 5 | 6 | let logger = new Logger({ name: "csv-stream" }); 7 | 8 | /** 9 | * Streams a csv using babyparse. It was necessary to use this rather than 10 | * the babyparse built-in streaming because otherwise, with large files, a 11 | * large amount of rows went missing. 12 | * 13 | * @class CsvStream 14 | * @extends {Transform} 15 | */ 16 | class CsvStream extends Transform { 17 | 18 | constructor({header, delimiter, exclude}) { 19 | super({ objectMode: true }); 20 | this.header = header; 21 | this.delimiter = delimiter; 22 | this.exclude = exclude; 23 | this.isFirstLine = true; 24 | } 25 | 26 | _transform(buffer, enc, next) { 27 | let bufferString = buffer.toString(); 28 | let csv = babyparse.parse(bufferString, { 29 | header: false, 30 | delimiter: this.delimiter, 31 | }); 32 | csv.data.forEach((row) => { 33 | let rowHash = {}; 34 | this.header.forEach((headerField, i) => { 35 | if (!_.includes(this.exclude, headerField)) { 36 | rowHash[headerField] = row[i]; 37 | } 38 | }); 39 | if (this.isFirstLine) { 40 | this.isFirstLine = false; 41 | } else { 42 | this.push(rowHash); 43 | } 44 | }); 45 | 46 | next(); 47 | } 48 | 49 | } 50 | 51 | module.exports = CsvStream; 52 | -------------------------------------------------------------------------------- /devops/clinicaltrialsapi-prod/ctapi.initd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # start/stop nodejs server 4 | # chkconfig: 345 94 90 5 | # description: start/stop nodejs for ctapi frontend 6 | # 7 | # 8 | ## Instructions ## 9 | ## copy this file into /etc/init.d 10 | ## chkconfig --add FILENAME 11 | ########## 12 | 13 | APP_NAME="search/api" 14 | APP_PATH="/local/content/deployment/ctapi" 15 | #ENVIRONMENT="development" 16 | ENVIRONMENT="production" 17 | USER="ctapi_user" 18 | #ADDRESS="127.0.0.1" 19 | PORT="3000" 20 | LOGFILE="$APP_PATH/logs/ctapi.sys.log" 21 | PIDFILE="/var/run/ctapi.pid" 22 | SET_PATH="cd $APP_PATH/$APP_NAME" 23 | CMD="$SET_PATH; nohup npm start >> $LOGFILE 2>&1 &" 24 | 25 | case "$1" in 26 | 27 | 'start') 28 | #echo $$ > $PIDFILE 29 | su - $USER -c "echo \"[`date -u +%Y-%m-%dT%T.%3NZ`] Starting\" >> $LOGFILE " 30 | su - $USER -c "$CMD" 31 | sleep 3 32 | echo $(netstat -tupln 2>&1 | grep -v grep | grep 3000 | awk '{print $7}' | cut -f 1 -d "/") > $PIDFILE 33 | ;; 34 | 'stop') 35 | su - $USER -c "echo \"[`date -u +%Y-%m-%dT%T.%3NZ`] Stopping\" >> $LOGFILE " 36 | 37 | if [ -r $PIDFILE ] ; then 38 | kill -9 `cat $PIDFILE` 39 | rm $PIDFILE 40 | fi 41 | 42 | ;; 43 | 'status') 44 | PID="" 45 | if [ -r $PIDFILE ] ; then 46 | PID=`cat $PIDFILE` 47 | fi 48 | 49 | if [ "$PID" != "" ] ; then 50 | echo "$APP_NAME is running PID=$PID " 51 | else 52 | echo "$APP_NAME is not running " 53 | fi 54 | ;; 55 | esac 56 | -------------------------------------------------------------------------------- /search/INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installing API/Indexer 2 | 3 | ## Configuration 4 | 5 | ### Attaching to Elastic Search Servers 6 | You can connect to one or more ElasticSearch servers in order to index content, 7 | and for the API to consume content. The configuration (config.json) should be as follows: 8 | #### Single Host 9 | ``` 10 | { 11 | "ES_HOST": "hostname", 12 | "ES_PORT": "9200" 13 | } 14 | ``` 15 | #### Multiple Hosts 16 | (we assume the nodes will all use the same port number) 17 | ``` 18 | { 19 | "ES_HOST": [ "host1", "host2", "host3" ], 20 | "ES_PORT": "9200" 21 | } 22 | ``` 23 | 24 | ## Preliminary Steps 25 | (assumes you done a git clone of the clinical-trials-search repository) 26 | 1. cd to the root of your local copy & ensure you are in correct branch. 27 | * we will refer to this path as <root> in subsequent steps 28 | 1. cd <root>/common && npm install 29 | 1. cd <root>/search/common && npm install 30 | 1. cd <root>/search/api && npm install 31 | 1. cd <root>/search/index && npm install 32 | 1. modify search configuration 33 | 1. cd <root>/search 34 | 1. edit config.json 35 | 1. enter your hosts and port 36 | 1. save 37 | 38 | ## Indexing Content 39 | ### Pre-Steps 40 | 1. Run the importer/transform 41 | 1. Copy the resulting trials_cleansed.json to the data folder 42 | 43 | ### Steps to Index 44 | 1. cd <root>/search/index 45 | 1. npm run index 46 | 47 | ## Running the API 48 | ### Pre-Steps 49 | 1. Run the indexer to populate an elastic search cluster. 50 | ### Steps to Running API 51 | 1. cd <root>/search/api 52 | 2. npm start 53 | -------------------------------------------------------------------------------- /search/index/indexer/trial/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysis": { 3 | "filter": { 4 | "english_stop": { 5 | "type": "stop", 6 | "stopwords": ["a", "an", "and", "are", "as", "at", "be", "but", "by", 7 | "for", "if", "in", "into", "is", "it", 8 | "no", "not", "of", "on", "or", "such", 9 | "that", "the", "their", "then", "there", "these", 10 | "they", "this", "to", "was", "will", "with", 11 | "2", "1", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" 12 | ] 13 | }, 14 | "english_stemmer": { 15 | "type": "stemmer", 16 | "language": "english" 17 | }, 18 | "english_possessive_stemmer": { 19 | "type": "stemmer", 20 | "language": "possessive_english" 21 | } 22 | }, 23 | "analyzer": { 24 | "keyword_ci": { 25 | "filter": "lowercase", 26 | "tokenizer": "keyword" 27 | }, 28 | "englishhtml": { 29 | "tokenizer": "standard", 30 | "char_filter": ["html_strip"], 31 | "filter": [ 32 | "english_possessive_stemmer", 33 | "lowercase", 34 | "english_stop", 35 | "english_stemmer" 36 | ] 37 | 38 | }, 39 | "englishfulltext": { 40 | "tokenizer": "standard", 41 | "filter": [ 42 | "english_possessive_stemmer", 43 | "lowercase", 44 | "english_stop", 45 | "english_stemmer" 46 | ] 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /search/client/tools/lib/routes-loader.js: -------------------------------------------------------------------------------- 1 | import glob from 'glob'; 2 | import { join } from 'path'; 3 | 4 | export default function(source) { 5 | this.cacheable(); 6 | const target = this.target; 7 | const callback = this.async(); 8 | 9 | if (target === 'node') { 10 | source = source.replace('import \'babel/polyfill\';', ''); // eslint-disable-line no-param-reassign 11 | } 12 | 13 | glob('**/*.{js,jsx}', { cwd: join(__dirname, '../../pages') }, (err, files) => { 14 | if (err) { 15 | return callback(err); 16 | } 17 | 18 | const lines = files.map((file) => { 19 | let path = '/' + file; 20 | 21 | if (path === '/index.js' || path === '/index.jsx') { 22 | path = '/'; 23 | } else if (path.endsWith('/index.js')) { 24 | path = path.substr(0, path.length - 9); 25 | } else if (path.endsWith('/index.jsx')) { 26 | path = path.substr(0, path.length - 10); 27 | } else if (path.endsWith('.js')) { 28 | path = path.substr(0, path.length - 3); 29 | } else if (path.endsWith('.jsx')) { 30 | path = path.substr(0, path.length - 4); 31 | } 32 | 33 | if (target === 'node' || path === '/404' || path === '/500') { 34 | return ` '${path}': () => require('./pages/${file}'),`; 35 | } 36 | 37 | return ` '${path}': () => new Promise(resolve => require(['./pages/${file}'], resolve)),`; 38 | }); 39 | 40 | if (lines.length) { 41 | return callback(null, source.replace(' routes = {', ' routes = {\n' + lines.join(''))); 42 | } 43 | 44 | return callback(new Error('Cannot find any routes.')); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /search/client/styles/base/_typography.scss: -------------------------------------------------------------------------------- 1 | body { 2 | @include font-feature-settings("kern", "liga", "pnum"); 3 | -webkit-font-smoothing: antialiased; 4 | color: $base-font-color; 5 | font-family: $base-font-family; 6 | font-size: $base-font-size; 7 | line-height: $base-line-height; 8 | } 9 | 10 | h1, 11 | h2, 12 | h3, 13 | h4, 14 | h5, 15 | h6 { 16 | font-family: $heading-font-family; 17 | font-size: $base-font-size; 18 | font-weight: normal; 19 | line-height: $heading-line-height; 20 | margin: 0 0 $small-spacing; 21 | } 22 | 23 | h1 { 24 | font-size: $base-font-size * 1.8; // 16 * 2 = 32spx 25 | 26 | @include media($small-screen) { 27 | font-size: $base-font-size * 2; 28 | } 29 | } 30 | 31 | h2 { 32 | font-size: $base-font-size * 1.75; // 16 * 1.75 = 28px 33 | } 34 | 35 | h3 { 36 | font-size: $base-font-size * 1.5; // 16 * 1.5 = 24px 37 | } 38 | 39 | h4 { 40 | font-size: $base-font-size * 1.25; // 16 * 1.25 = 20px 41 | } 42 | 43 | h5 { 44 | font-size: $base-font-size; 45 | } 46 | 47 | h6 { 48 | font-size: $base-font-size; 49 | } 50 | 51 | input { 52 | font-family: $base-font-family; 53 | font-size: $base-font-size; 54 | } 55 | 56 | 57 | p { 58 | font-size: $base-font-size * 1.1; 59 | margin: 0 0 $small-spacing; 60 | } 61 | 62 | a { 63 | color: $action-color; 64 | text-decoration: none; 65 | transition: color 0.1s linear; 66 | 67 | &:active, 68 | &:focus, 69 | &:hover { 70 | color: darken($action-color, 15%); 71 | } 72 | 73 | &:active, 74 | &:focus { 75 | outline: none; 76 | } 77 | } 78 | 79 | hr { 80 | border-left: none; 81 | border-right: none; 82 | border-top: none; 83 | margin: $base-spacing 0; 84 | } 85 | -------------------------------------------------------------------------------- /search/client/styles/modules/_customer-service-widget.scss: -------------------------------------------------------------------------------- 1 | $widget-color: darken($primary-color, 10); 2 | 3 | .customer-service-widget { 4 | background-color: $primary-color; 5 | border-radius: 1.4em; 6 | bottom: 2em; 7 | box-shadow: 0 4px 4px rgba($black, 0.1); 8 | color: $white; 9 | height: 2.8em; 10 | line-height: 2.8em; 11 | position: fixed; 12 | right: 3em; 13 | width: 8em; 14 | 15 | .widget-arrow { 16 | border-style: solid; 17 | border-width: 14px 12px 0 12px; 18 | border-color: $widget-color $transparent $transparent $transparent; 19 | bottom: -14px; 20 | height: 0; 21 | left: 50%; 22 | margin-left: -12px; 23 | position: absolute; 24 | width: 0; 25 | } 26 | 27 | .widget-detail { 28 | @include transition(top 0.4s ease); 29 | background-color: $widget-color; 30 | box-shadow: 4px 4px 4px rgba($black, 0.1); 31 | left: 50%; 32 | line-height: 1em; 33 | margin-left: -5.5em; 34 | opacity: 0; 35 | padding: 1.25em 1em 1.5em; 36 | position: absolute; 37 | visibility: hidden; 38 | top: -8.5em; 39 | width: 11em; 40 | 41 | &.active { 42 | opacity: 1; 43 | top: -9.5em; 44 | visibility: visible; 45 | } 46 | 47 | h4 { 48 | font-size: 1.1em; 49 | font-weight: bold; 50 | margin: 0; 51 | text-align: center; 52 | } 53 | 54 | p { 55 | font-size: 1em; 56 | margin: 0; 57 | text-align: center; 58 | 59 | &.widget-number { 60 | font-weight: bold; 61 | margin-top: 1em; 62 | } 63 | } 64 | } 65 | 66 | .widget-label { 67 | cursor: pointer; 68 | font-weight: bold; 69 | vertical-align: center; 70 | text-align: center; 71 | 72 | i { 73 | margin: 0.1em 0.4em 0 0; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /search/client/components/ToggleMore/ToggleMore.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class extends Component { 4 | 5 | constructor() { 6 | super(); 7 | 8 | this.state = { 9 | isShowingMore: false, 10 | canShowMore: false 11 | }; 12 | 13 | this.toggleMore = this.toggleMore.bind(this); 14 | } 15 | 16 | static propTypes = { 17 | items: PropTypes.array.isRequired, 18 | itemKey: PropTypes.string, 19 | numToShow: PropTypes.number 20 | }; 21 | 22 | componentDidMount() { 23 | const { items, numToShow } = this.props; 24 | if (items.length > numToShow) { 25 | this.setState({ 26 | canShowMore: true 27 | }); 28 | } 29 | } 30 | 31 | toggleMore() { 32 | this.setState({ 33 | isShowingMore: !this.state.isShowingMore 34 | }); 35 | } 36 | 37 | render() { 38 | const { items, numToShow, itemKey } = this.props; 39 | let { canShowMore, isShowingMore } = this.state; 40 | const delim = ", "; 41 | let showItems = items.map((item) => { 42 | if (itemKey) { 43 | return item[itemKey]; 44 | } 45 | return item; 46 | }); 47 | if (canShowMore && !isShowingMore) { 48 | showItems = showItems.slice(0, numToShow); 49 | } 50 | 51 | var canShowMoreToggle; 52 | if (canShowMore) { 53 | if (isShowingMore) { 54 | canShowMoreToggle = ( 55 | show less 56 | ) 57 | } else { 58 | canShowMoreToggle = ( 59 | show more 60 | ) 61 | } 62 | } 63 | 64 | return ( 65 | 66 | {showItems.join(delim)} 67 | {canShowMore && !isShowingMore ? "... " : " "} 68 | {canShowMoreToggle} 69 | 70 | ); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /search/client/tools/render.js: -------------------------------------------------------------------------------- 1 | import glob from 'glob'; 2 | import { join, dirname } from 'path'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom/server'; 5 | import Html from '../components/Html'; 6 | import task from './lib/task'; 7 | import fs from './lib/fs'; 8 | 9 | const DEBUG = !process.argv.includes('release'); 10 | 11 | function getPages() { 12 | return new Promise((resolve, reject) => { 13 | glob('**/*.js', { cwd: join(__dirname, '../pages') }, (err, files) => { 14 | if (err) { 15 | reject(err); 16 | } else { 17 | const result = files.map(file => { 18 | let path = '/' + file.substr(0, file.lastIndexOf('.')); 19 | if (path === '/index') { 20 | path = '/'; 21 | } else if (path.endsWith('/index')) { 22 | path = path.substr(0, path.lastIndexOf('/index')); 23 | } 24 | return { path, file }; 25 | }); 26 | resolve(result); 27 | } 28 | }); 29 | }); 30 | } 31 | 32 | async function renderPage(page, component) { 33 | const data = { 34 | body: ReactDOM.renderToString(component) 35 | }; 36 | let file = join(__dirname, '../build', page.file.substr(0, page.file.lastIndexOf('.')) + '.html'); 37 | let html = '\n' + ReactDOM.renderToStaticMarkup(); 38 | await fs.mkdir(dirname(file)); 39 | await fs.writeFile(file, html); 40 | if (!page.path.endsWith('/')) { 41 | file = join(__dirname, '../build', page.file.substr(0, page.file.lastIndexOf('.')) + '/index.html'); 42 | await fs.mkdir(dirname(file)); 43 | await fs.writeFile(file, html); 44 | } 45 | } 46 | 47 | export default task(async function render() { 48 | const pages = await getPages(); 49 | const { route } = require('../build/app.node'); 50 | for (const page of pages) { 51 | await route(page.path, renderPage.bind(undefined, page)); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /search/client/styles/vendor/font-awesome/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/Age/Age.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Url from '../../../../lib/Url'; 4 | 5 | class Age extends Component { 6 | 7 | constructor() { 8 | super(); 9 | 10 | this.className = "filter-age"; 11 | 12 | this.state = { 13 | age: null, 14 | minAgeField: "eligibility.structured.min_age_number_lte", 15 | maxAgeField: "eligibility.structured.max_age_number_gte" 16 | } 17 | 18 | this.onChange = this.onChange.bind(this); 19 | } 20 | 21 | componentDidMount() { 22 | this.updateState(); 23 | } 24 | 25 | updateState() { 26 | let { age, minAgeField, maxAgeField } = this.state; 27 | let params = Url.getParams(); 28 | 29 | if(params[minAgeField] !== age) { 30 | this.setState({ age: params[minAgeField] }) 31 | } 32 | } 33 | 34 | updateUrl() { 35 | let { age, minAgeField, maxAgeField } = this.state; 36 | let params = {}; 37 | 38 | if (age === "") { 39 | params[minAgeField] = null; 40 | params[maxAgeField] = null; 41 | Url.removeParams({ path: "/clinical-trials", params }); 42 | } else { 43 | params[minAgeField] = age; 44 | params[maxAgeField] = age; 45 | Url.overwriteParams({ path: "/clinical-trials", params }); 46 | } 47 | } 48 | 49 | onChange(event) { 50 | let age = event.target.value; 51 | if (this.state.age !== age) { 52 | this.setState({ age }, this.updateUrl); 53 | } 54 | } 55 | 56 | render() { 57 | return ( 58 |
59 | 60 | 67 |
68 | ); 69 | } 70 | 71 | } 72 | 73 | export default Age; 74 | -------------------------------------------------------------------------------- /search/api/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('api:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /search/api/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var Logger = require('../../common/logger'); 5 | var bunyanMiddleware = require('bunyan-middleware'); 6 | var cookieParser = require('cookie-parser'); 7 | var bodyParser = require('body-parser'); 8 | var cors = require('cors'); 9 | 10 | var routes = require('./routes/index'); 11 | 12 | var app = express(); 13 | 14 | // logging setup 15 | let logger = new Logger({name: "search-api"}); 16 | // app.use(bunyanMiddleware({ 17 | // headerName: 'X-Request-Id', 18 | // propertyName: 'reqId', 19 | // logName: 'req_id', 20 | // obscureHeaders: [], 21 | // logger: logger 22 | // })); 23 | 24 | // view engine setup 25 | app.set('views', path.join(__dirname, 'views')); 26 | app.set('view engine', 'jade'); 27 | 28 | // uncomment after placing your favicon in /public 29 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 30 | app.use(bodyParser.json()); 31 | app.use(bodyParser.urlencoded({ extended: false })); 32 | app.use(cookieParser()); 33 | app.use(express.static(path.join(__dirname, 'public'))); 34 | app.use(cors()); 35 | 36 | // routing 37 | app.use('/', routes); 38 | 39 | // catch 404 and forward to error handler 40 | app.use(function(req, res, next) { 41 | var err = new Error('Not Found'); 42 | err.status = 404; 43 | next(err); 44 | }); 45 | 46 | // error handlers 47 | 48 | // development error handler 49 | // will print stacktrace 50 | if (app.get('env') === 'development') { 51 | app.use(function(err, req, res, next) { 52 | res.status(err.status || 500); 53 | res.render('error', { 54 | message: err.message, 55 | error: err 56 | }); 57 | }); 58 | } 59 | 60 | // production error handler 61 | // no stacktraces leaked to user 62 | app.use(function(err, req, res, next) { 63 | res.status(err.status || 500); 64 | res.render('error', { 65 | message: err.message, 66 | error: {} 67 | }); 68 | }); 69 | 70 | const port = process.env.PORT || '3000'; 71 | logger.info(`Started API server at http://localhost:${port}/`); 72 | 73 | module.exports = app; 74 | -------------------------------------------------------------------------------- /devops/nginx_site.config: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name clinicaltrialsapi.cancer.gov; 4 | rewrite ^ https://$http_host$request_uri? permanent; 5 | } 6 | 7 | server { 8 | listen 443; 9 | 10 | server_name clinicaltrialsapi.cancer.gov; 11 | 12 | # auth_basic "Restricted"; 13 | # auth_basic_user_file /etc/nginx/.htpasswd; 14 | 15 | ssl on; 16 | ssl_certificate /etc/nginx/certs/wildcard_cancer_gov/public.crt; 17 | ssl_certificate_key /etc/nginx/certs/wildcard_cancer_gov/private.rsa; 18 | ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; 19 | ssl_prefer_server_ciphers on; 20 | ssl_dhparam /etc/nginx/certs/wildcard_cancer_gov/dhparams.pem; 21 | 22 | location / { 23 | proxy_pass http://172.31.55.190:3000; 24 | proxy_http_version 1.1; 25 | proxy_set_header Upgrade $http_upgrade; 26 | proxy_set_header Connection 'upgrade'; 27 | proxy_set_header Host $host; 28 | proxy_cache_bypass $http_upgrade; 29 | 30 | if ($request_method = OPTIONS ) { 31 | add_header Access-Control-Allow-Origin "*"; 32 | add_header Access-Control-Allow-Methods "GET, OPTIONS"; 33 | add_header Access-Control-Allow-Headers "Authorization"; 34 | add_header Access-Control-Allow-Credentials "true"; 35 | add_header Content-Length 0; 36 | add_header Content-Type text/plain; 37 | return 200; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /import/transform/stream/cleanse.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | const Transform = require("stream").Transform; 3 | const Utils = require("../../../common/utils"); 4 | const TermLoader = require("../../../common/term_loader"); 5 | const Logger = require("../../../common/logger"); 6 | 7 | let logger = new Logger({ name: "cleanse-stream" }); 8 | 9 | class CleanseStream extends Transform { 10 | 11 | constructor(terms) { 12 | super({ objectMode: true }); 13 | this.terms = terms; 14 | } 15 | 16 | _cleanseTerms(termType, terms) { 17 | let newTerms = terms.map((term) => { 18 | if (!term) { return term; } 19 | return this.terms[termType][Utils.transformStringToKey(term)]["term"]; 20 | }); 21 | return _.uniq(newTerms); 22 | } 23 | 24 | _transformTrialForTermType(trial, termType) { 25 | const _transform = (obj, pathArr) => { 26 | if (!obj || !pathArr) { return null; } 27 | let newObj = obj[pathArr[0]]; 28 | if (pathArr.length === 1) { 29 | if (newObj instanceof Array) { 30 | newObj = this._cleanseTerms(termType, newObj); 31 | } else { 32 | newObj = this._cleanseTerms(termType, [newObj])[0]; 33 | } 34 | } else { 35 | let newPathArr = pathArr.slice(1, pathArr.length); 36 | if (newObj instanceof Array) { 37 | newObj.forEach((o) => { 38 | _transform(o, newPathArr); 39 | }); 40 | } else { 41 | _transform(newObj, newPathArr); 42 | } 43 | } 44 | obj[pathArr[0]] = newObj; 45 | }; 46 | _transform(trial, termType.split(".")); 47 | } 48 | 49 | _transform(trial, enc, next) { 50 | logger.info(`Cleansing trial with nci_id (${trial.nci_id}).`); 51 | 52 | TermLoader.VALID_TERM_TYPES.forEach((termType) => { 53 | // special case: don't cleanse disease terms 54 | if (termType !== "_diseases") { 55 | this._transformTrialForTermType(trial, termType); 56 | } 57 | }); 58 | 59 | this.push(trial); 60 | next(); 61 | } 62 | 63 | } 64 | 65 | module.exports = CleanseStream; 66 | -------------------------------------------------------------------------------- /search/client/styles/pages/_clinical-trial.scss: -------------------------------------------------------------------------------- 1 | .clinical-trial-page { 2 | 3 | .back { 4 | margin-bottom: 2em; 5 | 6 | a { 7 | @include button(lighten($medium-gray, 5)); 8 | 9 | @include media($small-screen) { 10 | font-size: 1.1em; 11 | } 12 | } 13 | } 14 | 15 | .card { 16 | @extend %card; 17 | } 18 | 19 | .clinical-trial-detail { 20 | 21 | h1 { 22 | margin-top: 0; 23 | } 24 | 25 | h2 { 26 | font-size: 1.65em; 27 | margin: 1.5em 0 0.5em; 28 | } 29 | 30 | li { 31 | font-size: 1.1em; 32 | } 33 | 34 | p { 35 | &:last-child { 36 | margin-top: 0; 37 | } 38 | } 39 | 40 | ul { 41 | list-style-type: none; 42 | margin-top: 0; 43 | padding: 0; 44 | } 45 | 46 | .card { 47 | @include media($small-screen) { 48 | padding: 2em 1.5em; 49 | } 50 | } 51 | } 52 | 53 | .clinical-trial-location { 54 | margin-bottom: 10px; 55 | } 56 | 57 | .container { 58 | margin-top: 2em; 59 | @include direction-context(right-to-left){ 60 | 61 | .clinical-trial-detail { 62 | @include media($small-screen) { 63 | @include span-columns(6 of 9); 64 | } 65 | } 66 | 67 | .sidebar { 68 | @include media($small-screen) { 69 | @include span-columns(3 of 9); 70 | } 71 | } 72 | } 73 | } 74 | 75 | .sidebar { 76 | margin-bottom: 2em; 77 | 78 | @include media($small-screen) { 79 | padding: 2em 1em; 80 | } 81 | 82 | a { 83 | @include button(); 84 | display: inline-block; 85 | margin-top: 0.5em; 86 | } 87 | 88 | h1 { 89 | font-size: 1.35em; 90 | margin-bottom: 0.25em; 91 | 92 | @include media($small-screen) { 93 | font-size: 1.75em; 94 | margin-bottom: 0.5em; 95 | } 96 | } 97 | 98 | p { 99 | font-size: 1em; 100 | 101 | @include media($small-screen) { 102 | font-size: 1.1em; 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /search/client/styles/base/_variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Scaffolding 3 | * -------------------------------------------------------------------------- */ 4 | 5 | $body-bg: darken(#fff, 5%); 6 | 7 | /* 8 | * Colors 9 | * -------------------------------------------------------------------------- */ 10 | 11 | // General Colors 12 | $black: #000000; 13 | $blue: #477dca; 14 | $dark-gray: #333; 15 | $medium-gray: #999; 16 | $light-gray: #ddd; 17 | $extra-light-gray: #f2f2f2; 18 | $pink: #df628a; 19 | $white: #ffffff; 20 | $red: #d50f25; 21 | $transparent: transparent; 22 | $yellow: #ffff66; 23 | 24 | //App Specific Colors 25 | $primary-color: #1DAAF1; 26 | $secondary-color: #bb0e3d; 27 | $tertiary-color: #099; 28 | 29 | //Background Colors 30 | $base-background-color: $extra-light-gray; 31 | 32 | /* 33 | * Typography 34 | * -------------------------------------------------------------------------- */ 35 | 36 | //Font Colors 37 | $action-color: $primary-color; 38 | $base-font-color: lighten($dark-gray, 10); 39 | 40 | //Font Families 41 | $base-font-family: 'Source Sans Pro', 'Helvetica', sans-serif; 42 | $heading-font-family: 'Source Sans Pro', 'Helvetica', sans-serif; 43 | 44 | // Font Sizes 45 | $base-font-size: 1em; 46 | 47 | // Line Height 48 | $base-line-height: 1.5; 49 | $heading-line-height: 1.2; 50 | 51 | /* 52 | * Media queries breakpoints 53 | * -------------------------------------------------------------------------- */ 54 | 55 | $small-screen: 768px; 56 | $medium-screen: 992px; 57 | $large-screen: 1200px; 58 | 59 | /* 60 | * Responsive amounts 61 | * -------------------------------------------------------------------------- */ 62 | 63 | @media(min-width: $small-screen) { 64 | $padding: 10px; 65 | } 66 | 67 | @media(min-width: $medium-screen) { 68 | $padding: 20px; 69 | } 70 | 71 | @media(min-width: $large-screen) { 72 | $padding: 40px; 73 | } 74 | 75 | /* 76 | * Spacing 77 | * -------------------------------------------------------------------------- */ 78 | 79 | $base-border-radius: 3px; 80 | $base-spacing: $base-line-height * 1em; 81 | $small-spacing: $base-spacing / 2; 82 | $base-z-index: 0; 83 | -------------------------------------------------------------------------------- /search/client/styles/pages/_clinical-trials.scss: -------------------------------------------------------------------------------- 1 | .clinical-trials-page { 2 | 3 | .clinical-trial-result { 4 | @extend %card; 5 | margin: 20px 0; 6 | 7 | h3 { 8 | margin-top: 0; 9 | } 10 | } 11 | 12 | .clinical-trials { 13 | @include media($medium-screen) { 14 | @include span-columns(8); 15 | } 16 | } 17 | 18 | .flash-message { 19 | @extend %card; 20 | background-color: lighten($yellow, 20); 21 | } 22 | 23 | .gender-age-container { 24 | 25 | .filter-age, 26 | .filter-gender { 27 | @include span-columns(6); 28 | } 29 | } 30 | 31 | .search-status { 32 | margin-top: 2em; 33 | 34 | .search-results { 35 | font-size: 2em; 36 | font-weight: 300; 37 | margin-bottom: 0.25em; 38 | } 39 | } 40 | 41 | .remove-status { 42 | cursor: pointer; 43 | } 44 | } 45 | 46 | .filter-page { 47 | background-color: $primary-color; 48 | color: #fff; 49 | padding-bottom: 2em; 50 | 51 | .container { 52 | position: relative; 53 | } 54 | 55 | .toggle-show-filter { 56 | @include button(darken($primary-color, 15)); 57 | cursor: pointer; 58 | font-weight: bold; 59 | padding: 1em 2em; 60 | 61 | i { 62 | margin-left: 0.4em; 63 | } 64 | } 65 | 66 | .toggle-hide-filter { 67 | @include button(darken($primary-color, 20)); 68 | cursor: pointer; 69 | position: absolute; 70 | right: 2.5em; 71 | top: 0; 72 | } 73 | 74 | .filter-category-headers { 75 | border-bottom: 1px solid darken($primary-color, 10); 76 | display: flex; 77 | margin-bottom: 20px; 78 | 79 | .filter-category-header { 80 | background-color: darken($primary-color, 10%); 81 | border: 1px solid darken($primary-color, 12%); 82 | cursor: pointer; 83 | font-weight: bold; 84 | padding: 0.75em 2em; 85 | 86 | &.selected { 87 | background-color: darken($primary-color, 0%); 88 | border-bottom: 1px solid $primary-color; 89 | cursor: default; 90 | } 91 | } 92 | } 93 | 94 | .filters { 95 | @include span-columns(6 pf 9); 96 | 97 | .filters-column { 98 | @include span-columns(6); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /search/client/components/Search/Status/Status.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 3 | 4 | import StatusItem from './StatusItem'; 5 | import Url from '../../../lib/Url'; 6 | import ValidParams from '../../../lib/ValidParams'; 7 | 8 | export default class extends Component { 9 | 10 | _getDisplayType(type) { 11 | return ValidParams.getParamsByKey()[type]["display_name"].toLowerCase(); 12 | } 13 | 14 | getFieldStatuses() { 15 | let params = Url.getParams(); 16 | let fieldStatuses = []; 17 | 18 | Object.keys(params).forEach((key) => { 19 | let values = params[key]; 20 | // TODO: handle special case, make this better (pull out explicit logic) 21 | if (key === "eligibility.structured.max_age_number_gte") { 22 | if (values && values[0] === "") { 23 | return; 24 | } else { 25 | return fieldStatuses.push({ 26 | displayType: "Age", 27 | key: "age", 28 | statusItems: values.map((value) => { 29 | return { key, value }; 30 | }) 31 | }); 32 | } 33 | } else if (key === "eligibility.structured.min_age_number_lte") { 34 | // nada 35 | return; 36 | } 37 | let fieldStatus = { 38 | displayType: this._getDisplayType(key), 39 | key, 40 | statusItems: values.map((value) => { 41 | return { key, value }; 42 | }) 43 | } 44 | fieldStatuses.push(fieldStatus); 45 | }); 46 | 47 | return fieldStatuses; 48 | } 49 | 50 | static propTypes = { 51 | totalResults: PropTypes.number 52 | }; 53 | 54 | render() { 55 | let fieldStatuses = this.getFieldStatuses(); 56 | let { totalResults } = this.props; 57 | return ( 58 |
59 |

60 | Showing {totalResults} {totalResults === 1 ? "trial" : "trials"} 61 |

62 | {fieldStatuses.map((fieldStatus, i) => 63 |
64 | {fieldStatus.displayType}:{" "} 65 | {fieldStatus.statusItems.map((statusItem, i) => 66 | 67 | 68 | {i < (fieldStatus.statusItems.length) - 1 ? " or " : ""} 69 | 70 | )} 71 |
72 | )} 73 |
74 |
75 | ) 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /search/client/components/ClinicalTrial/Result/Result.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Link from '../../Link'; 3 | import ToggleMore from '../../ToggleMore'; 4 | 5 | export default class extends Component { 6 | 7 | static propTypes = { 8 | trial: PropTypes.object.isRequired, 9 | children: PropTypes.element, 10 | state: PropTypes.object 11 | }; 12 | 13 | getTreatments(trial) { 14 | let tHash = {}; 15 | if (trial.arms) { 16 | trial.arms.forEach((arm) => { 17 | arm.interventions.forEach((intervention) => { 18 | let treatment = intervention.intervention_name; 19 | if (treatment) { 20 | if (intervention.intervention_type) { 21 | treatment += ` (${intervention.intervention_type})`; 22 | } 23 | tHash[treatment] = 1; 24 | } 25 | }) 26 | }); 27 | } 28 | let treatments = Object.keys(tHash).map((treatment) => { 29 | return { 30 | display: treatment, 31 | link: `/clinical-trials?_treatments=${treatment}` 32 | }; 33 | }); 34 | return treatments; 35 | } 36 | 37 | getDiseases(trial) { 38 | let dHash = {}; 39 | if (trial.diseases) { 40 | trial.diseases.forEach((disease) => { 41 | let d = disease.disease; 42 | if (d) { 43 | dHash[d.display_name] = 1; 44 | } 45 | }); 46 | } 47 | return Object.keys(dHash); 48 | } 49 | 50 | render() { 51 | const { trial, children } = this.props; 52 | let treatments = this.getTreatments(trial); 53 | let diseases = this.getDiseases(trial); 54 | 55 | return ( 56 |
57 |

58 | 59 | {trial.brief_title} 60 | 61 |

62 |
63 | Status: {trial.current_trial_status} 64 |
65 |
66 | Phase: {trial.phase.phase.split("_").join(", ")} 67 |
68 |
69 | Treatment{treatments.length > 1 ? "s" : ""}:{" "} 70 | 73 |
74 |
75 | Disease{diseases.length > 1 ? "s" : ""}:{" "} 76 | 78 |
79 | {children} 80 |
81 | ); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/Text/Text.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import TagsInput from 'react-tagsinput'; 3 | 4 | import Url from '../../../../lib/Url'; 5 | 6 | class Text extends Component { 7 | 8 | constructor() { 9 | super(); 10 | 11 | this.className = "filter-text"; 12 | 13 | this.state = { 14 | selectedValues: [], 15 | inputText: "", 16 | placeholderText: "" 17 | } 18 | 19 | this.onChange = this.onChange.bind(this); 20 | this.onInputChange = this.onInputChange.bind(this); 21 | } 22 | 23 | static propTypes = { 24 | paramField: PropTypes.string.isRequired, 25 | displayName: PropTypes.string.isRequired 26 | }; 27 | 28 | escapeRegexCharacters(str) { 29 | return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 30 | } 31 | 32 | loadValuesState() { 33 | let params = Url.getParams(); 34 | let values = params[this.props.paramField] || []; 35 | if (JSON.stringify(this.state.selectedValues) !== JSON.stringify(values)) { 36 | this.setState({ 37 | selectedValues: values 38 | }); 39 | } 40 | } 41 | 42 | componentDidMount() { 43 | this.loadValuesState(); 44 | } 45 | 46 | componentWillUpdate() { 47 | this.loadValuesState(); 48 | } 49 | 50 | onChange(values) { 51 | let params = {}; 52 | params[this.props.paramField] = []; 53 | if (values && values.length) { 54 | params[this.props.paramField] = values; 55 | } 56 | Url.overwriteParams({ path: "/clinical-trials", params }); 57 | 58 | this.setState({ selectedValues: values }); 59 | } 60 | 61 | onInputChange(input) { 62 | this.setState({ inputText: this.escapeRegexCharacters(input) }); 63 | } 64 | 65 | renderTag(props) { 66 | let {tag, key, onRemove, classNameRemove, ...other} = props 67 | return ( 68 | 69 | onRemove(key)} /> 70 | {tag} 71 | 72 | ) 73 | } 74 | 75 | render() { 76 | const { paramField, displayName } = this.props; 77 | const htmlId = paramField.split(".").join("-"); 78 | const inputProps = { 79 | id: htmlId, 80 | placeholder: "" 81 | }; 82 | 83 | return ( 84 |
85 | 86 | 90 |
91 | ); 92 | } 93 | 94 | } 95 | 96 | export default Text; 97 | -------------------------------------------------------------------------------- /search/index/indexer/index_optimizer.js: -------------------------------------------------------------------------------- 1 | const async = require("async"); 2 | const _ = require("lodash"); 3 | const ElasticSearch = require("elasticsearch"); 4 | 5 | const AbstractIndexTool = require("./abstract_index_tool"); 6 | const Logger = require("../../../common/logger"); 7 | 8 | class ElasticSearchLogger extends Logger { 9 | get DEFAULT_LOGGER_NAME() { 10 | return "elasticsearch"; 11 | } 12 | } 13 | 14 | /** 15 | * Class for optimizing ElasticSearch Indexes 16 | * 17 | * @class AliasSwapper 18 | */ 19 | class IndexOptimizer extends AbstractIndexTool { 20 | 21 | get LOGGER_NAME() { 22 | return "index-optimizer"; 23 | } 24 | 25 | /** 26 | * Creates an instance of AliasSwapper. 27 | * 28 | * @param {any} adapter The search adapter to use for connecting to ElasticSearch 29 | */ 30 | constructor(adapter) { 31 | super(adapter); 32 | } 33 | 34 | /** 35 | * Optimizes (forceMerge) an index into 1 segment so that 36 | * all elasticsearch servers return the same scores for 37 | * searches. 38 | * 39 | * @param {any} indexName The index to optimize. 40 | * @param {any} callback 41 | */ 42 | forceMerge(indexName, callback) { 43 | this.logger.info( 44 | `Optimizing Index (${indexName})`); 45 | this.client.indices.forcemerge({ 46 | maxNumSegments: 1, 47 | waitForMerge: true, 48 | index: indexName, 49 | requestTimeout: 90000 50 | }, (err, response, status) => { 51 | if(err) { this.logger.error(err); } 52 | return callback(err, response); 53 | }); 54 | } 55 | 56 | /** 57 | * Initializes and executes the optimizing of for trials and terms 58 | * 59 | * @static 60 | * @param {any} adapter The search adapter to use for making requests to ElasticSearch 61 | * @param {any} trialIndexInfo Information about the index and alias for trials 62 | * @param {any} termIndexInfo Information about the index and alias for terms 63 | * @param {any} callback 64 | */ 65 | static init(adapter, trialIndexInfo, termIndexInfo, callback) { 66 | 67 | let optimizer = new IndexOptimizer(adapter); 68 | optimizer.logger.info(`Starting index optimization.`); 69 | 70 | async.waterfall([ 71 | (next) => { optimizer.forceMerge(trialIndexInfo.esIndex, next); }, 72 | (result, next) => { optimizer.forceMerge(termIndexInfo.esIndex, next); } 73 | ], (err) => { 74 | if(err) { optimizer.logger.error(err); } 75 | optimizer.logger.info(`Finished optimizing indices.`); 76 | return callback(err); 77 | }); 78 | } 79 | 80 | } 81 | 82 | module.exports = IndexOptimizer; -------------------------------------------------------------------------------- /search/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.1", 5 | "author": "Michael Balint (https://presidentialinnovationfellows.gov/)", 6 | "devDependencies": { 7 | "autoprefixer": "~6.0.3", 8 | "autosuggest-highlight": "~2.1.1", 9 | "babel": "~5.8.23", 10 | "babel-core": "~5.8.25", 11 | "babel-eslint": "~4.1.3", 12 | "babel-loader": "~5.3.2", 13 | "babel-plugin-react-transform": "~1.1.1", 14 | "bourbon": "^4.2.7", 15 | "bourbon-neat": "^1.8.0", 16 | "browser-sync": "~2.9.11", 17 | "chai": "~3.3.0", 18 | "css-loader": "~0.19.0", 19 | "del": "~2.0.2", 20 | "eslint": "~1.6.0", 21 | "eslint-config-airbnb": "0.1.0", 22 | "eslint-plugin-react": "~3.5.1", 23 | "extract-text-webpack-plugin": "^1.0.1", 24 | "fbjs": "~0.3.2", 25 | "file-loader": "~0.8.4", 26 | "git-repository": "~0.1.1", 27 | "glob": "~5.0.15", 28 | "history": "~2.1.1", 29 | "hygienist-middleware": "~0.1.0", 30 | "isomorphic-fetch": "~2.2.1", 31 | "lodash.merge": "~3.3.2", 32 | "mkdirp": "~0.5.1", 33 | "mocha": "~2.3.3", 34 | "ncp": "~2.0.0", 35 | "node-libs-browser": "~0.5.3", 36 | "node-sass": "^3.8.0", 37 | "postcss": "~5.0.10", 38 | "postcss-import": "~7.0.0", 39 | "postcss-loader": "~0.6.0", 40 | "postcss-scss": "^0.1.8", 41 | "precss": "~1.2.3", 42 | "query-string": "~4.2.1", 43 | "raw-loader": "~0.5.1", 44 | "react": "~0.14.0", 45 | "react-addons-css-transition-group": "^0.14.0", 46 | "react-autosuggest": "~3.7.4", 47 | "react-dom": "~0.14.0", 48 | "react-redux": "~4.4.5", 49 | "react-select": "^1.0.0-beta13", 50 | "react-tagsinput": "^3.8.1", 51 | "react-transform-catch-errors": "~1.0.0", 52 | "react-transform-hmr": "~1.0.1", 53 | "react-waypoint": "~2.0.3", 54 | "redbox-react": "~1.1.1", 55 | "redux": "~3.5.2", 56 | "sass-loader": "^3.2.0", 57 | "string-similarity": "~1.1.0", 58 | "style-loader": "~0.12.4", 59 | "through2": "~2.0.0", 60 | "url-loader": "~0.5.6", 61 | "webpack": "~1.12.2", 62 | "webpack-dev-middleware": "~1.2.0", 63 | "webpack-hot-middleware": "~2.4.1" 64 | }, 65 | "scripts": { 66 | "lint": "eslint src tools test", 67 | "test": "mocha --compilers js:babel/register --watch", 68 | "clean": "babel-node --eval \"require('./tools/clean')().catch(err => console.log(err.stack))\"", 69 | "build": "babel-node --eval \"require('./tools/build')().catch(err => console.log(err.stack))\"", 70 | "start": "babel-node --eval \"require('./tools/start')().catch(err => console.log(err.stack))\"", 71 | "deploy": "babel-node --eval \"require('./tools/deploy')().catch(err => console.log(err.stack))\"" 72 | }, 73 | "dependencies": {} 74 | } 75 | -------------------------------------------------------------------------------- /search/index/index.js: -------------------------------------------------------------------------------- 1 | const async = require("async"); 2 | const path = require("path"); 3 | const TrialIndexer = require("./indexer/trial/indexer"); 4 | const TermIndexer = require("./indexer/term/indexer"); 5 | const AliasSwapper = require("./indexer/alias_swapper"); 6 | const IndexCleaner = require("./indexer/index_cleaner"); 7 | const IndexOptimizer = require("./indexer/index_optimizer"); 8 | const Logger = require("../../common/logger"); 9 | const elasticsearchAdapter = require("../common/search_adapters/elasticsearch_adapter"); 10 | 11 | const TRIALS_FILEPATH = path.join(__dirname, 12 | '../../data/trials.out.03.cleansed'); 13 | 14 | const DAYS_TO_KEEP = 7; 15 | 16 | /** 17 | * Defines the class responsible for creating and managing the elasticsearch indexes 18 | * 19 | * @class Indexer 20 | */ 21 | class Indexer { 22 | 23 | /** 24 | * Creates an instance of Indexer. 25 | * 26 | */ 27 | constructor() { 28 | this.logger = new Logger({name: "indexer"}); 29 | } 30 | 31 | /** 32 | * Index the trials contained in the trials file 33 | * 34 | * @param {any} trials_file The path to a JSON file containing a collection of trials. 35 | */ 36 | index(trials_file) { 37 | this.logger.info("Started indexing."); 38 | 39 | let trialIndexInfo = false; 40 | let termIndexInfo = false; 41 | 42 | async.waterfall([ 43 | (next) => { TrialIndexer.init(elasticsearchAdapter, trials_file, next); }, 44 | (info, next) => { 45 | //Save out alias and trial index name 46 | trialIndexInfo = info; 47 | return next(null); 48 | }, 49 | (next) => { TermIndexer.init(elasticsearchAdapter, trials_file, next); }, 50 | (info, next) => { 51 | //Save out term index 52 | termIndexInfo = info; 53 | return next(null); 54 | }, 55 | //Optimize the index 56 | (next) => { IndexOptimizer.init(elasticsearchAdapter, trialIndexInfo, termIndexInfo, next); }, 57 | //if all went well, swap aliases 58 | (next) => { AliasSwapper.init(elasticsearchAdapter, trialIndexInfo, termIndexInfo, next); }, 59 | //clean up old indices 60 | (next) => { IndexCleaner.init(elasticsearchAdapter, trialIndexInfo.esAlias, termIndexInfo.esAlias, DAYS_TO_KEEP, next); } 61 | ], (err, status) => { 62 | if (err) { 63 | this.logger.info("Errors encountered. Exiting."); 64 | } else { 65 | this.logger.info("Finished indexing."); 66 | } 67 | }); 68 | } 69 | } 70 | 71 | //If we are running this module directly from Node this code will execute. 72 | //This will index all trials taking our default input. 73 | if (require.main === module) { 74 | let indexer = new Indexer(); 75 | indexer.index(TRIALS_FILEPATH); 76 | } 77 | 78 | module.exports = Indexer; 79 | -------------------------------------------------------------------------------- /common/utils.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | const latinize = require("latinize"); 3 | 4 | class Utils { 5 | 6 | static transformStringToKey(text) { 7 | return latinize(text) 8 | .toLowerCase() 9 | .replace(/[^a-z0-9 ]/g, " ") 10 | .replace(/\s\s+/g, " ") 11 | .replace(/ /g, "_") 12 | .replace("and_", "") 13 | .replace("of_", "") 14 | .replace("the_", ""); 15 | } 16 | 17 | static getFlattenedMappingProperties(mapping) { 18 | let props = {}; 19 | 20 | const _recurseMappingTree = (mappingTree, pathArr) => { 21 | if (mappingTree["properties"]) { 22 | _recurseMappingTree(mappingTree["properties"], pathArr); 23 | } else if (mappingTree["type"]) { 24 | props[pathArr.join(".")] = mappingTree["type"]; 25 | } else { 26 | Object.keys(mappingTree).forEach((key) => { 27 | _recurseMappingTree(mappingTree[key], pathArr.concat(key)); 28 | }); 29 | } 30 | }; 31 | 32 | _recurseMappingTree(mapping, []); 33 | return props; 34 | } 35 | 36 | static getFlattenedMappingPropertiesByType(mapping) { 37 | let props = {}; 38 | 39 | const _recurseMappingTree = (mappingTree, pathArr) => { 40 | if (mappingTree["properties"]) { 41 | //We need to add any nested objects for groupings. 42 | if (mappingTree["type"] == "nested") { 43 | if (!props[mappingTree["type"]]) { 44 | props[mappingTree["type"]] = []; 45 | } 46 | props[mappingTree["type"]].push(pathArr.join(".")); 47 | } 48 | _recurseMappingTree(mappingTree["properties"], pathArr); 49 | } else if (mappingTree["type"]) { 50 | if (!props[mappingTree["type"]]) { 51 | props[mappingTree["type"]] = []; 52 | } 53 | props[mappingTree["type"]].push(pathArr.join(".")); 54 | } else { 55 | Object.keys(mappingTree).forEach((key) => { 56 | _recurseMappingTree(mappingTree[key], pathArr.concat(key)); 57 | }); 58 | } 59 | }; 60 | 61 | _recurseMappingTree(mapping, []); 62 | return props; 63 | } 64 | 65 | static omitDeepKeys(collection, excludeKeys) { 66 | const omitFn = (value) => { 67 | if (value && typeof value === 'object') { 68 | excludeKeys.forEach((key) => { 69 | delete value[key]; 70 | }); 71 | } 72 | } 73 | return _.cloneDeepWith(collection, omitFn); 74 | } 75 | 76 | static omitPrivateKeys(collection) { 77 | const omitFn = (value) => { 78 | if (value && typeof value === 'object') { 79 | Object.keys(value).forEach((key) => { 80 | if (key[0] === "_") { 81 | delete value[key]; 82 | } 83 | }); 84 | } 85 | } 86 | return _.cloneDeepWith(collection, omitFn); 87 | } 88 | 89 | } 90 | 91 | module.exports = Utils; 92 | -------------------------------------------------------------------------------- /devops/clinicaltrialsapi-prod/ctapi-deploy-data: -------------------------------------------------------------------------------- 1 | PATH_BASE="/local/content/deployment" 2 | DATA_PATH="/local/content/ctapi_data" 3 | TOUT_PATH="/local/content/trialsout" 4 | 5 | ORI_PATH="/var/keys/ctapi-conf" 6 | 7 | APP_NAME=ctapi 8 | APP_USER=ctapi_user 9 | 10 | #SSH_USER=centos 11 | SSH_USER=ncimaint 12 | PROXYKEY_USER=ncimaint 13 | 14 | #use ncimaint key 15 | PROXYKEY_PATH="-i /var/keys/ncimaint.pem" 16 | KEY_PATH=$PROXYKEY_PATH 17 | 18 | SSH_OPT="-q -o StrictHostKeyChecking=no" 19 | 20 | PROXY_HOST=x.x.x.x 21 | 22 | PROXYCMD="ProxyCommand ssh ${SSH_OPT} ${PROXYKEY_PATH} ${PROXYKEY_USER}@${PROXY_HOST} nc %h %p" 23 | 24 | 25 | 26 | #disable ssh proxy 27 | #PROXYCMD="StrictHostKeyChecking=no" 28 | #KEY_PATH=$PROXYKEY_PATH 29 | 30 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME id 31 | 32 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 33 | [ -d ${DATA_PATH}.bak ] && sudo rm -rf ${DATA_PATH} ; \ 34 | [ -d ${DATA_PATH} ] && sudo mv ${DATA_PATH} ${DATA_PATH}.bak ; \ 35 | sudo mkdir ${DATA_PATH} ; \ 36 | sudo chmod o+xr $DATA_PATH ; \ 37 | sudo chown -R $APP_USER.$APP_USER $DATA_PATH ; " 38 | 39 | 40 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 41 | [ -d ${TOUT_PATH} ] && sudo rm -rf ${TOUT_PATH} ; \ 42 | sudo mkdir ${TOUT_PATH} " 43 | 44 | 45 | 46 | scp -r $SSH_OPT -o "$PROXYCMD" $KEY_PATH $ORI_PATH/get_trialsout.sh $SSH_USER@$HOST_NAME:/tmp 47 | scp -r $SSH_OPT -o "$PROXYCMD" $KEY_PATH $ORI_PATH/cron-index $SSH_USER@$HOST_NAME:/tmp 48 | #scp -r $SSH_OPT -o "$PROXYCMD" $KEY_PATH $ORI_PATH $SSH_USER@$HOST_NAME:/tmp 49 | 50 | 51 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 52 | sudo mv /tmp/get_trialsout.sh ${TOUT_PATH}; sudo chmod +x ${TOUT_PATH}/get_trialsout.sh; \ 53 | sudo ${TOUT_PATH}/get_trialsout.sh; sudo chown -R $APP_USER.$APP_USER ${TOUT_PATH} ;" 54 | 55 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo crontab /tmp/cron-index" 56 | 57 | 58 | ##### Deploy ######## 59 | 60 | 61 | SSH_USER=$APP_USER 62 | KEY_PATH="-i /var/keys/ctapi_user.pem" 63 | 64 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME id 65 | 66 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 67 | cd $DATA_PATH; \ 68 | curl -OL http://evs.nci.nih.gov/ftp1/NCI_Thesaurus/archive/16.08e_Release/Thesaurus_16.08e.FLAT.zip; \ 69 | curl -OL http://evs.nci.nih.gov/ftp1/NCI_Thesaurus/Neoplasm/Neoplasm_Core.csv; \ 70 | curl -OL http://www.cancer.gov/publishedcontent/Files/Configuration/data/zip_codes.json; \ 71 | unzip Thesaurus_16.08e.FLAT.zip; chmod -R go+r *" 72 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 73 | mv /local/content/trialsout/trials.out $DATA_PATH " 74 | 75 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "cd $PATH_BASE/$APP_NAME/import/transform && npm run transform-trials " 76 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "cd $PATH_BASE/$APP_NAME/search/index && npm run index " 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DEFINITIONS: 2 | 3 | PROVIDER: the National Cancer Institute (NCI), a participating institute of the National Institutes of Health (NIH), and an agency of the United States Government. 4 | 5 | SOFTWARE: the human readable source code form, the machine readable, binary, object code form, and the related documentation for the modules of the clinical-trials-search, which is an API for accessing clinical trial information. 6 | 7 | RECIPIENT: the party that downloads the software. 8 | 9 | By downloading or otherwise receiving the SOFTWARE, RECIPIENT may use and/or redistribute the SOFTWARE, with or without modification, subject to RECIPIENT'S agreement to the following terms: 10 | 11 | 1. THE SOFTWARE SHALL NOT BE USED IN THE TREATMENT OR DIAGNOSIS OF HUMAN SUBJECTS. RECIPIENT is responsible for compliance with all laws and regulations applicable to the use of the SOFTWARE. 12 | 13 | 2. The SOFTWARE that is distributed pursuant to this Agreement has been created by United States Government employees. In accordance with Title 17 of the United States Code, section 105, the SOFTWARE is not subject to copyright protection in the United States. Other than copyright, all rights, title and interest in the SOFTWARE shall remain with the PROVIDER. 14 | 15 | 3. RECIPIENT agrees to acknowledge PROVIDER'S contribution of the SOFTWARE in all written publications containing any data or information regarding or resulting from use of the SOFTWARE. 16 | 17 | 4. THE SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE PROVIDER OR THE INDIVIDUAL DEVELOPERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 18 | 19 | 5. RECIPIENT agrees not to use any trademarks, service marks, trade names, logos or product names of NCI or NIH to endorse or promote products derived from the SOFTWARE without specific, prior and written permission. 20 | 21 | 6. For sake of clarity, and not by way of limitation, RECIPIENT may add its own copyright statement to its modifications or derivative works of the SOFTWARE and may provide additional or different license terms and conditions in its sublicenses of modifications or derivative works of the SOFTWARE provided that RECIPIENT'S use, reproduction, and distribution of the SOFTWARE otherwise complies with the conditions stated in this Agreement. Whenever Recipient distributes or redistributes the SOFTWARE, a copy of this Agreement must be included with each copy of the SOFTWARE. 22 | -------------------------------------------------------------------------------- /search/client/styles/pages/_index.scss: -------------------------------------------------------------------------------- 1 | .index-page { 2 | 3 | .card { 4 | @extend %card; 5 | 6 | h1 { 7 | margin-top: 0; 8 | } 9 | } 10 | } 11 | 12 | .index-about { 13 | margin-top: 2em; 14 | 15 | h1 { 16 | font-weight: normal; 17 | font-size: 1.8em; 18 | margin-bottom: 0.25em; 19 | 20 | @include media($small-screen) { 21 | font-size: 2em; 22 | } 23 | } 24 | 25 | p { 26 | color: lighten($base-font-color, 5); 27 | } 28 | 29 | .main-content { 30 | @include media($small-screen) { 31 | @include span-columns(8); 32 | } 33 | } 34 | } 35 | 36 | .index-search { 37 | background-color: $primary-color; 38 | padding: 2em 0 4em; 39 | 40 | h1 { 41 | color: $white; 42 | font-weight: 300; 43 | margin-bottom: 0.3em; 44 | } 45 | 46 | .search-container { 47 | @include media($small-screen) { 48 | @include span-columns(8); 49 | } 50 | } 51 | } 52 | 53 | .omni-suggest { 54 | @include clearfix; 55 | width: 100%; 56 | 57 | .omni-suggest-highlight { 58 | // color: #ee0000; 59 | font-weight: bold; 60 | } 61 | 62 | .react-autosuggest__container { 63 | position: relative; 64 | } 65 | 66 | .react-autosuggest__input { 67 | border: none; 68 | float: left; 69 | font-size: 1.1em; 70 | height: 50px; 71 | padding: 10px 20px; 72 | width: 85%; 73 | 74 | @include media($small-screen) { 75 | width: 90%; 76 | } 77 | 78 | &:focus { 79 | outline: none; 80 | } 81 | } 82 | 83 | .react-autosuggest__suggestions-container { 84 | background-color: $white; 85 | border: 1px solid $white; 86 | box-shadow: 4px 4px 4px rgba($black, 0.1); 87 | font-family: $base-font-family; 88 | font-weight: 300; 89 | list-style-type: none; 90 | margin: 0; 91 | padding: 0; 92 | position: absolute; 93 | top: 60px; 94 | width: 85%; 95 | z-index: 2; 96 | 97 | @include media($small-screen) { 98 | width: 90%; 99 | } 100 | } 101 | 102 | .react-autosuggest__suggestion { 103 | cursor: pointer; 104 | overflow: hidden; 105 | padding: 10px 20px; 106 | text-overflow: ellipsis; 107 | white-space: nowrap; 108 | } 109 | 110 | .react-autosuggest__suggestion--focused { 111 | background-color: $light-gray; 112 | } 113 | 114 | .search-icon { 115 | background-color: darken($primary-color, 20); 116 | float: left; 117 | height: 50px; 118 | text-align: center; 119 | width: 15%; 120 | 121 | @include media($small-screen) { 122 | width: 10%; 123 | } 124 | 125 | input { 126 | box-sizing: border-box; 127 | height: 50px; 128 | padding: 13px; 129 | width: 100%; 130 | } 131 | } 132 | 133 | .suggestion-type { 134 | color: darken($primary-color, 10); 135 | font-weight: normal; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /search/index/indexer/abstract_index_tool.js: -------------------------------------------------------------------------------- 1 | const async = require("async"); 2 | const _ = require("lodash"); 3 | const ElasticSearch = require("elasticsearch"); 4 | 5 | const Logger = require("../../../common/logger"); 6 | const CONFIG = require("../../config.json"); 7 | 8 | class ElasticSearchLogger extends Logger { 9 | get DEFAULT_LOGGER_NAME() { 10 | return "elasticsearch"; 11 | } 12 | } 13 | 14 | /** 15 | * Base Class for Index Tools. This will allow us to share common ES functions 16 | * across tools used in the indexing process. 17 | * 18 | * @class AliasSwapper 19 | */ 20 | class AbstractIndexTool { 21 | 22 | get LOGGER_NAME() { 23 | return "alias-swapper"; 24 | } 25 | 26 | /** 27 | * Creates an instance of AliasSwapper. 28 | * 29 | * @param {any} adapter The search adapter to use for connecting to ElasticSearch 30 | */ 31 | constructor(adapter) { 32 | this.logger = new Logger({name: this.LOGGER_NAME}); 33 | 34 | this.client = adapter.getClient(); 35 | } 36 | 37 | 38 | 39 | /** 40 | * Gets an array of indices that are associated with the alias 41 | * 42 | * @param {any} aliasName The alias to check for. 43 | * @param {any} callback 44 | */ 45 | getIndexesForAlias(aliasName, callback) { 46 | this.logger.info( 47 | `Getting indexes for alias (${aliasName}).`); 48 | this.client.indices.getAlias({ 49 | alias: aliasName 50 | }, (err, response, status) => { 51 | let indices = new Array(); 52 | if(err) { this.logger.error(err); } 53 | else { 54 | _.forEach(response, function(item, key) { 55 | if (_.has(item, ['aliases', aliasName])) { 56 | indices.push(key); 57 | } 58 | }); 59 | } 60 | return callback(err, indices); 61 | }); 62 | } 63 | 64 | /** 65 | * Gets an array of indices that are associated with the alias 66 | * 67 | * @param {any} aliasName The alias to check for. 68 | * @param {any} callback 69 | */ 70 | aliasExists(aliasName, callback) { 71 | this.logger.info( 72 | `Checking existance of alias (${aliasName}).`); 73 | this.client.indices.existsAlias({ 74 | name: aliasName 75 | }, (err, response, status) => { 76 | if(err) { this.logger.error(err); } 77 | return callback(err, response); 78 | }); 79 | } 80 | 81 | 82 | 83 | /** 84 | * Initializes and executes the swapping of aliases for trials and terms 85 | * 86 | * @static 87 | * @param {any} adapter The search adapter to use for making requests to ElasticSearch 88 | * @param {any} trialIndexInfo Information about the index and alias for trials 89 | * @param {any} termIndexInfo Information about the index and alias for terms 90 | * @param {any} callback 91 | */ 92 | //IMPLEMENT THIS 93 | static init(adapter, trialIndexInfo, termIndexInfo, callback) { 94 | return callback(); 95 | } 96 | 97 | } 98 | 99 | module.exports = AbstractIndexTool; -------------------------------------------------------------------------------- /devops/datawarehouse-cron/copy_preamend.sql: -------------------------------------------------------------------------------- 1 | create or replace function copy_pre_amendment_records() returns void 2 | language plpgsql as $$ 3 | declare 4 | rec record; 5 | amendment_record record; 6 | study_nci_id varchar(50); 7 | amend_study_run_id varchar(50); 8 | results text; 9 | stmt text; 10 | columnNames text; 11 | tableNames varchar[] := ARRAY['dw_study','dw_study_anatomic_site','dw_study_arm_and_intervention', 12 | 'dw_study_collaborator','dw_study_disease','dw_study_eligibility_criteria','dw_study_grant', 13 | 'dw_study_other_identifier','dw_study_outcome_measure','dw_study_overall_status', 14 | 'dw_study_participating_site','dw_study_participating_site_investigators','dw_study_secondary_purpose','dw_study_association','dw_study_biomarker']; 15 | tableName varchar(1000); 16 | 17 | begin 18 | --Locate studies that has been amended and are undergoing an abstraction process 19 | for rec in select nci_id from dw_study where amendment_date is not null and lower(processing_status) 20 | in ('amendment submitted','accepted','on-hold') loop 21 | study_nci_id := rec.nci_id; 22 | 23 | foreach tableName in array tableNames 24 | loop 25 | -- Delete records 26 | IF tableName <> 'dw_study_association' THEN 27 | EXECUTE 'delete from ' || tableName || ' where nci_id=''' || rec.nci_id||''''; 28 | ELSE 29 | EXECUTE 'delete from ' || tableName || ' where study_a=''' || rec.nci_id||''''; 30 | END IF; 31 | --raise notice 'tableName here %',tableName; 32 | end loop; 33 | 34 | --Check if pre amendment copy exists 35 | for amendment_record in select run_id FROM hist_dw_study 36 | where nci_id=rec.nci_id 37 | and lower(processing_status) not in ('amendment submitted', 'accepted', 'on-hold', 'submitted', 'submission terminated', 'rejected') 38 | order by run_id desc limit 1 loop 39 | amend_study_run_id := amendment_record.run_id; 40 | foreach tableName in array tableNames 41 | loop 42 | IF tableName <> 'dw_study_association' THEN 43 | columnNames := array_to_string(ARRAY(SELECT c.column_name::text 44 | FROM information_schema.columns As c 45 | WHERE table_name = 'hist_'||tableName 46 | AND c.column_name NOT IN('run_id') 47 | ), ',') ; 48 | stmt := ' SELECT ' || columnNames || ' FROM hist_'||tableName|| ' WHERE nci_id='''||rec.nci_id||''' AND run_id='''||amend_study_run_id||'''' As sqlstmt ; 49 | ELSE 50 | columnNames := array_to_string(ARRAY(SELECT c.column_name::text 51 | FROM information_schema.columns As c 52 | WHERE table_name = 'hist_'||tableName 53 | AND c.column_name NOT IN('run_id') 54 | ), ',') ; 55 | stmt := ' SELECT ' || columnNames || ' FROM hist_'||tableName|| ' As o WHERE study_a='''||rec.nci_id||''' AND run_id='''||amend_study_run_id||''''As sqlstmt ; 56 | 57 | END IF; 58 | 59 | EXECUTE 'INSERT INTO '|| tableName || '(' || columnNames || ')' || stmt; 60 | end loop; 61 | 62 | end loop; 63 | end loop; 64 | end $$; 65 | 66 | select copy_pre_amendment_records(); -------------------------------------------------------------------------------- /devops/clinicaltrialsapi-prod/ctapi-config-web: -------------------------------------------------------------------------------- 1 | SSH_USER=ncimaint 2 | 3 | #KEY_PATH="-i /local/home/tomcatd/.ssh/awskey/cometprivatekey.pem" 4 | #PROXYKEY_PATH="-i /local/home/tomcatd/.ssh/awskey/sskey1.pem" 5 | 6 | #use ncimaint key 7 | PROXYKEY_PATH="-i /local/home/tomcatd/.ssh/awskey/ncimaint.pem" 8 | KEY_PATH=$PROXYKEY_PATH 9 | 10 | 11 | PROXY_HOST=x.x.x.x 12 | PROXYCMD="ProxyCommand ssh -o StrictHostKeyChecking=no ${PROXYKEY_PATH} ${SSH_USER}@${PROXY_HOST} nc %h %p" 13 | SSH_OPT="-o StrictHostKeyChecking=no" 14 | #SSH_OPT_PROXY="-o StrictHostKeyChecking=no -o '"'$PROXYCMD'"' " 15 | 16 | 17 | #disable ssh proxy 18 | #PROXYCMD="StrictHostKeyChecking=no" 19 | #KEY_PATH=$PROXYKEY_PATH 20 | 21 | RPM_PATH="/local/home/tomcatd/ctapi/rpms" 22 | TMP_PATH="~/install_tmp" 23 | CONF_PATH="/local/home/tomcatd/ctapi/conf" 24 | 25 | #copy all conf files 26 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " [ -d $TMP_PATH/conf ] && rm -rf $TMP_PATH/conf ;" 27 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " [ -d $TMP_PATH ] || mkdir $TMP_PATH ;" 28 | scp -r $SSH_OPT -o "$PROXYCMD" $KEY_PATH $CONF_PATH $SSH_USER@$HOST_NAME:$TMP_PATH 29 | 30 | # config apache 31 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo mv $TMP_PATH/conf/ctsapi-test.cancer.gov.crt /local/content/apache/conf/ssl.crt/" 32 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo mv $TMP_PATH/conf/ctsapi-test.cancer.gov.key /local/content/apache/conf/ssl.key/" 33 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo mv $TMP_PATH/conf/ctsapi.conf /local/content/apache/conf/vhosts/" 34 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo mv $TMP_PATH/conf/ctsapi-ssl.conf /local/content/apache/conf/vhosts-ssl/" 35 | 36 | 37 | # config nodejs 38 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo mv $TMP_PATH/conf/ctapi.initd /etc/init.d/ctapi" 39 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo chmod +x /etc/init.d/ctapi" 40 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo chkconfig ctapi on" 41 | 42 | #restart apache 43 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME 'sudo service apache restart ' 44 | 45 | 46 | # create a service user 47 | APP_USER=ctapi_user 48 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "id -u $APP_USER &>/dev/null || sudo adduser -u 4000 $APP_USER" 49 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo [ -d /home/$APP_USER/.ssh ] || sudo mkdir /home/$APP_USER/.ssh " 50 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo mv $TMP_PATH/conf/ctapi_user.pub /home/$APP_USER/.ssh/authorized_keys" 51 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo chown -R $APP_USER.$APP_USER /home/$APP_USER/.ssh" 52 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo chmod go-xrw /home/$APP_USER/.ssh" 53 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME 'sudo sed -i "/AllowUsers centos ncimaint/c\AllowUsers centos ncimaint ctapi_user" /etc/ssh/sshd_config' 54 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME 'sudo systemctl restart sshd' 55 | 56 | #delete all rpms copy 57 | #ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " [ -d $TMP_PATH ] && rm -rf $TMP_PATH ;" 58 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/Gender/Gender.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Url from '../../../../lib/Url'; 4 | 5 | class Gender extends Component { 6 | 7 | constructor() { 8 | super(); 9 | 10 | this.className = "filter-gender"; 11 | 12 | this.state = { 13 | isMaleChecked: true, 14 | isFemaleChecked: true 15 | } 16 | 17 | this.onChange = this.onChange.bind(this); 18 | } 19 | 20 | static propTypes = { 21 | paramField: PropTypes.string.isRequired, 22 | displayName: PropTypes.string.isRequired 23 | }; 24 | 25 | componentDidMount() { 26 | this.updateState(); 27 | } 28 | 29 | componentWillUpdate() { 30 | this.updateState(); 31 | } 32 | 33 | updateState() { 34 | let { isMaleChecked, isFemaleChecked } = this.state; 35 | let params = Url.getParams(); 36 | let paramValues = params[this.props.paramField] || []; 37 | let urlValues = {}; 38 | paramValues.forEach((paramValue) => { urlValues[paramValue.toLowerCase()] = 1; }); 39 | if (urlValues["male"] && urlValues["both"]) { 40 | if (!isMaleChecked) { 41 | this.setState({ isMaleChecked: true }); 42 | } 43 | } else if (urlValues["female"] && urlValues["both"]) { 44 | if (!isFemaleChecked) { 45 | this.setState({ isFemaleChecked: true }); 46 | } 47 | } else { 48 | if (!isMaleChecked || !isFemaleChecked) { 49 | this.setState({ 50 | isMaleChecked: true, 51 | isFemaleChecked: true 52 | }); 53 | } 54 | } 55 | } 56 | 57 | updateUrl() { 58 | let { isMaleChecked, isFemaleChecked } = this.state; 59 | let paramValues = []; 60 | if (isMaleChecked && isFemaleChecked) { 61 | // nada 62 | } else if (isMaleChecked) { 63 | paramValues = ["both", "male"]; 64 | } else if (isFemaleChecked) { 65 | paramValues = ["both", "female"]; 66 | } else { 67 | // nada 68 | } 69 | 70 | let params = {}; 71 | params[this.props.paramField] = paramValues; 72 | 73 | Url.overwriteParams({ path: "/clinical-trials", params }); 74 | } 75 | 76 | onChange(event) { 77 | let { isMaleChecked, isFemaleChecked } = this.state; 78 | switch(event.target.name) { 79 | case "male": 80 | return this.setState({isMaleChecked: !isMaleChecked}, this.updateUrl); 81 | case "female": 82 | return this.setState({isFemaleChecked: !isFemaleChecked}, this.updateUrl); 83 | } 84 | } 85 | 86 | render() { 87 | const { paramField, displayName } = this.props; 88 | 89 | return ( 90 |
91 |
92 | {displayName} 93 |
94 | 95 | 96 |
97 |
98 | 99 | 100 |
101 |
102 |
103 | ); 104 | } 105 | 106 | } 107 | 108 | export default Gender; 109 | -------------------------------------------------------------------------------- /search/index/indexer/trial/indexer.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const async = require("async"); 3 | const _ = require("lodash"); 4 | const Writable = require("stream").Writable; 5 | const JSONStream = require("JSONStream"); 6 | const moment = require("moment"); 7 | 8 | const AbstractIndexer = require("../abstract_indexer"); 9 | const Utils = require("../../../../common/utils"); 10 | 11 | const ES_MAPPING = require("./mapping.json"); 12 | const ES_SETTINGS = require("./settings.json"); 13 | const ES_PARAMS = { 14 | "esAlias": "cancer-clinical-trials", 15 | "esType": "trial", 16 | "esMapping": ES_MAPPING, 17 | "esSettings": ES_SETTINGS 18 | }; 19 | 20 | const transformStringToKey = Utils.transformStringToKey; 21 | 22 | class TrialIndexerStream extends Writable { 23 | 24 | constructor(trialIndexer) { 25 | super({objectMode: true}); 26 | this.trialIndexer = trialIndexer; 27 | this.logger = trialIndexer.logger; 28 | } 29 | 30 | _indexTrial(trial, done) { 31 | this.logger.info( 32 | `Indexing clinical trial with nci_id (${trial.nci_id}).`); 33 | 34 | this.trialIndexer.indexDocument({ 35 | "index": this.trialIndexer.esIndex, 36 | "type": this.trialIndexer.esType, 37 | "id": trial.nci_id, 38 | "body": trial 39 | }, (err, response, status) => { 40 | if(err) { this.logger.error(err); } 41 | this.trialIndexer.indexCounter++; 42 | 43 | return done(err, response); 44 | }); 45 | } 46 | 47 | _write(trial, enc, next) { 48 | this._indexTrial(trial, (err, response) => { 49 | return next(null, response); 50 | }); 51 | } 52 | } 53 | 54 | class TrialIndexer extends AbstractIndexer { 55 | 56 | get LOGGER_NAME() { 57 | return "trial-indexer"; 58 | } 59 | 60 | constructor(adapter, trials_file, params) { 61 | super(adapter, trials_file, params); 62 | this.indexCounter = 0; 63 | } 64 | 65 | indexFromTrialsJsonDump(callback) { 66 | let rs = fs.createReadStream(this.trials_file); 67 | let js = JSONStream.parse("*"); 68 | let is = new TrialIndexerStream(this); 69 | 70 | rs.pipe(js).pipe(is).on("finish", () => { 71 | this.logger.info(`Indexed ${this.indexCounter} ${this.esType} documents.`); 72 | callback(); 73 | }); 74 | } 75 | 76 | static init(adapter, trials_file, callback) { 77 | let indexer = new TrialIndexer(adapter, trials_file, ES_PARAMS); 78 | indexer.logger.info(`Started indexing (${indexer.esType}) indices.`); 79 | async.waterfall([ 80 | (next) => { indexer.indexExists(next); }, 81 | (exists, next) => { 82 | if(exists) { 83 | indexer.deleteIndex(next) 84 | } else { 85 | next(null, null); 86 | } 87 | }, 88 | (response, next) => { indexer.initIndex(next); }, 89 | (response, next) => { indexer.initMapping(next); }, 90 | (response, next) => { indexer.indexFromTrialsJsonDump(next); } 91 | ], (err) => { 92 | if(err) { indexer.logger.error(err); } 93 | indexer.logger.info(`Finished indexing (${indexer.esType}) indices.`); 94 | return callback(err,{ 95 | esIndex: indexer.esIndex, 96 | esAlias: indexer.esAlias 97 | }); 98 | }); 99 | } 100 | 101 | } 102 | 103 | module.exports = TrialIndexer; 104 | -------------------------------------------------------------------------------- /search/index/indexer/abstract_indexer.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const async = require("async"); 4 | const _ = require("lodash"); 5 | const JSONStream = require("JSONStream"); 6 | const ElasticSearch = require("elasticsearch"); 7 | const moment = require("moment"); 8 | 9 | const Logger = require("../../../common/logger"); 10 | const CONFIG = require("../../config.json"); 11 | 12 | class ElasticSearchLogger extends Logger { 13 | get DEFAULT_LOGGER_NAME() { 14 | return "elasticsearch"; 15 | } 16 | } 17 | 18 | class AbstractIndexer { 19 | 20 | get LOGGER_NAME() { 21 | return "abstract-indexer"; 22 | } 23 | 24 | constructor(adapter, trials_file, params) { 25 | this.logger = new Logger({name: this.LOGGER_NAME}); 26 | this.trials_file = trials_file; 27 | this.client = adapter.getClient(); 28 | 29 | this.esAlias = params.esAlias; 30 | 31 | //Index is based on time stamp 32 | //Get timestamp to append to alias name 33 | var now = moment(); 34 | let timestamp = now.format('YYYYMMDD_HHmmss'); 35 | 36 | //Set the index name to be alias appended with a timestamp. Seconds should be good for now. 37 | this.esIndex = this.esAlias + timestamp; 38 | 39 | this.esType = params.esType; 40 | this.esMapping = params.esMapping; 41 | this.esSettings = params.esSettings; 42 | } 43 | 44 | _toTitleCase(str) { 45 | return str.replace(/\w\S*/g, 46 | function(txt){ 47 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 48 | } 49 | ); 50 | } 51 | 52 | deleteIndex(callback) { 53 | this.logger.info(`Deleting index (${this.esIndex}).`); 54 | this.client.indices.delete({ 55 | index: this.esIndex 56 | }, (err, response, status) => { 57 | if(err) { this.logger.error(err); } 58 | return callback(err, response); 59 | }); 60 | } 61 | 62 | initIndex(callback) { 63 | this.logger.info(`Creating index (${this.esIndex}).`); 64 | this.client.indices.create({ 65 | index: this.esIndex, 66 | body: this.esSettings 67 | }, (err, response, status) => { 68 | if(err) { this.logger.error(err); } 69 | return callback(err, response); 70 | }); 71 | } 72 | 73 | indexExists(callback) { 74 | this.client.indices.exists({ 75 | index: this.esIndex 76 | }, (err, response, status) => { 77 | if(err) { this.logger.error(err); } 78 | return callback(err, response); 79 | }); 80 | } 81 | 82 | indexDocument(doc, callback) { 83 | this.client.index(doc, (err, response, status) => { 84 | if(err) { this.logger.error(err); } 85 | return callback(err, response); 86 | }); 87 | } 88 | 89 | initMapping(callback) { 90 | this.logger.info(`Updating mapping for index (${this.esIndex}).`); 91 | return this.client.indices.putMapping({ 92 | index: this.esIndex, 93 | type: this.esType, 94 | body: this.esMapping 95 | }, (err, response, status) => { 96 | if(err) { this.logger.error(err); } 97 | return callback(err, response); 98 | }); 99 | } 100 | 101 | // implement this 102 | static init(adapter, callback) { 103 | return callback(); 104 | } 105 | 106 | } 107 | 108 | module.exports = AbstractIndexer; 109 | -------------------------------------------------------------------------------- /devops/clinicaltrialsapi-prod/ctapi-deploy-code: -------------------------------------------------------------------------------- 1 | PATH_BASE="/local/content/deployment" 2 | DATA_PATH="/local/content/ctapi_data" 3 | 4 | APP_NAME=ctapi 5 | APP_USER=ctapi_user 6 | 7 | #SSH_USER=centos 8 | SSH_USER=ncimaint 9 | PROXYKEY_USER=ncimaint 10 | 11 | #use ncimaint key 12 | PROXYKEY_PATH="-i /var/keys/ncimaint.pem" 13 | KEY_PATH=$PROXYKEY_PATH 14 | 15 | SSH_OPT="-q -o StrictHostKeyChecking=no" 16 | 17 | PROXY_HOST=x.x.x.x 18 | 19 | PROXYCMD="ProxyCommand ssh ${SSH_OPT} ${PROXYKEY_PATH} ${PROXYKEY_USER}@${PROXY_HOST} nc %h %p" 20 | 21 | 22 | 23 | #disable ssh proxy 24 | #PROXYCMD="StrictHostKeyChecking=no" 25 | #KEY_PATH=$PROXYKEY_PATH 26 | 27 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME id 28 | 29 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 30 | [ -d ${PATH_BASE} ] || sudo mkdir ${PATH_BASE} ; \ 31 | sudo chmod o+xr $PATH_BASE ; \ 32 | sudo chown -R $APP_USER.$APP_USER $PATH_BASE ; " 33 | 34 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 35 | [ -d ${DATA_PATH} ] || sudo mkdir ${DATA_PATH} ; \ 36 | sudo chmod o+xr $DATA_PATH ; \ 37 | sudo chown -R $APP_USER.$APP_USER $DATA_PATH ; " 38 | 39 | 40 | ##### Deploy ######## 41 | 42 | 43 | SSH_USER=$APP_USER 44 | KEY_PATH="-i /var/keys/ctapi_user.pem" 45 | 46 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME id 47 | 48 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 49 | cd ${PATH_BASE}; \ 50 | [ -d backup ] || mkdir backup ; \ 51 | [ -d backup/$APP_NAME.bak ] && rm -rf backup/$APP_NAME.bak; \ 52 | [ -d $APP_NAME ] && mv $APP_NAME backup/$APP_NAME.bak; \ 53 | mkdir -p $APP_NAME/logs; \ 54 | ln -s /local/content/ctapi_data $APP_NAME/data; \ 55 | echo 0 " 56 | 57 | cat cts.tar | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "tar xf - -C $PATH_BASE/$APP_NAME" 58 | 59 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "cd $PATH_BASE/$APP_NAME/common && npm install " 60 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "cd $PATH_BASE/$APP_NAME/search/common && npm install " 61 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "cd $PATH_BASE/$APP_NAME/search/api && npm install " 62 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "cd $PATH_BASE/$APP_NAME/search/index && npm install " 63 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "cd $PATH_BASE/$APP_NAME/import/transform && npm install " 64 | 65 | 66 | if [ "$DEPLOY_ENV" = "production" ]; then 67 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME sed -i \'s/\"localhost\"/[\"172.17.5.201\", \"172.17.5.202\", \"172.17.5.203\"]/g\' $PATH_BASE/$APP_NAME/search/config.json 68 | elif [ "$DEPLOY_ENV" = "staging" ]; then 69 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME sed -i \'s/\"localhost\"/[\"172.18.5.201\", \"172.18.5.202\", \"172.18.5.203\"]/g\' $PATH_BASE/$APP_NAME/search/config.json 70 | elif [ "$DEPLOY_ENV" = "development" ]; then 71 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME sed -i \'s/\"localhost\"/[\"172.19.5.201\", \"172.19.5.202\", \"172.19.5.203\"]/g\' $PATH_BASE/$APP_NAME/search/config.json 72 | fi 73 | 74 | #### restart nodejs service 75 | 76 | SSH_USER=ncimaint 77 | KEY_PATH="-i /var/keys/ncimaint.pem" 78 | 79 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME id 80 | 81 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo service ctapi stop " 82 | 83 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo service ctapi start " 84 | -------------------------------------------------------------------------------- /devops/clinicaltrialsapi-prod/ctapi-install-backend: -------------------------------------------------------------------------------- 1 | #SSH_USER=centos 2 | SSH_USER=ncimaint 3 | 4 | #KEY_PATH="-i /local/home/tomcatd/.ssh/awskey/cometprivatekey.pem" 5 | #PROXYKEY_PATH="-i /local/home/tomcatd/.ssh/awskey/sskey1.pem" 6 | 7 | #use ncimaint key 8 | PROXYKEY_PATH="-i /local/home/tomcatd/.ssh/awskey/ncimaint.pem" 9 | KEY_PATH=$PROXYKEY_PATH 10 | 11 | 12 | PROXY_HOST=x.x.x.x 13 | PROXYCMD="ProxyCommand ssh -o StrictHostKeyChecking=no ${PROXYKEY_PATH} ${SSH_USER}@${PROXY_HOST} nc %h %p" 14 | SSH_OPT="-o StrictHostKeyChecking=no" 15 | #SSH_OPT_PROXY="-o StrictHostKeyChecking=no -o '"'$PROXYCMD'"' " 16 | 17 | #PROXYCMDTT="-o \"ProxyCommand ssh -tt -o StrictHostKeyChecking=no ${PROXYKEY_PATH} centos@${PROXY_HOST} nc %h %p \" " 18 | 19 | 20 | #disable ssh proxy 21 | #PROXYCMD="StrictHostKeyChecking=no" 22 | #KEY_PATH=$PROXYKEY_PATH 23 | 24 | RPM_PATH="/local/home/tomcatd/ctapi/rpms" 25 | TMP_PATH="~/install_tmp" 26 | 27 | 28 | # OS modify 29 | ssh -tt $SSH_OPT -o " $PROXYCMD " $KEY_PATH $SSH_USER@$HOST_NAME 'sudo sed -i "/Defaults requiretty/c\# Defaults requiretty" /etc/sudoers' 30 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " sudo systemctl disable iptables " 31 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " sudo systemctl stop iptables " 32 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " sudo unlink /etc/localtime; sudo ln -s /usr/share/zoneinfo/America/New_York /etc/localtime " 33 | 34 | 35 | #copy all rpms 36 | #ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " [ -d $TMP_PATH ] && [ -d $TMP_PATH/rpms ] && rm -rf $TMP_PATH/rpms ;" 37 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " [ -d $TMP_PATH ] || mkdir $TMP_PATH ;" 38 | scp -r $SSH_OPT -o "$PROXYCMD" $KEY_PATH $RPM_PATH $SSH_USER@$HOST_NAME:$TMP_PATH 39 | 40 | 41 | #install cbiit java 42 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 43 | sudo yum -y localinstall $TMP_PATH/rpms/cbiit-jdk1.8-102-1.el7.x86_64.rpm" 44 | 45 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " [ -f /usr/bin/java ] || sudo ln -s /usr/java8/bin/java /usr/bin/java" 46 | 47 | 48 | 49 | #install elasticsearch 50 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 51 | sudo yum -y localinstall https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/rpm/elasticsearch/2.3.5/elasticsearch-2.3.5.rpm" 52 | 53 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo systemctl enable elasticsearch" 54 | 55 | #install epel repo 56 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo yum -y install epel-release" 57 | 58 | 59 | #install monit 60 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo yum -y install monit" 61 | 62 | #install git 63 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo yum -y install git" 64 | 65 | # create elastic working dir 66 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo [ -d /local/elasticsearch ] || sudo mkdir /local/elasticsearch " 67 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo chmod 755 /local/elasticsearch" 68 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo chown -R elasticsearch:elasticsearch /local/elasticsearch" 69 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME 'sudo sed -i "/\#DATA_DIR=\/var\/lib\/elasticsearch/c\DATA_DIR=\/local\/elasticsearch" /etc/sysconfig/elasticsearch' 70 | 71 | #delete all rpms copy 72 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " [ -d $TMP_PATH ] && rm -rf $TMP_PATH ;" 73 | 74 | -------------------------------------------------------------------------------- /search/client/components/ClinicalTrial/Detail/Detail.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Fetch from 'isomorphic-fetch'; 3 | 4 | import ApiFetch from '../../../lib/ApiFetch.js'; 5 | import Link from '../../Link'; 6 | import Url from '../../../lib/Url'; 7 | import Location from '../../../lib/Location'; 8 | 9 | function createMarkup(text) { 10 | if(!text) { return {__html: ""}; } 11 | return {__html: text.replace(/(?:\r\n|\r|\n)/g, '
')}; 12 | } 13 | 14 | export default class extends Component { 15 | 16 | constructor() { 17 | super(); 18 | this.state = { 19 | id: null, 20 | trial: null, 21 | isLoading: true 22 | }; 23 | } 24 | 25 | componentDidMount() { 26 | let { id } = Url.getParams(); 27 | this.setState({ id }); 28 | ApiFetch(`clinical-trial/${id}`) 29 | .then(response => response.json()) 30 | .then((json) => { 31 | this.setState({ 32 | trial: json, 33 | isLoading: false 34 | }); 35 | }); 36 | } 37 | 38 | render() { 39 | let { id, trial, isLoading } = this.state; 40 | if (trial) { 41 | return ( 42 |
43 | 46 | 54 |
55 |
56 | 57 |

{trial.brief_title}

58 |
59 |
60 |

{trial.brief_summary}

61 |
62 |
63 |

Locations

64 | 75 |
76 |
77 |

Detailed Info

78 |
79 |
80 |
81 |
82 |
83 | ); 84 | } else if (!isLoading) { 85 | return ( 86 |
87 |
88 |
89 |

No trial with id {id} found.

90 |
91 |
92 |
93 | ); 94 | } else { 95 | return ( 96 |
97 | ) 98 | } 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /devops/clinicaltrialsapi-prod/ctapi-install-frontend: -------------------------------------------------------------------------------- 1 | #SSH_USER=centos 2 | SSH_USER=ncimaint 3 | 4 | #KEY_PATH="-i /local/home/tomcatd/.ssh/awskey/cometprivatekey.pem" 5 | #PROXYKEY_PATH="-i /local/home/tomcatd/.ssh/awskey/sskey1.pem" 6 | 7 | #use ncimaint key 8 | PROXYKEY_PATH="-i /local/home/tomcatd/.ssh/awskey/ncimaint.pem" 9 | KEY_PATH=$PROXYKEY_PATH 10 | 11 | 12 | PROXY_HOST=x.x.x.x 13 | PROXYCMD="ProxyCommand ssh -o StrictHostKeyChecking=no ${PROXYKEY_PATH} ${SSH_USER}@${PROXY_HOST} nc %h %p" 14 | SSH_OPT="-o StrictHostKeyChecking=no" 15 | #SSH_OPT_PROXY="-o StrictHostKeyChecking=no -o '"'$PROXYCMD'"' " 16 | 17 | #PROXYCMDTT="-o \"ProxyCommand ssh -tt -o StrictHostKeyChecking=no ${PROXYKEY_PATH} centos@${PROXY_HOST} nc %h %p \" " 18 | 19 | 20 | 21 | #disable ssh proxy 22 | #PROXYCMD="StrictHostKeyChecking=no" 23 | #KEY_PATH=$PROXYKEY_PATH 24 | 25 | RPM_PATH="/local/home/tomcatd/ctapi/rpms" 26 | TMP_PATH="~/install_tmp" 27 | 28 | # OS modify 29 | ssh -tt $SSH_OPT -o " $PROXYCMD " $KEY_PATH $SSH_USER@$HOST_NAME 'sudo sed -i "/Defaults requiretty/c\# Defaults requiretty" /etc/sudoers' 30 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " sudo systemctl disable iptables " 31 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " sudo systemctl stop iptables " 32 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " sudo unlink /etc/localtime; sudo ln -s /usr/share/zoneinfo/America/New_York /etc/localtime " 33 | 34 | 35 | #copy all rpms 36 | #ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " [ -d $TMP_PATH/rpms ] && rm -rf $TMP_PATH/rpms ;" 37 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " [ -d $TMP_PATH ] || mkdir $TMP_PATH " 38 | scp -r $SSH_OPT -o "$PROXYCMD" $KEY_PATH $RPM_PATH $SSH_USER@$HOST_NAME:$TMP_PATH 39 | 40 | 41 | 42 | #install cbiit apache 43 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 44 | sudo yum -y localinstall $TMP_PATH/rpms/cbiit-openssl1.0.1-t-2.el7.x86_64.rpm" 45 | 46 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 47 | sudo yum -y localinstall $TMP_PATH/rpms/cbiit-expat2-1.0-2.el7.x86_64.rpm" 48 | 49 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 50 | sudo yum -y localinstall $TMP_PATH/rpms/cbiit-openldap2.4-44-2.el7.x86_64.rpm" 51 | 52 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 53 | sudo yum -y localinstall $TMP_PATH/rpms/cbiit-pcre8-38-1.el7.x86_64.rpm" 54 | 55 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 56 | sudo yum -y localinstall $TMP_PATH/rpms/cbiit-openssl1.0.1-t-2.el7.x86_64.rpm" 57 | 58 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 59 | sudo yum -y localinstall $TMP_PATH/rpms/cbiit-apache2.4-20-1.el7.x86_64.rpm" 60 | 61 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " sudo service apache start " 62 | 63 | #install nodejs 64 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " \ 65 | curl --silent --location https://rpm.nodesource.com/setup_6.x | sudo -E bash -" 66 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " sudo yum install -y nodejs " 67 | 68 | 69 | 70 | #install epel repo 71 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo yum -y install epel-release" 72 | 73 | 74 | #install monit 75 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo yum -y install monit" 76 | 77 | #install git 78 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo yum -y install git" 79 | 80 | #install git 81 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME "sudo yum -y install unzip" 82 | 83 | #delete all rpms copy 84 | ssh $SSH_OPT -o "$PROXYCMD" $KEY_PATH $SSH_USER@$HOST_NAME " [ -d $TMP_PATH ] && rm -rf $TMP_PATH " 85 | -------------------------------------------------------------------------------- /search/client/lib/Url.js: -------------------------------------------------------------------------------- 1 | import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; 2 | import QueryString from 'query-string'; 3 | import Location from './Location'; 4 | 5 | // TODO: fix this, it's hacky 6 | let loc = canUseDOM ? location : {}; 7 | 8 | const _getUrlObj = () => { 9 | let params = QueryString.parse(loc.search); 10 | // ensure each param value is an array 11 | Object.keys(params).forEach((key) => { 12 | if (!(params[key] instanceof Array)) { 13 | params[key] = [params[key]]; 14 | } 15 | }); 16 | let path = loc.pathname ? loc.pathname.toString() : "/"; 17 | return { path, params }; 18 | }; 19 | 20 | const _addParams = (params, addParams) => { 21 | Object.keys(addParams).forEach((key) => { 22 | let values = addParams[key]; 23 | if (!(values instanceof Array)) { 24 | values = [values]; 25 | } 26 | if (!params[key]) { 27 | params[key] = []; 28 | } 29 | values.forEach((value) => { 30 | params[key].push(value); 31 | }); 32 | }); 33 | return params; 34 | } 35 | 36 | const _overwriteParams = (params, addParams) => { 37 | Object.keys(addParams).forEach((key) => { 38 | let values = addParams[key]; 39 | if (!(values instanceof Array)) { 40 | values = [values]; 41 | } 42 | if (!params[key]) { 43 | params[key] = []; 44 | } 45 | params[key] = values; 46 | }); 47 | return params; 48 | } 49 | 50 | const _removeParams = (params, removeParams) => { 51 | Object.keys(removeParams).forEach((key) => { 52 | let values = removeParams[key]; 53 | if (!(values instanceof Array)) { 54 | values = [values]; 55 | } 56 | values.forEach((value) => { 57 | if (value) { 58 | let i = params[key].findIndex((pValue) => { 59 | return pValue === value; 60 | }); 61 | if (i >= 0) { 62 | params[key].splice(i, 1); 63 | } 64 | } else { 65 | params[key] = []; 66 | } 67 | }); 68 | if (!params[key].length) { 69 | delete params[key]; 70 | } 71 | }); 72 | return params; 73 | } 74 | 75 | const _updateUrl = (path, params) => { 76 | let url = path; 77 | let query = QueryString.stringify(params); 78 | if (query) { url += '?' + query; } 79 | Location.push(url); 80 | } 81 | 82 | class Url { 83 | 84 | static get DEFAULT_PARAMS() { 85 | return { 86 | "current_trial_status": "Active" 87 | }; 88 | } 89 | 90 | static getParams() { 91 | return _getUrlObj()["params"]; 92 | } 93 | 94 | static areParamsDiff(params) { 95 | // TODO: hacky 96 | return QueryString.stringify(params) !== QueryString.stringify(Url.getParams()); 97 | } 98 | 99 | static stringifyParams(params) { 100 | return QueryString.stringify(params); 101 | } 102 | 103 | static newParams({ path, params }) { 104 | _updateUrl(path, _addParams({}, params)); 105 | } 106 | 107 | static newParamsWithDefault({ path, params }) { 108 | _updateUrl(path, _addParams(Url.DEFAULT_PARAMS, params)); 109 | } 110 | 111 | static addParams({ path, params }) { 112 | let currentParams = _getUrlObj().params; 113 | _updateUrl(path, _addParams(currentParams, params)); 114 | } 115 | 116 | static removeParams({ path, params }) { 117 | let currentParams = _getUrlObj().params; 118 | _updateUrl(path, _removeParams(currentParams, params)); 119 | } 120 | 121 | static overwriteParams({ path, params }) { 122 | let currentParams = _getUrlObj().params; 123 | _updateUrl(path, _overwriteParams(currentParams, params)); 124 | } 125 | 126 | static clearParams({ path }) { 127 | _updateUrl(path, {}); 128 | } 129 | 130 | static clearParamsToDefault({ path }) { 131 | _updateUrl(path, Url.DEFAULT_PARAMS); 132 | } 133 | 134 | } 135 | 136 | export default Url; 137 | -------------------------------------------------------------------------------- /search/client/components/ClinicalTrial/Results/Results.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Waypoint from 'react-waypoint'; 4 | import Fetch from 'isomorphic-fetch'; 5 | 6 | import ApiFetch from '../../../lib/ApiFetch.js'; 7 | import ClinicalTrialResult from '../Result'; 8 | import PromptFiltering from '../../Search/PromptFiltering'; 9 | import Link from '../../Link' 10 | import Location from '../../../lib/Location'; 11 | import Url from '../../../lib/Url'; 12 | 13 | export default class extends Component { 14 | 15 | get LOAD_SIZE() { 16 | return 10; 17 | } 18 | 19 | constructor() { 20 | super(); 21 | 22 | this.state = { 23 | query: {}, 24 | trials: [], 25 | size: this.LOAD_SIZE, 26 | from: 0, 27 | hasMore: false, 28 | isLoading: false, 29 | queryLoaded: false, 30 | total: 0 31 | }; 32 | 33 | this.loadTrials = this.loadTrials.bind(this); 34 | } 35 | 36 | static propTypes = { 37 | reportTotalResults: PropTypes.func 38 | }; 39 | 40 | loadTrials() { 41 | const { trials, size, from, isLoading } = this.state; 42 | let query = Url.getParams(); 43 | let queryCopy = Url.getParams(); 44 | query.size = size; 45 | query.from = from; 46 | query.include = [ 47 | "nci_id", 48 | "brief_title", 49 | "current_trial_status", 50 | "phase", 51 | "arms.interventions.intervention_name", 52 | "arms.interventions.intervention_type", 53 | "diseases.disease.display_name" 54 | ] 55 | this.setState({ 56 | isLoading: true, 57 | query: queryCopy 58 | }); 59 | const { reportTotalResults } = this.props; 60 | ApiFetch(`clinical-trials?${Url.stringifyParams(query)}`) 61 | .then(response => response.json()) 62 | .then((json) => { 63 | let numTrialsLoaded = from + size; 64 | let hasMore = json.total > numTrialsLoaded; 65 | this.setState({ 66 | trials: from ? trials.concat(json.trials) : json.trials, 67 | from: numTrialsLoaded, 68 | hasMore, 69 | isLoading: false, 70 | queryLoaded: true, 71 | total: json.total 72 | }); 73 | reportTotalResults(json.total); 74 | }); 75 | } 76 | 77 | componentDidMount() { 78 | if (!this.state.queryLoaded) { 79 | this.loadTrials(); 80 | } 81 | } 82 | 83 | componentWillUpdate() {} 84 | 85 | componentDidUpdate() { 86 | const queryChanged = Url.areParamsDiff(this.state.query); 87 | if (queryChanged && this.state.queryLoaded) { 88 | this.setState({ from: 0, queryLoaded: false }, () => { 89 | this.loadTrials(); 90 | }); 91 | } 92 | } 93 | 94 | componentWillReceiveProps(nextProps) {} 95 | 96 | placeWaypoint(itemNum, listLength) { 97 | if(listLength >= this.LOAD_SIZE && itemNum === listLength - 3) { 98 | return ( 99 | 100 | ) 101 | } 102 | } 103 | 104 | render() { 105 | const { trials, total } = this.state; 106 | 107 | if (total) { 108 | return ( 109 |
110 | 111 |
112 | {trials.map((trial, i) => 113 | 114 | {this.placeWaypoint(i, trials.length)} 115 | 116 | )} 117 |
118 |
119 | ) 120 | } else { 121 | return ( 122 |
123 | No results found. Start a new search? 124 |
125 | ) 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /search/index/sample_queries: -------------------------------------------------------------------------------- 1 | curl -XPOST "http://localhost:9200/cancer-clinical-trials/_suggest" -d ' 2 | { 3 | "trial": { 4 | "nested": { 5 | "path": "diseases", 6 | "text": "progressive disease", 7 | "completion": { 8 | "field": "disease_suggest" 9 | } 10 | } 11 | } 12 | }' 13 | 14 | curl -XPOST "http://localhost:9200/cancer-clinical-trials/_search" -d ' 15 | { 16 | "query": { 17 | "bool": { 18 | "must": [ 19 | { 20 | "nested": { 21 | "path": "diseases", 22 | "query": { 23 | "bool": { 24 | "must": [ 25 | { "match": { "disease_suggest": "progressive disease" }} 26 | ] 27 | }}}} 28 | ] 29 | }}}' 30 | 31 | curl -XPOST "http://localhost:9200/cancer-terms/_search?explain&pretty" -d ' 32 | { 33 | "query": { 34 | "match": { 35 | "term": "myeloma" 36 | } 37 | } 38 | }' 39 | 40 | curl -XPOST "http://localhost:9200/cancer-clinical-trials/_search" -d ' 41 | { 42 | "query": { 43 | "match": { 44 | "anatomic_sites": "Multiple Myeloma" 45 | } 46 | } 47 | }' 48 | 49 | curl -XPOST "http://localhost:9200/cancer-clinical-trials/_search?explain&pretty=1" -d ' 50 | { 51 | "query": { 52 | "match": { 53 | "_all": "myeloma" 54 | } 55 | }, 56 | "size": 0 57 | }' 58 | 59 | curl -XPOST "http://localhost:9200/cancer-terms/_search?explain&pretty=1" -d ' 60 | { 61 | "query": { 62 | "match": { 63 | "term": "small cell" 64 | } 65 | } 66 | }' 67 | 68 | curl -XPOST "http://localhost:9200/cancer-terms/_search?explain&pretty=1" -d ' 69 | { 70 | "query": { 71 | "function_score": { 72 | "query": { 73 | "bool": { 74 | "must": [ 75 | { 76 | "match": { 77 | "term": "basal cell carcinoma" 78 | } 79 | }, 80 | { 81 | "match": { 82 | "term": { 83 | "type": "phrase", 84 | "query": "basal cell carcinoma" 85 | } 86 | } 87 | } 88 | ] 89 | } 90 | }, 91 | "functions": [ 92 | { 93 | "field_value_factor": { 94 | "field": "count_normalized", 95 | "factor": 0.25 96 | } 97 | } 98 | ], 99 | "boost_mode": "multiply" 100 | } 101 | } 102 | }' 103 | 104 | curl -XPOST "http://localhost:9200/cancer-terms/_search?pretty=1" -d ' 105 | { 106 | "aggs" : { 107 | "count_normalized_ranges" : { 108 | "range" : { 109 | "field" : "count_normalized", 110 | "ranges" : [ 111 | { "to" : 0.1 }, 112 | { "from" : 0.1, "to" : 0.2 }, 113 | { "from" : 0.2, "to" : 0.3 }, 114 | { "from" : 0.3, "to" : 0.4 }, 115 | { "from" : 0.4, "to" : 0.5 }, 116 | { "from" : 0.5, "to" : 0.6 }, 117 | { "from" : 0.6, "to" : 0.7 }, 118 | { "from" : 0.7, "to" : 0.8 }, 119 | { "from" : 0.8, "to" : 0.9 }, 120 | { "from" : 0.9 } 121 | ] 122 | } 123 | } 124 | } 125 | }' 126 | 127 | curl -XPOST "http://localhost:9200/cancer-clinical-trials/_search?explain&pretty=1" -d ' 128 | { 129 | "query": { 130 | "match": { 131 | "_all": "myeloma" 132 | } 133 | }, 134 | "size": 0 135 | }' 136 | 137 | curl 'localhost:9200/cancer-terms/_analyze?pretty=1&analyzer=autocomplete_search' -d 'dana-farber' 138 | 139 | curl -XPOST "http://localhost:9200/cancer-clinical-trials/_search" -d ' 140 | { 141 | "query": { 142 | "bool": { 143 | "filter": { 144 | "term": { 145 | "nci_id": "nci-2009-00376" 146 | } 147 | } 148 | } 149 | } 150 | }' 151 | -------------------------------------------------------------------------------- /examples/full-text-search.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Full text search example 6 | 7 | 8 | 9 | 85 | 86 | 87 | 88 |
89 | 90 |


91 |

92 | 93 |

94 | 95 |
96 |
97 | 98 |
99 | 100 | 101 | -------------------------------------------------------------------------------- /search/client/components/Search/Filter/Select/Select.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import ReactSelect from 'react-select'; 3 | import AutosuggestHighlight from 'autosuggest-highlight'; 4 | 5 | import ApiFetch from '../../../../lib/ApiFetch.js'; 6 | import Url from '../../../../lib/Url'; 7 | 8 | class Select extends Component { 9 | 10 | constructor() { 11 | super(); 12 | 13 | this.className = "filter-select"; 14 | 15 | this.state = { 16 | selectedValues: [], 17 | inputText: "", 18 | minimumInput: 0, 19 | placeholderText: "" 20 | } 21 | 22 | this.getOptions = this.getOptions.bind(this); 23 | this.onChange = this.onChange.bind(this); 24 | this.onInputChange = this.onInputChange.bind(this); 25 | this.optionRenderer = this.optionRenderer.bind(this); 26 | } 27 | 28 | static propTypes = { 29 | paramField: PropTypes.string.isRequired, 30 | displayName: PropTypes.string.isRequired 31 | }; 32 | 33 | escapeRegexCharacters(str) { 34 | return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 35 | } 36 | 37 | componentWillUpdate() { 38 | let params = Url.getParams(); 39 | let paramValues = params[this.props.paramField] || []; 40 | let values = paramValues.map((value) => { 41 | return { term: value }; 42 | }); 43 | if (JSON.stringify(this.state.selectedValues) !== JSON.stringify(values)) { 44 | this.setState({ 45 | selectedValues: values 46 | }); 47 | } 48 | } 49 | 50 | getOptions(input, callback) { 51 | let { paramField } = this.props; 52 | ApiFetch(`terms?term=&term_type=${paramField}&size=100`) 53 | .then((response) => { 54 | return response.json(); 55 | }).then((json) => { 56 | return callback(null, { 57 | options: json.terms, 58 | complete: true 59 | }); 60 | }); 61 | } 62 | 63 | onChange(values) { 64 | let params = {}; 65 | params[this.props.paramField] = []; 66 | if (values && values.length) { 67 | params[this.props.paramField] = values.map((value) => { 68 | return value.term; 69 | }); 70 | } 71 | Url.overwriteParams({ path: "/clinical-trials", params }); 72 | 73 | this.setState({ selectedValues: values }); 74 | } 75 | 76 | onInputChange(input) { 77 | this.setState({ inputText: this.escapeRegexCharacters(input) }); 78 | } 79 | 80 | optionRenderer(option) { 81 | const query = this.state.inputText; 82 | const suggestionText = option.term; 83 | const matches = AutosuggestHighlight.match(suggestionText, query); 84 | const parts = AutosuggestHighlight.parse(suggestionText, matches); 85 | return 86 | { 87 | parts.map((part, index) => { 88 | const className = part.highlight ? 'filter-select-highlight' : null; 89 | 90 | return ( 91 | {part.text} 92 | ); 93 | }) 94 | } 95 | 96 | } 97 | 98 | render() { 99 | const { paramField, displayName } = this.props; 100 | const htmlId = paramField.split(".").join("-"); 101 | const inputProps = { 102 | id: htmlId, 103 | }; 104 | 105 | return ( 106 |
107 | 108 | 121 |
122 | ); 123 | } 124 | 125 | } 126 | 127 | export default Select; 128 | -------------------------------------------------------------------------------- /search/index/test/integration/indexer/index_cleaner.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | const IndexCleaner = require("../../../indexer/index_cleaner"); 3 | const TestESAdapter = require("../../../../common/search_adapters/testable_elasticsearch_adapter"); 4 | const async = require("async"); 5 | 6 | describe('IndexCleaner', _ => { 7 | 8 | //TODO: Either load real data into local ES server before tests, OR 9 | //Replace instances of TestESAdapter with a mock Adapter object and move 10 | //these tests to a unit tests folder. 11 | 12 | it('Should return indices older than 5 days ago', function(done) { 13 | this.timeout = 15000; 14 | 15 | let alias = "cancer-clinical-trials"; 16 | let expectedIndices = [ 17 | "cancer-clinical-trials2016816_171717", 18 | "cancer-clinical-trials2016818_131343", 19 | "cancer-clinical-trials2016818_181844", 20 | "cancer-clinical-trials2016818_121254", 21 | "cancer-clinical-trials2016818_8856", 22 | "cancer-clinical-trials2016818_13130", 23 | "cancer-clinical-trials2016818_884", 24 | "cancer-clinical-trials2016823_101049", 25 | "cancer-clinical-trials2016818_8832", 26 | "cancer-clinical-trials2016818_9932", 27 | "cancer-clinical-trials2016818_882", 28 | "cancer-clinical-trials2016817_222211", 29 | "cancer-clinical-trials2016819_111159", 30 | "cancer-clinical-trials2016818_13136", 31 | "cancer-clinical-trials2016826_151513", 32 | "cancer-clinical-trials2016818_222252", 33 | "cancer-clinical-trials2016818_11119", 34 | "cancer-clinical-trials2016822_11116", 35 | "cancer-clinical-trials2016818_8811" 36 | ]; 37 | 38 | let cleaner = new IndexCleaner(TestESAdapter); 39 | 40 | cleaner.getIndices(alias, 5, (err, indices)=>{ 41 | expect(indices.sort()).to.deep.eql(expectedIndices.sort()); 42 | done(); 43 | }) 44 | 45 | }); 46 | 47 | 48 | it('Should remove the index associated from the alias from the list of indices', function(done) { 49 | 50 | let alias = "cancer-clinical-trials"; 51 | 52 | let indices = [ 53 | 'cancer-clinical-trials201691_9930', 54 | 'cancer-clinical-trials2016818_181844', 55 | 'cancer-clinical-trials2016826_151513' 56 | ]; 57 | 58 | let expectedIndices = [ 59 | 'cancer-clinical-trials2016818_181844', 60 | 'cancer-clinical-trials2016826_151513' 61 | ]; 62 | 63 | let cleaner = new IndexCleaner(TestESAdapter); 64 | cleaner.filterAliasedIndices(alias, indices, (err, filteredIndices) => { 65 | 66 | expect(filteredIndices).to.eql(expectedIndices); 67 | 68 | done(); 69 | }); 70 | 71 | }) 72 | 73 | it('Should delete an index', function(done) { 74 | 75 | let index = 'cancer-clinical-trials2016830_111127'; 76 | let aliasName = 'cancer-clinical-trials'; 77 | 78 | let cleaner = new IndexCleaner(TestESAdapter); 79 | 80 | async.waterfall([ 81 | (next) => { cleaner.deleteIndices(index,next); }, //Gets all indices older than 7 days 82 | (next) => { cleaner.getIndices(aliasName, 0, next); }, 83 | (indices, next) => { 84 | expect(indices).to.not.include(index); 85 | next(false); 86 | } 87 | ], (err) => { 88 | //TODO: Make better error message -- this is important when 89 | //we delete an index that does not exist or 90 | expect(err).to.be.null; 91 | return done(); 92 | }); 93 | }) 94 | 95 | it ('Should delete all old indices', function(done) { 96 | 97 | let aliasName = 'cancer-clinical-trials'; 98 | let numdays = 8; 99 | 100 | let cleaner = new IndexCleaner(TestESAdapter); 101 | 102 | cleaner.cleanIndicesForAlias(aliasName, numdays, (err) => { 103 | //TODO: This is throwing an uncaught assertion somewhere 104 | return done(err); 105 | }); 106 | }) 107 | 108 | }); 109 | -------------------------------------------------------------------------------- /search/api/test/search/searcher.spec.js: -------------------------------------------------------------------------------- 1 | //import { describe, it } from 'mocha'; 2 | 3 | import { expect } from 'chai'; 4 | const Bodybuilder = require("bodybuilder"); 5 | const querystring = require('querystring'); 6 | const Searcher = require("../../search/searcher"); 7 | const AbstractSearchAdapter = require("../../../common/search_adapters/abstract_search_adapter"); 8 | 9 | /** 10 | * Represents a mock ES adapter for use by the Searcher class. 11 | * 12 | * @class SearcherMockClient 13 | * @extends {AbstractSearchAdapter} 14 | */ 15 | class SearcherMockAdapter extends AbstractSearchAdapter { 16 | 17 | /** 18 | * Creates an instance of SearcherMockClient. 19 | * 20 | */ 21 | constructor() { 22 | console.log("CONSTRUCTOR") 23 | super(); 24 | this.client = false; 25 | } 26 | } 27 | 28 | describe('searcher', _ => { 29 | 30 | it('Should Build a NCT ID Query', () => { 31 | let searcher = new Searcher(new SearcherMockAdapter()); 32 | //let query = {}; 33 | let query = searcher._searchTrialById("NCT02289950"); 34 | 35 | expect(query).to.eql({ 36 | query: { 37 | match: { 38 | "nct_id": "NCT02289950" 39 | } 40 | } 41 | }); 42 | 43 | }); 44 | 45 | it('Should Build a NCI ID Query', () => { 46 | let searcher = new Searcher(new SearcherMockAdapter()); 47 | let query = searcher._searchTrialById("NCI-2015-00253"); 48 | 49 | expect(query).to.eql({ 50 | query: { 51 | match: { 52 | "nci_id": "NCI-2015-00253" 53 | } 54 | } 55 | }); 56 | 57 | }); 58 | 59 | 60 | it('Should Add String Filter With Single Value', () => { 61 | let searcher = new Searcher(new SearcherMockAdapter()); 62 | let body = new Bodybuilder(); 63 | let q = querystring.parse("current_trial_status=Active"); 64 | searcher._addStringFilters(body, q); 65 | 66 | 67 | expect(body.build("v2")).to.eql({ 68 | query: { 69 | bool: { 70 | filter: { 71 | term: { 72 | "current_trial_status": "active" 73 | } 74 | } 75 | } 76 | } 77 | }); 78 | 79 | }); 80 | 81 | it('Should Add String Filter with multiple values', () => { 82 | let searcher = new Searcher(new SearcherMockAdapter()); 83 | let body = new Bodybuilder(); 84 | let q = querystring.parse("current_trial_status=Active¤t_trial_status=Temporarily+Closed+to+Accrual"); 85 | searcher._addStringFilters(body, q); 86 | 87 | 88 | expect(body.build("v2")).to.eql({ 89 | query: { 90 | bool: { 91 | filter: { 92 | bool: { 93 | "must": [ 94 | { 95 | "query": { 96 | "bool": { 97 | "filter": { 98 | "bool": { 99 | "should": [ 100 | { 101 | "term": { "current_trial_status": "active" } 102 | }, 103 | { 104 | "term": { "current_trial_status": "temporarily closed to accrual" } 105 | } 106 | ] 107 | } 108 | } 109 | } 110 | } 111 | } 112 | ] 113 | } 114 | } 115 | } 116 | } 117 | }); 118 | 119 | }); 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /search/client/lib/ValidParams.js: -------------------------------------------------------------------------------- 1 | const params = [ 2 | { 3 | "param_key": "_diseases", 4 | "display_name": "Disease", 5 | "filter_type": "suggest", 6 | "category": "get started" 7 | }, { 8 | "param_key": "_locations", 9 | "display_name": "Location", 10 | "filter_type": "suggest", 11 | "category": "get started" 12 | }, { 13 | "param_key": "sites.org_family", 14 | "display_name": "Network/Organization", 15 | "filter_type": "suggest", 16 | "category": "get started" 17 | }, { 18 | "param_key": "eligibility.structured.gender", 19 | "display_name": "Gender", 20 | "filter_type": "gender", 21 | "category": "get started" 22 | }, { 23 | "param_key": "sites.org_name", 24 | "display_name": "Hospital/Center", 25 | "filter_type": "suggest", 26 | "category": "organization" 27 | }, { 28 | "param_key": "_treatments", 29 | "display_name": "Treatment", 30 | "filter_type": "suggest", 31 | "category": "treatment" 32 | }, { 33 | "param_key": "arms.interventions.intervention_type", 34 | "display_name": "Treatment Type", 35 | "filter_type": "select", 36 | "category": "treatment" 37 | }, { 38 | "param_key": "acronym", 39 | "display_name": "Acronym", 40 | "filter_type": "text", 41 | "category": "advanced" 42 | }, { 43 | "param_key": "brief_title", 44 | "display_name": "Brief Title", 45 | "filter_type": "text", 46 | "category": "advanced" 47 | }, { 48 | "param_key": "official_title", 49 | "display_name": "Official Title", 50 | "filter_type": "text", 51 | "category": "advanced" 52 | }, { 53 | "param_key": "current_trial_status", 54 | "display_name": "Status", 55 | "filter_type": "select", 56 | "category": "advanced" 57 | }, { 58 | "param_key": "eligibility.structured.max_age_number", 59 | "display_name": "Max Age", 60 | "filter_type": "number", 61 | "category": "get started" 62 | }, { 63 | "param_key": "eligibility.structured.min_age_number", 64 | "display_name": "Min Age", 65 | "filter_type": "number", 66 | "category": "get started" 67 | }, { 68 | "param_key": "nci_id", 69 | "display_name": "NCI ID", 70 | "filter_type": "text", 71 | "category": "advanced" 72 | }, { 73 | "param_key": "nct_id", 74 | "display_name": "NCT ID", 75 | "filter_type": "text", 76 | "category": "advanced" 77 | }, { 78 | "param_key": "phase.phase", 79 | "display_name": "Phase", 80 | "filter_type": "select", 81 | "category": "advanced" 82 | }, { 83 | // "param_key": "date_last_updated_anything", 84 | // "display_name": "Date Updated", 85 | // "filter_type": "date", 86 | // "category": "advanced" 87 | // }, { 88 | // "param_key": "completion_date", 89 | // "display_name": "Date Completed", 90 | // "filter_type": "date", 91 | // "category": "advanced" 92 | // }, { 93 | // "param_key": "start_date", 94 | // "display_name": "Date Started", 95 | // "filter_type": "date", 96 | // "category": "advanced" 97 | // }, { 98 | "param_key": "anatomic_sites", 99 | "display_name": "Anatomic Site", 100 | "filter_type": "select", 101 | "category": "advanced" 102 | }, { 103 | "param_key": "_all", 104 | "display_name": "All Text", 105 | "filter_type": "text", 106 | "category": "advanced" 107 | } 108 | ]; 109 | 110 | class ValidParams { 111 | 112 | static getParamsByCategory() { 113 | let paramsByCategory = {}; 114 | params.forEach((param) => { 115 | if (!paramsByCategory[param["category"]]) { 116 | paramsByCategory[param["category"]] = {}; 117 | } 118 | paramsByCategory[param["category"]][param["param_key"]] = { 119 | display_name: param.display_name, 120 | filter_type: param.filter_type 121 | }; 122 | }); 123 | return paramsByCategory; 124 | } 125 | 126 | static getParamsByKey() { 127 | let paramsByKey = {}; 128 | params.forEach((param) => { 129 | if (!paramsByKey[param["param_key"]]) { 130 | paramsByKey[param["param_key"]] = {}; 131 | } 132 | paramsByKey[param["param_key"]] = { 133 | display_name: param.display_name, 134 | filter_type: param.filter_type, 135 | category: param.category 136 | }; 137 | }); 138 | return paramsByKey; 139 | } 140 | 141 | }; 142 | 143 | export default ValidParams; 144 | -------------------------------------------------------------------------------- /search/index/indexer/alias_swapper.js: -------------------------------------------------------------------------------- 1 | const async = require("async"); 2 | const _ = require("lodash"); 3 | const ElasticSearch = require("elasticsearch"); 4 | 5 | const AbstractIndexTool = require("./abstract_index_tool"); 6 | const Logger = require("../../../common/logger"); 7 | const CONFIG = require("../../config.json"); 8 | 9 | class ElasticSearchLogger extends Logger { 10 | get DEFAULT_LOGGER_NAME() { 11 | return "elasticsearch"; 12 | } 13 | } 14 | 15 | /** 16 | * Class for Swapping out ElasticSearch Aliases 17 | * 18 | * @class AliasSwapper 19 | */ 20 | class AliasSwapper extends AbstractIndexTool { 21 | 22 | get LOGGER_NAME() { 23 | return "alias-swapper"; 24 | } 25 | 26 | /** 27 | * Creates an instance of AliasSwapper. 28 | * 29 | * @param {any} adapter The search adapter to use for connecting to ElasticSearch 30 | */ 31 | constructor(adapter) { 32 | super(adapter); 33 | } 34 | 35 | /** 36 | * Gets an array of indices that are associated with the alias 37 | * 38 | * @param {any} aliasName The alias to check for. 39 | * @param {any} callback 40 | */ 41 | swapAlias(actions, callback) { 42 | this.logger.info( 43 | `Swapping aliases.`); 44 | this.client.indices.updateAliases({ 45 | body: { 46 | actions: actions 47 | } 48 | }, (err, response, status) => { 49 | if(err) { this.logger.error(err); } 50 | return callback(err, response); 51 | }); 52 | } 53 | 54 | 55 | /** 56 | * Initializes and executes the swapping of aliases for trials and terms 57 | * 58 | * @static 59 | * @param {any} adapter The search adapter to use for making requests to ElasticSearch 60 | * @param {any} trialIndexInfo Information about the index and alias for trials 61 | * @param {any} termIndexInfo Information about the index and alias for terms 62 | * @param {any} callback 63 | */ 64 | static init(adapter, trialIndexInfo, termIndexInfo, callback) { 65 | 66 | 67 | let swapper = new AliasSwapper(adapter); 68 | swapper.logger.info(`Starting alias swapping.`); 69 | 70 | //Find out who is using aliases 71 | 72 | 73 | async.waterfall([ 74 | //Get indexes for trial alias 75 | (next) => { swapper.aliasExists(trialIndexInfo.esAlias, next); }, 76 | (exists, next) => { 77 | if(exists) { 78 | swapper.getIndexesForAlias(trialIndexInfo.esAlias, next) 79 | } else { 80 | //Empty Array of Indexes used by Alias 81 | next(null, []); 82 | } 83 | }, 84 | (indexesForAlias, next) => { 85 | trialIndexInfo.currentAliasIndexes = indexesForAlias; 86 | next(null); 87 | }, 88 | 89 | //Get indexes for term alias 90 | (next) => { swapper.aliasExists(termIndexInfo.esAlias, next); }, 91 | (exists, next) => { 92 | if(exists) { 93 | swapper.getIndexesForAlias(termIndexInfo.esAlias, next) 94 | } else { 95 | //Empty Array of Indexes used by Alias 96 | next(null, []); 97 | } 98 | }, 99 | (indexesForAlias, next) => { 100 | termIndexInfo.currentAliasIndexes = indexesForAlias; 101 | next(null); 102 | }, 103 | //Build the removal and addions. 104 | (next) => { 105 | 106 | let actions = []; 107 | 108 | //Loop over the trial and term indexes and setup the add/removes for this 109 | //swap. 110 | [trialIndexInfo, termIndexInfo].forEach(indexType => { 111 | indexType.currentAliasIndexes.forEach((index) => { 112 | actions.push({ 113 | "remove": { 114 | "index": index, 115 | "alias": indexType.esAlias 116 | } 117 | }) 118 | }); 119 | actions.push({ 120 | "add": { 121 | "index": indexType.esIndex, 122 | "alias": indexType.esAlias 123 | } 124 | }); 125 | }); 126 | 127 | swapper.swapAlias(actions, next); 128 | } 129 | ], (err) => { 130 | if(err) { swapper.logger.error(err); } 131 | swapper.logger.info(`Finished swapping aliases.`); 132 | return callback(err); 133 | }); 134 | } 135 | 136 | } 137 | 138 | module.exports = AliasSwapper; -------------------------------------------------------------------------------- /search/index/indexer/term/indexer.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const async = require("async"); 3 | const _ = require("lodash"); 4 | const Writable = require("stream").Writable; 5 | 6 | const AbstractIndexer = require("../abstract_indexer"); 7 | const Utils = require("../../../../common/utils"); 8 | const TermLoader = require("../../../../common/term_loader"); 9 | 10 | const ES_MAPPING = require("./mapping.json"); 11 | const ES_SETTINGS = require("./settings.json"); 12 | const ES_PARAMS = { 13 | "esAlias": "cancer-terms", 14 | "esType": "term", 15 | "esMapping": ES_MAPPING, 16 | "esSettings": ES_SETTINGS 17 | }; 18 | 19 | class TermIndexerStream extends Writable { 20 | 21 | constructor(termIndexer) { 22 | super({objectMode: true}); 23 | this.termIndexer = termIndexer; 24 | this.logger = termIndexer.logger; 25 | } 26 | 27 | _indexTerm(termDoc, done) { 28 | let id = `${termDoc.term_key}_${termDoc.term_type}`; 29 | this.logger.info(`Indexing term (${id}).`); 30 | this.termIndexer.indexDocument({ 31 | "index": this.termIndexer.esIndex, 32 | "type": this.termIndexer.esType, 33 | "id": id, 34 | "body": termDoc 35 | }, (err, response, status) => { 36 | if(err) { this.logger.error(err); } 37 | this.termIndexer.indexCounter++; 38 | 39 | return done(err, response); 40 | }); 41 | }; 42 | 43 | _write(termDoc, enc, next) { 44 | this._indexTerm(termDoc, (err, response) => { 45 | return next(null, response); 46 | }); 47 | } 48 | 49 | } 50 | 51 | class TermIndexer extends AbstractIndexer { 52 | 53 | get LOGGER_NAME() { 54 | return "term-indexer"; 55 | } 56 | 57 | constructor(adapter, trials_file, params) { 58 | super(adapter, trials_file, params); 59 | this.indexCounter = 0; 60 | } 61 | 62 | indexTermsForType(termType, callback) { 63 | let is = new TermIndexerStream(this); 64 | is.on("error", (err) => { this.logger.error(err); }); 65 | 66 | this.indexCounter = 0; 67 | 68 | // if we don't have any terms to index, don't let everything fail, return 69 | if (!this.terms || !this.terms[termType] || _.isEmpty(this.terms[termType])) { 70 | return callback(); 71 | } 72 | 73 | let maxTermCount = _.max( 74 | _.map(_.values(this.terms[termType]), (term) => { 75 | return term.count; 76 | }) 77 | ); 78 | _.forOwn(this.terms[termType], (termObj, termKey) => { 79 | let term = termObj["term"]; 80 | // let terms = termObj["terms"]; 81 | let count = termObj["count"]; 82 | // let count_normalized = count / maxTermCount; 83 | // TODO: approximation, should figure out more exact normalization... 84 | let count_normalized = Math.log(count / maxTermCount + 1) / Math.log(2); 85 | let doc = { 86 | "term_key": termKey, 87 | "term": term, 88 | "term_type": termType, 89 | "count": count, 90 | "count_normalized": count_normalized 91 | }; 92 | if (termType === "_diseases") { 93 | doc.codes = termObj["codes"]; 94 | } 95 | is.write(doc); 96 | }); 97 | is.end(); 98 | 99 | is.on("finish", () => { 100 | this.logger.info(`Indexed ${this.indexCounter} ${this.esType} ${termType} documents.`); 101 | return callback(); 102 | }); 103 | } 104 | 105 | indexTerms(callback) { 106 | const _indexTermsForType = (termType, next) => { 107 | this.indexTermsForType(termType, next); 108 | }; 109 | async.eachSeries(TermLoader.VALID_TERM_TYPES, _indexTermsForType, callback); 110 | } 111 | 112 | loadTermsFromTrialsJsonFile(callback) { 113 | let rs = fs.createReadStream(this.trials_file); 114 | let termLoader = new TermLoader(); 115 | termLoader.loadTermsFromTrialsJsonReadStream(rs, (err) => { 116 | if (err) { 117 | this.logger.error(err); 118 | return callback(err); 119 | } 120 | this.terms = termLoader.terms; 121 | return callback(); 122 | }); 123 | } 124 | 125 | static init(adapter, trials_file, callback) { 126 | let indexer = new TermIndexer(adapter, trials_file, ES_PARAMS); 127 | indexer.logger.info(`Started indexing (${indexer.esType}) indices.`); 128 | async.waterfall([ 129 | (next) => { indexer.indexExists(next); }, 130 | (exists, next) => { 131 | if(exists) { 132 | indexer.deleteIndex(next) 133 | } else { 134 | next(null, null); 135 | } 136 | }, 137 | (response, next) => { indexer.initIndex(next); }, 138 | (response, next) => { indexer.initMapping(next); }, 139 | (response, next) => { indexer.loadTermsFromTrialsJsonFile(next); }, 140 | (next) => { indexer.indexTerms(next) } 141 | ], (err) => { 142 | if(err) { indexer.logger.error(err); } 143 | indexer.logger.info(`Finished indexing (${indexer.esType}) indices.`); 144 | callback(err,{ 145 | esIndex: indexer.esIndex, 146 | esAlias: indexer.esAlias 147 | }); 148 | }); 149 | } 150 | 151 | } 152 | 153 | module.exports = TermIndexer; 154 | -------------------------------------------------------------------------------- /search/client/static/images/nci-logo-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | --------------------------------------------------------------------------------