Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add variant recommendation page #426

Open
wants to merge 3 commits into
base: feat/var-rec
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,11 @@
<version>${jsonwebtoken.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-sdk-java</artifactId>
<version>2.22.7</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
Expand Down Expand Up @@ -1257,4 +1262,4 @@
</build>
</profile>
</profiles>
</project>
</project>
2 changes: 2 additions & 0 deletions src/main/java/org/mskcc/oncokb/curation/config/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ public final class Constants {

public static final String DEFAULT_GENE_SYNONMN_SOURCE = "cBioPortal";

public static final String ONCOKB_S3_BUCKET = "oncokb-bucket";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zhx828 do you want this to be an environment variable?


private Constants() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,22 @@ public class ApplicationProperties extends org.mskcc.oncokb.meta.model.applicati

private FirebaseProperties firebase;

private AwsProperties aws;

private String oncokbDataRepoDir;

private OncoKbCoreConfig oncokbCore;

private String nihEutilsToken;

public AwsProperties getAws() {
return aws;
}

public void setAws(AwsProperties aws) {
this.aws = aws;
}

public OncoKbCoreConfig getOncokbCore() {
return oncokbCore;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.mskcc.oncokb.curation.config.application;

public class AwsProperties {

// Copy from oncoKb-Public
private String accessKeyId;
private String secretAccessKey;
private String region;

public String getAccessKeyId() {
return this.accessKeyId;
}

public void setAccessKeyId(String accessKeyId) {
this.accessKeyId = accessKeyId;
}

public String getSecretAccessKey() {
return this.secretAccessKey;
}

public void setSecretAccessKey(String secretAccessKey) {
this.secretAccessKey = secretAccessKey;
}

public String getRegion() {
return this.region;
}

public void setRegion(String region) {
this.region = region;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public SecurityFilterChain oauthFilterChain(HttpSecurity http, MvcRequestMatcher
.requestMatchers(mvc.pattern("/api/authenticate")).permitAll()
.requestMatchers(mvc.pattern("/api/auth-info")).permitAll()
.requestMatchers(mvc.pattern("/api/logout")).permitAll()
.requestMatchers("/api/variant-recommendation/**").permitAll()
.requestMatchers(mvc.pattern("/api/admin/**")).hasAuthority(AuthoritiesConstants.ADMIN)
.requestMatchers(mvc.pattern("/api/audit/**")).hasAuthority(AuthoritiesConstants.ADMIN)
.requestMatchers(mvc.pattern("/api/account/firebase-token")).hasAnyAuthority(AuthoritiesConstants.CURATOR, AuthoritiesConstants.USER)
Expand Down
58 changes: 58 additions & 0 deletions src/main/java/org/mskcc/oncokb/curation/service/S3Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.mskcc.oncokb.curation.service;

import java.util.Optional;
import org.mskcc.oncokb.curation.config.application.ApplicationProperties;
import org.mskcc.oncokb.curation.config.application.AwsProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.sync.ResponseTransformer;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;

@Service
public class S3Service {

private final Logger log = LoggerFactory.getLogger(S3Service.class);
private final ApplicationProperties applicationProperties;
private S3Client s3Client;

public S3Service(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
AwsProperties awsProperties = applicationProperties.getAws();
if (awsProperties != null) {
String accessKeyId = awsProperties.getAccessKeyId();
String secretAccessKey = awsProperties.getSecretAccessKey();
String region = awsProperties.getRegion();
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretAccessKey);
s3Client = S3Client.builder().region(Region.of(region)).credentialsProvider(StaticCredentialsProvider.create(awsCreds)).build();
log.info("S3 Client successfully initialized");
} else {
log.error("AWS credentials not configured properly");
}
}

/**
* Get an object from aws s3
* @param bucket s3 bucket name
* @param objectPath the path of the object
* @return a S3 object
*/
public Optional<ResponseInputStream<GetObjectResponse>> getObject(String bucket, String objectPath) {
try {
ResponseInputStream<GetObjectResponse> s3object = s3Client.getObject(
GetObjectRequest.builder().bucket(bucket).key(objectPath).build(),
ResponseTransformer.toInputStream()
);
return Optional.of(s3object);
} catch (Exception e) {
log.error(e.getMessage(), e);
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.mskcc.oncokb.curation.web.rest;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.json.JSONArray;
import org.json.JSONObject;
import org.mskcc.oncokb.curation.config.Constants;
import org.mskcc.oncokb.curation.service.S3Service;
import org.mskcc.oncokb.curation.web.rest.errors.BadRequestAlertException;
import org.mskcc.oncokb.curation.web.rest.errors.ResourceNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;

@RestController
@RequestMapping("/api")
public class VariantRecommendController {

private final Logger log = LoggerFactory.getLogger(VariantRecommendController.class);

private static final String ENTITY_NAME = "variant-recommendation";

@Autowired
private S3Service s3Service;

@GetMapping("/variant-recommendation/{filename}")
public ResponseEntity<String> requestData(@PathVariable String filename) throws IOException {
Optional<ResponseInputStream<GetObjectResponse>> s3object = s3Service.getObject(Constants.ONCOKB_S3_BUCKET, filename);
if (s3object.isPresent()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(s3object.orElseThrow(), StandardCharsets.UTF_8));
String headerLine = reader.readLine();
if (headerLine == null) {
throw new BadRequestAlertException("File is empty", ENTITY_NAME, "fileempty");
}
String[] headers = headerLine.split("\t");
JSONArray jsonArray = new JSONArray();
String line;
while ((line = reader.readLine()) != null) {
String[] data = line.split("\t");
JSONObject jsonObject = new JSONObject();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you able to use Gson?

for (int i = 0; i < headers.length; i++) {
jsonObject.put(headers[i], "n/a".equals(data[i]) ? null : data[i]);
}
jsonArray.put(jsonObject);
}
return ResponseEntity.ok(jsonArray.toString());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you double that the content type is Content-Type: application/json; in the response header?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

} else {
throw new ResourceNotFoundException("File not Found", ENTITY_NAME, "nofile");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public final class ErrorConstants {
public static final URI USER_NOT_APPROVED = URI.create(PROBLEM_BASE_URL + "user-not-approved");
public static final URI EMAIL_ALREADY_USED_TYPE = URI.create(PROBLEM_BASE_URL + "/email-already-used");
public static final URI LOGIN_ALREADY_USED_TYPE = URI.create(PROBLEM_BASE_URL + "/login-already-used");
public static final URI RESOURCE_NOT_FOUND = URI.create(PROBLEM_BASE_URL + "/resource-not-found");

public ErrorConstants() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.mskcc.oncokb.curation.web.rest.errors;

import org.springframework.http.HttpStatus;
import org.springframework.web.ErrorResponseException;
import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder;

public class ResourceNotFoundException extends ErrorResponseException {

private static final long serialVersionUID = 1L;

private final String entityName;

private final String errorKey;

public ResourceNotFoundException(String defaultMessage, String entityName, String errorKey) {
super(
HttpStatus.NOT_FOUND,
ProblemDetailWithCauseBuilder.instance()
.withStatus(HttpStatus.NOT_FOUND.value())
.withType(ErrorConstants.RESOURCE_NOT_FOUND)
.withTitle(defaultMessage)
.withProperty("message", "error." + errorKey)
.withProperty("params", entityName)
.build(),
null
);
this.entityName = entityName;
this.errorKey = errorKey;
}

public String getEntityName() {
return entityName;
}

public String getErrorKey() {
return errorKey;
}
}
8 changes: 8 additions & 0 deletions src/main/webapp/app/components/sidebar/NavigationSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,14 @@ export const NavigationSidebar: React.FunctionComponent<StoreProps> = ({ isNavSi
nav={<NavLink to={PAGE_ROUTE.CURATION} />}
/>
)}
{props.isUser && (
<MenuItemCollapsible
isCollapsed={isNavSidebarCollapsed}
text="Variant Recommendation"
icon={<FiFileText size={DEFAULT_NAV_ICON_SIZE} />}
nav={<NavLink to={PAGE_ROUTE.VARIANR_REC} />}
/>
)}
{props.isUser && (
<>
<MenuItemCollapsible
Expand Down
6 changes: 6 additions & 0 deletions src/main/webapp/app/config/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export enum FEATURE_PAGE_ROUTE {
OAUTH = '/oauth2/authorization/oidc',
SEARCH = '/search',
SWAGGER = '/swagger-ui/index.html',
/* Below are variant recommendation related paths */
VARIANR_REC = '/variant-recommendation',
/* Below are curation related paths */
CURATION = '/curation',
CURATION_SOMATIC = '/curation/somatic',
Expand Down Expand Up @@ -441,3 +443,7 @@ export const KEYCLOAK_UNAUTHORIZED_PARAM = 'unauthorized';
*/
export const PRIORITY_ENTITY_MENU_ITEM_KEY = 'oncokbCuration-entityMenuPriorityKey';
export const SOMATIC_GERMLINE_SETTING_KEY = 'oncokbCuration-somaticGermlineSettingKey';
/* Files name in S3 bucket */
export const ALL_FREQUENCY_FILE = 'msk_impact_2017_all_frequency.txt';
export const TYPE_FREQUENCY_FILE = 'msk_impact_2017_type_frequency.txt';
export const DETAILED_FREQUENCY_FILE = 'msk_impact_2017_detailed_frequency.txt';
3 changes: 1 addition & 2 deletions src/main/webapp/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import ReactDOM from 'react-dom';
import { Provider } from 'mobx-react';
import { createStores } from 'app/stores';
import { createBrowserHistory } from 'history';

import setupAxiosInterceptors from './config/axios-interceptor';
import ErrorBoundary from './shared/error/error-boundary';
import AppComponent from './app';
Expand Down Expand Up @@ -94,7 +93,7 @@ const render = Component =>
<Component />
</Provider>
</ErrorBoundary>,
rootEl
rootEl,
);

render(AppComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useState } from 'react';
import { formatPercentage } from 'app/shared/util/utils';
import classnames from 'classnames';
import * as styles from './styles.module.scss';

interface RangeSliderProps {
min: number;
max: number;
range: [number, number];
step: number;
onChange: (newRange: [number, number]) => void;
}

export const TwoThumbSlider = ({ min, max, range, step, onChange }: RangeSliderProps) => {
const [minValue, setMinValue] = useState(range ? range[0] : min);
const [maxValue, setMaxValue] = useState(range ? range[1] : max);

const handleMinChange = e => {
e.preventDefault();
const newMinVal = Math.min(+e.target.value, maxValue);
setMinValue(newMinVal);
onChange([newMinVal, maxValue]);
};

const handleMaxChange = e => {
e.preventDefault();
const newMaxVal = Math.max(+e.target.value, minValue);
setMaxValue(newMaxVal);
onChange([minValue, newMaxVal]);
};

const minPos = ((minValue - min) / ((max ?? 100) - min)) * 100;
const maxPos = ((maxValue - min) / ((max ?? 100) - min)) * 100;

return (
<div className={styles.wrapper}>
<div className={styles['range-slider']}>
<div className={styles['range-labels']}>
<span className={classnames(styles['range-label'], styles['range-label-start'])}>{formatPercentage(minValue)}</span>
<span className={classnames(styles['range-label'], styles['range-label-end'])}>{formatPercentage(maxValue)}</span>
</div>
<input type="range" value={minValue} min={min} max={max} step={step} onChange={handleMinChange} />
<input type="range" value={maxValue} min={min} max={max} step={step} onChange={handleMaxChange} />
<div className={styles['track-wrapper']}>
<div className={styles['track']} />
<div className={styles['range-between']} style={{ left: `${minPos}%`, right: `${100 - maxPos}%` }} />
<div className={styles['control']} style={{ left: `${minPos}%` }} />
<div className={styles['control']} style={{ left: `${maxPos}%` }} />
</div>
</div>
</div>
);
};

export default TwoThumbSlider;
Loading
Loading