Skip to content

Commit acac809

Browse files
committed
Add variant recommendation page
1 parent dc20eb3 commit acac809

23 files changed

+1016
-3
lines changed

pom.xml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@
321321
<artifactId>firebase-admin</artifactId>
322322
<version>9.1.1</version>
323323
</dependency>
324+
<dependency>
325+
<groupId>com.googlecode.json-simple</groupId>
326+
<artifactId>json-simple</artifactId>
327+
<version>1.1</version>
328+
</dependency>
324329
<dependency>
325330
<groupId>io.sentry</groupId>
326331
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
@@ -343,6 +348,11 @@
343348
<version>${jsonwebtoken.version}</version>
344349
<scope>runtime</scope>
345350
</dependency>
351+
<dependency>
352+
<groupId>software.amazon.awssdk</groupId>
353+
<artifactId>aws-sdk-java</artifactId>
354+
<version>2.22.7</version>
355+
</dependency>
346356
<dependency>
347357
<groupId>org.json</groupId>
348358
<artifactId>json</artifactId>
@@ -1257,4 +1267,4 @@
12571267
</build>
12581268
</profile>
12591269
</profiles>
1260-
</project>
1270+
</project>

src/main/java/org/mskcc/oncokb/curation/config/Constants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,7 @@ public final class Constants {
2222

2323
public static final String DEFAULT_GENE_SYNONMN_SOURCE = "cBioPortal";
2424

25+
public static final String ONCOKB_S3_BUCKET = "oncokb-bucket";
26+
2527
private Constants() {}
2628
}

src/main/java/org/mskcc/oncokb/curation/config/application/ApplicationProperties.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,22 @@ public class ApplicationProperties extends org.mskcc.oncokb.meta.model.applicati
1919

2020
private FirebaseProperties firebase;
2121

22+
private AwsProperties aws;
23+
2224
private String oncokbDataRepoDir;
2325

2426
private OncoKbCoreConfig oncokbCore;
2527

2628
private String nihEutilsToken;
2729

30+
public AwsProperties getAws() {
31+
return aws;
32+
}
33+
34+
public void setAws(AwsProperties aws) {
35+
this.aws = aws;
36+
}
37+
2838
public OncoKbCoreConfig getOncokbCore() {
2939
return oncokbCore;
3040
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.mskcc.oncokb.curation.config.application;
2+
3+
public class AwsProperties {
4+
5+
// Copy from oncoKb-Public
6+
private String accessKeyId;
7+
private String secretAccessKey;
8+
private String region;
9+
10+
public String getAccessKeyId() {
11+
return this.accessKeyId;
12+
}
13+
14+
public void setAccessKeyId(String accessKeyId) {
15+
this.accessKeyId = accessKeyId;
16+
}
17+
18+
public String getSecretAccessKey() {
19+
return this.secretAccessKey;
20+
}
21+
22+
public void setSecretAccessKey(String secretAccessKey) {
23+
this.secretAccessKey = secretAccessKey;
24+
}
25+
26+
public String getRegion() {
27+
return this.region;
28+
}
29+
30+
public void setRegion(String region) {
31+
this.region = region;
32+
}
33+
}

src/main/java/org/mskcc/oncokb/curation/config/security/SecurityConfigurationOAuth.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public SecurityFilterChain oauthFilterChain(HttpSecurity http, MvcRequestMatcher
8888
.requestMatchers(mvc.pattern("/api/authenticate")).permitAll()
8989
.requestMatchers(mvc.pattern("/api/auth-info")).permitAll()
9090
.requestMatchers(mvc.pattern("/api/logout")).permitAll()
91+
.requestMatchers("/api/variant-recommendation/**").permitAll()
9192
.requestMatchers(mvc.pattern("/api/admin/**")).hasAuthority(AuthoritiesConstants.ADMIN)
9293
.requestMatchers(mvc.pattern("/api/audit/**")).hasAuthority(AuthoritiesConstants.ADMIN)
9394
.requestMatchers(mvc.pattern("/api/account/firebase-token")).hasAnyAuthority(AuthoritiesConstants.CURATOR, AuthoritiesConstants.USER)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.mskcc.oncokb.curation.service;
2+
3+
import java.util.Optional;
4+
import org.mskcc.oncokb.curation.config.application.ApplicationProperties;
5+
import org.mskcc.oncokb.curation.config.application.AwsProperties;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.stereotype.Service;
9+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
10+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
11+
import software.amazon.awssdk.core.ResponseInputStream;
12+
import software.amazon.awssdk.core.sync.ResponseTransformer;
13+
import software.amazon.awssdk.regions.Region;
14+
import software.amazon.awssdk.services.s3.S3Client;
15+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
16+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
17+
18+
@Service
19+
public class S3Service {
20+
21+
private final Logger log = LoggerFactory.getLogger(S3Service.class);
22+
private final ApplicationProperties applicationProperties;
23+
private S3Client s3Client;
24+
25+
public S3Service(ApplicationProperties applicationProperties) {
26+
this.applicationProperties = applicationProperties;
27+
AwsProperties awsProperties = applicationProperties.getAws();
28+
if (awsProperties != null) {
29+
String accessKeyId = awsProperties.getAccessKeyId();
30+
String secretAccessKey = awsProperties.getSecretAccessKey();
31+
String region = awsProperties.getRegion();
32+
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretAccessKey);
33+
s3Client = S3Client.builder().region(Region.of(region)).credentialsProvider(StaticCredentialsProvider.create(awsCreds)).build();
34+
log.info("S3 Client successfully initialized");
35+
} else {
36+
log.error("AWS credentials not configured properly");
37+
}
38+
}
39+
40+
/**
41+
* Get an object from aws s3
42+
* @param bucket s3 bucket name
43+
* @param objectPath the path of the object
44+
* @return a S3 object
45+
*/
46+
public Optional<ResponseInputStream<GetObjectResponse>> getObject(String bucket, String objectPath) {
47+
try {
48+
ResponseInputStream<GetObjectResponse> s3object = s3Client.getObject(
49+
GetObjectRequest.builder().bucket(bucket).key(objectPath).build(),
50+
ResponseTransformer.toInputStream()
51+
);
52+
return Optional.of(s3object);
53+
} catch (Exception e) {
54+
log.error(e.getMessage(), e);
55+
return Optional.empty();
56+
}
57+
}
58+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package org.mskcc.oncokb.curation.web.rest;
2+
3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStreamReader;
6+
import java.nio.charset.StandardCharsets;
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import java.util.Optional;
10+
import org.json.simple.JSONObject;
11+
import org.mskcc.oncokb.curation.config.Constants;
12+
import org.mskcc.oncokb.curation.service.S3Service;
13+
import org.mskcc.oncokb.curation.web.rest.errors.BadRequestAlertException;
14+
import org.mskcc.oncokb.curation.web.rest.errors.ResourceNotFoundException;
15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
17+
import org.springframework.beans.factory.annotation.Autowired;
18+
import org.springframework.http.ResponseEntity;
19+
import org.springframework.web.bind.annotation.GetMapping;
20+
import org.springframework.web.bind.annotation.PathVariable;
21+
import org.springframework.web.bind.annotation.RequestMapping;
22+
import org.springframework.web.bind.annotation.RestController;
23+
import software.amazon.awssdk.core.ResponseInputStream;
24+
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
25+
26+
@RestController
27+
@RequestMapping("/api")
28+
public class VariantRecommendController {
29+
30+
private final Logger log = LoggerFactory.getLogger(VariantRecommendController.class);
31+
32+
private static final String ENTITY_NAME = "variant-recommendation";
33+
34+
@Autowired
35+
private S3Service s3Service;
36+
37+
@GetMapping("/variant-recommendation/{filename}")
38+
public ResponseEntity<List<JSONObject>> requestData(@PathVariable String filename) throws IOException {
39+
Optional<ResponseInputStream<GetObjectResponse>> s3object = s3Service.getObject(Constants.ONCOKB_S3_BUCKET, filename);
40+
if (s3object.isPresent()) {
41+
BufferedReader reader = new BufferedReader(new InputStreamReader(s3object.get(), StandardCharsets.UTF_8));
42+
String headerLine = reader.readLine();
43+
if (headerLine == null) {
44+
throw new BadRequestAlertException("File is empty", ENTITY_NAME, "fileempty");
45+
}
46+
String[] headers = headerLine.split("\t");
47+
List<JSONObject> jsonList = new ArrayList<>();
48+
String line;
49+
while ((line = reader.readLine()) != null) {
50+
String[] data = line.split("\t");
51+
JSONObject jsonObject = new JSONObject();
52+
for (int i = 0; i < headers.length; i++) {
53+
jsonObject.put(headers[i], "n/a".equals(data[i]) ? null : data[i]);
54+
}
55+
jsonList.add(jsonObject);
56+
}
57+
58+
return ResponseEntity.ok(jsonList);
59+
} else {
60+
throw new ResourceNotFoundException("File not Found", ENTITY_NAME, "nofile");
61+
}
62+
}
63+
}

src/main/java/org/mskcc/oncokb/curation/web/rest/errors/ErrorConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public final class ErrorConstants {
1212
public static final URI USER_NOT_APPROVED = URI.create(PROBLEM_BASE_URL + "user-not-approved");
1313
public static final URI EMAIL_ALREADY_USED_TYPE = URI.create(PROBLEM_BASE_URL + "/email-already-used");
1414
public static final URI LOGIN_ALREADY_USED_TYPE = URI.create(PROBLEM_BASE_URL + "/login-already-used");
15+
public static final URI RESOURCE_NOT_FOUND = URI.create(PROBLEM_BASE_URL + "/resource-not-found");
1516

1617
public ErrorConstants() {}
1718
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.mskcc.oncokb.curation.web.rest.errors;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.web.ErrorResponseException;
5+
import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder;
6+
7+
public class ResourceNotFoundException extends ErrorResponseException {
8+
9+
private static final long serialVersionUID = 1L;
10+
11+
private final String entityName;
12+
13+
private final String errorKey;
14+
15+
public ResourceNotFoundException(String defaultMessage, String entityName, String errorKey) {
16+
super(
17+
HttpStatus.NOT_FOUND,
18+
ProblemDetailWithCauseBuilder.instance()
19+
.withStatus(HttpStatus.NOT_FOUND.value())
20+
.withType(ErrorConstants.RESOURCE_NOT_FOUND)
21+
.withTitle(defaultMessage)
22+
.withProperty("message", "error." + errorKey)
23+
.withProperty("params", entityName)
24+
.build(),
25+
null
26+
);
27+
this.entityName = entityName;
28+
this.errorKey = errorKey;
29+
}
30+
31+
public String getEntityName() {
32+
return entityName;
33+
}
34+
35+
public String getErrorKey() {
36+
return errorKey;
37+
}
38+
}

src/main/webapp/app/components/sidebar/NavigationSidebar.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,14 @@ export const NavigationSidebar: React.FunctionComponent<StoreProps> = ({ isNavSi
248248
nav={<NavLink to={PAGE_ROUTE.CURATION} />}
249249
/>
250250
)}
251+
{props.isUser && (
252+
<MenuItemCollapsible
253+
isCollapsed={isNavSidebarCollapsed}
254+
text="Variant Recommendation"
255+
icon={<FiFileText size={DEFAULT_NAV_ICON_SIZE} />}
256+
nav={<NavLink to={PAGE_ROUTE.VARIANR_REC} />}
257+
/>
258+
)}
251259
{props.isUser && (
252260
<>
253261
<MenuItemCollapsible

src/main/webapp/app/config/constants/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ export enum FEATURE_PAGE_ROUTE {
108108
OAUTH = '/oauth2/authorization/oidc',
109109
SEARCH = '/search',
110110
SWAGGER = '/swagger-ui/index.html',
111+
/* Below are variant recommendation related paths */
112+
VARIANR_REC = '/variant-recommendation',
111113
/* Below are curation related paths */
112114
CURATION = '/curation',
113115
CURATION_SOMATIC = '/curation/somatic',
@@ -441,3 +443,7 @@ export const KEYCLOAK_UNAUTHORIZED_PARAM = 'unauthorized';
441443
*/
442444
export const PRIORITY_ENTITY_MENU_ITEM_KEY = 'oncokbCuration-entityMenuPriorityKey';
443445
export const SOMATIC_GERMLINE_SETTING_KEY = 'oncokbCuration-somaticGermlineSettingKey';
446+
/* Files name in S3 bucket */
447+
export const ALL_FREQUENCY_FILE = 'msk_impact_2017_all_frequency.txt';
448+
export const TYPE_FREQUENCY_FILE = 'msk_impact_2017_type_frequency.txt';
449+
export const DETAILED_FREQUENCY_FILE = 'msk_impact_2017_detailed_frequency.txt';

src/main/webapp/app/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom';
33
import { Provider } from 'mobx-react';
44
import { createStores } from 'app/stores';
55
import { createBrowserHistory } from 'history';
6-
76
import setupAxiosInterceptors from './config/axios-interceptor';
87
import ErrorBoundary from './shared/error/error-boundary';
98
import AppComponent from './app';
@@ -94,7 +93,7 @@ const render = Component =>
9493
<Component />
9594
</Provider>
9695
</ErrorBoundary>,
97-
rootEl
96+
rootEl,
9897
);
9998

10099
render(AppComponent);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React, { useState } from 'react';
2+
import formatPercentage from 'app/pages/variantRecommendation/FormatPercentage';
3+
import classnames from 'classnames';
4+
import * as styles from './styles.module.scss';
5+
6+
interface RangeSliderProps {
7+
min: number;
8+
max: number;
9+
range: [number, number];
10+
step: number;
11+
onChange: (newRange: [number, number]) => void;
12+
}
13+
14+
export const TwoRangeSlider = ({ min, max, range, step, onChange }: RangeSliderProps) => {
15+
const [minValue, setMinValue] = useState(range ? range[0] : min);
16+
const [maxValue, setMaxValue] = useState(range ? range[1] : max);
17+
18+
const handleMinChange = e => {
19+
e.preventDefault();
20+
const newMinVal = Math.min(+e.target.value, maxValue);
21+
setMinValue(newMinVal);
22+
onChange([newMinVal, maxValue]);
23+
};
24+
25+
const handleMaxChange = e => {
26+
e.preventDefault();
27+
const newMaxVal = Math.max(+e.target.value, minValue);
28+
setMaxValue(newMaxVal);
29+
onChange([minValue, newMaxVal]);
30+
};
31+
32+
const minPos = ((minValue - min) / ((max ?? 100) - min)) * 100;
33+
const maxPos = ((maxValue - min) / ((max ?? 100) - min)) * 100;
34+
35+
return (
36+
<div className={styles.wrapper}>
37+
<div className={styles['range-slider']}>
38+
<div className={styles['range-labels']}>
39+
<span className={classnames(styles['range-label'], styles['range-label-start'])}>{formatPercentage(minValue)}</span>
40+
<span className={classnames(styles['range-label'], styles['range-label-end'])}>{formatPercentage(maxValue)}</span>
41+
</div>
42+
<input type="range" value={minValue} min={min} max={max} step={step} onChange={handleMinChange} />
43+
<input type="range" value={maxValue} min={min} max={max} step={step} onChange={handleMaxChange} />
44+
<div className={styles['track-wrapper']}>
45+
<div className={styles['track']} />
46+
<div className={styles['range-between']} style={{ left: `${minPos}%`, right: `${100 - maxPos}%` }} />
47+
<div className={styles['control']} style={{ left: `${minPos}%` }} />
48+
<div className={styles['control']} style={{ left: `${maxPos}%` }} />
49+
</div>
50+
</div>
51+
</div>
52+
);
53+
};
54+
55+
export default TwoRangeSlider;

0 commit comments

Comments
 (0)