diff --git a/CHANGELOG.md b/CHANGELOG.md index 14d2c0fed6..0aab6dbe70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,19 @@ 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 \_, starting from SWB mainline, 5.0.0. +## [5.0.0_1.4.2](https://github.com/hms-dbmi/service-workbench-on-aws/compare/v5.0.0_1.4.1...v5.0.0_1.4.2) (03/14/2024) +- Emergency change to add login-blocking warning and system wide application warnings. + +## [5.0.0_1.4.1](https://github.com/hms-dbmi/service-workbench-on-aws/compare/v5.0.0_1.4.0...v5.0.0_1.4.1) (03/01/2024) +- Add copy changes and parameter for registration page. +- Add PIC-SURE landing page if stage parameter is set. +- Bugfix: Move conda env config to install_kernel script. + ## [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. +- 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. diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UserStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UserStore.js index 531e66af94..8af526f609 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UserStore.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UserStore.js @@ -18,12 +18,14 @@ import { types } from 'mobx-state-tree'; import { getUser } from '@aws-ee/base-ui/dist/helpers/api'; import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore'; +import { branding } from '@aws-ee/base-ui/dist/helpers/settings'; import { User } from './User'; const UserStore = BaseStore.named('UserStore') .props({ user: types.maybe(User), + picsureLanding: branding.picsure.dualBranding, }) .actions(self => { // save the base implementation of cleanup @@ -36,6 +38,11 @@ const UserStore = BaseStore.named('UserStore') self.user = User.create(user); }); }, + bypassLanding() { + self.runInAction(() => { + self.picsureLanding = false; + }); + }, cleanup: () => { self.user = undefined; superCleanup(); @@ -48,6 +55,10 @@ const UserStore = BaseStore.named('UserStore') return _.isEmpty(self.user); }, + get defaultLocation() { + return self.picsureLanding ? '/landing' : '/dashboard'; + }, + // TODO this method should really be moved to the User model and renamed to something like projectIdOptions get projectIdDropdown() { const result = _.map(self.user.projectId, id => ({ key: id, value: id, text: id })); diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/__tests__/__fixtures__/authentication-provider-configs.js b/addons/addon-base-rest-api/packages/base-controllers/lib/__tests__/__fixtures__/authentication-provider-configs.js index 966ccb0976..54d30d8349 100644 --- a/addons/addon-base-rest-api/packages/base-controllers/lib/__tests__/__fixtures__/authentication-provider-configs.js +++ b/addons/addon-base-rest-api/packages/base-controllers/lib/__tests__/__fixtures__/authentication-provider-configs.js @@ -235,6 +235,42 @@ const cognitoType = { $id: '#/properties/customRegister', type: 'boolean', }, + appAlerts: { + $id: '#/properties/appAlerts', + type: 'object', + properties: { + loginBlocking: { + $id: '#/properties/appAlerts/loginBlocking', + type: 'string', + }, + banner: { + $id: '#/properties/appAlerts/banner', + type: 'array', + items: { + $id: '#/properties/appAlerts/banner/items', + type: 'object', + properties: { + title: { + $id: '#/properties/appAlerts/banner/items/title', + type: 'string', + }, + text: { + $id: '#/properties/appAlerts/banner/items/text', + type: 'string', + }, + type: { + $id: '#/properties/appAlerts/banner/items/type', + type: 'string', + }, + dismissable: { + $id: '#/properties/appAlerts/banner/items/dismissable', + type: 'boolean', + }, + }, + }, + }, + }, + }, federatedIdentityProviders: { $id: '#/properties/providerConfig/properties/federatedIdentityProviders', type: 'array', @@ -362,6 +398,7 @@ const publicConfigurations = [ 'https://test-raas1.auth.us-east-1.amazoncognito.com/oauth2/authorize?response_type=token&client_id=199999999991&redirect_uri=https://12345.cloudfront.net&idp_identifier=datalake.example.com', signOutUri: 'https://test-raas1.auth.us-east-1.amazoncognito.com/logout?client_id=199999999991&logout_uri=https://12345.cloudfront.net', + customRegister: false, }, { id: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_poolId2', @@ -374,6 +411,7 @@ const publicConfigurations = [ clientId: '28888888888882', enableNativeUserPoolUsers: false, customRegister: false, + appAlerts: undefined, }, { id: 'datalake2.example.com', @@ -384,6 +422,7 @@ const publicConfigurations = [ 'https://test-raas2.auth.us-east-1.amazoncognito.com/login?response_type=token&client_id=28888888888882&redirect_uri=https://12345.cloudfront.net&idp_identifier=datalake2.example.com', signOutUri: 'https://test-raas2.auth.us-east-1.amazoncognito.com/logout?client_id=28888888888882&logout_uri=https://12345.cloudfront.net', + customRegister: false, }, ]; diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/authentication-provider-public-controller.js b/addons/addon-base-rest-api/packages/base-controllers/lib/authentication-provider-public-controller.js index 92d4c97835..75e313c08a 100644 --- a/addons/addon-base-rest-api/packages/base-controllers/lib/authentication-provider-public-controller.js +++ b/addons/addon-base-rest-api/packages/base-controllers/lib/authentication-provider-public-controller.js @@ -42,6 +42,8 @@ async function configure(context) { credentialHandlingType: provider.config.type.config.credentialHandlingType, signInUri: provider.config.signInUri, signOutUri: provider.config.signOutUri, + customRegister: provider.config.customRegister || false, + appAlerts: provider.appAlerts, }; if (provider.config.type.type !== cognitoAuthType) { @@ -58,7 +60,6 @@ async function configure(context) { userPoolId: provider.config.userPoolId, clientId: provider.config.clientId, enableNativeUserPoolUsers: provider.config.enableNativeUserPoolUsers, - customRegister: provider.config.customRegister || false, }; if (cognitoPublicInfo.enableNativeUserPoolUsers) { diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/authentication-provider-config-service.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/authentication-provider-config-service.js index 71285ae873..11c62132b2 100644 --- a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/authentication-provider-config-service.js +++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/authentication-provider-config-service.js @@ -26,6 +26,7 @@ const deSerializeProviderConfig = providerConfigStr => JSON.parse(providerConfig const toProviderConfig = dbResultItem => _.assign({}, dbResultItem, { + appAlerts: dbResultItem.appAlerts, config: dbResultItem && deSerializeProviderConfig(dbResultItem.config), }); diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/create-cognito-user-pool-schema.json b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/create-cognito-user-pool-schema.json index 40ec554670..b5e5785b27 100644 --- a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/create-cognito-user-pool-schema.json +++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/create-cognito-user-pool-schema.json @@ -50,6 +50,42 @@ "$id": "#/properties/customRegister", "type": "boolean" }, + "appAlerts": { + "$id": "#/properties/appAlerts", + "type": "object", + "properties": { + "loginBlocking": { + "$id": "#/properties/appAlerts/loginBlocking", + "type": "string" + }, + "banner": { + "$id": "#/properties/appAlerts/banner", + "type": "array", + "items": { + "$id": "#/properties/appAlerts/banner/items", + "type": "object", + "properties": { + "title": { + "$id": "#/properties/appAlerts/banner/items/title", + "type": "string" + }, + "text": { + "$id": "#/properties/appAlerts/banner/items/text", + "type": "string" + }, + "type": { + "$id": "#/properties/appAlerts/banner/items/type", + "type": "string" + }, + "dismissable": { + "$id": "#/properties/appAlerts/banner/items/dismissable", + "type": "boolean" + } + } + } + } + } + }, "federatedIdentityProviders": { "$id": "#/properties/providerConfig/properties/federatedIdentityProviders", "type": "array", diff --git a/addons/addon-base-ui/packages/base-ui/src/AppContainer.js b/addons/addon-base-ui/packages/base-ui/src/AppContainer.js index 8ca614626f..09d739c09c 100644 --- a/addons/addon-base-ui/packages/base-ui/src/AppContainer.js +++ b/addons/addon-base-ui/packages/base-ui/src/AppContainer.js @@ -20,15 +20,12 @@ import { inject, observer } from 'mobx-react'; import { getEnv } from 'mobx-state-tree'; import { Message, Container } from 'semantic-ui-react'; -import { branding } from './helpers/settings'; - // expected props // - pluginRegistry (via injection) // - app (via injection) // - location (from react router) class AppContainer extends Component { componentDidMount() { - document.title = branding.page.title; document.querySelector("link[rel='shortcut icon']").href = this.props.assets.images.faviconIcon; document.querySelector("link[rel='icon']").href = this.props.assets.images.faviconImage; } diff --git a/addons/addon-base-ui/packages/base-ui/src/helpers/settings.js b/addons/addon-base-ui/packages/base-ui/src/helpers/settings.js index 6b7219642c..d41d21e274 100644 --- a/addons/addon-base-ui/packages/base-ui/src/helpers/settings.js +++ b/addons/addon-base-ui/packages/base-ui/src/helpers/settings.js @@ -23,24 +23,30 @@ const autoLogoutTimeoutInMinutes = process.env.REACT_APP_AUTO_LOGOUT_TIMEOUT_IN_ const branding = { login: { - title: process.env.REACT_APP_BRAND_LOGIN_TITLE, - subtitle: process.env.REACT_APP_BRAND_LOGIN_SUBTITLE, + title: process.env.REACT_APP_LOGIN_TITLE, + subtitle: process.env.REACT_APP_LOGIN_SUBTITLE, + warning: process.env.REACT_APP_LOGIN_WARNING, + tosLink: process.env.REACT_APP_LOGIN_TOS === 'true', + links: process.env.REACT_APP_LOGIN_LINKS, }, register: { title: process.env.REACT_APP_USER_REGISTRATION_TITLE, - summary: process.env.REACT_APP_USER_REGISTRATION_SUMMARY, + subtitle: process.env.REACT_APP_USER_REGISTRATION_SUBTITLE, 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', + picsure: { + dualBranding: process.env.REACT_APP_PICSURE_DUALBRANDING === 'true', + url: process.env.REACT_APP_PICSURE_URL || '', + browserTitle: process.env.REACT_APP_PICSURE_BROWSER_TITLE, + title: process.env.REACT_APP_PICSURE_TITLE, + subtitle: process.env.REACT_APP_PICSURE_SUBTITLE, }, main: { - title: process.env.REACT_APP_BRAND_MAIN_TITLE, - loginWarning: process.env.REACT_APP_LOGIN_WARNING, + browserTitle: process.env.REACT_APP_BROWSER_TITLE, }, page: { - title: process.env.REACT_APP_BRAND_PAGE_TITLE, + title: process.env.REACT_APP_PAGE_TITLE, help: process.env.REACT_APP_HELP_URL, }, }; diff --git a/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfig.js b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfig.js index 31a9788b19..d681ce818e 100644 --- a/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfig.js +++ b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfig.js @@ -51,6 +51,18 @@ function adjustRedirectUri(uri, redirectType = 'login') { return adjustedUri; } +const BannerAlert = types.model({ + title: types.maybeNull(types.string), + text: types.string, + type: types.optional(types.string, 'info'), + dismissable: types.optional(types.boolean, true), +}); + +const AppAlerts = types.model({ + loginBlocking: types.maybeNull(types.string), + banner: types.optional(types.array(BannerAlert), []), +}); + const AuthenticationProviderPublicConfig = types .model('AuthenticationProviderPublicConfig', { id: '', @@ -61,6 +73,7 @@ const AuthenticationProviderPublicConfig = types signOutUri: '', enableNativeUserPoolUsers: types.maybeNull(types.boolean), customRegister: types.maybeNull(types.boolean), + appAlerts: types.maybeNull(AppAlerts), }) .actions(self => ({ cleanup() { diff --git a/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfigsStore.js b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfigsStore.js index 46d0503fd3..36f545fdb9 100644 --- a/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfigsStore.js +++ b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfigsStore.js @@ -71,6 +71,12 @@ const AuthenticationProviderPublicConfigsStore = BaseStore.named('Authentication const configs = self.authenticationProviderPublicConfigs || []; return configs.find(({ type }) => type === nativeUserPool) || {}; }, + get loginBlocking() { + return _.get(this.nativeUserPool, 'appAlerts.loginBlocking', false); + }, + get bannerAlerts() { + return _.get(this.nativeUserPool, 'appAlerts.banner', []).slice(); + }, toAuthenticationProviderFromId(authenticationProviderId) { return _.find(self.authenticationProviderPublicConfigs, { id: authenticationProviderId }); }, diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/Login.js b/addons/addon-base-ui/packages/base-ui/src/parts/Login.js index 9539d15735..cd4a94200d 100644 --- a/addons/addon-base-ui/packages/base-ui/src/parts/Login.js +++ b/addons/addon-base-ui/packages/base-ui/src/parts/Login.js @@ -18,7 +18,7 @@ import React from 'react'; import { withRouter } from 'react-router-dom'; import { observable, action, decorate, runInAction } from 'mobx'; import { observer, inject } from 'mobx-react'; -import { Button, Form, Grid, Header, Segment, Label, Input, Select, Image } from 'semantic-ui-react'; +import { Button, Form, Grid, Header, Label, Input, Select, Image } from 'semantic-ui-react'; import { displayError } from '../helpers/notification'; import { branding } from '../helpers/settings'; @@ -130,7 +130,26 @@ class Login extends React.Component { ); }); + renderBrandingHeader = () => + this.props.Header ? ( + this.props.Header + ) : ( + <> + +
+ {branding.login.title} + {branding.login.subtitle} +
+ + ); + render() { + const loginBlocking = this.props.authenticationProviderPublicConfigsStore.loginBlocking; + const BrandingHeader = this.renderBrandingHeader(); + if (loginBlocking) { + return ; + } + const error = !!(this.usernameError || this.passwordError || this.authenticationProviderError); const authenticationProviderOptions = this.getStore().authenticationProviderOptions; @@ -161,10 +180,16 @@ class Login extends React.Component { const additionalLoginComponents = this.props.AdditionalLoginComponents || (() => <>); const collectUserNamePassword = this.props.authentication.shouldCollectUserNamePassword; - const renderBrandingLogo = ; + return (
- + +
- - {renderBrandingLogo} -
- {branding.login.title} - {branding.login.subtitle} -
- - {renderAuthenticationProviders()} + {renderAuthenticationProviders()} - {collectUserNamePassword && ( - - - {this.usernameError && ( - - )} - - )} + {collectUserNamePassword && ( + + + {this.usernameError && ( + + )} + + )} - {collectUserNamePassword && ( - - - {this.passwordError && ( - - )} - - )} + {collectUserNamePassword && ( + + + {this.passwordError && ( + + )} + + )} - - {additionalLoginComponents(this)} -
+ + {additionalLoginComponents(this)}
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/MainLayout.js b/addons/addon-base-ui/packages/base-ui/src/parts/MainLayout.js index 9771d61638..e7d85ab0ba 100644 --- a/addons/addon-base-ui/packages/base-ui/src/parts/MainLayout.js +++ b/addons/addon-base-ui/packages/base-ui/src/parts/MainLayout.js @@ -15,10 +15,10 @@ import _ from 'lodash'; import React from 'react'; -import { decorate, action } from 'mobx'; +import { decorate, action, observable, runInAction } from 'mobx'; import { inject, observer } from 'mobx-react'; import { withRouter } from 'react-router-dom'; -import { Menu, Icon, Image } from 'semantic-ui-react'; +import { Menu, Icon, Image, Message } from 'semantic-ui-react'; import { createLink } from '../helpers/routing'; import { displayError } from '../helpers/notification'; @@ -27,6 +27,21 @@ import { branding, versionAndDate } from '../helpers/settings'; // expected props // - userStore (via injection) class MainLayout extends React.Component { + constructor(props) { + super(props); + + runInAction(() => { + this.banners = this.props.authenticationProviderPublicConfigsStore.bannerAlerts; + }); + } + + handleDismiss = bannerText => { + const index = this.banners.findIndex(({ text }) => text === bannerText); + if (index !== -1) { + this.banners.splice(index, 1); + } + }; + goto = pathname => () => { const location = this.props.location; const link = createLink({ @@ -79,7 +94,7 @@ class MainLayout extends React.Component { })} , - + - {branding.main.title} + {branding.page.title} {versionAndDate} @@ -107,6 +122,32 @@ class MainLayout extends React.Component { }} key="ml3" > + {this.banners.length > 0 && ( +
+ {this.banners.map(({ title, text, type, dismissable }) => { + const icons = { + error: 'exclamation triangle', + info: 'info circle', + success: 'check circle', + warning: 'exclamation triangle', + }; + return ( + this.handleDismiss(text) : undefined} + header={title} + content={text} + /> + ); + })} +
+ )} {this.props.children}
, ]; @@ -115,7 +156,14 @@ class MainLayout extends React.Component { // see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da decorate(MainLayout, { + banners: observable, handleLogout: action, + handleDismiss: action, }); -export default inject('authentication', 'userStore', 'assets')(withRouter(observer(MainLayout))); +export default inject( + 'authentication', + 'userStore', + 'assets', + 'authenticationProviderPublicConfigsStore', +)(withRouter(observer(MainLayout))); diff --git a/addons/addon-custom/packages/main/css/overrides.css b/addons/addon-custom/packages/main/css/overrides.css index 8b129a5ea6..3c9b4cf818 100644 --- a/addons/addon-custom/packages/main/css/overrides.css +++ b/addons/addon-custom/packages/main/css/overrides.css @@ -25,3 +25,21 @@ button.link { padding: 0px; cursor: pointer; } + +.ui.dimmer { + padding: 0px; + margin: 0px; +} + +.ui.fullscreen.modal { + width: 100% !important; + height: 100%; + padding: 0px; + margin: 0px !important; + border-radius: 0px; +} +.center ol, +.center ul, +.left { + text-align: left; +} \ No newline at end of file diff --git a/addons/addon-custom/packages/main/src/extend/withAuth.js b/addons/addon-custom/packages/main/src/extend/withAuth.js index 07aadb689f..b65bd4b18f 100644 --- a/addons/addon-custom/packages/main/src/extend/withAuth.js +++ b/addons/addon-custom/packages/main/src/extend/withAuth.js @@ -25,6 +25,8 @@ import { branding } from '@aws-ee/base-ui/dist/helpers/settings'; import TermsPage from '../parts/TermsPage'; import Register from '../parts/Register'; +import BrandingHeader from '../parts/BrandingHeader'; +import { renderHTML } from '../helpers/utils'; /* eslint-disable react/jsx-no-bind */ @@ -56,17 +58,18 @@ function RegisterLogin(enableCustomRegister) { Register )} - {branding.tos.onLanding && ( + {branding.login.tosLink && ( <> Terms of Service
)} - {branding.main.loginWarning} + {branding.login.links && renderHTML(branding.login.links)} + {branding.login.warning} ); } - return ; + return ; } class AuthWrapper extends React.Component { @@ -89,6 +92,12 @@ class AuthWrapper extends React.Component { render() { const { app, location } = this.props; + + document.title = + branding.picsure.dualBranding && !app.userAuthenticated + ? branding.picsure.browserTitle + : branding.main.browserTitle; + if (app.userAuthenticated) { return this.renderAuthComp(true); } diff --git a/addons/addon-custom/packages/main/src/helpers/utils.js b/addons/addon-custom/packages/main/src/helpers/utils.js new file mode 100644 index 0000000000..d23e27beee --- /dev/null +++ b/addons/addon-custom/packages/main/src/helpers/utils.js @@ -0,0 +1,15 @@ +/* eslint-disable import/prefer-default-export */ +import React from 'react'; +import * as DOMPurify from 'dompurify'; + +function renderHTML(content) { + const cleanContent = DOMPurify.sanitize(content, { USE_PROFILES: { html: true }, ADD_ATTR: ['target'] }); + + // This method sets html from a string. We're pulling this from the config file made by + // an approved admin, and we're sanitizing using dompurify package. + // https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml + // eslint-disable-next-line react/no-danger + return
; +} + +export { renderHTML }; diff --git a/addons/addon-custom/packages/main/src/parts/BrandingHeader.js b/addons/addon-custom/packages/main/src/parts/BrandingHeader.js new file mode 100644 index 0000000000..2e16e7d3fb --- /dev/null +++ b/addons/addon-custom/packages/main/src/parts/BrandingHeader.js @@ -0,0 +1,72 @@ +import React from 'react'; +import { inject } from 'mobx-react'; + +import { Grid, Image, Header, Message } from 'semantic-ui-react'; +import { branding } from '@aws-ee/base-ui/dist/helpers/settings'; + +import { renderHTML } from '../helpers/utils'; + +export function BrandingHeader({ copy, assets, picsureBoxes = true, authenticationProviderPublicConfigsStore }) { + const borders = { margin: '0px 10px', border: 'solid #2A5FA3 2px', borderRadius: '4px', padding: '10px' }; + const maxImageWidth = { height: 'auto', maxWidth: '350px', margin: 'auto' }; + const loginBlocking = authenticationProviderPublicConfigsStore.loginBlocking || false; + + return ( + <> + + + + + + + + +
+
+ {copy.title} +
+ {loginBlocking && ( +
+ +
+ )} + {renderHTML(copy.subtitle)} +
+
+
+ {branding.picsure.dualBranding && picsureBoxes && ( + + +
+

+ Service Workbench +

+

Simple, accessible cloud computing & secure data storage.

+ + Learn More + +
+
+ +
+

+ PIC-Sure +

+

A self-service, easily navigable patient-level clinical data search and cohort tool.

+ + Learn More + +
+
+
+ )} +
+ + ); +} + +export default inject('assets', 'authenticationProviderPublicConfigsStore')(BrandingHeader); diff --git a/addons/addon-custom/packages/main/src/parts/PicSureLanding.js b/addons/addon-custom/packages/main/src/parts/PicSureLanding.js new file mode 100644 index 0000000000..221dfb3b83 --- /dev/null +++ b/addons/addon-custom/packages/main/src/parts/PicSureLanding.js @@ -0,0 +1,70 @@ +import React from 'react'; +import { inject, observer } from 'mobx-react'; +import { decorate, computed } from 'mobx'; +import { Button, Grid, Modal } from 'semantic-ui-react'; + +import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing'; +import { branding } from '@aws-ee/base-ui/dist/helpers/settings'; +import BrandingHeader from './BrandingHeader'; + +class PicSureLanding extends React.Component { + componentDidMount() { + document.title = branding.picsure.browserTitle; + } + + get userStore() { + return this.props.userStore; + } + + gotoLogin() { + return () => { + this.userStore.bypassLanding(); + gotoFn(this)('/'); + }; + } + + render() { + const borders = { + margin: '0px 10px', + border: 'solid #2A5FA3 2px', + borderRadius: '4px', + padding: '10px', + backgroundColor: 'white', + }; + const h3 = { textTransform: 'uppercase', color: '#2A5FA3', textDecoration: 'underline' }; + + return ( + + + + + + + + + + + + + + + + ); + } +} + +// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da +decorate(PicSureLanding, { + userStore: computed, +}); +export default inject('assets', 'userStore')(observer(PicSureLanding)); diff --git a/addons/addon-custom/packages/main/src/parts/Register.js b/addons/addon-custom/packages/main/src/parts/Register.js index 053fe40f9c..82d81c176b 100644 --- a/addons/addon-custom/packages/main/src/parts/Register.js +++ b/addons/addon-custom/packages/main/src/parts/Register.js @@ -3,20 +3,16 @@ import React from 'react'; import { observable, action, decorate, runInAction } from 'mobx'; import { inject, observer } from 'mobx-react'; import { withRouter } from 'react-router-dom'; -import { Form, Container, Grid, Dimmer, Loader, Header, Segment, Image, Label, Icon } from 'semantic-ui-react'; -import * as DOMPurify from 'dompurify'; +import { Form, Container, Grid, Dimmer, Loader, Segment, Label, Icon } from 'semantic-ui-react'; import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing'; import { branding } from '@aws-ee/base-ui/dist/helpers/settings'; +import BrandingHeader from './BrandingHeader'; import { getRegisterFormFields, formValidationErrors } from '../models/RegisterForm'; import { registerUser } from '../helpers/api'; import TermsModal from './TermsModel'; -const styles = { - header: { fontFamily: 'Handel Gothic,Futura,Trebuchet MS,Arial,sans-serif' }, - bodyText: { fontFamily: 'Futura,Trebuchet MS,Arial,sans-serif' }, -}; const termsState = { accepted: { value: 'accepted', icon: 'check circle outline', color: 'green', label: 'I have read and accept the' }, declined: { value: 'declined', icon: 'times circle outline', color: 'red', label: 'I have declined the' }, @@ -64,16 +60,6 @@ class Register extends React.Component { ); } - renderHTML(content) { - const cleanContent = DOMPurify.sanitize(content, { USE_PROFILES: { html: true } }); - - // This method sets html from a string. We're pulling this from the config file made by - // an approved admin, and we're sanitizing using dompurify package. - // https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml - // eslint-disable-next-line react/no-danger - return
; - } - setTerms(terms) { return () => { this.termsModalButton.focus(); @@ -112,10 +98,6 @@ class Register extends React.Component { renderRegisterationForm() { return (
-
- {branding.register.title} -
- {this.renderHTML(branding.register.summary)} Submitting registration @@ -137,7 +119,7 @@ class Register extends React.Component {
)} - Create a new Service Workbench account + Register
@@ -153,43 +135,44 @@ class Register extends React.Component { renderConfirmation() { return ( -
-
- SUCCESS! -
- {this.renderHTML(branding.register.success)} -
+ ); } - renderContent() { - const { location } = this.props; - + renderRegister() { return ( - - - - - - - - - - - - {location.pathname === '/register' && this.renderRegisterationForm()} - {location.pathname === '/register-confirmation' && this.renderConfirmation()} - - - + <> + + + + {this.renderRegisterationForm()} + + + ); } + renderContent() { + const { location } = this.props; + if (location.pathname === '/register') { + return this.renderRegister(); + } + if (location.pathname === '/register-confirmation') { + return this.renderConfirmation(); + } + return <>; + } + handleSubmit = action(async event => { event.preventDefault(); event.stopPropagation(); diff --git a/addons/addon-custom/packages/main/src/parts/Terms.js b/addons/addon-custom/packages/main/src/parts/Terms.js index d5149ffcdc..17fdb62f37 100644 --- a/addons/addon-custom/packages/main/src/parts/Terms.js +++ b/addons/addon-custom/packages/main/src/parts/Terms.js @@ -1,22 +1,10 @@ import React from 'react'; import { Header } from 'semantic-ui-react'; -import * as DOMPurify from 'dompurify'; import tos from '../../data/terms'; - -const readableStyle = { fontSize: 'max(12pt, 1.2rem)', fontFamily: 'Calibri' }; +import { renderHTML } from '../helpers/utils'; class Terms extends React.PureComponent { - renderHTML(content) { - const cleanContent = DOMPurify.sanitize(content, { USE_PROFILES: { html: true } }); - - // This method sets html from a string. We're pulling this from the config file made by - // an approved admin, and we're sanitizing using dompurify package. - // https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml - // eslint-disable-next-line react/no-danger - return
; - } - render() { const populatedTerms = Object.entries(tos[0].fields) .reduce((terms, [field, value]) => terms.replaceAll(`{${field.toUpperCase()}}`, value), tos[0].terms) @@ -34,7 +22,7 @@ class Terms extends React.PureComponent {
Terms as of {date}
- {this.renderHTML(populatedTerms)} + {renderHTML(populatedTerms)}
); } diff --git a/addons/addon-custom/packages/main/src/parts/TermsModel.js b/addons/addon-custom/packages/main/src/parts/TermsModel.js index a93b5d1f64..9b94ede713 100644 --- a/addons/addon-custom/packages/main/src/parts/TermsModel.js +++ b/addons/addon-custom/packages/main/src/parts/TermsModel.js @@ -55,7 +55,7 @@ class TermsModal extends React.Component { trigger, className = '', closeOnDimmerClick = false, - title = `${branding.main.title} Terms of Service`, + title = `${branding.page.title} Terms of Service`, } = this.props; return ( diff --git a/addons/addon-custom/packages/main/src/parts/TermsPage.js b/addons/addon-custom/packages/main/src/parts/TermsPage.js index d3db7d4746..0d0889e8fb 100644 --- a/addons/addon-custom/packages/main/src/parts/TermsPage.js +++ b/addons/addon-custom/packages/main/src/parts/TermsPage.js @@ -27,7 +27,7 @@ class TermsPage extends React.Component {
- {branding.main.title} + {branding.page.title}
diff --git a/addons/addon-custom/packages/main/src/plugins/routes-plugin.js b/addons/addon-custom/packages/main/src/plugins/routes-plugin.js index 811dcdc9a6..25bf2e6624 100644 --- a/addons/addon-custom/packages/main/src/plugins/routes-plugin.js +++ b/addons/addon-custom/packages/main/src/plugins/routes-plugin.js @@ -12,11 +12,13 @@ * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ +import _ from 'lodash'; import withAuth from '@aws-ee/base-ui/dist/withAuth'; import TermsPage from '../parts/TermsPage'; import Register from '../parts/Register'; +import PicSureLanding from '../parts/PicSureLanding'; /** * Adds your routes to the given routesMap. @@ -29,10 +31,35 @@ import Register from '../parts/Register'; */ // eslint-disable-next-line no-unused-vars function registerRoutes(routesMap, { location, appContext }) { - const routes = new Map([...routesMap, ['/register', withAuth(Register)], ['/legal', withAuth(TermsPage)]]); + const routes = new Map([ + ...routesMap, + ['/landing', withAuth(PicSureLanding)], + ['/register', withAuth(Register)], + ['/legal', withAuth(TermsPage)], + ]); return routes; } -const plugin = { registerRoutes }; +/** + * Returns default route. By default this method returns the + * '/dashboard' route as the default route for all non-root users and returns + * '/users' route for root user. + * @returns {{search: *, state: *, hash: *, pathname: string}} + */ +function getDefaultRouteLocation({ location, appContext }) { + const userStore = appContext.userStore; + const userDefaultLocation = _.get(userStore, 'defaultLocation'); + + const defaultLocation = { + pathname: userDefaultLocation, + search: location.search, // we want to keep any query parameters + hash: location.hash, + state: location.state, + }; + + return defaultLocation; +} + +const plugin = { registerRoutes, getDefaultRouteLocation }; export default plugin; diff --git a/main/config/settings/.defaults.yml b/main/config/settings/.defaults.yml index 8131357e55..c831e3d5e9 100644 --- a/main/config/settings/.defaults.yml +++ b/main/config/settings/.defaults.yml @@ -235,4 +235,40 @@ dbEnvironmentsSc: ${self:custom.settings.dbPrefix}-EnvironmentsSc # Use Custom AMIs that are already managed by other process like golden image pipeline useCustomAmi: false -useAwsProfile: false \ No newline at end of file +useAwsProfile: false + +# Custom Branding +browserTitle: "Service Workbench" +pageTitle: "Service Workbench on AWS (${self:custom.settings.envName}/${self:custom.settings.awsRegion})" + +# URL for help link. Can be empty, a single url, or comma seperated key value pairs like "Some Link=https://www.google.com/?q=what,Other Link=https://www.example.com/" +helpUrl: "User Guide=https://pic-sure.gitbook.io/service-workbench/general-user-guide/introduction,Help Desk=https://bch-gic.atlassian.net/servicedesk/customer/portal/1" + +# Enable user registration +enableCustomRegistration: true + +# Require the TOS to be accepted before registration can occur. +tosRequired: false + +# Registration Copy +userRegistrationTitle: "WELCOME TO AIM-AHEAD!" +userRegistrationSummary: "

Create a Data Exploration + Analysis Login to access available tools:

" +userRegistrationSuccess: '

Your AIM-AHEAD account has been successfully created. What you should expect next:

  1. The AIM-AHEAD administrator will review your account.
  2. You will receive an email sent from Okta to create a password.
  3. Login to AIM-AHEAD and start your research.

AIM-AHEAD Service Workbench User Guide
AIM-AHEAD PIC-SURE User Guide

' + +# Login Copy +loginTitle: "WELCOME TO AIM-AHEAD!" +loginSubtitle: "The Data Exploration + Analysis Login gives you access to the following tools that enable researchers to search and analyze data." +loginWarning: "" +loginLinks: 'Reset Password
FAQ' + +# Should the TOS link be enabled on the login page. +loginTos: false + +# Should dual picsure and swb branding be shown +picsureDualBranding: true +piscureUrl: "https://pic-sure.aim-ahead-dev.host/" +picsureBrowserTitle: "AIM-AHEAD" + +# PIC-Sure landing copy +picsureTitle: "WELCOME TO AIM-AHEAD!" +picsureSubtitle: '

You are logged in to the Data Exploration + Analysis tool suite.

' \ No newline at end of file diff --git a/main/config/settings/example.yml b/main/config/settings/example.yml index 7b450c0962..a387d11fa6 100644 --- a/main/config/settings/example.yml +++ b/main/config/settings/example.yml @@ -149,19 +149,38 @@ HostedZoneId: 'Z02455261RJ9QQPVHFZGA' # Stack policies are applied here: addons/addon-stack-policy/packages/stack-policy/lib/steps/update-cfn-stack-policy.js #isAppStreamEnabled: false -#--- Custom User Registration -# Enable and customize the parameters below to customize the registration page. +# Custom Branding +#browserTitle: "Service Workbench" +#pageTitle: "Service Workbench" + +# 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 + +# Enable user registration #enableCustomRegistration: false -#userRegistrationTitle: "WELCOME TO SERVICE WORKBENCH" -#userRegistrationSummary: "

Service Workbench provides a self-service, three-click, on-demand service for researchers to build research environments in minutes without needing cloud infrastructure knowledge. Fill out the form below to create your account on Service Workbench hosted on AWS.

" -#userRegistrationSuccess: "

Your Service Workbench account has been successfully created. What you should expect next:

  1. The Service Workbench administrator will review your account.
  2. Once your account is activated, you can login to Service Workbench and start your research.
" -#loginWarning: "WARNING: You are entering a secure environment." # Require the TOS to be accepted before registration can occur. #tosRequired: true -# Should the TOS link be enabled on the landing page. -#tosLinkOnLanding: true +# Registration Copy +#userRegistrationTitle: "WELCOME TO SERVICE WORKBENCH" +#userRegistrationSummary: "Register to start launching analysis workspaces" +#userRegistrationSuccess: "Your Service Workbench account has been successfully created." -# 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 \ No newline at end of file +# Login Copy +#loginTitle: "WELCOME TO SERVICE WORKBENCH" +#loginSubtitle: "Login to start launching analyis workspaces" +#loginWarning: "WARNING: You are entering a secure environment." +#loginLinks: 'FAQ' + +# Should the TOS link be enabled on the login page. +#loginTos: true + +# Should dual picsure and swb branding be shown +#picsureDualBranding: true +#piscureUrl: '' +#picsureBrowserTitle: "PIC-Sure & Service Workbench" + +# PIC-Sure landing copy +#picsureTitle: "WELCOME TO SERVICE WORKBENCH" +#picsureSubtitle: "You are logged in. Please select an application to continue" diff --git a/main/solution/post-deployment/config/environment-files/environments/sagemaker.sh b/main/solution/post-deployment/config/environment-files/environments/sagemaker.sh index 54c5b1376e..402e7beda2 100644 --- a/main/solution/post-deployment/config/environment-files/environments/sagemaker.sh +++ b/main/solution/post-deployment/config/environment-files/environments/sagemaker.sh @@ -1,17 +1,5 @@ #!/usr/bin/env bash -# --------------------------- Load Kernels to Conda -------------------------- # -KERNEL_PATH="/home/ec2-user/SageMaker/.kernels" -echo "Adding $KERNEL_PATH to conda configuration" -mkdir -p $KERNEL_PATH -chown ec2-user:ec2-user $KERNEL_PATH -cat << EOF >> /home/ec2-user/.condarc -envs_dirs: - - $KERNEL_PATH - - /home/ec2-user/anaconda3/envs -EOF -echo "Finished Adding $KERNEL_PATH to conda configuration" - # --------------------------------- Idle Stop -------------------------------- # if [ "$AUTO_STOP_IDLE_TIME" != "0" ]; then echo "Installing the idle stop script" diff --git a/main/solution/post-deployment/config/environment-files/get_bootstrap.sh b/main/solution/post-deployment/config/environment-files/get_bootstrap.sh index c7796d872f..ce803d1ec5 100644 --- a/main/solution/post-deployment/config/environment-files/get_bootstrap.sh +++ b/main/solution/post-deployment/config/environment-files/get_bootstrap.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash + +# DEPRECATED - use bootstrap.sh instead + bootstrap_s3_location="$1" s3_mounts="$2" diff --git a/main/solution/post-deployment/config/environment-files/install_kernel.sh b/main/solution/post-deployment/config/environment-files/install_kernel.sh index 33d4e200e2..db183c5291 100644 --- a/main/solution/post-deployment/config/environment-files/install_kernel.sh +++ b/main/solution/post-deployment/config/environment-files/install_kernel.sh @@ -23,3 +23,14 @@ if [ "$kernels" != "" ]; then fi done fi + +# --------------------------- Load Kernels to Conda -------------------------- # +echo "Adding $KERNEL_PATH to conda configuration" +mkdir -p $KERNEL_PATH +chown ec2-user:ec2-user $KERNEL_PATH +cat << EOF >> /home/ec2-user/.condarc +envs_dirs: + - $KERNEL_PATH + - /home/ec2-user/anaconda3/envs +EOF +echo "Finished Adding $KERNEL_PATH to conda configuration" \ No newline at end of file diff --git a/main/solution/post-deployment/config/infra/functions.yml b/main/solution/post-deployment/config/infra/functions.yml index 4bb1012f2f..6b871858e4 100644 --- a/main/solution/post-deployment/config/infra/functions.yml +++ b/main/solution/post-deployment/config/infra/functions.yml @@ -42,8 +42,6 @@ postDeployment: APP_STUDY_DATA_BUCKET_NAME: ${self:custom.settings.studyDataBucketName} APP_ENABLE_NATIVE_USER_POOL_USERS: ${self:custom.settings.enableNativeUserPoolUsers} APP_ENABLE_CUSTOM_REGISTRATION: ${self:custom.settings.enableCustomRegistration} - APP_TOS_LINK_ON_LANDING : ${self:custom.settings.tosLinkOnLanding} - APP_USER_REGISTRATION_TOS_REQUIRED: ${self:custom.settings.tosRequired} APP_AUTO_CONFIRM_NATIVE_USERS: ${self:custom.settings.autoConfirmNativeUsers} APP_NATIVE_ADMIN_PASSWORD_PARAM_NAME: ${self:custom.settings.nativeAdminPasswordParamName} APP_ENV_NAME: ${self:custom.settings.envName} diff --git a/main/solution/post-deployment/config/settings/.defaults.yml b/main/solution/post-deployment/config/settings/.defaults.yml index 9f6cc110ff..eed64ce4dd 100644 --- a/main/solution/post-deployment/config/settings/.defaults.yml +++ b/main/solution/post-deployment/config/settings/.defaults.yml @@ -36,12 +36,6 @@ enableUserSignUps: true # Please also include the registration parameters to customize the registration page. enableCustomRegistration: false -# Require the TOS to be accepted before registration can occur. -tosRequired: true - -# Should the TOS link be enabled on the landing page. -tosLinkOnLanding: true - # Cognito domain prefix. Note random string will be padded at the end if specified domain is not available cognitoUserPoolDomainPrefix: ${self:custom.settings.envName}-${self:custom.settings.solutionName} diff --git a/main/solution/ui/config/environment/env-template.yml b/main/solution/ui/config/environment/env-template.yml index a4dd23453b..82a63f259b 100644 --- a/main/solution/ui/config/environment/env-template.yml +++ b/main/solution/ui/config/environment/env-template.yml @@ -9,10 +9,6 @@ REACT_APP_API_URL: ${self:custom.settings.apiUrl} REACT_APP_WEBSITE_URL: ${self:custom.settings.websiteUrl} REACT_APP_STAGE: ${self:custom.settings.envName} REACT_APP_REGION: ${self:custom.settings.awsRegion} -REACT_APP_BRAND_PAGE_TITLE: ${self:custom.settings.brandPageTitle} -REACT_APP_BRAND_MAIN_TITLE: ${self:custom.settings.brandMainTitle} -REACT_APP_BRAND_LOGIN_TITLE: ${self:custom.settings.brandLoginTitle} -REACT_APP_BRAND_LOGIN_SUBTITLE: ${self:custom.settings.brandLoginSubtitle} REACT_APP_AUTO_LOGOUT_TIMEOUT_IN_MINUTES: ${self:custom.settings.autoLogoutTimeoutInMinutes} REACT_APP_ENV_MGMT_ROLE_NAME: ${self:custom.settings.envMgmtRoleName} REACT_APP_ENABLE_BUILT_IN_WORKSPACES: ${self:custom.settings.enableBuiltInWorkspaces} @@ -21,13 +17,24 @@ REACT_APP_ENABLE_EGRESS_STORE: ${self:custom.settings.enableEgressStore} REACT_APP_SITE_ENV_TYPE: ${self:custom.settings.envType} REACT_APP_IS_APP_STREAM_ENABLED: ${self:custom.settings.isAppStreamEnabled} REACT_APP_ENABLE_FLOW_LOGS: ${self:custom.settings.enableFlowLogs} +# Custom Branding +REACT_APP_BROWSER_TITLE: ${self:custom.settings.browserTitle} +REACT_APP_PAGE_TITLE: ${self:custom.settings.pageTitle} +REACT_APP_HELP_URL: ${self:custom.settings.helpUrl} +REACT_APP_LOGIN_TITLE: ${self:custom.settings.loginTitle} +REACT_APP_LOGIN_SUBTITLE: ${self:custom.settings.loginSubtitle} +REACT_APP_LOGIN_WARNING: ${self:custom.settings.loginWarning} +REACT_APP_LOGIN_TOS: ${self:custom.settings.loginTos} +REACT_APP_LOGIN_LINKS: ${self:custom.settings.loginLinks} REACT_APP_USER_REGISTRATION_TITLE: ${self:custom.settings.userRegistrationTitle} -REACT_APP_USER_REGISTRATION_SUMMARY: ${self:custom.settings.userRegistrationSummary} +REACT_APP_USER_REGISTRATION_SUBTITLE: ${self:custom.settings.userRegistrationSummary} REACT_APP_USER_REGISTRATION_SUCCESS: ${self:custom.settings.userRegistrationSuccess} -REACT_APP_LOGIN_WARNING: ${self:custom.settings.loginWarning} -REACT_APP_HELP_URL: ${self:custom.settings.helpUrl} REACT_APP_USER_REGISTRATION_TOS_REQUIRED: ${self:custom.settings.tosRequired} -REACT_APP_TOS_LINK_ON_LANDING: ${self:custom.settings.tosLinkOnLanding} +REACT_APP_PICSURE_DUALBRANDING: ${self:custom.settings.picsureDualBranding} +REACT_APP_PICSURE_URL: ${self:custom.settings.piscureUrl} +REACT_APP_PICSURE_BROWSER_TITLE: ${self:custom.settings.picsureBrowserTitle} +REACT_APP_PICSURE_TITLE: ${self:custom.settings.picsureTitle} +REACT_APP_PICSURE_SUBTITLE: ${self:custom.settings.picsureSubtitle} # ======================================================================== # Overrides for .env.local @@ -36,5 +43,4 @@ REACT_APP_TOS_LINK_ON_LANDING: ${self:custom.settings.tosLinkOnLanding} localOverrides: REACT_APP_LOCAL_DEV: true REACT_APP_API_URL: 'http://localhost:4000' - REACT_APP_WEBSITE_URL: 'http://localhost:3000' - REACT_APP_BRAND_PAGE_TITLE: LOCAL ${self:custom.settings.brandPageTitle} \ No newline at end of file + REACT_APP_WEBSITE_URL: 'http://localhost:3000' \ No newline at end of file diff --git a/main/solution/ui/config/settings/.defaults.yml b/main/solution/ui/config/settings/.defaults.yml index 6aad9fa6f1..4ebf9c41da 100644 --- a/main/solution/ui/config/settings/.defaults.yml +++ b/main/solution/ui/config/settings/.defaults.yml @@ -16,27 +16,8 @@ websiteCloudFrontId: ${cf:${self:custom.settings.infrastructureStackName}.CloudF # URL of the website websiteUrl: ${cf:${self:custom.settings.infrastructureStackName}.WebsiteUrl} -# Branding -brandPageTitle: 'Service Workbench' -brandMainTitle: 'Service Workbench on AWS (${self:custom.settings.envName}/${self:custom.settings.awsRegion})' -brandLoginTitle: 'Service Workbench' -brandLoginSubtitle: 'Service Workbench on AWS (${self:custom.settings.envName}/${self:custom.settings.awsRegion})' - versionAndDate: 'Version ${self:custom.settings.versionNumber} (${self:custom.settings.versionDate})' # After how many minutes should the auto logout dialog be displayed? once displayed the user has 1 minute to dismiss # the dialog, otherwise they will be automatically logged out autoLogoutTimeoutInMinutes: 30 - -# Registration page -enableCustomRegistration: false -userRegistrationTitle: "WELCOME TO SERVICE WORKBENCH" -userRegistrationSummary: "

Service Workbench provides a self-service, three-click, on-demand service for researchers to build research environments in minutes without needing cloud infrastructure knowledge. Fill out the form below to create your account on Service Workbench hosted on AWS.

" -userRegistrationSuccess: "

Your Service Workbench account has been successfully created. What you should expect next:

  1. The Service Workbench administrator will review your account.
  2. Once your account is activated, you can login to Service Workbench and start your research.
" -loginWarning: "" - -# Require the TOS to be accepted before registration can occur. -tosRequired: true - -# Should the TOS link be enabled on the landing page. -tosLinkOnLanding: true