From 250efadfb170b5e5d4b096f36eca60d288ca360c Mon Sep 17 00:00:00 2001 From: Brian Date: Fri, 8 Sep 2017 12:22:17 -0500 Subject: [PATCH 1/7] adding api changes --- api/build.gradle | 2 + api/docker-compose.yaml | 2 + api/libproject/devstart.rb | 14 +++ .../workbench/api/CohortBuildController.java | 85 +++++++++++++++++++ .../webapp/WEB-INF/appengine-web.xml.template | 1 + 5 files changed, 104 insertions(+) create mode 100644 api/src/main/java/org/pmiops/workbench/api/CohortBuildController.java diff --git a/api/build.gradle b/api/build.gradle index 1d0f9f71191..55be6941706 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -120,6 +120,7 @@ dependencies { compile 'mysql:mysql-connector-java:+' compile 'com.google.cloud.sql:mysql-socket-factory:+' compile 'com.google.appengine:appengine:+' + compile 'com.google.appengine:appengine-api-1.0-sdk:+' compile('org.springframework.boot:spring-boot-starter-web:+') { exclude module: 'spring-boot-starter-tomcat' } @@ -136,6 +137,7 @@ dependencies { compile 'joda-time:joda-time:+' compile 'javax.inject:javax.inject:1' compile 'io.swagger:swagger-annotations:+' + compile 'com.google.cloud:google-cloud-bigquery:+' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.+' diff --git a/api/docker-compose.yaml b/api/docker-compose.yaml index 55ea3e1f7af..83decae660c 100644 --- a/api/docker-compose.yaml +++ b/api/docker-compose.yaml @@ -16,6 +16,8 @@ services: - gradle-cache:/root/.gradle - .:/w:cached command: ./gradlew appengineRun + environment: + - GOOGLE_APPLICATION_CREDENTIALS=/w/sa-key.json env_file: - db/vars.env ports: diff --git a/api/libproject/devstart.rb b/api/libproject/devstart.rb index 9c8e0dd39cc..f188d873248 100644 --- a/api/libproject/devstart.rb +++ b/api/libproject/devstart.rb @@ -1,5 +1,6 @@ require_relative "utils/common" require "io/console" +require "json" require "optparse" require "tempfile" @@ -58,6 +59,19 @@ def dev_up(args) common.run_inline %W{docker-compose up -d db} common.status "Running database migrations..." common.run_inline %W{docker-compose run db-migration} + common.status "Creating service account for API..." + iam_account = "bigqueryservice@all-of-us-workbench-test.iam.gserviceaccount.com" + common.run_inline %W{ + gcloud iam service-accounts keys create sa-key.json --iam-account #{iam_account} + } + at_exit { + # use JSON library to parse sa-key.json and extract key ID + keyFile = File.read("sa-key.json") + key_id = JSON.parse(keyFile)["private_key_id"] + common.run_inline %W{ + gcloud iam service-accounts keys delete #{key_id} --iam-account #{iam_account} + } + } common.status "Starting API. This can take a while. Thoughts on reducing development cycle time" common.status "are here:" common.status " https://github.com/all-of-us/workbench/blob/master/api/doc/2017/dev-cycle.md" diff --git a/api/src/main/java/org/pmiops/workbench/api/CohortBuildController.java b/api/src/main/java/org/pmiops/workbench/api/CohortBuildController.java new file mode 100644 index 00000000000..e1dbac9d0d1 --- /dev/null +++ b/api/src/main/java/org/pmiops/workbench/api/CohortBuildController.java @@ -0,0 +1,85 @@ +package org.pmiops.workbench.api; + +import com.google.cloud.bigquery.*; +import org.pmiops.workbench.model.Criteria; +import org.pmiops.workbench.model.CriteriaListResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +@RestController +public class CohortBuilderController implements CohortBuilderApiDelegate { + + private static final Logger log = Logger.getLogger(CohortBuilderController.class.getName()); + + @Override + public ResponseEntity getCriteriaByTypeAndParentId(String type, String parentId) { + + // Instantiates a client + BigQuery bigquery = BigQueryOptions.getDefaultInstance().getService(); + + QueryJobConfiguration queryConfig = + QueryJobConfiguration.newBuilder( + "SELECT id, " + + "type, " + + "code, " + + "name, " + + "est_count, " + + "is_group, " + + "is_selectable, " + + "domain_id " + + "FROM `pmi-drc-api-test.synpuf.icd9_criteria` " + + "where parent_id = 0 " + + "order by id asc;") + // Use standard SQL syntax for queries. + // See: https://cloud.google.com/bigquery/sql-reference/ + .setUseLegacySql(false) + .build(); + + // Create a job ID so that we can safely retry. + JobId jobId = JobId.of(UUID.randomUUID().toString()); + Job queryJob = bigquery.create(JobInfo.newBuilder(queryConfig).setJobId(jobId).build()); + + // Wait for the query to complete. + try { + queryJob = queryJob.waitFor(); + } catch (Exception e) { + log.log(Level.INFO, "Timeout occurred: ", e); + return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).build(); + } + + // Check for errors + if (queryJob == null) { + throw new RuntimeException("Job no longer exists"); + } else if (queryJob.getStatus().getError() != null) { + // You can also look at queryJob.getStatus().getExecutionErrors() for all + // errors, not just the latest one. + throw new RuntimeException(queryJob.getStatus().getError().toString()); + } + + // Get the results. + QueryResponse response = bigquery.getQueryResults(jobId); + QueryResult result = response.getResult(); + + CriteriaListResponse criteriaResponse = new CriteriaListResponse(); + for (List row : result.iterateAll()) { + final Criteria criteria = new Criteria(); + criteria.setId(row.get(0).getLongValue()); + criteria.setType(row.get(1).getStringValue()); + criteria.setCode(row.get(2).getStringValue()); + criteria.setName(row.get(3).getStringValue()); + criteria.setCount(row.get(4).isNull() ? 0 : row.get(4).getLongValue()); + criteria.setGroup(row.get(5).getBooleanValue()); + criteria.setSelectable(row.get(6).getBooleanValue()); + criteria.setDomainId(row.get(7).isNull() ? null : row.get(7).getStringValue()); + criteriaResponse.addItemsItem(criteria); + } + + return ResponseEntity.ok(criteriaResponse); + } +} diff --git a/api/src/main/webapp/WEB-INF/appengine-web.xml.template b/api/src/main/webapp/WEB-INF/appengine-web.xml.template index 350e73675f2..a1d43619ba1 100644 --- a/api/src/main/webapp/WEB-INF/appengine-web.xml.template +++ b/api/src/main/webapp/WEB-INF/appengine-web.xml.template @@ -2,6 +2,7 @@ api java8 true + all-of-us-workbench-test From 2becee2183e413035f259c174bab8f5f3805211c Mon Sep 17 00:00:00 2001 From: Brian Date: Fri, 8 Sep 2017 14:07:12 -0500 Subject: [PATCH 2/7] adding new wizard components --- .gitignore | 1 + ...ller.java => CohortBuilderController.java} | 0 api/src/main/resources/workbench.yaml | 71 +++++++++++++++++++ .../wizard-modal/wizard-modal.component.html | 3 +- .../wizard-tree-parent.component.css | 0 .../wizard-tree-parent.component.html | 46 ++++++++++++ .../wizard-tree-parent.component.ts | 44 ++++++++++++ 7 files changed, 164 insertions(+), 1 deletion(-) rename api/src/main/java/org/pmiops/workbench/api/{CohortBuildController.java => CohortBuilderController.java} (100%) create mode 100644 ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.css create mode 100644 ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.html create mode 100644 ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.ts diff --git a/.gitignore b/.gitignore index 6d9c1a35399..f84eb2f8915 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ node_modules npm-debug.log tools/*.jar tools/cloud_sql_proxy +/api/sa-key.json .DS_Store diff --git a/api/src/main/java/org/pmiops/workbench/api/CohortBuildController.java b/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java similarity index 100% rename from api/src/main/java/org/pmiops/workbench/api/CohortBuildController.java rename to api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java diff --git a/api/src/main/resources/workbench.yaml b/api/src/main/resources/workbench.yaml index 930ee5cb024..1705a5cabf3 100644 --- a/api/src/main/resources/workbench.yaml +++ b/api/src/main/resources/workbench.yaml @@ -310,6 +310,30 @@ paths: required: true description: ID of the workspace containing the cohort definition + # Cohort Builder ####################################################################### + /api/v1/cohort-builder/criteria/{type}/{parentId}: + get: + tags: + - cohortBuilder + description: Returns builder criteria tree with the specified criteria type and parentId + operationId: "getCriteriaByTypeAndParentId" + parameters: + - in: path + name: type + type: string + required: true + description: the specific type of criteria to get + - in: path + name: parentId + type: string + required: true + description: fetch children of parentId + responses: + "200": + description: A collection of criteria + schema: + $ref: "#/definitions/CriteriaListResponse" + ########################################################################################## ## DEFINITIONS ########################################################################################## @@ -470,3 +494,50 @@ definitions: phoneNumber: description: the user's phone number type: string + + CriteriaListResponse: + type: "object" + required: + - "items" + properties: + items: + type: "array" + items: + $ref: "#/definitions/Criteria" + + Criteria: + type: "object" + required: + - "id" + - "type" + - "code" + - "name" + - "group" + - "selectable" + properties: + id: + description: id of the criteria + type: integer + format: int64 + type: + description: type of criteria + type: string + code: + description: what level of data access the user has + type: string + name: + description: description of criteria + type: string + count: + description: est. count in the cdr + type: integer + format: int64 + group: + description: specifies of child or parent + type: boolean + selectable: + description: specifies if use can search with + type: boolean + domainId: + description: clue to determine which tables to search + type: string \ No newline at end of file diff --git a/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.html b/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.html index 2d4e7efa76d..2f7ff8f39f0 100644 --- a/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.html +++ b/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.html @@ -9,7 +9,8 @@ Choose {{criteriaType.toUpperCase()}} Codes - + + diff --git a/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.css b/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.css new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.html b/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.html new file mode 100644 index 00000000000..4ddde711677 --- /dev/null +++ b/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.html @@ -0,0 +1,46 @@ +
+
+ + + + + Loading... + + + Loading... + + + + + + + + + + + +
+
+ +
+
+ diff --git a/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.ts b/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.ts new file mode 100644 index 00000000000..045b0721bb8 --- /dev/null +++ b/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.ts @@ -0,0 +1,44 @@ +import { Component, OnInit, Input, OnDestroy, ViewEncapsulation } from '@angular/core'; +import { SearchService, BroadcastService } from '../service'; +import { Criteria, SearchParameter } from '../model'; +import { Subscription } from 'rxjs/Subscription'; +import 'rxjs/add/operator/mergeMap'; + +@Component({ + selector: 'app-wizard-tree-parent', + templateUrl: 'wizard-tree-parent.component.html', + styleUrls: ['wizard-tree-parent.component.css'], + encapsulation: ViewEncapsulation.None +}) +export class WizardTreeParentComponent implements OnInit, OnDestroy { + + nodes: Criteria[] = []; + private subscription: Subscription; + loading: boolean; + + constructor(private searchService: SearchService, + private broadcastService: BroadcastService) {} + + ngOnInit(): void { + this.loading = true; + this.subscription = + this.broadcastService.selectedCriteriaType$ + .mergeMap(criteriaType => this.searchService.getParentNodes(criteriaType)) + .subscribe(nodes => { + this.nodes = nodes; + this.loading = false; + }); + } + + public selectCriteria(node): void { + node.values.push(new SearchParameter(node.code, node.domainId)); + this.broadcastService.selectCriteria(node); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + +} + + From 80da71f797a956db919b505debdceaef4daa2bf5 Mon Sep 17 00:00:00 2001 From: Brian Date: Fri, 8 Sep 2017 14:33:43 -0500 Subject: [PATCH 3/7] RW-15 cleanup of code for this story --- ui/src/app/app.module.ts | 7 +- .../criteria-group.component.ts | 2 +- .../search/model/search-result.ts | 25 +-- .../search/service/broadcast.service.ts | 55 ++++- .../search/service/search.service.ts | 18 +- .../wizard-modal/wizard-modal.component.css | 51 +++++ .../wizard-modal/wizard-modal.component.html | 195 +++--------------- .../wizard-modal/wizard-modal.component.ts | 100 +++++---- .../wizard-select/wizard-select.component.ts | 12 +- .../wizard-tree-parent.component.html | 2 +- .../wizard-tree-parent.component.ts | 16 +- 11 files changed, 236 insertions(+), 247 deletions(-) diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index c72b3dc579a..8a6dfab10ec 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -15,7 +15,7 @@ import {HomePageComponent} from 'app/views/home-page/component'; import {SignInService} from 'app/services/sign-in.service'; import {WorkspaceComponent} from 'app/views/workspace/component'; import {WorkspaceEditComponent} from 'app/views/workspace-edit/component'; -import {CohortsService, WorkspacesService, Configuration, ConfigurationParameters} from 'generated'; +import {CohortsService, WorkspacesService, Configuration, ConfigurationParameters, CohortBuilderService} from 'generated'; import {environment} from 'environments/environment'; import { SearchGroupComponent } from 'app/views/cohort-builder/search/search-group/search-group.component'; import { CriteriaTreeComponent } from 'app/views/cohort-builder/search/criteria-tree/criteria-tree.component'; @@ -31,6 +31,7 @@ import { AnnotationsComponent } from 'app/views/cohort-builder/review/annotation import { MedicationsComponent } from 'app/views/cohort-builder/review/medications/medications.component'; import { WizardSelectComponent } from 'app/views/cohort-builder/search/wizard-select/wizard-select.component'; import { WizardModalComponent } from 'app/views/cohort-builder/search/wizard-modal/wizard-modal.component'; +import { WizardTreeParentComponent } from './views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component'; import { BroadcastService, SearchService } from './views/cohort-builder/search/service'; @@ -68,6 +69,7 @@ export function getConfiguration(signInService: SignInService): Configuration { MedicationsComponent, WizardSelectComponent, WizardModalComponent, + WizardTreeParentComponent, CohortEditComponent, HomePageComponent, WorkspaceComponent, @@ -84,7 +86,8 @@ export function getConfiguration(signInService: SignInService): Configuration { CohortsService, WorkspacesService, BroadcastService, - SearchService + SearchService, + CohortBuilderService ], // This specifies the top-level component, to load first. diff --git a/ui/src/app/views/cohort-builder/search/criteria-group/criteria-group.component.ts b/ui/src/app/views/cohort-builder/search/criteria-group/criteria-group.component.ts index e8fccd6777e..0333a41ed9c 100644 --- a/ui/src/app/views/cohort-builder/search/criteria-group/criteria-group.component.ts +++ b/ui/src/app/views/cohort-builder/search/criteria-group/criteria-group.component.ts @@ -46,7 +46,7 @@ export class CriteriaGroupComponent implements OnInit, OnDestroy { this.hasOccurrencesSelectList = this.searchService.getHasOccurrencesSelectList(); this.daysOrYearsSelectList = this.searchService.getDaysOrYearsSelectList(); - this.eventOccurredDuringSelectList = this.searchService.getEventOccurredDuringSelectList(); + // this.eventOccurredDuringSelectList = this.searchService.getEventOccurredDuringSelectList(); if (this.modifierList.length === 0) { this.ageAtEvent = new Modifier(); diff --git a/ui/src/app/views/cohort-builder/search/model/search-result.ts b/ui/src/app/views/cohort-builder/search/model/search-result.ts index d56945a2b2a..6df01caf11c 100644 --- a/ui/src/app/views/cohort-builder/search/model/search-result.ts +++ b/ui/src/app/views/cohort-builder/search/model/search-result.ts @@ -1,5 +1,4 @@ import { Criteria, Modifier, Subject, SearchParameter } from './'; -import { CriteriaGroupComponent } from '../criteria-group/criteria-group.component'; import { SearchResponse } from './search-response'; export class SearchResult { @@ -15,35 +14,27 @@ export class SearchResult { constructor() - constructor(description: string, criteriaGroup: CriteriaGroupComponent) + constructor(description: string, criteriaList: Criteria[], modifierList: Modifier[]) - constructor(description?: string, criteriaGroup?: CriteriaGroupComponent) { - if (description && criteriaGroup) { - this.update(description, criteriaGroup); + constructor(description?: string, criteriaList?: Criteria[], modifierList?: Modifier[]) { + if (description && criteriaList && modifierList) { + this.update(description, criteriaList, modifierList); } } - update(description: string, criteriaGroup: CriteriaGroupComponent) { + update(description: string, criteriaList: Criteria[], modifierList: Modifier[]) { this.description = description; - criteriaGroup.modifierList ? - this.modifierList = criteriaGroup.modifierList : - this.modifierList = []; - criteriaGroup.criteriaList ? - this.criteriaList = criteriaGroup.criteriaList : - this.criteriaList = []; + criteriaList ? this.criteriaList = criteriaList : this.criteriaList = []; + modifierList ? this.modifierList = modifierList : this.modifierList = []; this.count = -1; for (const criteria of this.criteriaList) { this.searchType = criteria.type; - this.values.push(new SearchParameter(criteria.code, - criteria.type.startsWith('DEMO') ? - criteria.type : - criteria.domainId)); + this.values.push(new SearchParameter(criteria.code, criteria.type.startsWith('DEMO') ? criteria.type : criteria.domainId)); } } updateWithResponse(response: SearchResponse) { this.resultSet = response.subjectList; - this.values = response.searchParameterList; this.count = response.subjectList.length; } diff --git a/ui/src/app/views/cohort-builder/search/service/broadcast.service.ts b/ui/src/app/views/cohort-builder/search/service/broadcast.service.ts index efa5047c741..952aecb1210 100644 --- a/ui/src/app/views/cohort-builder/search/service/broadcast.service.ts +++ b/ui/src/app/views/cohort-builder/search/service/broadcast.service.ts @@ -1,7 +1,8 @@ -import { Injectable } from '@angular/core'; +import { Injectable, ComponentRef } from '@angular/core'; import { Subject } from 'rxjs/Subject'; import { ReplaySubject } from 'rxjs/ReplaySubject'; -import { Criteria, SearchGroup, SearchResult } from '../model'; +import { Criteria, SearchGroup, SearchResult, Modifier } from '../model'; +import { SearchRequest } from '../model/search-request'; @Injectable() export class BroadcastService { @@ -12,6 +13,11 @@ export class BroadcastService { */ private selectedCriteria = new Subject(); + /** + * Represents the selected criteria type. + */ + private selectedCriteriaType = new ReplaySubject(1); + /** * Represents the selected search group for the * active modal. @@ -39,12 +45,28 @@ export class BroadcastService { */ private updatedCharts = new Subject(); + /** + * Represents the summary page that needs updating. + */ + private summaryCriteriaGroup = new Subject(); + + /** + * Represents the summary page that needs updating. + */ + private summaryModifierList = new Subject(); + /** * The Observable that allows other components * to subscribe to this criteria subject. */ public selectedCriteria$ = this.selectedCriteria.asObservable(); + /** + * The Observable that allows other components + * to subscribe to this criteria type subject. + */ + public selectedCriteriaType$ = this.selectedCriteriaType.asObservable(); + /** * The Observable that allows other components * to subscribe to this search group subject. @@ -75,6 +97,18 @@ export class BroadcastService { */ public updatedCharts$ = this.updatedCharts.asObservable(); + /** + * The Observable that allows other components + * to subscribe to this event. + */ + public summaryCriteriaGroup$ = this.summaryCriteriaGroup.asObservable(); + + /** + * The Observable that allows other components + * to subscribe to this event. + */ + public summaryModifierList$ = this.summaryModifierList.asObservable(); + /** * Add the specified criteria to the subject. * @@ -84,6 +118,15 @@ export class BroadcastService { this.selectedCriteria.next(criteria); } + /** + * Add the specified criteria type to the subject. + * + * @param criteriaType + */ + selectCriteriaType(criteriaType: string) { + this.selectedCriteriaType.next(criteriaType); + } + /** * Add the specified search group to the subject. * @@ -132,4 +175,12 @@ export class BroadcastService { updateCharts(gender: any[], race: any[]) { this.updatedCharts.next({'gender': gender, 'race': race}); } + + setSummaryCriteriaGroup(criteriaList: Criteria[]) { + this.summaryCriteriaGroup.next(criteriaList); + } + + setSummaryModifierList(modifierList: Modifier[]) { + this.summaryModifierList.next(modifierList); + } } diff --git a/ui/src/app/views/cohort-builder/search/service/search.service.ts b/ui/src/app/views/cohort-builder/search/service/search.service.ts index 14b8e8aae66..9ac26851aa3 100644 --- a/ui/src/app/views/cohort-builder/search/service/search.service.ts +++ b/ui/src/app/views/cohort-builder/search/service/search.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, OnInit, OnDestroy } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import { Criteria, CriteriaType, SearchRequest, SearchResponse } from '../model'; @@ -20,7 +20,7 @@ const AGE_AT_EVENT: string[] = ['Any', 'GTE >=', 'LTE <=', 'Between']; const EVENT_DATE: string[] = ['Any', 'Within x year(s)', 'GTE >=', 'LTE <=', 'Between']; -const EVENT_OCCURRED_DURING: string[] = ['Inpatient visit', 'Outpatient visit']; +const VISIT_TYPE: string[] = ['Any', 'Inpatient visit', 'Outpatient visit']; const DAYS_OR_YEARS: string[] = ['Days', 'Years']; @@ -49,8 +49,8 @@ export class SearchService { return HAS_OCCURRENCES; } - getEventOccurredDuringSelectList(): string[] { - return EVENT_OCCURRED_DURING; + getVisitTypeSelectList(): string[] { + return VISIT_TYPE; } getDaysOrYearsSelectList(): string[] { @@ -58,18 +58,20 @@ export class SearchService { } getParentNodes(type: string): Observable { - return this.http.get(this.baseUrl + '/' + type.toLowerCase() + '/0') + return this.http.get('/api/' + type.toLowerCase() + '/0') .map(res => res.json()); } getChildNodes(criteria: any): Observable { - return this.http.get(this.baseUrl + '/' + criteria.type.toLowerCase() + '/' + criteria.id) + return this.http.get('/api/' + criteria.type.toLowerCase() + '/' + criteria.id) .map(res => res.json()); } getResults(searchRequest: SearchRequest): Observable { - return this.http.post(this.baseUrl + '/search', searchRequest) - .map(res => res.json()); + return this.http.post('/api/searchrequest', searchRequest) + .map( + res => res.json() + ); } } diff --git a/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.css b/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.css index e69de29bb2d..815211843f8 100644 --- a/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.css +++ b/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.css @@ -0,0 +1,51 @@ +.modal-body { + height: 100% !important; + overflow: hidden; +} + +.clr-wizard-content { + height: 100% !important; +} + +.modal-dialog.modal-xl { + width: 88% !important; +} + +.clr-wizard-page { + height: 100% !important; +} + +.box { + border: 1px solid #d7d7d7; +} + +.clr-treenode-link { + display: -webkit-box; +} + +.scrolling-tree { + overflow: auto; + height: 100%; +} + +.row-height { + height: 100%; +} + +.codes-height { + height: 100% +} + +.criteria-group-height { + height: 100%; +} + +.scrolling-criteria-group { + height: 95%; + overflow: auto; +} + +/*.text-truncate {*/ + /*width: 230px;*/ +/*}*/ + diff --git a/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.html b/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.html index 2f7ff8f39f0..6d974a8379c 100644 --- a/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.html +++ b/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.html @@ -1,172 +1,41 @@ - + Add Criteria To Cohort - Cancel - Back - Next - Finish - Choose {{criteriaType.toUpperCase()}} Codes - - + + Cancel + Back + Next + + - -
-
- Modifiers -
-
-
- -
- - - - - -  and  - -
-
-
- -
- - - - - - - - - - -
-
-
- -
- - - - - -  within  - - - -   - - apart - -
-
-
- -
- -
-
-
-
-
-

Selected ICD9 Codes

-
-
-
- 001-Cholera -
-
-
-
- OR - 002-Typhoid and paratyphoid fevers -
-
-
-
- OR - 003-Other salmonella infections -
-
-
-
- OR - 004-Shinellosis -
-
-
-
-
-
+ + + + + + + + + + + + - - Totals -
-
-
- - × - - Contains group - 001-Cholera - [001...] -
-
20
-
-
-
- - × - - OR - Contains group - 002-Typhoid and paratyphoid fevers - [002...] -
-
16
-
-
-
- - × - - OR - Contains group - 003-Other salmonella infections - [003...] -
-
32
-
-
-
- - × - - OR - Contains group - 004-Shinellosis - [004...] -
-
50
-
-
-
Total Count: 100
-
+ + + + + + + + + + +
diff --git a/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.ts b/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.ts index 61eb07769c6..d3b8254f468 100644 --- a/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.ts +++ b/ui/src/app/views/cohort-builder/search/wizard-modal/wizard-modal.component.ts @@ -1,61 +1,81 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; -import { Modifier } from '../model'; -import { SearchService } from '../service'; -import {Wizard} from 'clarity-angular/wizard/wizard'; +import { Component, OnInit, ViewChild, ViewEncapsulation, OnDestroy, ComponentRef } from '@angular/core'; +import { Wizard } from 'clarity-angular/wizard/wizard'; +import { SearchGroup, SearchResult, Criteria, Modifier, SearchRequest } from '../model'; +import { BroadcastService, SearchService } from '../service'; +import { Subscription } from 'rxjs/Subscription'; @Component({ selector: 'app-wizard-modal', templateUrl: './wizard-modal.component.html', - styleUrls: ['./wizard-modal.component.css'] + styleUrls: ['./wizard-modal.component.css'], + encapsulation: ViewEncapsulation.None }) -export class WizardModalComponent implements OnInit { +export class WizardModalComponent implements OnInit, OnDestroy { @ViewChild('wizard') wizard: Wizard; - modifierList: Modifier[] = []; - ageAtEventSelectList: string[] = []; - eventDateSelectList: string[] = []; - hasOccurrencesSelectList: string[] = []; - eventOccurredDuringSelectList: string[] = []; - daysOrYearsSelectList: string[] = []; - private alive = true; - ageAtEvent: Modifier; - eventDate: Modifier; - hasOccurrences: Modifier; + private selectedSearchGroup: SearchGroup; + private selectedSearchResult: SearchResult; + private criteriaList: Criteria[] = []; + private modifierList: Modifier[] = []; criteriaType: string; + wizardModalRef: ComponentRef; + private criteriaTypeSubscription: Subscription; + private searchGroupSubscription: Subscription; + private criteriaListSubscription: Subscription; + private modifierListSubscription: Subscription; - constructor(private searchService: SearchService) { } + constructor(private broadcastService: BroadcastService, + private searchService: SearchService) { } ngOnInit() { - this.ageAtEventSelectList = this.searchService.getAgeAtEventSelectList(); - - this.eventDateSelectList = this.searchService.getEventDateSelectList(); + this.criteriaTypeSubscription = this.broadcastService.selectedCriteriaType$ + .subscribe(criteriaType => this.criteriaType = criteriaType); + this.searchGroupSubscription = this.broadcastService.selectedSearchGroup$ + .subscribe(searchGroup => this.selectedSearchGroup = searchGroup); + this.criteriaListSubscription = this.broadcastService.summaryCriteriaGroup$ + .subscribe(criteriaList => this.criteriaList = criteriaList); + this.modifierListSubscription = this.broadcastService.summaryModifierList$ + .subscribe(modifierList => this.modifierList = modifierList); + } - this.hasOccurrencesSelectList = this.searchService.getHasOccurrencesSelectList(); - this.daysOrYearsSelectList = this.searchService.getDaysOrYearsSelectList(); + public doCustomClick(type: string): void { + if ('finish' === type) { + this.wizard.finish(); + this.updateSearchResults(); + } + } - this.eventOccurredDuringSelectList = this.searchService.getEventOccurredDuringSelectList(); + public doCancel() { + this.wizard.finish(); + this.wizardModalRef.destroy(); + } - if (this.modifierList.length === 0) { - this.ageAtEvent = new Modifier(); - this.ageAtEvent.operator = this.ageAtEventSelectList[0]; - this.ageAtEvent.name = 'ageAtEvent'; - this.modifierList.push(this.ageAtEvent); + updateSearchResults() { + this.updateOrCreateSearchResult(); - this.eventDate = new Modifier(); - this.eventDate.operator = this.eventDateSelectList[0]; - this.eventDate.name = 'eventDate'; - this.modifierList.push(this.eventDate); + this.searchGroupSubscription = this.searchService.getResults( + new SearchRequest(this.criteriaType.toUpperCase(), this.selectedSearchResult)) + .subscribe(response => { + this.selectedSearchResult.updateWithResponse(response); + this.broadcastService.updateCounts(this.selectedSearchGroup, this.selectedSearchResult); + this.wizardModalRef.destroy(); + }); + } - this.hasOccurrences = new Modifier(); - this.hasOccurrences.operator = this.hasOccurrencesSelectList[0]; - this.hasOccurrences.value3 = this.daysOrYearsSelectList[0]; - this.hasOccurrences.name = 'hasOccurrences'; - this.modifierList.push(this.hasOccurrences); + updateOrCreateSearchResult() { + if (this.selectedSearchResult) { + this.selectedSearchResult.update(this.criteriaType.toUpperCase() + ' Group', this.criteriaList, this.modifierList); } else { - this.ageAtEvent = this.modifierList[0]; - this.eventDate = this.modifierList[1]; - this.hasOccurrences = this.modifierList[2]; + this.selectedSearchResult = new SearchResult(this.criteriaType.toUpperCase() + ' Group', this.criteriaList, this.modifierList); + this.selectedSearchGroup.results.push(this.selectedSearchResult); } } + ngOnDestroy() { + this.criteriaTypeSubscription.unsubscribe(); + this.searchGroupSubscription.unsubscribe(); + this.criteriaListSubscription.unsubscribe(); + this.modifierListSubscription.unsubscribe(); + } + } diff --git a/ui/src/app/views/cohort-builder/search/wizard-select/wizard-select.component.ts b/ui/src/app/views/cohort-builder/search/wizard-select/wizard-select.component.ts index ebbcfa761c8..08b4cc655db 100644 --- a/ui/src/app/views/cohort-builder/search/wizard-select/wizard-select.component.ts +++ b/ui/src/app/views/cohort-builder/search/wizard-select/wizard-select.component.ts @@ -1,7 +1,6 @@ -import { Component, OnInit, ComponentFactoryResolver, - ViewChild, ViewContainerRef } from '@angular/core'; +import { Component, OnInit, ComponentFactoryResolver, ViewChild, ViewContainerRef } from '@angular/core'; import { CriteriaType } from '../model'; -import { SearchService } from '../service'; +import { SearchService, BroadcastService } from '../service'; import { WizardModalComponent } from '../wizard-modal/wizard-modal.component'; @Component({ @@ -16,6 +15,7 @@ export class WizardSelectComponent implements OnInit { parent: ViewContainerRef; constructor(private searchService: SearchService, + private broadcastService: BroadcastService, private componentFactoryResolver: ComponentFactoryResolver) { } ngOnInit() { @@ -23,10 +23,10 @@ export class WizardSelectComponent implements OnInit { } openWizard(criteriaType: string) { - const wizardModalComponent = - this.componentFactoryResolver.resolveComponentFactory(WizardModalComponent); + const wizardModalComponent = this.componentFactoryResolver.resolveComponentFactory(WizardModalComponent); const wizardModalRef = this.parent.createComponent(wizardModalComponent); - wizardModalRef.instance.criteriaType = criteriaType; + this.broadcastService.selectCriteriaType(criteriaType); + wizardModalRef.instance.wizardModalRef = wizardModalRef; wizardModalRef.instance.wizard.open(); } diff --git a/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.html b/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.html index 4ddde711677..bdfeb5b72bd 100644 --- a/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.html +++ b/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.html @@ -40,7 +40,7 @@
- +
diff --git a/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.ts b/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.ts index 045b0721bb8..6b2c06899d4 100644 --- a/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.ts +++ b/ui/src/app/views/cohort-builder/search/wizard-tree-parent/wizard-tree-parent.component.ts @@ -1,8 +1,10 @@ import { Component, OnInit, Input, OnDestroy, ViewEncapsulation } from '@angular/core'; -import { SearchService, BroadcastService } from '../service'; -import { Criteria, SearchParameter } from '../model'; +import { BroadcastService } from '../service'; +import { SearchParameter } from '../model'; import { Subscription } from 'rxjs/Subscription'; import 'rxjs/add/operator/mergeMap'; +import { CohortBuilderService } from 'generated'; +import { Criteria } from 'generated'; @Component({ selector: 'app-wizard-tree-parent', @@ -12,25 +14,25 @@ import 'rxjs/add/operator/mergeMap'; }) export class WizardTreeParentComponent implements OnInit, OnDestroy { - nodes: Criteria[] = []; + nodes: Criteria[]; private subscription: Subscription; loading: boolean; - constructor(private searchService: SearchService, + constructor(private cohortBuilderService: CohortBuilderService, private broadcastService: BroadcastService) {} ngOnInit(): void { this.loading = true; this.subscription = this.broadcastService.selectedCriteriaType$ - .mergeMap(criteriaType => this.searchService.getParentNodes(criteriaType)) + .mergeMap(criteriaType => this.cohortBuilderService.getCriteriaByTypeAndParentId(criteriaType, '0')) .subscribe(nodes => { - this.nodes = nodes; + this.nodes = nodes.items; this.loading = false; }); } - public selectCriteria(node): void { + public selectCriteria(node: any): void { node.values.push(new SearchParameter(node.code, node.domainId)); this.broadcastService.selectCriteria(node); } From f430f820fb07a2ea0d1415cc6527df0a3378e166 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 11 Sep 2017 09:32:23 -0500 Subject: [PATCH 4/7] RW-15 updated to proper description --- api/src/main/resources/workbench.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/main/resources/workbench.yaml b/api/src/main/resources/workbench.yaml index 1705a5cabf3..5e8645d7052 100644 --- a/api/src/main/resources/workbench.yaml +++ b/api/src/main/resources/workbench.yaml @@ -523,7 +523,7 @@ definitions: description: type of criteria type: string code: - description: what level of data access the user has + description: code that identifies this criteria type: string name: description: description of criteria @@ -533,10 +533,10 @@ definitions: type: integer format: int64 group: - description: specifies of child or parent + description: specifies if child or parent type: boolean selectable: - description: specifies if use can search with + description: specifies if user can search with type: boolean domainId: description: clue to determine which tables to search From 3b0ebfe35abdc052d6d89687390ace68fa7c2cc3 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 11 Sep 2017 09:34:50 -0500 Subject: [PATCH 5/7] RW-15 implemented parameterized query --- .../api/CohortBuilderController.java | 61 +++++++------------ 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java b/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java index e1dbac9d0d1..cb2fda4f6cf 100644 --- a/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java +++ b/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java @@ -3,13 +3,10 @@ import com.google.cloud.bigquery.*; import org.pmiops.workbench.model.Criteria; import org.pmiops.workbench.model.CriteriaListResponse; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; import java.util.List; -import java.util.UUID; -import java.util.logging.Level; import java.util.logging.Logger; @RestController @@ -20,50 +17,38 @@ public class CohortBuilderController implements CohortBuilderApiDelegate { @Override public ResponseEntity getCriteriaByTypeAndParentId(String type, String parentId) { - // Instantiates a client - BigQuery bigquery = BigQueryOptions.getDefaultInstance().getService(); + BigQuery bigquery = + new BigQueryOptions.DefaultBigqueryFactory().create(BigQueryOptions.getDefaultInstance()); - QueryJobConfiguration queryConfig = - QueryJobConfiguration.newBuilder( - "SELECT id, " + - "type, " + - "code, " + - "name, " + - "est_count, " + - "is_group, " + - "is_selectable, " + - "domain_id " + - "FROM `pmi-drc-api-test.synpuf.icd9_criteria` " + - "where parent_id = 0 " + - "order by id asc;") - // Use standard SQL syntax for queries. - // See: https://cloud.google.com/bigquery/sql-reference/ + String queryString = + "SELECT id, type, code, name, est_count, is_group, is_selectable, domain_id " + + "FROM `pmi-drc-api-test.synpuf.icd9_crtieria` " + + "WHERE parent_id = @parentId " + + "order by id asc"; + QueryRequest queryRequest = + QueryRequest.newBuilder(queryString) + .addNamedParameter("parentId", QueryParameterValue.string(parentId)) .setUseLegacySql(false) .build(); - // Create a job ID so that we can safely retry. - JobId jobId = JobId.of(UUID.randomUUID().toString()); - Job queryJob = bigquery.create(JobInfo.newBuilder(queryConfig).setJobId(jobId).build()); + // Execute the query. + QueryResponse response = bigquery.query(queryRequest); - // Wait for the query to complete. - try { - queryJob = queryJob.waitFor(); - } catch (Exception e) { - log.log(Level.INFO, "Timeout occurred: ", e); - return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).build(); + // Wait for the job to finish + while (!response.jobCompleted()) { + response = bigquery.getQueryResults(response.getJobId()); } - // Check for errors - if (queryJob == null) { - throw new RuntimeException("Job no longer exists"); - } else if (queryJob.getStatus().getError() != null) { - // You can also look at queryJob.getStatus().getExecutionErrors() for all - // errors, not just the latest one. - throw new RuntimeException(queryJob.getStatus().getError().toString()); + // Check for errors. + if (response.hasErrors()) { + String firstError = ""; + if (response.getExecutionErrors().size() != 0) { + firstError = response.getExecutionErrors().get(0).getMessage(); + } + throw new RuntimeException(firstError); } - // Get the results. - QueryResponse response = bigquery.getQueryResults(jobId); + // Print all pages of the results. QueryResult result = response.getResult(); CriteriaListResponse criteriaResponse = new CriteriaListResponse(); From fa029e629c9e2cfb8216d4101a9a7a7f7a80ae3d Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 11 Sep 2017 15:07:06 -0500 Subject: [PATCH 6/7] RW-15 added a couple of helper methods --- .../api/CohortBuilderController.java | 64 ++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java b/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java index cb2fda4f6cf..715027901f7 100644 --- a/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java +++ b/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java @@ -6,7 +6,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.logging.Logger; @RestController @@ -14,20 +16,42 @@ public class CohortBuilderController implements CohortBuilderApiDelegate { private static final Logger log = Logger.getLogger(CohortBuilderController.class.getName()); + public static final String CRTIERIA_QUERY = "SELECT id, type, code, name, est_count, is_group, is_selectable, domain_id\n" + + "FROM `pmi-drc-api-test.synpuf.%s`\n" + + "WHERE parent_id = @parentId\n" + + "order by id asc"; + @Override public ResponseEntity getCriteriaByTypeAndParentId(String type, String parentId) { + QueryResult result = getQueryResult(type, parentId); + + Map rm = getResultMapper(result); + + CriteriaListResponse criteriaResponse = new CriteriaListResponse(); + for (List row : result.iterateAll()) { + final Criteria criteria = new Criteria(); + criteria.setId(row.get(rm.get("id")).getLongValue()); + criteria.setType(row.get(rm.get("type")).getStringValue()); + criteria.setCode(row.get(rm.get("code")).getStringValue()); + criteria.setName(row.get(rm.get("name")).getStringValue()); + criteria.setCount(row.get(rm.get("est_count")).isNull() ? 0 : row.get(rm.get("est_count")).getLongValue()); + criteria.setGroup(row.get(rm.get("is_group")).getBooleanValue()); + criteria.setSelectable(row.get(rm.get("is_selectable")).getBooleanValue()); + criteria.setDomainId(row.get(rm.get("domain_id")).isNull() ? null : row.get(rm.get("domain_id")).getStringValue()); + criteriaResponse.addItemsItem(criteria); + } + + return ResponseEntity.ok(criteriaResponse); + } + + protected QueryResult getQueryResult(String type, String parentId) { BigQuery bigquery = new BigQueryOptions.DefaultBigqueryFactory().create(BigQueryOptions.getDefaultInstance()); - String queryString = - "SELECT id, type, code, name, est_count, is_group, is_selectable, domain_id " - + "FROM `pmi-drc-api-test.synpuf.icd9_crtieria` " - + "WHERE parent_id = @parentId " - + "order by id asc"; QueryRequest queryRequest = - QueryRequest.newBuilder(queryString) - .addNamedParameter("parentId", QueryParameterValue.string(parentId)) + QueryRequest.newBuilder(getQueryString(type)) + .addNamedParameter("parentId", QueryParameterValue.int64(new Integer(parentId))) .setUseLegacySql(false) .build(); @@ -48,23 +72,19 @@ public ResponseEntity getCriteriaByTypeAndParentId(String throw new RuntimeException(firstError); } - // Print all pages of the results. - QueryResult result = response.getResult(); + return response.getResult(); + } - CriteriaListResponse criteriaResponse = new CriteriaListResponse(); - for (List row : result.iterateAll()) { - final Criteria criteria = new Criteria(); - criteria.setId(row.get(0).getLongValue()); - criteria.setType(row.get(1).getStringValue()); - criteria.setCode(row.get(2).getStringValue()); - criteria.setName(row.get(3).getStringValue()); - criteria.setCount(row.get(4).isNull() ? 0 : row.get(4).getLongValue()); - criteria.setGroup(row.get(5).getBooleanValue()); - criteria.setSelectable(row.get(6).getBooleanValue()); - criteria.setDomainId(row.get(7).isNull() ? null : row.get(7).getStringValue()); - criteriaResponse.addItemsItem(criteria); + protected Map getResultMapper(QueryResult result) { + Map resultMapper = new HashMap(); + int i = 0; + for (Field field : result.getSchema().getFields()) { + resultMapper.put(field.getName(), i++); } + return resultMapper; + } - return ResponseEntity.ok(criteriaResponse); + protected String getQueryString(String type) { + return String.format(CRTIERIA_QUERY, type + "_criteria"); } } From 91889fcf0d0107a0f107d5fe056af1c1325d1e5a Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 11 Sep 2017 15:27:27 -0500 Subject: [PATCH 7/7] RW-15 reformatted sql statement and corrected a misspelling --- .../workbench/api/CohortBuilderController.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java b/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java index 715027901f7..5311dcade54 100644 --- a/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java +++ b/api/src/main/java/org/pmiops/workbench/api/CohortBuilderController.java @@ -16,10 +16,18 @@ public class CohortBuilderController implements CohortBuilderApiDelegate { private static final Logger log = Logger.getLogger(CohortBuilderController.class.getName()); - public static final String CRTIERIA_QUERY = "SELECT id, type, code, name, est_count, is_group, is_selectable, domain_id\n" - + "FROM `pmi-drc-api-test.synpuf.%s`\n" - + "WHERE parent_id = @parentId\n" - + "order by id asc"; + public static final String CRITERIA_QUERY = + "SELECT id,\n" + + "type,\n" + + "code,\n" + + "name,\n" + + "est_count,\n" + + "is_group,\n" + + "is_selectable,\n" + + "domain_id\n" + + "FROM `pmi-drc-api-test.synpuf.%s`\n" + + "WHERE parent_id = @parentId\n" + + "order by id asc"; @Override public ResponseEntity getCriteriaByTypeAndParentId(String type, String parentId) { @@ -85,6 +93,6 @@ protected Map getResultMapper(QueryResult result) { } protected String getQueryString(String type) { - return String.format(CRTIERIA_QUERY, type + "_criteria"); + return String.format(CRITERIA_QUERY, type + "_criteria"); } }