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

GIC Release 1.4.0 #168

Merged
merged 5 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file. As our fork has diverged from AWS SWB mainline branch, we are noting the SWB version and the lab version together, as <swb version>\_<lab version>, starting from SWB mainline, 5.0.0.

## [5.0.0_1.4.0](https://github.com/hms-dbmi/service-workbench-on-aws/compare/v5.0.0_1.3.2...v5.0.0_1.4.0) (01/25/2024)
* Parameterize user register TOS acceptance.
* Add comma separated study whitelist in stage file.
* Add Jira support widget.
* Add dropdown to help link if more than one url is given.

## [5.0.0_1.3.2](https://github.com/hms-dbmi/service-workbench-on-aws/compare/v5.0.0_1.3.1...v5.0.0_1.3.2) (12/21/2023)
- Update register page email regex validation: validatorjs schema regex fields need slashes before and after the regex, or else it returns a validation error.

Expand Down
4 changes: 4 additions & 0 deletions addons/addon-base-ui/packages/base-ui/src/helpers/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const branding = {
title: process.env.REACT_APP_USER_REGISTRATION_TITLE,
summary: process.env.REACT_APP_USER_REGISTRATION_SUMMARY,
success: process.env.REACT_APP_USER_REGISTRATION_SUCCESS,
tosRequired: process.env.REACT_APP_USER_REGISTRATION_TOS_REQUIRED === 'true',
},
tos: {
onLanding: process.env.REACT_APP_TOS_LINK_ON_LANDING === 'true',
},
main: {
title: process.env.REACT_APP_BRAND_MAIN_TITLE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@
"pattern": "\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d:[0-5]\\d|Z)"
}
},
"required": ["email", "firstName", "lastName", "acceptedTerms"]
"required": ["email", "firstName", "lastName"]
}
8 changes: 6 additions & 2 deletions addons/addon-custom/packages/main/src/extend/withAuth.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ function RegisterLogin(enableCustomRegister) {
Register
</Button>
)}
<Link to="/legal">Terms of Service</Link>
<br />
{branding.tos.onLanding && (
<>
<Link to="/legal">Terms of Service</Link>
<br />
</>
)}
{branding.main.loginWarning}
</>
);
Expand Down
36 changes: 32 additions & 4 deletions addons/addon-custom/packages/main/src/parts/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Switch, Redirect, withRouter } from 'react-router-dom';
import { action, decorate, computed } from 'mobx';
import { inject, observer } from 'mobx-react';
import { getEnv } from 'mobx-state-tree';
import { Menu } from 'semantic-ui-react';
import { Menu, Dropdown, DropdownMenu, DropdownItem } from 'semantic-ui-react';

import { getRoutes, getMenuItems, getDefaultRouteLocation } from '@aws-ee/base-ui/dist/helpers/plugins-util';
import MainLayout from '@aws-ee/base-ui/dist/parts/MainLayout';
Expand Down Expand Up @@ -73,14 +73,42 @@ class RegisterApp extends React.Component {
}
}

openHelp() {
window.open(branding.page.help, '_blank');
openHelp(url) {
return () => window.open(url, '_blank');
}

getLinks() {
const helpLinks = (branding.page.help || '').split(',').filter(x => x);
if (helpLinks.length < 2) {
return helpLinks;
}
return helpLinks.reduce((items, link) => {
const [text, ...urlParts] = link.split('=');
const url = urlParts.join('='); // if there are query params then we need to rebuild the url
if (!text || !url) {
console.error(`Poorly formed help link was provided: "${link}"`, 'Expected: "Text=URL"');
return items;
}
return [...items, { text, url }];
}, []);
}

appMenuItems() {
const links = this.getLinks();
return (
<>
{branding.page.help && <Menu.Item onClick={this.openHelp}>Help</Menu.Item>}
{links.length === 1 && <Menu.Item onClick={this.openHelp(links[0])}>Help</Menu.Item>}
{links.length > 1 && (
<Dropdown item text="Help">
<DropdownMenu>
{links.map(({ text, url }) => (
<DropdownItem key={text} onClick={this.openHelp(url)}>
{text}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
)}
<TermsModal // Clickable Terms menu item
trigger={<Menu.Item>Terms of Service</Menu.Item>}
closeOnDimmerClick
Expand Down
68 changes: 39 additions & 29 deletions addons/addon-custom/packages/main/src/parts/Register.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,32 @@ class Register extends React.Component {
};
}

renderTOS() {
return (
<>
{this.terms.value !== termsState.unset.value && <Icon name={this.terms.icon} color={this.terms.color} />}
{this.terms.label} &nbsp;
<TermsModal
trigger={
<button
id="terms-modal"
className="link"
type="button"
ref={ref => {
this.termsModalButton = ref;
}}
>
Terms of Service
</button>
}
closeOnDimmerClick
acceptAction={this.setTerms(termsState.accepted)}
declineAction={this.setTerms(termsState.declined)}
/>
</>
);
}

renderRegisterationForm() {
return (
<Form size="large" loading={this.loading} onSubmit={this.handleSubmit}>
Expand All @@ -101,40 +127,16 @@ class Register extends React.Component {

{this.renderField('email')}
</div>
{branding.register.tosRequired && <div className="center mt2">{this.renderTOS()}</div>}
<div className="center mt3">
{this.terms.value !== termsState.unset.value && <Icon name={this.terms.icon} color={this.terms.color} />}
{this.terms.label} &nbsp;
<TermsModal
trigger={
<button
id="terms-modal"
className="link"
type="button"
ref={ref => {
this.termsModalButton = ref;
}}
>
Terms of Service
</button>
}
closeOnDimmerClick
acceptAction={this.setTerms(termsState.accepted)}
declineAction={this.setTerms(termsState.declined)}
/>
</div>
<div className="mt3 center">
<div>
<Form.Field>
{this.errors.form && (
<div className="mb1">
<Label prompt>{this.errors.form}</Label>
</div>
)}
<Form.Button
id="register-submit"
disabled={this.terms.value !== termsState.accepted.value}
color="green"
>
<Form.Button id="register-submit" disabled={this.submitDisabled()} color="green">
Create a new Service Workbench account
</Form.Button>
</Form.Field>
Expand All @@ -145,6 +147,10 @@ class Register extends React.Component {
);
}

submitDisabled() {
return branding.register.tosRequired ? this.terms.value !== termsState.accepted.value : false;
}

renderConfirmation() {
return (
<div>
Expand Down Expand Up @@ -207,20 +213,24 @@ class Register extends React.Component {
return;
}

// Validate that the terms have been accepted
if (this.terms.value !== termsState.accepted.value) {
// Validate that the terms have been accepted, if required
let acceptedTerms;
if (branding.register.tosRequired && this.terms.value !== termsState.accepted.value) {
runInAction(() => {
this.errors.form = termsErrorText;
this.formProcessing = false;
});
return;
}
if (branding.register.tosRequired) {
acceptedTerms = new Date().toISOString();
}

const result = await registerUser({
firstName: this.user.firstName,
lastName: this.user.lastName,
email: this.user.email,
acceptedTerms: new Date().toISOString(),
acceptedTerms,
});
// if we encounter an error then don't continue to process the form and instead display a message
if (result.error) {
Expand Down
8 changes: 7 additions & 1 deletion main/config/settings/example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,11 @@ HostedZoneId: 'Z02455261RJ9QQPVHFZGA'
#userRegistrationSuccess: "<p>Your Service Workbench account has been successfully created. What you should expect next:</p><ol><li>The Service Workbench administrator will review your account.</li><li>Once your account is activated, you can login to Service Workbench and start your research.</li></ol>"
#loginWarning: "WARNING: You are entering a secure environment."

# URL for help link
# Require the TOS to be accepted before registration can occur.
#tosRequired: true

# Should the TOS link be enabled on the landing page.
#tosLinkOnLanding: true

# URL for help link. Can be empty, a single url, or a list like, "Some Link=https://www.google.com/?q=what,Other Link=https://www.example.com/"
#helpUrl: about:blank
1 change: 1 addition & 0 deletions main/solution/backend/config/infra/functions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ openDataScrapeHandler:
APP_STUDY_DATA_BUCKET_NAME: ${self:custom.settings.studyDataBucketName}
APP_CUSTOM_USER_AGENT: ${self:custom.settings.customUserAgent}
APP_IS_APP_STREAM_ENABLED: ${self:custom.settings.isAppStreamEnabled}
APP_OPEN_DATA_SCRAPE_WHITELIST: ${self:custom.settings.openDataScrapeWhitelist}

events:
- schedule:
Expand Down
3 changes: 3 additions & 0 deletions main/solution/backend/config/settings/.defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,6 @@ dataSourceReachabilityHandlerRoleArn: 'arn:aws:iam::${self:custom.settings.awsAc

# The stack name of the 'backend' serverless service
backendStackName: ${self:custom.settings.namespace}-backend

# custom list of open studies to include when scraping that are not part in the filtered tag list
openDataScrapeWhitelist: ''
Original file line number Diff line number Diff line change
Expand Up @@ -113,33 +113,83 @@ describe('fetchOpenData', () => {
sha: 'abc2',
};

const whitelistStudy = {
name: 'Study 3',
description: 'Example study 3',
tags: ['other-tag'],
resources: [
{
description: 'Description for Study 3',
arn: 'arn:aws:s3:::study3',
region: 'us-east-1',
type: 'S3 Bucket',
},
],
id: 'study-3',
sha: 'abc2',
};

it('has invalid study (Invalid ARN)', async () => {
const fileUrls = ['firstFileUrl'];
const requiredTags = ['genetic'];
const studyWhitelist = [];
const fetchFile = jest.fn();
fetchFile.mockReturnValueOnce(invalidStudy);

const result = await fetchOpenData({ fileUrls, requiredTags, log: consoleLogger, fetchFile });
const result = await fetchOpenData({ fileUrls, requiredTags, studyWhitelist, log: consoleLogger, fetchFile });
expect(result).toEqual([]);
});

it('has one valid study', async () => {
const fileUrls = ['firstFileUrl'];
const requiredTags = ['genetic'];
const studyWhitelist = [];
const fetchFile = jest.fn();
fetchFile.mockReturnValueOnce(validStudy);

const result = await fetchOpenData({ fileUrls, requiredTags, log: consoleLogger, fetchFile });
const result = await fetchOpenData({ fileUrls, requiredTags, studyWhitelist, log: consoleLogger, fetchFile });
expect(result).toEqual([validStudyOpenData]);
});

it('has one valid study and one invalid study (Invalid ARN)', async () => {
const fileUrls = ['firstFileUrl'];
const fileUrls = ['firstFileUrl', 'fileUrl2'];
const requiredTags = ['genetic'];
const studyWhitelist = [];
const fetchFile = jest.fn();
fetchFile.mockReturnValueOnce(validStudy).mockReturnValueOnce(invalidStudy);

const result = await fetchOpenData({ fileUrls, requiredTags, log: consoleLogger, fetchFile });
const result = await fetchOpenData({ fileUrls, requiredTags, studyWhitelist, log: consoleLogger, fetchFile });
expect(result).toEqual([validStudyOpenData]);
});

it('has whitelisted study is included', async () => {
const fileUrls = ['firstFileUrl', 'fileUrl2', 'fileUrl3'];
const requiredTags = ['genetic'];
const studyWhitelist = ['study-3'];
const fetchFile = jest.fn();
fetchFile
.mockReturnValueOnce(validStudy)
.mockReturnValueOnce(invalidStudy)
.mockReturnValueOnce(whitelistStudy);

const result = await fetchOpenData({ fileUrls, requiredTags, studyWhitelist, log: consoleLogger, fetchFile });
expect(result).toEqual([
validStudyOpenData,
{
description: 'Example study 3',
id: 'study-3',
name: 'Study 3',
resources: [
{
arn: 'arn:aws:s3:::study3',
description: 'Description for Study 3',
region: 'us-east-1',
type: 'S3 Bucket',
},
],
sha: 'abc2',
tags: ['other-tag'],
},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,16 @@ if (typeof fetch !== 'function' && fetch.default && typeof fetch.default === 'fu
fetch = fetch.default;
}

const newHandler = async ({ studyService, log = consoleLogger } = {}) => {
const newHandler = async ({ studyService, log = consoleLogger, settings } = {}) => {
const whitelist = settings.get('openDataScrapeWhitelist') || '';
const scrape = {
githubApiUrl: 'https://api.github.com',
rawGithubUrl: 'https://raw.githubusercontent.com',
owner: 'awslabs',
repository: 'open-data-registry',
ref: 'main',
subtree: 'datasets',
studyWhitelist: whitelist.split(',').filter(x => x),
filterTags: [
'genetic',
'genomic',
Expand Down Expand Up @@ -193,15 +195,15 @@ const newHandler = async ({ studyService, log = consoleLogger } = {}) => {
return async () => fetchAndSaveOpenData(fetchDatasetFiles, scrape, log, fetchFile, basicProjection, studyService);
};

const fetchOpenData = async ({ fileUrls, requiredTags, log, fetchFile }) => {
const fetchOpenData = async ({ fileUrls, requiredTags, studyWhitelist, log, fetchFile }) => {
log.info(`Fetching ${fileUrls.length} metadata files`);
const metadata = await Promise.all(fileUrls.map(fetchFile));

log.info(`Filtering for ${requiredTags} tags and resources with valid ARNs`);
log.info(`Filtering for ${requiredTags} tags, ${studyWhitelist} whitelist studies, and resources with valid ARNs`);
const validS3Arn = new RegExp(/^arn:aws:s3:.*:.*:.+$/);
const filtered = metadata.filter(({ tags, resources }) => {
const filtered = metadata.filter(({ id, tags, resources }) => {
return (
requiredTags.some(filterTag => tags.includes(filterTag)) &&
(studyWhitelist.includes(id) || requiredTags.some(filterTag => tags.includes(filterTag))) &&
resources.every(resource => {
return resource.type === 'S3 Bucket' && validS3Arn.test(resource.arn);
})
Expand Down Expand Up @@ -237,7 +239,13 @@ async function saveOpenData(log, simplified, studyService) {

const fetchAndSaveOpenData = async (fetchDatasetFiles, scrape, log, fetchFile, basicProjection, studyService) => {
const fileUrls = await fetchDatasetFiles();
const openData = await fetchOpenData({ fileUrls, requiredTags: scrape.filterTags, log, fetchFile });
const openData = await fetchOpenData({
fileUrls,
requiredTags: scrape.filterTags,
studyWhitelist: scrape.studyWhitelist,
log,
fetchFile,
});

const simplifiedStudyData = openData.map(basicProjection);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const initHandler = (async () => {
await container.initServices();
const studyService = await container.find('studyService');
const log = await container.find('log');
return newHandler({ studyService, log });
const settings = await container.find('settings');
return newHandler({ studyService, log, settings });
})();

// eslint-disable-next-line import/prefer-default-export
Expand Down
Loading
Loading