diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 2bdeca0e8c..6b0cc97fcb 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -9,10 +9,11 @@ Checklist:
- [ ] Have you successfully deployed to an AWS account with your changes?
- [ ] Have you written new tests for your core changes, as applicable?
- [ ] Have you successfully tested with your changes locally?
+- [ ] Have you updated openapi.yaml if you made updates to API definition (including add, delete or update parameter and request data schema)?
- [ ] If you had to run manual tests, have you considered automating those tests by adding them to [end-to-end tests](../main/end-to-end-tests/README.md)?
AS review ticket id:
-By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
+By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
\ No newline at end of file
diff --git a/.github/workflows/unit-test-code-analysis.yml b/.github/workflows/unit-test-code-analysis.yml
index 1510797c63..8285311cc4 100644
--- a/.github/workflows/unit-test-code-analysis.yml
+++ b/.github/workflows/unit-test-code-analysis.yml
@@ -8,6 +8,7 @@ on:
pull_request:
branches:
- develop
+ - 'feat-*'
jobs:
static-code-analysis-and-unit-test:
name: Unit Tests & Code Analysis
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92d0f4779f..6736de794c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,22 @@
All notable changes to this project will be documented in this file.
+## [2.1.0] - 2021-03-12
+
+### Added
+- fix: Upgraded react-dev-utils yarn dependency version
+- feat: Added Bring Your Own Bucket(BYOB) functionality
+- feat: Added integration testing for all APIs
+- feat: Added OpenAPI documentation
+- feat: Removed unused APIs- listWorkflowInstancesByStatus and createAuthenticationProviderConfig
+
+## [2.0.3] - 2021-03-12
+- chore(deps): bump websocket-extensions from 0.1.3 to 0.1.4
+- test: fix flaky integ tests
+- fix: emr workspace image. Lock jupyterlab to version 2.2.6
+- test: Implemented integration tests for service catalog workspaces
+- feat: verbose integ test log
+
## [2.0.2] - 2021-03-03
### Added
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/api.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/api.js
index 71229b8aa2..10a163799d 100644
--- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/api.js
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/api.js
@@ -37,11 +37,11 @@ function getAwsAccountBudget(accountUUID) {
}
function createAwsAccountBudget(budgetConfiguration) {
- return httpApiPut(`api/budgets/aws-account`, { data: budgetConfiguration });
+ return httpApiPost(`api/budgets/aws-account`, { data: budgetConfiguration });
}
function updateAwsAccountBudget(budgetConfiguration) {
- return httpApiPost(`api/budgets/aws-account`, { data: budgetConfiguration });
+ return httpApiPut(`api/budgets/aws-account`, { data: budgetConfiguration });
}
function addUsers(users) {
@@ -288,7 +288,55 @@ function getWindowsRpInfo(envId, connectionId) {
return httpApiGet(`api/workspaces/service-catalog/${envId}/connections/${connectionId}/windows-rdp-info`);
}
-// API Functions Insertion Point (do not change this text, it is being used by hygen cli)
+function getDataSourceAccounts() {
+ return httpApiGet(`api/data-sources/accounts/`);
+}
+
+function getDataSourceStudies(accountId) {
+ return httpApiGet(`api/data-sources/accounts/${accountId}/studies`);
+}
+
+function checkAccountReachability(accountId) {
+ return httpApiPost('api/data-sources/accounts/ops/reachability', {
+ data: { id: accountId, type: 'dsAccount' },
+ });
+}
+
+function checkStudyReachability(studyId) {
+ return httpApiPost('api/data-sources/accounts/ops/reachability', {
+ data: { id: studyId, type: 'study' },
+ });
+}
+
+function registerAccount(account) {
+ return httpApiPost('api/data-sources/accounts', {
+ data: account,
+ });
+}
+
+function registerBucket(accountId, bucket) {
+ return httpApiPost(`api/data-sources/accounts/${accountId}/buckets`, {
+ data: bucket,
+ });
+}
+
+function registerStudy(accountId, bucketName, study) {
+ return httpApiPost(`api/data-sources/accounts/${accountId}/buckets/${bucketName}/studies`, {
+ data: study,
+ });
+}
+
+function generateAccountCfnTemplate(accountId) {
+ return httpApiPost(`api/data-sources/accounts/${accountId}/cfn`, {
+ data: {},
+ });
+}
+
+function updateRegisteredAccount(accountId, data) {
+ return httpApiPut(`api/data-sources/accounts/${accountId}`, {
+ data,
+ });
+}
export {
addIndex,
@@ -351,4 +399,13 @@ export {
updateScEnvironmentCidrs,
sendSshKey,
getWindowsRpInfo,
+ getDataSourceAccounts,
+ getDataSourceStudies,
+ checkStudyReachability,
+ checkAccountReachability,
+ registerAccount,
+ registerBucket,
+ registerStudy,
+ generateAccountCfnTemplate,
+ updateRegisteredAccount,
};
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/errors.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/errors.js
index 37c17c8abc..0de062c4b1 100644
--- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/errors.js
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/errors.js
@@ -62,4 +62,8 @@ const isForbidden = error => {
return _.get(error, 'code') === 'forbidden';
};
-export { boom, isNotFound, isTokenExpired, isForbidden };
+const isAlreadyExists = error => {
+ return _.get(error, 'code') === 'alreadyExists';
+};
+
+export { boom, isNotFound, isTokenExpired, isForbidden, isAlreadyExists };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/constants/aws-regions.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/constants/aws-regions.js
new file mode 100644
index 0000000000..337efd7639
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/constants/aws-regions.js
@@ -0,0 +1,49 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+/* eslint-disable import/prefer-default-export */
+import _ from 'lodash';
+
+const regionNames = [
+ 'us-east-1',
+ 'us-east-2',
+ 'us-west-1',
+ 'us-west-2',
+ 'af-south-1',
+ 'ap-east-1',
+ 'ap-south-1',
+ 'ap-northeast-1',
+ 'ap-northeast-2',
+ 'ap-northeast-3',
+ 'ap-southeast-1',
+ 'ap-southeast-2',
+ 'ca-central-1',
+ 'eu-central-1',
+ 'eu-north-1',
+ 'eu-south-1',
+ 'eu-west-1',
+ 'eu-west-2',
+ 'eu-west-3',
+ 'me-south-1',
+ 'sa-east-1',
+];
+
+const regionOptions = _.map(regionNames, name => ({
+ key: name,
+ value: name,
+ text: name,
+}));
+
+export { regionNames, regionOptions };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/constants/bucket.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/constants/bucket.js
new file mode 100644
index 0000000000..1e246348a4
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/constants/bucket.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+/* eslint-disable import/prefer-default-export */
+
+const encryptionOptions = [
+ {
+ text: 'AWS Key Management Service key (SSE-KMS)',
+ value: 'kms',
+ },
+ {
+ text: 'Amazon S3 key (SSE-S3)',
+ value: 's3',
+ },
+ {
+ text: 'Disabled',
+ value: 'none',
+ },
+];
+
+export { encryptionOptions };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceAccount.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceAccount.js
new file mode 100644
index 0000000000..db3f6a288c
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceAccount.js
@@ -0,0 +1,238 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import { values } from 'mobx';
+import { types } from 'mobx-state-tree';
+
+import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils';
+
+import { DataSourceStudy } from './DataSourceStudy';
+import { StackInfo } from './StackInfo';
+
+const states = {
+ pending: {
+ id: 'pending',
+ display: 'Pending',
+ color: 'orange',
+ },
+ error: {
+ id: 'error',
+ display: 'Unavailable',
+ color: 'red',
+ },
+ reachable: {
+ id: 'reachable',
+ display: 'Available',
+ color: 'green',
+ },
+};
+
+// ==================================================================
+// DataSourceAccount
+// ==================================================================
+const DataSourceAccount = types
+ .model('DataSourceAccount', {
+ id: '',
+ rev: types.maybe(types.number),
+ name: '',
+ createdAt: '',
+ createdBy: '',
+ updatedAt: '',
+ updatedBy: '',
+ stackCreated: false,
+ mainRegion: '',
+ qualifier: '',
+ contactInfo: types.optional(types.maybeNull(types.string), ''),
+ stack: '',
+ status: '',
+ statusMsg: '',
+ statusAt: '',
+ description: types.optional(types.maybeNull(types.string), ''),
+ type: '', // managed vs unmanaged
+ templateIdExpected: '',
+ templateIdFound: '',
+ stackId: '',
+ buckets: types.array(types.frozen()),
+ studies: types.map(DataSourceStudy),
+ stackInfo: types.optional(StackInfo, {}),
+ })
+ .actions(self => ({
+ setDataSourceAccount(raw = {}) {
+ _.forEach(raw, (value, key) => {
+ if (value === 'studies') return; // we don't want to update the studies
+ if (value === 'stackInfo') return; // we don't want to update the stack info
+ self[key] = value;
+ });
+
+ // We want to take care of thee statusMsg because it might come as undefined
+ if (_.isUndefined(raw.statusMsg)) self.statusMsg = '';
+ },
+
+ setStudies(studies) {
+ consolidateToMap(self.studies, studies, (existing, newItem) => {
+ existing.setStudy(newItem);
+ });
+ },
+
+ setStudy(study) {
+ self.studies.set(study.id, study);
+
+ return self.studies.get(study.id);
+ },
+
+ setBucket(bucket) {
+ // Because buckets are frozen, we need to deep clone first
+ const buckets = _.cloneDeep(self.buckets);
+ buckets.push(bucket);
+ self.buckets = buckets;
+
+ return bucket;
+ },
+
+ setStackInfo(stackInfo) {
+ self.stackInfo.setStackInfo(stackInfo);
+ },
+ }))
+
+ // eslint-disable-next-line no-unused-vars
+ .views(self => ({
+ get studiesList() {
+ return _.orderBy(values(self.studies), ['id'], ['asc']);
+ },
+
+ getStudy(studyId) {
+ return self.studies.get(studyId);
+ },
+
+ get state() {
+ return states[self.status] || states.reachable;
+ },
+
+ get pendingState() {
+ return self.status === 'pending';
+ },
+
+ get errorState() {
+ return self.status === 'error';
+ },
+
+ get reachableState() {
+ return self.status === 'reachable';
+ },
+
+ get statusMessageInfo() {
+ const msg = self.statusMsg;
+ const info = {
+ prefix: '',
+ color: 'grey',
+ message: msg,
+ };
+
+ if (_.isEmpty(msg)) return info;
+
+ if (_.startsWith(msg, 'WARN|||')) {
+ info.prefix = 'WARN';
+ info.message = _.nth(_.split(msg, '|||'), 1);
+ info.color = 'orange';
+ } else if (_.startsWith(msg, 'ERR|||')) {
+ info.prefix = 'ERR';
+ info.message = _.nth(_.split(msg, '|||'), 1);
+ info.color = 'red';
+ }
+
+ return info;
+ },
+
+ get stackOutDated() {
+ return !_.isEmpty(self.stackId) && self.stackCreated && self.templateIdExpected !== self.templateIdFound;
+ },
+
+ get incorrectStackNameProvisioned() {
+ return _.isEmpty(self.stackId) && self.stackCreated;
+ },
+
+ getBucket(name) {
+ return _.find(self.buckets, bucket => bucket.name === name);
+ },
+
+ get bucketNames() {
+ return _.map(self.buckets, bucket => bucket.name);
+ },
+
+ getStudiesForBucket(name) {
+ return _.filter(values(self.studies), study => study.bucket === name);
+ },
+
+ get emailCommonSection() {
+ const names = self.bucketNames;
+ const lines = ['Dear Admin,', '', 'We are requesting access to the following bucket(s) and studies:'];
+ _.forEach(names, name => {
+ lines.push(`\nBucket name: ${name}`);
+ const studies = self.getStudiesForBucket(name);
+ _.forEach(studies, study => {
+ lines.push(` - folder: ${study.folder}`);
+ lines.push(` access: ${study.friendlyAccessType}`);
+ });
+ });
+
+ lines.push('');
+ lines.push(
+ 'For your convenience, you can follow these steps to configure the account for the requested access:\n',
+ );
+
+ return lines;
+ },
+
+ get updateStackEmailTemplate() {
+ const { id, mainRegion, stackInfo = {} } = self;
+ const { cfnConsoleUrl, updateStackUrl, urlExpiry } = stackInfo;
+ const lines = _.slice(self.emailCommonSection);
+
+ lines.push(
+ `1 - Log in to the aws console using the correct account. Please ensure that you are using the correct account # ${id} and region ${mainRegion}\n`,
+ );
+ lines.push(`2 - Go to the AWS CloudFormation console ${cfnConsoleUrl}\n`);
+ lines.push(` You need to visit the AWS CloudFormation console page before you can follow the next link\n`);
+ lines.push(`3 - Click on the following link\n`);
+ lines.push(` ${updateStackUrl}\n`);
+ lines.push(
+ ' The link takes you to the CloudFormation console where you can review the stack information and provision it.\n',
+ );
+ lines.push(` Note: the link expires at ${new Date(urlExpiry).toISOString()}`);
+ lines.push(`\n\nRegards,\nService Workbench admin`);
+ return lines.join('\n');
+ },
+
+ get createStackEmailTemplate() {
+ const { id, mainRegion, stackInfo = {} } = self;
+ const { createStackUrl, urlExpiry } = stackInfo;
+ const lines = _.slice(self.emailCommonSection);
+
+ lines.push(
+ `1 - Log in to the aws console using the correct account. Please ensure that you are using the correct account # ${id} and region ${mainRegion}\n`,
+ );
+ lines.push(`2 - Click on the following link\n`);
+ lines.push(` ${createStackUrl}\n`);
+ lines.push(
+ ' The link takes you to the CloudFormation console where you can review the stack information and provision it.\n',
+ );
+ lines.push(` Note: the link expires at ${new Date(urlExpiry).toISOString()}`);
+ lines.push(`\n\nRegards,\nService Workbench admin`);
+ return lines.join('\n');
+ },
+ }));
+
+export { DataSourceAccount }; // eslint-disable-line import/prefer-default-export
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceAccountStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceAccountStore.js
new file mode 100644
index 0000000000..4bbb4ac11f
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceAccountStore.js
@@ -0,0 +1,96 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import { getParent, types } from 'mobx-state-tree';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { getDataSourceStudies } from '../../helpers/api';
+import { DataSourceStudyStore } from './DataSourceStudyStore';
+import { DataSourceStackInfoStore } from './DataSourceStackInfoStore';
+
+// ==================================================================
+// DataSourceAccountStore
+// ==================================================================
+const DataSourceAccountStore = BaseStore.named('DataSourceAccountStore')
+ .props({
+ accountId: '',
+ studyStores: types.map(DataSourceStudyStore),
+ stackInfoStore: types.maybe(DataSourceStackInfoStore),
+ tickPeriod: 60 * 1000, // 1 minute
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const studies = await getDataSourceStudies(self.accountId);
+ const account = self.account;
+ account.setStudies(studies);
+ },
+
+ getStudyStore(studyId) {
+ let entry = self.studyStores.get(studyId);
+ if (!entry) {
+ // Lazily create the store
+ self.studyStores.set(studyId, DataSourceStudyStore.create({ accountId: self.accountId, studyId }));
+ entry = self.studyStores.get(studyId);
+ }
+
+ return entry;
+ },
+
+ getStackInfoStore() {
+ let entry = self.stackInfoStore;
+ if (!entry) {
+ // Lazily create the store
+ self.stackInfoStore = DataSourceStackInfoStore.create({ accountId: self.accountId });
+ entry = self.stackInfoStore;
+ }
+
+ return entry;
+ },
+
+ cleanup: () => {
+ self.accountId = '';
+ self.studyStores.clear();
+ self.stackInfoStore.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get account() {
+ const parent = getParent(self, 2);
+ const a = parent.getAccount(self.accountId);
+ return a;
+ },
+
+ get studiesTotal() {
+ const account = self.account || { studies: {} };
+ return account.studies.size;
+ },
+
+ getStudy(studyId) {
+ return self.account.getStudy(studyId);
+ },
+ }));
+
+// Note: Do NOT register this in the global context, if you want to gain access to an instance
+// use dataSourceAccountsStore.getDataSourceAccountStore()
+// eslint-disable-next-line import/prefer-default-export
+export { DataSourceAccountStore };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceAccountsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceAccountsStore.js
new file mode 100644
index 0000000000..f635cec850
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceAccountsStore.js
@@ -0,0 +1,188 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import React from 'react';
+import { Header } from 'semantic-ui-react';
+import { values } from 'mobx';
+import { types } from 'mobx-state-tree';
+import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import {
+ getDataSourceAccounts,
+ checkStudyReachability,
+ checkAccountReachability,
+ registerAccount,
+ registerBucket,
+ registerStudy,
+ updateRegisteredAccount,
+} from '../../helpers/api';
+import { DataSourceAccount } from './DataSourceAccount';
+import { DataSourceAccountStore } from './DataSourceAccountStore';
+
+// ==================================================================
+// DataSourceAccountsStore
+// ==================================================================
+const DataSourceAccountsStore = BaseStore.named('DataSourceAccountsStore')
+ .props({
+ accounts: types.map(DataSourceAccount),
+ accountStores: types.map(DataSourceAccountStore),
+ tickPeriod: 3 * 60 * 1000, // 3 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const accounts = await getDataSourceAccounts();
+ self.runInAction(() => {
+ consolidateToMap(self.accounts, accounts, (existing, newItem) => {
+ existing.setDataSourceAccount(newItem);
+ });
+ });
+ },
+
+ addAccount(raw) {
+ const id = raw.id;
+ const previous = self.accounts.get(id);
+
+ if (!previous) {
+ self.accounts.set(raw.id, raw);
+ } else {
+ previous.setDataSourceAccount(raw);
+ }
+ },
+
+ getAccountStore(accountId) {
+ let entry = self.accountStores.get(accountId);
+ if (!entry) {
+ // Lazily create the store
+ self.accountStores.set(accountId, DataSourceAccountStore.create({ accountId }));
+ entry = self.accountStores.get(accountId);
+ }
+
+ return entry;
+ },
+
+ async updateAccount(account) {
+ const updatedAccount = await updateRegisteredAccount(account.id, _.omit(account, ['id']));
+ const existingAccount = self.getAccount(account.id);
+
+ // If we get null values for the props, we need to change them to empty string
+ if (_.isEmpty(updatedAccount.contactInfo)) {
+ updatedAccount.contactInfo = '';
+ }
+
+ if (_.isEmpty(updatedAccount.description)) {
+ updatedAccount.description = '';
+ }
+
+ if (_.isEmpty(updatedAccount.name)) {
+ updatedAccount.name = '';
+ }
+
+ existingAccount.setDataSourceAccount(updatedAccount);
+ },
+
+ async registerAccount(account) {
+ const newAccount = await registerAccount(account);
+ self.addAccount(newAccount);
+
+ return self.getAccount(account.id);
+ },
+
+ async registerBucket(accountId, bucket = {}) {
+ const normalizedBucket = { ...bucket, awsPartition: 'aws', access: 'roles' };
+ const account = self.getAccount(accountId);
+ if (_.isEmpty(account)) throw new Error(`Account #${accountId} is not loaded yet`);
+
+ const newBucket = await registerBucket(accountId, normalizedBucket);
+
+ return account.setBucket(newBucket);
+ },
+
+ async registerStudy(accountId, bucketName, study = {}) {
+ const account = self.getAccount(accountId);
+ if (_.isEmpty(account)) throw new Error(`Account #${accountId} is not loaded yet`);
+
+ const newStudy = await registerStudy(accountId, bucketName, study);
+
+ return account.setStudy(newStudy);
+ },
+
+ async checkAccountReachability(accountId) {
+ const accountEntity = await checkAccountReachability(accountId);
+ const account = self.getAccount(accountId);
+ if (account) account.setDataSourceAccount(accountEntity);
+ },
+
+ async checkStudyReachability(studyId) {
+ const studyEntity = await checkStudyReachability(studyId);
+ const account = self.getAccount(studyEntity.accountId);
+ const study = account.getStudy(studyId);
+ if (study) study.setStudy(studyEntity);
+ },
+
+ cleanup: () => {
+ self.accounts.clear();
+ self.accountStores.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.accounts.size === 0;
+ },
+
+ get total() {
+ return self.accounts.size;
+ },
+
+ get list() {
+ return _.orderBy(values(self.accounts), ['createdAt', 'name'], ['desc', 'asc']);
+ },
+
+ getAccount(id) {
+ return self.accounts.get(id);
+ },
+
+ get dropdownOptions() {
+ const result = _.map(values(self.accounts), account => ({
+ key: account.id,
+ value: account.id,
+ text: account.id,
+ content: (
+
+ ),
+ }));
+
+ return result;
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.dataSourceAccountsStore = DataSourceAccountsStore.create({}, appContext);
+}
+
+export { DataSourceAccountsStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceStackInfoStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceStackInfoStore.js
new file mode 100644
index 0000000000..947dcdd22b
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceStackInfoStore.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import { getParent } from 'mobx-state-tree';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { generateAccountCfnTemplate } from '../../helpers/api';
+
+// ==================================================================
+// DataSourceStackInfoStore
+// ==================================================================
+const DataSourceStackInfoStore = BaseStore.named('DataSourceStackInfoStore')
+ .props({
+ accountId: '',
+ tickPeriod: 2 * 60 * 1000, // 2 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const account = self.account;
+ const stackInfo = await generateAccountCfnTemplate(self.accountId);
+ account.setStackInfo(stackInfo);
+ },
+
+ cleanup: () => {
+ self.accountId = '';
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get account() {
+ const parent = getParent(self);
+ const a = parent.account;
+ return a;
+ },
+ }));
+
+// Note: Do NOT register this in the global context, if you want to gain access to an instance
+// use dataSourceAccountStore.getDataSourceStackInfoStore()
+// eslint-disable-next-line import/prefer-default-export
+export { DataSourceStackInfoStore };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceStudy.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceStudy.js
new file mode 100644
index 0000000000..acc6f02342
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceStudy.js
@@ -0,0 +1,137 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import { types } from 'mobx-state-tree';
+
+const states = {
+ pending: {
+ id: 'pending',
+ display: 'Pending',
+ color: 'orange',
+ },
+ error: {
+ id: 'error',
+ display: 'Unavailable',
+ color: 'red',
+ },
+ reachable: {
+ id: 'reachable',
+ display: 'Available',
+ color: 'green',
+ },
+};
+
+// ==================================================================
+// DataSourceStudy
+// ==================================================================
+const DataSourceStudy = types
+ .model('DataSourceStudy', {
+ id: '',
+ rev: types.maybe(types.number),
+ name: '',
+ folder: '',
+ accountId: '',
+ awsPartition: 'aws',
+ bucket: '',
+ accessType: '',
+ bucketAccess: '',
+ qualifier: '',
+ appRoleArn: '',
+ category: '',
+ region: '',
+ kmsScope: '',
+ kmsArn: '',
+ status: '',
+ statusMsg: '',
+ statusAt: '',
+ createdAt: '',
+ createdBy: '',
+ updatedAt: '',
+ updatedBy: '',
+ permissions: types.maybe(types.frozen()),
+ })
+ .actions(self => ({
+ setStudy(raw = {}) {
+ _.forEach(raw, (value, key) => {
+ if (value === 'permissions') return; // we don't want to update the permissions
+ if (_.isArray(value)) {
+ self[key].replace(value);
+ } else {
+ self[key] = value;
+ }
+ });
+ // We want to take care of thee statusMsg because it might come as undefined
+ if (_.isUndefined(raw.statusMsg)) self.statusMsg = '';
+ },
+
+ setPermissions(permissions = {}) {
+ self.permissions = permissions;
+ },
+ }))
+
+ // eslint-disable-next-line no-unused-vars
+ .views(self => ({
+ get friendlyAccessType() {
+ if (self.accessType === 'readonly') return 'Read Only';
+ if (self.accessType === 'writeonly') return 'Write Only';
+ return 'Read & Write';
+ },
+
+ get myStudies() {
+ return self.category === 'My Studies';
+ },
+
+ get state() {
+ return states[self.status] || states.reachable;
+ },
+
+ get pendingState() {
+ return self.status === 'pending';
+ },
+
+ get errorState() {
+ return self.status === 'error';
+ },
+
+ get reachableState() {
+ return self.status === 'reachable';
+ },
+
+ get statusMessageInfo() {
+ const msg = self.statusMsg;
+ const info = {
+ prefix: '',
+ color: 'grey',
+ message: msg,
+ };
+
+ if (_.isEmpty(msg)) return info;
+
+ if (_.startsWith(msg, 'WARN|||')) {
+ info.prefix = 'WARN';
+ info.message = _.nth(_.split(msg, '|||'), 1);
+ info.color = 'orange';
+ } else if (_.startsWith(msg, 'ERR|||')) {
+ info.prefix = 'ERR';
+ info.message = _.nth(_.split(msg, '|||'), 1);
+ info.color = 'red';
+ }
+
+ return info;
+ },
+ }));
+
+export { DataSourceStudy }; // eslint-disable-line import/prefer-default-export
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceStudyStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceStudyStore.js
new file mode 100644
index 0000000000..7fb42e09b8
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/DataSourceStudyStore.js
@@ -0,0 +1,66 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import { getParent } from 'mobx-state-tree';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { getStudyPermissions } from '../../helpers/api';
+
+// ==================================================================
+// DataSourceStudyStore
+// ==================================================================
+const DataSourceStudyStore = BaseStore.named('DataSourceStudyStore')
+ .props({
+ accountId: '',
+ studyId: '',
+ tickPeriod: 1 * 60 * 1000, // 1 minute
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const study = self.study;
+ const permissions = await getStudyPermissions(self.studyId);
+ if (_.isUndefined(study)) return;
+ study.setPermissions(permissions);
+ },
+
+ cleanup: () => {
+ self.accountId = '';
+ self.studyId = '';
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get account() {
+ const parent = getParent(self, 2);
+ const a = parent.account;
+ return a;
+ },
+ get study() {
+ return self.account.getStudy(self.studyId);
+ },
+ }));
+
+// Note: Do NOT register this in the global context, if you want to gain access to an instance
+// use dataSourceAccountsStore.getDataSourceStudyStore()
+// eslint-disable-next-line import/prefer-default-export
+export { DataSourceStudyStore };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/StackInfo.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/StackInfo.js
new file mode 100644
index 0000000000..78d648ebbf
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/StackInfo.js
@@ -0,0 +1,61 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import { types } from 'mobx-state-tree';
+
+// ==================================================================
+// StackInfo
+// ==================================================================
+const StackInfo = types
+ .model('StackInfo', {
+ id: '',
+ name: '',
+ region: '',
+ accountId: '',
+ stackId: '',
+ template: types.optional(types.frozen(), {}),
+ signedUrl: '',
+ createStackUrl: '',
+ updateStackUrl: '',
+ cfnConsoleUrl: '',
+ urlExpiry: 0,
+ })
+ .actions(self => ({
+ setStackInfo(raw = {}) {
+ _.forEach(raw, (value, key) => {
+ self[key] = value;
+ });
+ },
+ }))
+
+ // eslint-disable-next-line no-unused-vars
+ .views(self => ({
+ get formattedTemplate() {
+ return JSON.stringify(self.template, null, 2);
+ },
+
+ get hasUpdateStackUrl() {
+ return !_.isEmpty(self.updateStackUrl);
+ },
+
+ get expired() {
+ const now = Date.now();
+
+ return self.urlExpiry < now + 1000 * 60; // lets buffer 1 minute
+ },
+ }));
+
+export { StackInfo }; // eslint-disable-line import/prefer-default-export
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/RegisterStudyWizard.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/RegisterStudyWizard.js
new file mode 100644
index 0000000000..57e9d470ce
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/RegisterStudyWizard.js
@@ -0,0 +1,207 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import { types, getEnv } from 'mobx-state-tree';
+
+import Operations from '../../operations/Operations';
+import RegisterAccountOperation from './operations/RegisterAccount';
+import RegisterBucketOperation from './operations/RegisterBucket';
+import RegisterStudyOperation from './operations/RegisterStudy';
+import PrepareCfnOperation from './operations/PrepareCfn';
+
+// ==================================================================
+// RegisterStudyWizard
+// ==================================================================
+const RegisterStudyWizard = types
+ .model('RegisterStudyWizard', {
+ step: '',
+ accountId: '',
+ })
+
+ .volatile(_self => ({
+ operations: undefined,
+ }))
+
+ .actions(() => ({
+ // I had issues using runInAction from mobx
+ // the issue is discussed here https://github.com/mobxjs/mobx-state-tree/issues/915
+ runInAction(fn) {
+ return fn();
+ },
+ }))
+
+ .actions(self => ({
+ afterCreate: () => {
+ self.step = 'start';
+ self.operations = new Operations();
+ },
+
+ submit: async (formData = {}) => {
+ const providedAccount = formData.account || {};
+ const providedBucket = formData.bucket || {};
+ const studies = formData.studies || [];
+ const ops = self.operations;
+ const accountsStore = self.accountsStore;
+ const existingAccount = self.getAccount(providedAccount.id);
+ const existingBucket = existingAccount ? existingAccount.getBucket(providedBucket.name) : undefined;
+
+ self.accountId = providedAccount.id;
+ ops.clear();
+
+ if (_.isEmpty(existingAccount)) {
+ ops.add(new RegisterAccountOperation({ account: providedAccount, accountsStore }));
+ }
+
+ if (_.isEmpty(existingBucket)) {
+ ops.add(new RegisterBucketOperation({ accountId: providedAccount.id, bucket: providedBucket, accountsStore }));
+ }
+
+ _.forEach(studies, providedStudy => {
+ const study = { ...providedStudy };
+ // lets determine the kmsScope
+ const sse = providedBucket.sse;
+ const kmsArn = study.kmsArn;
+ if (!_.isEmpty(kmsArn)) study.kmsScope = 'study';
+ else if (sse === 'kms') study.kmsScope = 'bucket';
+ else study.kmsScope = 'none';
+
+ // make sure adminUsers is an array, this is because in the form drop down if the study is my studies, then
+ // we ask for a single value, which will not return an array
+ if (!_.isArray(study.adminUsers)) {
+ study.adminUsers = [study.adminUsers];
+ }
+
+ ops.add(
+ new RegisterStudyOperation({
+ accountId: providedAccount.id,
+ bucket: providedBucket,
+ study: removeEmpty(study),
+ accountsStore,
+ }),
+ );
+ });
+
+ ops.add(new PrepareCfnOperation({ accountId: providedAccount.id, accountsStore }));
+
+ self.step = 'submit';
+ await ops.run();
+ },
+
+ retry: async () => {
+ self.step = 'submit';
+ await self.operations.rerun();
+ },
+
+ reset: () => {
+ self.cleanup();
+ },
+
+ advanceToNextStep: () => {
+ if (self.step === 'start') {
+ self.step = 'input';
+ } else if (self.step === 'submit') {
+ self.step = 'cfn';
+ }
+ },
+
+ cleanup: () => {
+ self.step = 'start';
+ if (self.operations) self.operations.clear();
+ self.accountId = '';
+ },
+ }))
+
+ // eslint-disable-next-line no-unused-vars
+ .views(self => ({
+ get isStartStep() {
+ return self.step === 'start';
+ },
+
+ get isInputStep() {
+ return self.step === 'input';
+ },
+
+ get isSubmitStep() {
+ return self.step === 'submit';
+ },
+
+ get isCfnStep() {
+ return self.step === 'cfn';
+ },
+
+ get dropdownAccountOptions() {
+ const accountsStore = getEnv(self).dataSourceAccountsStore;
+
+ return accountsStore.dropdownOptions;
+ },
+
+ get processedAccount() {
+ if (_.isEmpty(self.accountId)) return {};
+
+ return self.getAccount(self.accountId);
+ },
+
+ get accountsStore() {
+ return getEnv(self).dataSourceAccountsStore;
+ },
+
+ getAccount(id) {
+ return self.accountsStore.getAccount(id);
+ },
+
+ getBucket({ accountId, bucketName }) {
+ const account = self.getAccount(accountId);
+ if (_.isEmpty(account)) return undefined;
+
+ return _.find(account.buckets, bucket => bucket.name === bucketName);
+ },
+
+ getBucketRegion({ accountId, bucketName }) {
+ const bucket = self.getBucket({ accountId, bucketName });
+ if (_.isEmpty(bucket)) return undefined;
+
+ return bucket.region;
+ },
+
+ getDropdownBucketOptions(accountId) {
+ const account = self.getAccount(accountId);
+ if (_.isEmpty(account)) return [];
+
+ return _.map(account.buckets, bucket => ({
+ key: bucket.name,
+ value: bucket.name,
+ text: bucket.name,
+ }));
+ },
+ }));
+
+// Given an object returns a new object where all empty/undefined properties are removed
+function removeEmpty(obj) {
+ const result = {};
+ _.forEach(_.keys(obj), key => {
+ if (!_.isEmpty(obj[key])) {
+ result[key] = obj[key];
+ }
+ });
+
+ return result;
+}
+
+function registerContextItems(appContext) {
+ appContext.registerStudyWizard = RegisterStudyWizard.create({}, appContext);
+}
+
+export { RegisterStudyWizard, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/operations/PrepareCfn.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/operations/PrepareCfn.js
new file mode 100644
index 0000000000..648e1b96a4
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/operations/PrepareCfn.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import { decorate, action } from 'mobx';
+
+import { delay } from '@aws-ee/base-ui/dist/helpers/utils';
+
+import { generateAccountCfnTemplate } from '../../../../helpers/api';
+import Operation from '../../../operations/Operation';
+
+class PrepareCfnOperation extends Operation {
+ constructor({ accountId, accountsStore }) {
+ super();
+ this.accountId = accountId;
+ this.name = `Preparing the latest CloudFormation for account #${accountId}`;
+ this.accountsStore = accountsStore;
+ }
+
+ async doRun() {
+ const accountsStore = this.accountsStore;
+ const stackInfo = await generateAccountCfnTemplate(this.accountId);
+
+ await delay(0.5); // We don't have strong read when we load the accounts, therefore we have this delay in place
+ await accountsStore.load();
+
+ const account = accountsStore.getAccount(this.accountId);
+ account.setStackInfo(stackInfo);
+
+ this.setMessage(`Successfully prepared CloudFormation for account #${this.accountId}`);
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(PrepareCfnOperation, {
+ doRun: action,
+});
+
+export default PrepareCfnOperation;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/operations/RegisterAccount.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/operations/RegisterAccount.js
new file mode 100644
index 0000000000..7bb9a0c462
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/operations/RegisterAccount.js
@@ -0,0 +1,51 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import { decorate, action } from 'mobx';
+
+import { isAlreadyExists } from '../../../../helpers/errors';
+import Operation from '../../../operations/Operation';
+
+class RegisterAccountOperation extends Operation {
+ constructor({ account = {}, accountsStore }) {
+ super();
+ const { id } = account;
+ this.account = account;
+ this.name = `Registering account #${id}`;
+ this.accountsStore = accountsStore;
+ }
+
+ async doRun() {
+ const { id } = this.account;
+ this.setMessage(`Registering AWS account #${id}`);
+ try {
+ await this.accountsStore.registerAccount(this.account);
+ this.setMessage(`Successfully registered account #${id}`);
+ } catch (error) {
+ // Check if the error is about existing account, if so, then skip it
+ if (!isAlreadyExists(error)) throw error;
+
+ this.markSkipped();
+ this.setMessage(`Skipping account registration, the account is already registered`);
+ }
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(RegisterAccountOperation, {
+ doRun: action,
+});
+
+export default RegisterAccountOperation;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/operations/RegisterBucket.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/operations/RegisterBucket.js
new file mode 100644
index 0000000000..1515e77be1
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/operations/RegisterBucket.js
@@ -0,0 +1,51 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import { decorate, action } from 'mobx';
+
+import { isAlreadyExists } from '../../../../helpers/errors';
+import Operation from '../../../operations/Operation';
+
+class RegisterBucketOperation extends Operation {
+ constructor({ accountId, bucket = {}, accountsStore }) {
+ super();
+ this.accountId = accountId;
+ this.bucket = bucket;
+ this.name = `Registering bucket ${bucket.name}`;
+ this.accountsStore = accountsStore;
+ }
+
+ async doRun() {
+ const { name } = this.bucket;
+ this.setMessage(`Registering bucket${name}`);
+ try {
+ await this.accountsStore.registerBucket(this.accountId, this.bucket);
+ this.setMessage(`Successfully registered bucket ${name}`);
+ } catch (error) {
+ // Check if the error is about existing bucket, if so, then skip it
+ if (!isAlreadyExists(error)) throw error;
+
+ this.markSkipped();
+ this.setMessage(`Skipping bucket registration, the bucket is already registered`);
+ }
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(RegisterBucketOperation, {
+ doRun: action,
+});
+
+export default RegisterBucketOperation;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/operations/RegisterStudy.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/operations/RegisterStudy.js
new file mode 100644
index 0000000000..9b4542a2cc
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/data-sources/register/operations/RegisterStudy.js
@@ -0,0 +1,43 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import { decorate, action } from 'mobx';
+
+import Operation from '../../../operations/Operation';
+
+class RegisterStudyOperation extends Operation {
+ constructor({ accountId, bucket = {}, study = {}, accountsStore }) {
+ super();
+ this.accountId = accountId;
+ this.bucket = bucket;
+ this.study = study;
+ this.name = `Registering study ${study.name || study.id}`;
+ this.accountsStore = accountsStore;
+ }
+
+ async doRun() {
+ const study = this.study;
+ this.setMessage(`Registering study ${study.name || study.id}`);
+ await this.accountsStore.registerStudy(this.accountId, this.bucket.name, this.study);
+ this.setMessage(`Successfully registered study ${study.name || study.id}`);
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(RegisterStudyOperation, {
+ doRun: action,
+});
+
+export default RegisterStudyOperation;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/RegisterStudyForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/RegisterStudyForm.js
new file mode 100644
index 0000000000..60c13e074c
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/RegisterStudyForm.js
@@ -0,0 +1,128 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import { createFormSeparatedFormat } from '../../helpers/form';
+
+function getRegisterStudyForm() {
+ const fields = [
+ 'account.id',
+ 'account.name',
+ 'account.description',
+ 'account.contactInfo',
+ 'account.mainRegion',
+ 'bucket.name',
+ 'bucket.region',
+ 'bucket.sse',
+ 'bucket.kmsArn',
+ 'studies',
+ 'studies[].id',
+ 'studies[].name',
+ 'studies[].folder',
+ 'studies[].kmsArn',
+ 'studies[].category',
+ 'studies[].accessType',
+ 'studies[].projectId',
+ 'studies[].description',
+ 'studies[].adminUsers',
+ ];
+
+ const labels = {
+ 'account.id': 'AWS Account ID',
+ 'account.name': 'Account Name',
+ 'account.contactInfo': 'Contact Information',
+ 'account.mainRegion': 'Region',
+ 'bucket.name': 'Bucket Name',
+ 'bucket.region': 'Bucket Region',
+ 'bucket.sse': 'Bucket Default Encryption',
+ 'bucket.kmsArn': 'KMS Arn',
+ 'studies': 'Studies',
+ 'studies[].id': 'Study Id',
+ 'studies[].name': 'Study Name',
+ 'studies[].folder': 'Study Folder',
+ 'studies[].kmsArn': 'Study KMS Arn',
+ 'studies[].category': 'Type',
+ 'studies[].accessType': 'Access',
+ 'studies[].projectId': 'Project',
+ 'studies[].description': 'Description',
+ 'studies[].adminUsers': 'Admin',
+ };
+
+ const placeholders = {
+ 'account.id': 'Type the AWS account id',
+ 'account.name': 'Give a name to this account. This is for UI display purposes only',
+ 'account.mainRegion': 'Pick a region',
+ 'account.contactInfo':
+ '(Optional) Type the contact information for the admins of this account. This information is purely for your convenience and it does not have any impact on the registration process.',
+ 'bucket.name': 'The name of the bucket',
+ 'bucket.region': 'Pick the bucket region',
+ 'bucket.sse': 'Bucket encryption',
+ 'bucket.kmsArn': 'KMS Arn (alias arn is not supported)',
+ 'studies[].id': 'A unique id for the study',
+ 'studies[].name': 'A name for the study',
+ 'studies[].folder': 'The study path in the bucket',
+ 'studies[].kmsArn': 'Only provide the kms arn if it is different for this study',
+ 'studies[].projectId': 'The project to associate with the study',
+ };
+
+ const extra = {
+ 'account.id': {
+ explain: 'The AWS account id that owns the bucket that contains the studies',
+ },
+ 'account.mainRegion': {
+ explain: 'Pick a region that you intend to deploy the CloudFormation stack in',
+ },
+ 'studies[].category': {
+ yesLabel: 'My Study',
+ noLabel: 'Organization Study',
+ yesValue: 'My Studies',
+ noValue: 'Organization',
+ },
+
+ 'studies[].accessType': {
+ yesLabel: 'Read Only',
+ noLabel: 'Read & Write',
+ yesValue: 'readonly',
+ noValue: 'readwrite',
+ },
+ };
+
+ const rules = {
+ 'account.id': 'required|min:12|max:12|regex:/^[0-9]+$/',
+ 'account.name': 'required|max:300',
+ 'account.mainRegion': 'required',
+ 'bucket.name': 'required',
+ 'bucket.region': 'required',
+ 'bucket.sse': 'required',
+ 'bucket.kmsArn': 'required',
+ 'studies': 'required',
+ 'studies[].id': 'required|string|between:1,100|regex:/^[A-Za-z0-9-_]+$/',
+ 'studies[].name': 'string|max:2048',
+ 'studies[].folder': 'required|min:1|max:1000',
+ 'studies[].kmsArn': 'string|max:90',
+ 'studies[].category': 'required',
+ 'studies[].adminUsers': 'required',
+ };
+
+ const values = {
+ bucket: {
+ sse: 'kms',
+ },
+ };
+
+ return createFormSeparatedFormat({ fields, labels, placeholders, extra, rules, values });
+}
+
+// eslint-disable-next-line import/prefer-default-export
+export { getRegisterStudyForm };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UpdateRegisteredAccountForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UpdateRegisteredAccountForm.js
new file mode 100644
index 0000000000..eb38cf1bd2
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UpdateRegisteredAccountForm.js
@@ -0,0 +1,43 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import { createForm } from '../../helpers/form';
+
+function getAccountForm(account) {
+ const fields = {
+ name: {
+ label: 'Account Name',
+ placeholder: 'Give a name to this account. This is for UI display purposes only',
+ rules: 'required|max:300',
+ value: account.name,
+ },
+ contactInfo: {
+ label: 'Contact Information',
+ placeholder:
+ '(Optional) Type the contact information for the admins of this account. This information is purely for your convenience and it does not have any impact on the registration process.',
+ rules: 'max:2048',
+ value: account.contactInfo,
+ },
+ description: {
+ label: 'Description',
+ placeholder: '(Optional) A description for the account',
+ rules: 'max:2048',
+ value: account.description,
+ },
+ };
+ return createForm(fields);
+}
+
+export { getAccountForm }; // eslint-disable-line import/prefer-default-export
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/helpers/Operation.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/helpers/Operation.js
new file mode 100644
index 0000000000..531db1ebb1
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/helpers/Operation.js
@@ -0,0 +1,77 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import { types } from 'mobx-state-tree';
+import { toErr, Err } from '@aws-ee/base-ui/dist/models/Err';
+
+// ==================================================================
+// Operation
+// ==================================================================
+const Operation = types
+ .model('Operation', {
+ id: '',
+ state: 'initial', // initial, processing, completed
+ error: types.maybe(Err),
+ })
+
+ .actions(() => ({
+ // I had issues using runInAction from mobx
+ // the issue is discussed here https://github.com/mobxjs/mobx-state-tree/issues/915
+ runInAction(fn) {
+ return fn();
+ },
+ }))
+
+ .actions(self => ({
+ async run(fn) {
+ self.state = 'processing';
+ try {
+ await fn();
+ self.runInAction(() => {
+ self.error = undefined;
+ });
+ } catch (error) {
+ self.runInAction(() => {
+ self.error = toErr(error);
+ });
+ throw error;
+ } finally {
+ self.runInAction(() => {
+ self.state = 'completed';
+ });
+ }
+ },
+ }))
+
+ // eslint-disable-next-line no-unused-vars
+ .views(self => ({
+ get initial() {
+ return self.state === 'initial';
+ },
+ get processing() {
+ return self.state === 'processing';
+ },
+ get completed() {
+ return self.state === 'completed';
+ },
+ get hasError() {
+ return !!self.error;
+ },
+ get errorMessage() {
+ return self.error ? self.error.message || 'unknown error' : '';
+ },
+ }));
+
+export { Operation }; // eslint-disable-line import/prefer-default-export
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/operations/Operation.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/operations/Operation.js
new file mode 100644
index 0000000000..0cab0971d1
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/operations/Operation.js
@@ -0,0 +1,119 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import { decorate, observable, action, computed } from 'mobx';
+
+let counter = 0;
+
+/**
+ * A generic class that represents an operation. This class is not meant to be instantiated directly, instead you want
+ * to extend this class and provide a method named 'doRun'
+ */
+class Operation {
+ constructor() {
+ this.status = 'notStarted';
+ this.error = '';
+ this.privateSkipped = false;
+ counter += 1;
+ this.id = `${Date.now()}-${counter}`;
+ }
+
+ async run(payload) {
+ try {
+ this.privateSkipped = false;
+ this.clearError();
+ this.clearMessage();
+ this.markRunning();
+ await this.doRun(payload);
+ this.markFinished();
+ } catch (error) {
+ this.setError(error);
+ this.markFinished();
+ }
+ }
+
+ markRunning() {
+ this.status = 'running';
+ }
+
+ markFinished() {
+ this.status = 'finished';
+ }
+
+ markSkipped() {
+ this.markFinished();
+ this.privateSkipped = true;
+ }
+
+ clearError() {
+ this.error = '';
+ }
+
+ setError(error) {
+ if (_.isString(error)) this.error = error;
+ else this.error = error.message;
+ }
+
+ setMessage(message = '') {
+ this.message = message;
+ }
+
+ clearMessage() {
+ this.message = '';
+ }
+
+ get running() {
+ return this.status === 'running';
+ }
+
+ get hasError() {
+ return this.error !== '';
+ }
+
+ get skipped() {
+ return !this.failure && this.status === 'finished' && this.privateSkipped;
+ }
+
+ get success() {
+ return this.status === 'finished' && !this.hasError && !this.privateSkipped;
+ }
+
+ get failure() {
+ return this.status === 'finished' && this.hasError;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(Operation, {
+ id: observable,
+ status: observable,
+ message: observable,
+ error: observable,
+ running: computed,
+ hasError: computed,
+ success: computed,
+ failure: computed,
+ skipped: computed,
+ markRunning: action,
+ markFinished: action,
+ markSkipped: action,
+ clearError: action,
+ setError: action,
+ clearMessage: action,
+ setMessage: action,
+});
+
+export default Operation;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/operations/Operations.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/operations/Operations.js
new file mode 100644
index 0000000000..e214bfa5c4
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/operations/Operations.js
@@ -0,0 +1,142 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+/* eslint-disable no-await-in-loop */
+/* eslint-disable no-continue */
+/* eslint-disable no-restricted-syntax */
+import _ from 'lodash';
+import { decorate, observable, action, computed } from 'mobx';
+
+class Operations {
+ constructor() {
+ this.ops = [];
+ this.status = 'notStarted'; // this is the overall status for all operations
+ this.payload = {};
+ }
+
+ add(op) {
+ this.ops.push(op);
+ }
+
+ async run(payload) {
+ if (this.status === 'running') return;
+ this.payload = payload;
+ this.markRunning();
+ for (const op of this.ops) {
+ if (op.success) continue;
+ if (op.running) continue;
+ if (op.skipped) continue;
+ await op.run(this.payload);
+ }
+ this.markFinished();
+ }
+
+ async rerun() {
+ if (this.status === 'running') return;
+ this.markNotStarted();
+
+ this.run(this.payload);
+ }
+
+ markNotStarted() {
+ this.status = 'notStarted';
+ }
+
+ markRunning() {
+ this.status = 'running';
+ }
+
+ markFinished() {
+ this.status = 'finished';
+ }
+
+ clear() {
+ this.ops.clear();
+ this.status = 'notStarted';
+ this.payload = {};
+ }
+
+ get running() {
+ return this.status === 'running';
+ }
+
+ get notStarted() {
+ return this.status === 'notStarted';
+ }
+
+ get started() {
+ return !this.notStarted;
+ }
+
+ get success() {
+ if (this.status !== 'finished') return false;
+ if (this.empty) return true;
+
+ let result = true;
+ // eslint-disable-next-line consistent-return
+ _.forEach(this.ops, op => {
+ if (op.failure) {
+ result = false;
+ return false; // to stop the forEach loop since we got the answer we want
+ }
+ });
+
+ return result;
+ }
+
+ // If we have one or more operations that failed
+ get failure() {
+ if (this.status !== 'finished') return false;
+ return !this.success;
+ }
+
+ // True if all operations failed (not even skipped)
+ get allFailed() {
+ if (this.status !== 'finished') return false;
+ if (this.empty) return false;
+
+ let result = true;
+ // eslint-disable-next-line consistent-return
+ _.forEach(this.ops, op => {
+ if (op.success || op.skipped) {
+ result = false;
+ return false; // to stop the forEach loop since we got the answer we want
+ }
+ });
+
+ return result;
+ }
+
+ get empty() {
+ return this.ops.length === 0;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(Operations, {
+ ops: observable,
+ status: observable,
+ running: computed,
+ success: computed,
+ failure: computed,
+ allFailed: computed,
+ notStarted: computed,
+ markRunning: action,
+ markFinished: action,
+ markNotStarted: action,
+ clear: action,
+});
+
+export default Operations;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudiesStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudiesStore.js
index 05c69766ee..0a04c0ee7b 100644
--- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudiesStore.js
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudiesStore.js
@@ -85,6 +85,7 @@ const StudiesStore = BaseStore.named('StudiesStore')
cleanup: () => {
self.studies.clear();
+ self.studyStores.clear();
superCleanup();
},
};
@@ -122,6 +123,21 @@ function registerContextItems(appContext) {
[categories.organization.id]: StudiesStore.create({ category: categories.organization.name }),
[categories.openData.id]: StudiesStore.create({ category: categories.openData.name }),
};
+
+ appContext.cleanupMap = {
+ // This method is going to be automatically called when the logout is invoked
+ cleanup: () => {
+ _.forEach(appContext.studiesStoresMap, obj => {
+ if (_.isFunction(obj.cleanup)) {
+ try {
+ obj.cleanup();
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ });
+ },
+ };
}
export { registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/Study.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/Study.js
index d51e7f858d..42307573d0 100644
--- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/Study.js
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/Study.js
@@ -15,10 +15,43 @@
import { types, applySnapshot } from 'mobx-state-tree';
+import _ from 'lodash';
import { StudyFilesStore } from './StudyFilesStore';
import { StudyPermissionsStore } from './StudyPermissionsStore';
import { categories } from './categories';
+// 'pending', 'error', 'default'
+const states = {
+ pending: {
+ key: 'pending',
+ display: 'PENDING',
+ color: 'orange',
+ tip:
+ 'This study is in the process of being configured. Once the configuration is completed by the Service Workbench admins, the study status will become available.',
+ canChangePermission: false,
+ canSelect: false,
+ spinner: true,
+ },
+ error: {
+ key: 'error',
+ display: 'UNAVAILABLE',
+ color: 'red',
+ tip: 'There seems to be an issue accessing the study files. Please contact Service Workbench admins.',
+ canChangePermission: false,
+ canSelect: false,
+ spinner: false,
+ },
+ reachable: {
+ key: 'default',
+ display: 'AVAILABLE',
+ color: 'green',
+ tip: 'The study is available and ready for use.',
+ canChangePermission: true,
+ canSelect: true,
+ spinner: false,
+ },
+};
+
// ==================================================================
// Study
// ==================================================================
@@ -27,8 +60,10 @@ const Study = types
id: types.identifier,
rev: types.maybe(types.number),
name: '',
+ status: types.maybe(types.string),
category: '',
projectId: '',
+ accessType: types.maybe(types.string),
access: types.optional(types.array(types.string), []),
resources: types.optional(types.array(types.model({ arn: types.string })), []),
description: types.maybeNull(types.string),
@@ -73,6 +108,20 @@ const Study = types
return self.category === categories.organization.name; // TODO the backend should really send an id and not a name
},
+ get state() {
+ if (self.status) {
+ return _.cloneDeep(states[self.status]);
+ }
+ return _.cloneDeep(states.reachable);
+ },
+
+ get userTypes() {
+ if (self.accessType === 'readonly') {
+ return ['admin', 'readonly'];
+ }
+ return ['admin', 'readwrite', 'readonly'];
+ },
+
get canUpload() {
return self.access.includes('admin') || self.access.includes('readwrite');
},
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissions.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissions.js
index 81e7c25515..4f2ca70a8d 100644
--- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissions.js
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissions.js
@@ -22,17 +22,30 @@ import { types } from 'mobx-state-tree';
const StudyPermissions = types
.model('StudyPermissions', {
id: types.identifier,
- adminUsers: types.optional(types.array(types.string), []),
- readonlyUsers: types.optional(types.array(types.string), []),
- readwriteUsers: types.optional(types.array(types.string), []),
+ adminUsers: types.array(types.string),
+ readonlyUsers: types.array(types.string),
+ readwriteUsers: types.array(types.string),
+ writeonlyUsers: types.array(types.string),
createdAt: '',
createdBy: '',
updatedAt: '',
updatedBy: '',
})
- .views(_self => ({
- get userTypes() {
- return ['admin', 'readwrite', 'readonly'];
+ .actions(self => ({
+ setStudyPermissions(raw = {}) {
+ self.adminUsers.replace(raw.adminUsers || []);
+ self.readonlyUsers.replace(raw.readonlyUsers || []);
+ self.readwriteUsers.replace(raw.readwriteUsers || []);
+ self.writeonlyUsers.replace(raw.writeonlyUsers || []);
+ self.createdAt = raw.createdAt;
+ self.createdBy = raw.createdBy;
+ self.updatedAt = raw.updatedAt;
+ self.updatedBy = raw.updatedBy;
+ },
+ }))
+ .views(self => ({
+ isStudyAdmin(uid) {
+ return self.adminUsers.some(adminUid => adminUid === uid);
},
}));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissionsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissionsStore.js
index 1876ae2539..0414a2d42b 100644
--- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissionsStore.js
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissionsStore.js
@@ -15,7 +15,7 @@
/* eslint-disable import/prefer-default-export */
import _ from 'lodash';
-import { types } from 'mobx-state-tree';
+import { getParent, types } from 'mobx-state-tree';
import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
import { getStudyPermissions, updateStudyPermissions } from '../../helpers/api';
@@ -38,21 +38,28 @@ const StudyPermissionsStore = BaseStore.named('StudyPermissionsStore')
return {
doLoad: async () => {
const newPermissions = await getStudyPermissions(self.studyId);
- if (!self.studyPermissions || !_.isEqual(self.studyPermissions, newPermissions)) {
- self.runInAction(() => {
- self.studyPermissions = newPermissions;
- });
- }
+ self.runInAction(() => {
+ if (!self.studyPermissions) {
+ self.studyPermissions = StudyPermissions.create({
+ id: self.studyId,
+ ...newPermissions,
+ });
+ } else {
+ self.studyPermissions.setStudyPermissions(newPermissions);
+ }
+ });
},
cleanup: () => {
+ self.studyPermissions = undefined;
superCleanup();
},
update: async selectedUserIds => {
const updateRequest = { usersToAdd: [], usersToRemove: [] };
- self.studyPermissions.userTypes.forEach(type => {
+ const parent = getParent(self, 1);
+ parent.userTypes.forEach(type => {
const userToRequestFormat = uid => ({ uid, permissionLevel: type });
// Set selected users as "usersToAdd" (API is idempotent)
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceAccountCard.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceAccountCard.js
new file mode 100644
index 0000000000..c0a2cb97af
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceAccountCard.js
@@ -0,0 +1,227 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+/* eslint-disable max-classes-per-file */
+import _ from 'lodash';
+import React from 'react';
+import { decorate, computed, observable, action, runInAction } from 'mobx';
+import { observer, inject, Observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Header, Tab, Label, Menu, Button, Message } from 'semantic-ui-react';
+import TimeAgo from 'react-timeago';
+
+import { niceNumber, swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import { isStoreError, isStoreNew, isStoreLoading } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import By from '../helpers/By';
+import DataSourceStudiesList from './DataSourceStudiesList';
+import DataSourceAccountCfn from './DataSourceAccountCfn';
+import DataSourceAccountInfo from './DataSourceAccountInfo';
+import { Operation } from '../../models/helpers/Operation';
+import AccountConnectionPanel from './parts/AccountConnectionPanel';
+import AccountStatusMessage from './parts/AccountStatusMessage';
+
+// This component is used with the TabPane to replace the default Segment wrapper since
+// we don't want to display the border.
+// eslint-disable-next-line react/prefer-stateless-function
+class TabPaneWrapper extends React.Component {
+ render() {
+ return <>{this.props.children}>;
+ }
+}
+
+// expected props
+// - account (via prop)
+// - dataSourceAccountsStore (via injection)
+class DataSourceAccountCard extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.expanded = false;
+ this.connectionPanel = {
+ show: false,
+ operation: Operation.create({}),
+ };
+ });
+ }
+
+ get account() {
+ return this.props.account;
+ }
+
+ get accountsStore() {
+ return this.props.dataSourceAccountsStore;
+ }
+
+ getAccountStore() {
+ const accountsStore = this.accountsStore;
+ const account = this.account || {};
+ return accountsStore.getAccountStore(account.id);
+ }
+
+ handleCheckConnection = () => {
+ this.connectionPanel.show = true;
+
+ const account = this.account;
+ const accountsStore = this.accountsStore;
+ const operation = this.connectionPanel.operation;
+ const doWork = async () => {
+ await accountsStore.checkAccountReachability(account.id);
+ };
+
+ swallowError(operation.run(doWork));
+ };
+
+ handleDismissPanel = () => {
+ this.connectionPanel.show = false;
+ };
+
+ render() {
+ const account = this.account;
+ const operation = this.connectionPanel.operation;
+ const showPanel = this.connectionPanel.show;
+ const reachable = account.reachableState;
+ const hasMsg = !_.isEmpty(account.statusMessageInfo.message);
+ const showMsg = !showPanel && (!reachable || (reachable && hasMsg));
+
+ return (
+
+
+ Test Connection
+
+ {this.renderTitle(account)}
+ {this.renderStatus(account)}
+ {showMsg &&
}
+ {showPanel && (
+
+ )}
+ {this.renderStackMismatch(account)}
+ {this.renderTabs()}
+
+ );
+ }
+
+ renderTabs() {
+ const getMenuItemLabel = () => {
+ const store = this.getAccountStore();
+ const emptySpan = null;
+ if (!store) return emptySpan;
+ if (isStoreError(store)) return emptySpan;
+ if (isStoreNew(store)) return emptySpan;
+ if (isStoreLoading(store)) return emptySpan;
+ return {niceNumber(store.studiesTotal)} ;
+ };
+
+ const account = this.account;
+ const panes = [
+ {
+ menuItem: Studies {getMenuItemLabel()} ,
+ render: () => (
+
+ {() => }
+
+ ),
+ },
+ {
+ menuItem: 'CloudFormation',
+ render: () => (
+
+ {() => }
+
+ ),
+ },
+ {
+ menuItem: 'Account Information',
+ render: () => (
+
+ {() => }
+
+ ),
+ },
+ ];
+
+ return ;
+ }
+
+ renderTitle(account) {
+ return (
+
+ {account.name}
+
+
+ Registered
+ —
+
+
+ Status checked
+ —
+
+ AWS Account # {account.id}
+
+
+ );
+ }
+
+ renderStatus(account) {
+ const { state } = account;
+ return (
+
+ {state.display}
+
+ );
+ }
+
+ renderStackMismatch(account) {
+ const stackOutDated = account.stackOutDated;
+ const incorrectStackNameProvisioned = account.incorrectStackNameProvisioned;
+
+ if (!stackOutDated && !incorrectStackNameProvisioned) return null;
+
+ if (incorrectStackNameProvisioned) {
+ return (
+
+ Incorrect stack name
+
+ It seems that the correct CloudFormation stack was deployed to AWS account {account.id} but with an
+ incorrect stack name. Please ensure that you have the latest CloudFormation template deployed with the stack
+ name {account.stack} in the account. If you just updated the stack you can run the connection test again.
+
+
+ );
+ }
+
+ return (
+
+ Stack is outdated
+
+ It seems that the CloudFormation stack {account.stack} deployed to AWS account {account.id} is outdated
+ and does not contain the latest changes made. Please use the latest CloudFormation template to update the
+ stack. If you just updated the stack you can run the connection test again.
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(DataSourceAccountCard, {
+ accountsStore: computed,
+ account: computed,
+ handleCheckConnection: action,
+ handleDismissPanel: action,
+ connectionPanel: observable,
+});
+
+export default inject('dataSourceAccountsStore')(withRouter(observer(DataSourceAccountCard)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceAccountCfn.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceAccountCfn.js
new file mode 100644
index 0000000000..5404d26131
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceAccountCfn.js
@@ -0,0 +1,80 @@
+import React from 'react';
+import { decorate, computed } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import { isStoreReady, isStoreLoading, isStoreError, stopHeartbeat } from '@aws-ee/base-ui/dist/models/BaseStore';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import ProgressPlaceHolder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+
+import AccountCfnPanel from './parts/AccountCfnPanel';
+
+// expected props
+// - account (via prop)
+// - dataSourceAccountsStore (via injection)
+class DataSourceAccountCfn extends React.Component {
+ componentDidMount() {
+ const store = this.getStackInfoStore();
+ if (!isStoreReady(store)) {
+ swallowError(store.load());
+ }
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getStackInfoStore();
+ stopHeartbeat(store);
+ }
+
+ get account() {
+ return this.props.account;
+ }
+
+ get accountsStore() {
+ return this.props.dataSourceAccountsStore;
+ }
+
+ getStackInfoStore() {
+ const accountsStore = this.accountsStore;
+ const account = this.account || {};
+ const accountStore = accountsStore.getAccountStore(account.id);
+
+ return accountStore.getStackInfoStore();
+ }
+
+ render() {
+ const store = this.getStackInfoStore();
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreReady(store)) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return content;
+ }
+
+ renderMain() {
+ const account = this.account;
+
+ return (
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(DataSourceAccountCfn, {
+ accountsStore: computed,
+ account: computed,
+});
+
+export default inject('dataSourceAccountsStore')(withRouter(observer(DataSourceAccountCfn)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceAccountInfo.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceAccountInfo.js
new file mode 100644
index 0000000000..0cbb3e1d81
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceAccountInfo.js
@@ -0,0 +1,119 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import React from 'react';
+import { decorate, computed, runInAction, observable, action } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Button } from 'semantic-ui-react';
+
+import { displaySuccess, displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import Form from '@aws-ee/base-ui/dist/parts/helpers/fields/Form';
+import Input from '@aws-ee/base-ui/dist/parts/helpers/fields/Input';
+import TextArea from '@aws-ee/base-ui/dist/parts/helpers/fields/TextArea';
+
+import { getAccountForm } from '../../models/forms/UpdateRegisteredAccountForm';
+
+// expected props
+// - account (via prop)
+// - dataSourceAccountsStore (via injection)
+class DataSourceAccountInfo extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.form = getAccountForm(props.account);
+ });
+ }
+
+ get account() {
+ return this.props.account;
+ }
+
+ get accountsStore() {
+ return this.props.dataSourceAccountsStore;
+ }
+
+ getFields(names, container) {
+ const form = container || this.form;
+ return _.map(names, name => form.$(name));
+ }
+
+ handleCancel = () => {
+ this.form.reset();
+ };
+
+ handleSave = async form => {
+ const account = this.account;
+ const accountsStore = this.accountsStore;
+ const formData = form.values();
+
+ const data = { ...formData, id: account.id, rev: account.rev };
+ try {
+ await accountsStore.updateAccount(data);
+ runInAction(() => {
+ this.form = getAccountForm(data);
+ });
+ displaySuccess('Account information updated successfully');
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ render() {
+ const form = this.form;
+ const isDirty = form.isDirty;
+
+ return (
+
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(DataSourceAccountInfo, {
+ accountsStore: computed,
+ account: computed,
+ form: observable,
+ handleCancel: action,
+});
+
+export default inject('dataSourceAccountsStore')(withRouter(observer(DataSourceAccountInfo)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceAccountsList.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceAccountsList.js
new file mode 100644
index 0000000000..d708181680
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceAccountsList.js
@@ -0,0 +1,133 @@
+import React from 'react';
+import _ from 'lodash';
+import { decorate, computed, action } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Container, Segment, Header, Icon, Button, Label } from 'semantic-ui-react';
+
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import {
+ isStoreReady,
+ isStoreLoading,
+ isStoreEmpty,
+ isStoreNotEmpty,
+ isStoreError,
+ stopHeartbeat,
+} from '@aws-ee/base-ui/dist/models/BaseStore';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import ProgressPlaceHolder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+
+import DataSourceAccountCard from './DataSourceAccountCard';
+
+// expected props
+// - dataSourceAccountsStore (via injection)
+class DataSourceAccountsList extends React.Component {
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ const store = this.accountsStore;
+ if (!isStoreReady(store)) {
+ swallowError(store.load());
+ }
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.envsStore;
+ stopHeartbeat(store);
+ }
+
+ get accountsStore() {
+ return this.props.dataSourceAccountsStore;
+ }
+
+ handleRegisterStudies = event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const goto = gotoFn(this);
+ goto(`/data-sources/register`);
+ };
+
+ render() {
+ const store = this.accountsStore;
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreEmpty(store)) {
+ content = this.renderEmpty();
+ } else if (isStoreNotEmpty(store)) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderTitle()}
+ {content}
+
+ );
+ }
+
+ renderMain() {
+ const store = this.accountsStore;
+ const list = store.list;
+
+ return (
+ <>
+ {_.map(list, item => (
+
+
+
+ ))}
+ >
+ );
+ }
+
+ renderEmpty() {
+ return (
+
+
+
+ No data sources
+ To create a data source, click Register Studies.
+
+
+ );
+ }
+
+ renderTitle() {
+ return (
+
+
+
+ Data Sources {this.renderTotal()}
+
+
+
+ Register Studies
+
+
+
+ );
+ }
+
+ renderTotal() {
+ const store = this.accountsStore;
+ if (isStoreError(store) || isStoreLoading(store)) return null;
+
+ return {store.total} ;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(DataSourceAccountsList, {
+ accountsStore: computed,
+ handleRegisterStudies: action,
+});
+
+export default inject('dataSourceAccountsStore')(withRouter(observer(DataSourceAccountsList)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceStudiesList.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceStudiesList.js
new file mode 100644
index 0000000000..eb86c4d192
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceStudiesList.js
@@ -0,0 +1,120 @@
+import React from 'react';
+import _ from 'lodash';
+import { decorate, computed } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Segment, Header, Icon, Table } from 'semantic-ui-react';
+
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import {
+ isStoreReady,
+ isStoreLoading,
+ isStoreEmpty,
+ isStoreNotEmpty,
+ isStoreError,
+ stopHeartbeat,
+} from '@aws-ee/base-ui/dist/models/BaseStore';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import ProgressPlaceHolder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+
+import DataSourceStudyRow from './DataSourceStudyRow';
+
+// expected props
+// - account (via prop)
+// - dataSourceAccountsStore (via injection)
+class DataSourceStudiesList extends React.Component {
+ componentDidMount() {
+ const store = this.getAccountStore();
+ if (!isStoreReady(store)) {
+ swallowError(store.load());
+ }
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getAccountStore();
+ stopHeartbeat(store);
+ }
+
+ get account() {
+ return this.props.account;
+ }
+
+ get accountsStore() {
+ return this.props.dataSourceAccountsStore;
+ }
+
+ getAccountStore() {
+ const accountsStore = this.accountsStore;
+ const account = this.account || {};
+ return accountsStore.getAccountStore(account.id);
+ }
+
+ render() {
+ const store = this.getAccountStore();
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreEmpty(store)) {
+ content = this.renderEmpty();
+ } else if (isStoreNotEmpty(store)) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return content;
+ }
+
+ renderMain() {
+ const account = this.account;
+ const accountStore = this.getAccountStore();
+ const list = account.studiesList;
+ const getStudyStore = study => accountStore.getStudyStore(study.id);
+
+ return (
+
+
+
+
+
+ Study Id
+ Path
+ Type
+ Access
+ Status
+
+
+
+ {_.map(list, item => (
+
+ ))}
+
+
+
+ );
+ }
+
+ renderEmpty() {
+ return (
+
+
+
+ No registered studies
+ To add studies, click Register Studies.
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(DataSourceStudiesList, {
+ accountsStore: computed,
+ account: computed,
+});
+
+export default inject('dataSourceAccountsStore')(withRouter(observer(DataSourceStudiesList)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceStudyRow.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceStudyRow.js
new file mode 100644
index 0000000000..867ae9649c
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/DataSourceStudyRow.js
@@ -0,0 +1,322 @@
+import React from 'react';
+import _ from 'lodash';
+import { decorate, computed, runInAction, action, observable } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Icon, Table, Label, Grid, Button } from 'semantic-ui-react';
+import TimeAgo from 'react-timeago';
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import UserLabels from '@aws-ee/base-ui/dist/parts/helpers/UserLabels';
+import { isStoreReady, isStoreLoading, isStoreError, stopHeartbeat } from '@aws-ee/base-ui/dist/models/BaseStore';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import ProgressPlaceHolder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+
+import { Operation } from '../../models/helpers/Operation';
+import StudyConnectionPanel from './parts/StudyConnectionPanel';
+import StudyStatusMessage from './parts/StudyStatusMessage';
+
+// expected props
+// - study (via prop)
+// - store (via prop) (this the study store)
+// - dataSourceAccountsStore (via injection)
+// - usersStore (via injection)
+class DataSourceStudyRow extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.expanded = false;
+ this.connectionPanel = {
+ show: false,
+ operation: Operation.create({}),
+ };
+ });
+ }
+
+ componentWillUnmount() {
+ const store = this.studyStore;
+ stopHeartbeat(store);
+ }
+
+ get study() {
+ return this.props.study;
+ }
+
+ get accountsStore() {
+ return this.props.dataSourceAccountsStore;
+ }
+
+ get usersStore() {
+ return this.props.usersStore;
+ }
+
+ get studyStore() {
+ return this.props.store;
+ }
+
+ handleExpandClick = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.expanded = !this.expanded;
+ const store = this.studyStore;
+
+ if (!isStoreReady(store) && this.expanded) {
+ swallowError(store.load());
+ store.startHeartbeat();
+ }
+
+ if (!this.expanded) {
+ stopHeartbeat(store);
+ }
+ };
+
+ handleCheckConnection = () => {
+ this.connectionPanel.show = true;
+
+ const study = this.study;
+ const accountsStore = this.accountsStore;
+ const operation = this.connectionPanel.operation;
+ const doWork = async () => {
+ await accountsStore.checkStudyReachability(study.id);
+ };
+
+ swallowError(operation.run(doWork));
+ };
+
+ handleDismissPanel = () => {
+ this.connectionPanel.show = false;
+ };
+
+ render() {
+ const expanded = this.expanded;
+ const item = this.study;
+ const { id, category, folder, friendlyAccessType, state } = item;
+ const value = text => {_.isEmpty(text) ? 'Not Provided' : text} ;
+ const iconName = expanded ? 'angle down' : 'angle right';
+
+ return (
+ <>
+
+
+
+
+ {value(id)}
+ {value(folder)}
+ {value(category)}
+ {value(friendlyAccessType)}
+ {this.renderStatus(state)}
+
+ {expanded && (
+
+ {this.renderExpanded()}
+
+ )}
+ >
+ );
+ }
+
+ renderStatus(state, classnames) {
+ return (
+
+
+ {state.display}
+
+
+ );
+ }
+
+ renderExpanded() {
+ const store = this.studyStore;
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else {
+ content = this.renderExpandedContent();
+ }
+
+ return content;
+ }
+
+ renderExpandedContent() {
+ const study = this.study;
+ const operation = this.connectionPanel.operation;
+ const showPanel = this.connectionPanel.show;
+
+ return (
+
+
+
+ Test Connection
+
+
+ {!study.reachableState && !showPanel &&
}
+ {showPanel &&
}
+
+
+ {this.renderDetailTablePart1()}
+ {this.renderDetailTablePart2()}
+
+ {this.renderDetailTablePart3()}
+ {this.renderPermissionsTable()}
+
+ );
+ }
+
+ renderDetailTablePart1() {
+ const store = this.studyStore;
+ const study = store.study;
+ const { id, name, state, statusAt, folder } = study;
+ const naIfEmpty = value => (_.isEmpty(value) ? 'N/A' : value);
+ const renderRow = (key, value) => (
+
+
+ {key}
+
+
+ {value}
+
+
+ );
+
+ return (
+
+
+
+
+ Status
+
+
+ {this.renderStatus(state, 'flex-auto mr1')}
+
+ Status checked
+
+
+
+
+ {renderRow('ID', id)}
+ {renderRow('Name', naIfEmpty(name))}
+ {renderRow('Path', folder)}
+
+
+ );
+ }
+
+ renderDetailTablePart2() {
+ const store = this.studyStore;
+ const study = store.study;
+ const { category, friendlyAccessType, bucket, projectId, region } = study;
+ const naIfEmpty = value => (_.isEmpty(value) ? 'N/A' : value);
+ const renderRow = (key, value) => (
+
+
+ {key}
+
+
+ {value}
+
+
+ );
+ const bucketRow = (
+
+
+ Bucket
+
+
+ {bucket}
+ {region}
+
+
+ );
+
+ return (
+
+
+ {renderRow('Project', naIfEmpty(projectId))}
+ {renderRow('Type', category)}
+ {renderRow('Access', friendlyAccessType)}
+ {bucketRow}
+
+
+ );
+ }
+
+ renderDetailTablePart3() {
+ const store = this.studyStore;
+ const study = store.study;
+ const { description, kmsScope, kmsArn } = study;
+ const naIfEmpty = value => (_.isEmpty(value) ? 'None' : value);
+ const kms = kmsScope === 'bucket' ? 'Use bucket default encryption' : kmsArn;
+ const renderRow = (key, value) => (
+
+
+ {key}
+
+
+ {value}
+
+
+ );
+
+ return (
+
+
+ {renderRow('KMS Arn', naIfEmpty(kms))}
+ {renderRow('Description', naIfEmpty(description))}
+
+
+ );
+ }
+
+ renderPermissionsTable() {
+ const store = this.studyStore;
+ const study = store.study;
+ const { accessType, myStudies } = study;
+ const { adminUsers = [], readonlyUsers = [], readwriteUsers = [] } = study.permissions || {};
+ const showReadonly = accessType === 'readonly' || accessType === 'readwrite';
+ const showReadwrite = accessType === 'readwrite';
+
+ return (
+
+
+ {this.renderUsersRow('Admin', adminUsers)}
+ {showReadonly && !myStudies && this.renderUsersRow('Read Only', readonlyUsers)}
+ {showReadwrite && !myStudies && this.renderUsersRow('Read & Write', readwriteUsers)}
+
+
+ );
+ }
+
+ renderUsersRow(title, userIds = []) {
+ const userIdentifiers = _.map(userIds, uid => ({ uid }));
+ const users = this.usersStore.asUserObjects(userIdentifiers);
+
+ return (
+
+
+ {title}
+
+
+ {!_.isEmpty(userIds) && }
+ {_.isEmpty(userIds) && None }
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(DataSourceStudyRow, {
+ accountsStore: computed,
+ study: computed,
+ studyStore: computed,
+ usersStore: computed,
+ handleExpandClick: action,
+ handleCheckConnection: action,
+ handleDismissPanel: action,
+ expanded: observable,
+ connectionPanel: observable,
+});
+
+export default inject('dataSourceAccountsStore', 'usersStore')(withRouter(observer(DataSourceStudyRow)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/AccountCfnPanel.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/AccountCfnPanel.js
new file mode 100644
index 0000000000..52a3a347dd
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/AccountCfnPanel.js
@@ -0,0 +1,317 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+import { decorate, computed, runInAction, observable } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Header, Divider, List, Form, TextArea, Message, Button } from 'semantic-ui-react';
+import TimeAgo from 'react-timeago';
+
+import YesNo from '@aws-ee/base-ui/dist/parts/helpers/fields/YesNo';
+import SelectionButtons from '@aws-ee/base-ui/dist/parts/helpers/fields/SelectionButtons';
+
+import CopyToClipboard from '../../helpers/CopyToClipboard';
+import { createForm } from '../../../helpers/form';
+
+const adminOptions = [
+ {
+ text: 'I have admin access',
+ value: 'admin',
+ },
+ {
+ text: 'I do not have admin access',
+ value: 'notAdmin',
+ },
+];
+
+// expected props
+// - account (via prop)
+// - largeText (via prop) default to false
+class AccountCfnPanel extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ // We want to create a simple one button form
+ const account = props.account || {};
+ const { hasUpdateStackUrl } = account.stackInfo || {};
+ const fields = {
+ managed: {
+ value: 'admin',
+ },
+ createOrUpdate: {
+ extra: {
+ yesLabel: 'Stack Create',
+ noLabel: 'Stack Update',
+ yesValue: 'create',
+ noValue: 'update',
+ showHeader: false,
+ },
+ value: hasUpdateStackUrl ? 'update' : 'create',
+ },
+ };
+ this.form = createForm(fields);
+ });
+ }
+
+ get account() {
+ return this.props.account || {};
+ }
+
+ get stackInfo() {
+ return this.account.stackInfo || {};
+ }
+
+ get largeText() {
+ return this.props.largeText;
+ }
+
+ get textSize() {
+ return this.largeText ? 'large' : 'medium';
+ }
+
+ render() {
+ const { id } = this.account;
+ const form = this.form;
+ const field = form.$('managed');
+
+ return (
+ <>
+ {this.renderCfnTemplate()}
+
+ {this.renderSteps()}
+ >
+ );
+ }
+
+ renderCfnTemplate() {
+ const stackInfo = this.stackInfo;
+ const { name, formattedTemplate } = stackInfo;
+
+ return (
+
+
+ CloudFormation Stack Name
+
+
+
+
+ );
+ }
+
+ renderSteps() {
+ // We need to determine if this is for creating the stack or updating the stack
+ const form = this.form;
+ const stackInfo = this.stackInfo;
+ const { hasUpdateStackUrl } = stackInfo;
+ const field = form.$('createOrUpdate');
+ const update = field.value === 'update';
+ const hasAdminAccess = form.$('managed').value === 'admin';
+
+ return (
+ <>
+
+
+ {hasUpdateStackUrl && }
+
+ {!update && hasAdminAccess && this.renderCreateSteps()}
+ {update && hasAdminAccess && this.renderUpdateSteps()}
+ {!hasAdminAccess && this.renderEmailTemplate(update)}
+ >
+ );
+ }
+
+ renderCreateSteps() {
+ const account = this.account;
+ const textSize = this.textSize;
+ const stackInfo = this.stackInfo;
+ const { id, mainRegion } = account;
+ const { createStackUrl } = stackInfo;
+
+ return (
+
+
+
+ In a separate browser tab, login to the aws console using the correct account.
+
+ Attention
+
+ Ensure that you are logged in to the aws account # {id} and region {mainRegion}
+
+
+
+
+ Click on the Create Stack button, this opens a separate browser tab and takes you to the
+ CloudFormation console where you can review the stack information and provision it.
+
+
+
+ Create Stack
+
+ {this.renderExpires(stackInfo)}
+
+
+
+
+
+
+
+ While the stack is being provisioned, it is okay to navigate away from this page and come back to the Data
+ Source list page where you can test the connection once the stack is finished deploying.
+
+
+
+ );
+ }
+
+ renderUpdateSteps() {
+ const account = this.account;
+ const stackInfo = this.stackInfo;
+ const textSize = this.textSize;
+ const { id, mainRegion } = account;
+ const { updateStackUrl, cfnConsoleUrl } = stackInfo;
+
+ return (
+
+
+
+ In a separate browser tab, login to the aws console using the correct account.
+
+ Attention
+
+ Ensure that you are logged in to the aws account # {id} and region {mainRegion}
+
+
+
+
+ Go to the{' '}
+
+ AWS CloudFormation Console
+
+
+
+ You need to visit the AWS CloudFormation console page before you can click on the Update Stack button
+
+
+
+
+ Click on the Update Stack button, this opens a separate browser tab and takes you to the
+ CloudFormation console where you can review the stack information and provision it.
+
+
+
+ Update Stack
+
+ {this.renderExpires(stackInfo)}
+
+
+
+
+
+
+
+ While the stack is being provisioned, it is okay to navigate away from this page and come back to the Data
+ Source list page where you can test the connection once the stack is finished deploying.
+
+
+
+ );
+ }
+
+ renderEmailTemplate(update = false) {
+ const account = this.account;
+ const stackInfo = this.stackInfo;
+ const textSize = this.textSize;
+ const emailTemplate = update ? account.updateStackEmailTemplate : account.createStackEmailTemplate;
+
+ return (
+
+
+ You can use the following email template to send an email to the admin of the account
+
+
+
+
{this.renderExpires(stackInfo)}
+
+
+
+
+
+ );
+ }
+
+ renderExpires(stackInfo) {
+ const { urlExpiry, expired } = stackInfo;
+
+ if (expired) {
+ return (
+
+ Expired
+
+ );
+ }
+
+ return (
+
+ Expires
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(AccountCfnPanel, {
+ account: computed,
+ stackInfo: computed,
+ largeText: computed,
+ textSize: computed,
+ form: observable,
+});
+
+export default inject()(withRouter(observer(AccountCfnPanel)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/AccountConnectionPanel.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/AccountConnectionPanel.js
new file mode 100644
index 0000000000..c7d2e472d0
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/AccountConnectionPanel.js
@@ -0,0 +1,101 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import React from 'react';
+import { decorate, computed, action } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Progress } from 'semantic-ui-react';
+
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+import AccountStatusMessage from './AccountStatusMessage';
+
+// expected props
+// - onCancel (via props) a call back function when the user clicks on Done
+// - account (via props)
+// - operation (via props)
+class AccountConnectionPanel extends React.Component {
+ get account() {
+ return this.props.account;
+ }
+
+ get operation() {
+ return this.props.operation;
+ }
+
+ get progress() {
+ return this.operation.progress;
+ }
+
+ handleCancel = () => {
+ if (!_.isFunction(this.props.onCancel)) return;
+ this.props.onCancel();
+ };
+
+ render() {
+ return (
+
+ {this.renderError()}
+ {this.renderProcessing()}
+ {this.renderMessage()}
+
+ );
+ }
+
+ renderProcessing() {
+ const operation = this.operation;
+ const processing = operation.processing;
+ if (!processing) return null;
+ return (
+
+
+ Checking Connection
+
+
+ );
+ }
+
+ renderError() {
+ const operation = this.operation;
+ const processing = operation.processing;
+ if (processing) return null;
+
+ if (!operation.hasError) return null;
+ return ;
+ }
+
+ renderMessage() {
+ const operation = this.operation;
+ const account = this.account;
+ const processing = operation.processing;
+
+ if (processing) return null;
+ if (operation.hasError) return null;
+
+ return ;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(AccountConnectionPanel, {
+ account: computed,
+ operation: computed,
+ progress: computed,
+ handleCancel: action,
+});
+
+export default inject()(withRouter(observer(AccountConnectionPanel)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/AccountStatusMessage.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/AccountStatusMessage.js
new file mode 100644
index 0000000000..2c94364644
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/AccountStatusMessage.js
@@ -0,0 +1,184 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import React from 'react';
+import { decorate, computed, runInAction, observable, action } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Message } from 'semantic-ui-react';
+
+// expected props
+// - account (via props)
+// - onCancel (via props) a call back function when the user clicks on Done
+class AccountStatusMessage extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.expanded = false;
+ });
+ }
+
+ get account() {
+ return this.props.account;
+ }
+
+ handleExpand = () => {
+ this.expanded = !this.expanded;
+ };
+
+ handleCancel = () => {
+ if (!_.isFunction(this.props.onCancel)) return;
+ this.props.onCancel();
+ };
+
+ render() {
+ return (
+
+ {this.renderAvailable()}
+ {this.renderPending()}
+ {this.renderError()}
+
+ );
+ }
+
+ renderAvailable() {
+ const account = this.account;
+
+ if (!account.reachableState) return null;
+ const expanded = this.expanded;
+ const expandText = expanded ? 'less' : 'more';
+ const msg = account.statusMessageInfo.message;
+ const hasMsg = !_.isEmpty(msg);
+
+ if (!hasMsg) {
+ return (
+
+ Available
+ The account is reachable and available for use.
+
+ );
+ }
+
+ return (
+
+ Available
+
+ The account is reachable but only partially configured. Some studies might be inaccessible.
+
+ {expandText}
+
+
+ {expanded && (
+ <>
+
+ This is usually an indication that the CloudFormation stack that is deployed to AWS account #{account.id}{' '}
+ is out of sync with the CloudFormation template generated by the SWB application.
+
+ {msg}
+ >
+ )}
+
+ );
+ }
+
+ renderPending() {
+ const account = this.account;
+ const expanded = this.expanded;
+ const expandText = expanded ? 'less' : 'more';
+ const msg = account.statusMessageInfo.message;
+
+ if (!account.pendingState) return null;
+
+ return (
+
+ Not available yet
+
+ The account is in the process of being connected with the application. It is unreachable until the
+ CloudFormation stack is successfully deploy.
+
+ {expandText}
+
+
+ {expanded && (
+
+ CloudFormation stack already deployed?
+ {this.renderTips()}
+
+ )}
+ {expanded && !_.isEmpty(msg) && (
+
+
Message received from the server
+
{msg}
+
+ )}
+
+ );
+ }
+
+ renderError() {
+ const account = this.account;
+ const expanded = this.expanded;
+ const expandText = expanded ? 'less' : 'more';
+ const msg = account.statusMessageInfo.message;
+
+ if (!account.errorState) return null;
+
+ return (
+
+ Not available
+
+ The account is unreachable. This is usually an indication of a problem during the CloudFormation stack
+ deployment.
+
+ {expandText}
+
+
+ {expanded && this.renderTips()}
+ {expanded && !_.isEmpty(msg) && (
+
+
Message received from the server
+
{msg}
+
+ )}
+
+ );
+ }
+
+ renderTips() {
+ const account = this.account;
+ return (
+
+
+ Check if the CloudFormation stack is deployed in the correct AWS account #{account.id}
+
+
+ Check if the CloudFormation stack is deployed in the correct AWS region '{account.mainRegion}'
+
+ Try the connection check test again
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(AccountStatusMessage, {
+ account: computed,
+ expanded: observable,
+ handleExpand: action,
+ handleCancel: action,
+});
+
+export default inject()(withRouter(observer(AccountStatusMessage)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/StudyConnectionPanel.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/StudyConnectionPanel.js
new file mode 100644
index 0000000000..c3e93aaedd
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/StudyConnectionPanel.js
@@ -0,0 +1,101 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import React from 'react';
+import { decorate, computed, action } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Progress } from 'semantic-ui-react';
+
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+import StudyStatusMessage from './StudyStatusMessage';
+
+// expected props
+// - onCancel (via props) a call back function when the user clicks on Done
+// - study (via props)
+// - operation (via props)
+class StudyConnectionPanel extends React.Component {
+ get study() {
+ return this.props.study;
+ }
+
+ get operation() {
+ return this.props.operation;
+ }
+
+ get progress() {
+ return this.operation.progress;
+ }
+
+ handleCancel = () => {
+ if (!_.isFunction(this.props.onCancel)) return;
+ this.props.onCancel();
+ };
+
+ render() {
+ return (
+
+ {this.renderError()}
+ {this.renderProcessing()}
+ {this.renderMessage()}
+
+ );
+ }
+
+ renderProcessing() {
+ const operation = this.operation;
+ const processing = operation.processing;
+ if (!processing) return null;
+ return (
+
+
+ Checking Connection
+
+
+ );
+ }
+
+ renderError() {
+ const operation = this.operation;
+ const processing = operation.processing;
+ if (processing) return null;
+
+ if (!operation.hasError) return null;
+ return ;
+ }
+
+ renderMessage() {
+ const operation = this.operation;
+ const study = this.study;
+ const processing = operation.processing;
+
+ if (processing) return null;
+ if (operation.hasError) return null;
+
+ return ;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(StudyConnectionPanel, {
+ study: computed,
+ operation: computed,
+ progress: computed,
+ handleCancel: action,
+});
+
+export default inject()(withRouter(observer(StudyConnectionPanel)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/StudyStatusMessage.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/StudyStatusMessage.js
new file mode 100644
index 0000000000..af92276e04
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/parts/StudyStatusMessage.js
@@ -0,0 +1,152 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import React from 'react';
+import { decorate, computed, runInAction, observable, action } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Message } from 'semantic-ui-react';
+
+// expected props
+// - study (via props)
+// - onCancel (via props) a call back function when the user clicks on Done
+class StudyStatusMessage extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.expanded = false;
+ });
+ }
+
+ get study() {
+ return this.props.study;
+ }
+
+ handleExpand = () => {
+ this.expanded = !this.expanded;
+ };
+
+ handleCancel = () => {
+ if (!_.isFunction(this.props.onCancel)) return;
+ this.props.onCancel();
+ };
+
+ render() {
+ return (
+
+ {this.renderAvailable()}
+ {this.renderPending()}
+ {this.renderError()}
+
+ );
+ }
+
+ renderAvailable() {
+ const study = this.study;
+
+ if (!study.reachableState) return null;
+
+ return (
+
+ Available
+ The study is reachable and available for use.
+
+ );
+ }
+
+ renderPending() {
+ const study = this.study;
+ const expanded = this.expanded;
+ const expandText = expanded ? 'less' : 'more';
+ const msg = study.statusMessageInfo.message;
+
+ if (!study.pendingState) return null;
+
+ return (
+
+ Not available yet
+
+ The study is in the process of being connected with the application. It is unreachable until the
+ CloudFormation stack is successfully deploy.
+
+ {expandText}
+
+
+ {expanded && (
+
+ CloudFormation stack already deployed?
+
+ Check if the CloudFormation stack is deployed in the correct AWS account
+ Check if the CloudFormation stack is deployed in the correct AWS region
+ Try the connection check test again
+
+
+ )}
+ {expanded && !_.isEmpty(msg) && (
+
+
Message received from the server
+
{msg}
+
+ )}
+
+ );
+ }
+
+ renderError() {
+ const study = this.study;
+ const expanded = this.expanded;
+ const expandText = expanded ? 'less' : 'more';
+ const msg = study.statusMessageInfo.message;
+
+ if (!study.errorState) return null;
+
+ return (
+
+ Not available
+
+ The study is unreachable. This is usually an indication of a problem during the CloudFormation stack
+ deployment.
+
+ {expandText}
+
+
+ {expanded && (
+
+ Check if the CloudFormation stack is deployed in the correct AWS account
+ Check if the CloudFormation stack is deployed in the correct AWS region
+ Try the connection check test again
+
+ )}
+ {expanded && !_.isEmpty(msg) && (
+
+
Message received from the server
+
{msg}
+
+ )}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(StudyStatusMessage, {
+ study: computed,
+ expanded: observable,
+ handleExpand: action,
+ handleCancel: action,
+});
+
+export default inject()(withRouter(observer(StudyStatusMessage)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/CfnStep.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/CfnStep.js
new file mode 100644
index 0000000000..ae845c49d2
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/CfnStep.js
@@ -0,0 +1,108 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+import { decorate, computed, action } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Segment, Button, Header, List } from 'semantic-ui-react';
+
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+
+import AccountCfnPanel from '../parts/AccountCfnPanel';
+
+// expected props
+// - wizard (via prop)
+class CfnStep extends React.Component {
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ get wizard() {
+ return this.props.wizard;
+ }
+
+ get account() {
+ return this.wizard.processedAccount;
+ }
+
+ handleNext = () => {
+ const goto = gotoFn(this);
+ this.wizard.reset();
+
+ goto('/data-sources');
+ };
+
+ render() {
+ const account = this.account;
+ return (
+ <>
+
+
+ {this.renderWhatIsNext()}
+
+ {this.renderButtons()}
+
+ >
+ );
+ }
+
+ renderWhatIsNext() {
+ return (
+ <>
+
+
+
+
+ Review the content of the CloudFormation template to familiarize yourself with the roles and policies that
+ will be created in the account.
+
+
+ Once provisioned or updated, the stack creates or updates the necessary roles and policies to allow the
+ Service Workbench application access to the studies and make them available to the designated researchers.
+
+ Follow the steps outlined below
+
+ Once you complete the steps below and while the stack is being provisioned or updated, you can click on{' '}
+ Done . This will take you to the Data Sources list page where you can test the connection once the
+ stack is finished deploying. You will also be able to view this information from within the Data Sources
+ list page.
+
+
+ >
+ );
+ }
+
+ renderButtons() {
+ return (
+
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(CfnStep, {
+ wizard: computed,
+ account: computed,
+ handleNext: action,
+});
+
+export default inject()(withRouter(observer(CfnStep)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/InputStep.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/InputStep.js
new file mode 100644
index 0000000000..31f69cccae
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/InputStep.js
@@ -0,0 +1,412 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import React from 'react';
+import { decorate, computed, runInAction, observable, action } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Button, Segment, Header, Divider, Label } from 'semantic-ui-react';
+
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import Form from '@aws-ee/base-ui/dist/parts/helpers/fields/Form';
+import DropDown from '@aws-ee/base-ui/dist/parts/helpers/fields/DropDown';
+import Input from '@aws-ee/base-ui/dist/parts/helpers/fields/Input';
+import TextArea from '@aws-ee/base-ui/dist/parts/helpers/fields/TextArea';
+import SelectionButtons from '@aws-ee/base-ui/dist/parts/helpers/fields/SelectionButtons';
+import YesNo from '@aws-ee/base-ui/dist/parts/helpers/fields/YesNo';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+
+import { regionOptions } from '../../../models/constants/aws-regions';
+import { encryptionOptions } from '../../../models/constants/bucket';
+import { getRegisterStudyForm } from '../../../models/forms/RegisterStudyForm';
+
+const fieldRuleKey = (container, name) => `${container.key}-${name}`;
+
+// expected props
+// - wizard (via prop)
+// - userStore (via injection)
+// - usersStore (via injection)
+class InputStep extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.form = getRegisterStudyForm();
+ // We keep the field 'rules' in this map to help when we disable and then enable them. For example, if we disable
+ // a required a field, then even if the field has a value, the validation will fail. So, if we disable a required
+ // field, we need to also remove its rules but we need to bring the rules back if we enable the field again
+ this.fieldRulesMap = {};
+ // lets add the existing field rules to the map
+ const [accountField] = this.getFields(['account']);
+ const [bucketField] = this.getFields(['bucket']);
+
+ this.rememberFieldsRules(accountField, ['name', 'mainRegion', 'contactInfo']);
+ this.rememberFieldsRules(bucketField, ['name', 'region', 'sse', 'kmsArn']);
+ });
+ }
+
+ get wizard() {
+ return this.props.wizard;
+ }
+
+ get projectIdOptions() {
+ return this.props.userStore.projectIdDropdown;
+ }
+
+ get userIdOptions() {
+ // We want to filter out certain user types
+ const list = this.props.usersStore.list;
+ const result = [];
+ _.forEach(list, user => {
+ if (!user.isActive) return;
+ if (user.isRootUser) return;
+ if (user.isAdmin || user.isInternalResearcher || user.userRole === 'admin') {
+ result.push({
+ key: user.id,
+ value: user.id,
+ text: user.longDisplayName,
+ });
+ }
+ });
+
+ return result;
+ }
+
+ getFields(names, container) {
+ const form = container || this.form;
+ return _.map(names, name => form.$(name));
+ }
+
+ disableFields(container, names) {
+ _.forEach(names, name => {
+ const field = container.$(name);
+ field.set('rules', null);
+ field.set('disabled', true);
+ field.resetValidation();
+ });
+ }
+
+ enableFields(container, names) {
+ _.forEach(names, name => {
+ const field = container.$(name);
+ field.set('rules', this.fieldRulesMap[fieldRuleKey(container, name)]);
+ field.set('disabled', false);
+ field.resetValidation();
+ });
+ }
+
+ resetFields(container, names) {
+ _.forEach(names, name => {
+ const field = container.$(name);
+ field.reset();
+ });
+ }
+
+ rememberFieldsRules(container, names) {
+ _.forEach(names, name => {
+ const field = container.$(name);
+ this.fieldRulesMap[fieldRuleKey(container, name)] = field.rules || {};
+ });
+ }
+
+ syncKmsArnField() {
+ const [field] = this.getFields(['bucket']);
+ const showKmsArn = field.$('sse').value === 'kms';
+ const kmsArn = field.$('kmsArn').value;
+
+ if (showKmsArn) {
+ field.$('kmsArn').set(kmsArn);
+ this.enableFields(field, ['kmsArn']);
+ } else {
+ this.resetFields(field, ['kmsArn']);
+ this.disableFields(field, ['kmsArn']);
+ }
+ }
+
+ handleSave = async form => {
+ const data = form.values();
+ swallowError(this.wizard.submit(data));
+ };
+
+ handleCancel = () => {
+ const goto = gotoFn(this);
+ this.wizard.reset();
+
+ goto('/data-sources');
+ };
+
+ handleAccountChange = accountId => {
+ const wizard = this.wizard;
+ const account = wizard.getAccount(accountId);
+ const accountExists = !_.isEmpty(account);
+ const [accountField] = this.getFields(['account']);
+ const [bucketField] = this.getFields(['bucket']);
+
+ accountField.$('id').resetValidation();
+
+ this.resetFields(bucketField, ['name', 'region', 'sse']);
+ this.enableFields(bucketField, ['name', 'region', 'sse']);
+ this.syncKmsArnField();
+
+ if (!accountExists) {
+ this.resetFields(accountField, ['name', 'mainRegion', 'contactInfo']);
+ this.enableFields(accountField, ['name', 'mainRegion', 'contactInfo']);
+
+ return;
+ }
+
+ accountField.$('mainRegion').set(account.mainRegion);
+ accountField.$('name').set(account.name);
+ accountField.$('contactInfo').set(account.contactInfo || '');
+ this.disableFields(accountField, ['name', 'mainRegion', 'contactInfo']);
+ };
+
+ handleBucketChange = bucketName => {
+ const wizard = this.wizard;
+ const [accountField] = this.getFields(['account']);
+ const accountId = accountField.$('id').value;
+ const bucket = wizard.getBucket({ accountId, bucketName });
+ const bucketExists = !_.isEmpty(bucket);
+ const [field] = this.getFields(['bucket']);
+
+ field.$('name').resetValidation();
+
+ if (!bucketExists) {
+ this.resetFields(field, ['region', 'sse', 'kmsArn']);
+ this.enableFields(field, ['region', 'sse']);
+ this.syncKmsArnField();
+ return;
+ }
+
+ field.$('region').set(bucket.region);
+ field.$('sse').set(bucket.sse);
+ field.$('kmsArn').set(bucket.kmsArn);
+ this.syncKmsArnField();
+ this.disableFields(field, ['region', 'sse', 'kmsArn']);
+ };
+
+ handleEncryptionChange = () => {
+ this.syncKmsArnField();
+ };
+
+ handleAddStudy = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ const [studies] = this.getFields(['studies']);
+ const newField = studies.add();
+
+ newField.$('category').set('Organization'); // Set the default
+ newField.$('accessType').set('readonly'); // Set the default
+ newField.$('adminUsers').set([]);
+ };
+
+ handleDeleteStudy = key =>
+ action(event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.form.$('studies').del(key);
+ });
+
+ handleStudyTypeChange = studyField =>
+ action(value => {
+ if (value === 'My Studies') {
+ studyField.$('adminUsers').set('');
+ } else {
+ studyField.$('adminUsers').set([]);
+ }
+ });
+
+ render() {
+ return (
+ <>
+
+
+ {this.renderForm()}
+
+ >
+ );
+ }
+
+ renderForm() {
+ const wizard = this.wizard;
+ const form = this.form;
+ const [accountField, bucketField, studies] = this.getFields(['account', 'bucket', 'studies']);
+ const accountOptions = wizard.dropdownAccountOptions;
+ const accountId = accountField.$('id').value;
+ const accountIdValid = accountField.$('id').isValid;
+ const bucketOptions = wizard.getDropdownBucketOptions(accountId);
+ const showKmsArn = bucketField.$('sse').value === 'kms';
+ const studiesSize = studies.size;
+ const addButtonLabel = studiesSize > 0 ? 'Add Another Study' : 'Add Study';
+
+ return (
+
+ {({ processing, /* onSubmit, */ onCancel }) => (
+ <>
+
+
+
+
+
+
+
+
+ Bucket Information
+
+
+
+
+
+
+
+
+ {showKmsArn && }
+
+
+ Studies
+
+
+ {studies.map(field => this.renderStudyField({ field }))}
+
+
+
+
+
+
+ >
+ )}
+
+ );
+ }
+
+ renderStudyField({ field }) {
+ const myStudies = field.$('category').value === 'My Studies';
+
+ return (
+
+
+ X
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(InputStep, {
+ handleCancel: action,
+ handleSave: action,
+ handleAccountChange: action,
+ handleBucketChange: action,
+ handleEncryptionChange: action,
+ handleAddStudy: action,
+ handleDeleteStudy: action,
+ handleStudyTypeChange: action,
+ projectIdOptions: computed,
+ wizard: computed,
+ userIdOptions: computed,
+ form: observable,
+});
+
+export default inject('userStore', 'usersStore')(withRouter(observer(InputStep)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/RegisterStudy.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/RegisterStudy.js
new file mode 100644
index 0000000000..3c8ee2f41e
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/RegisterStudy.js
@@ -0,0 +1,112 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+import { decorate, computed, runInAction } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Container, Header, Icon } from 'semantic-ui-react';
+
+import Stores from '@aws-ee/base-ui/dist/models/Stores';
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import ProgressPlaceHolder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+
+import InputStep from './InputStep';
+import SubmitStep from './SubmitStep';
+import CfnStep from './CfnStep';
+import StartStep from './StartStep';
+
+// expected props
+// - dataSourceAccountsStore (via injection)
+class RegisterStudy extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.stores = new Stores([this.accountsStore, this.usersStore]);
+ });
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ swallowError(this.stores.load());
+ }
+
+ get accountsStore() {
+ return this.props.dataSourceAccountsStore;
+ }
+
+ get usersStore() {
+ return this.props.usersStore;
+ }
+
+ get wizard() {
+ return this.props.registerStudyWizard;
+ }
+
+ render() {
+ const stores = this.stores;
+
+ let content = null;
+ if (stores.hasError) {
+ content = ;
+ } else if (stores.loading) {
+ content = ;
+ } else if (stores.ready) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderTitle()}
+ {content}
+
+ );
+ }
+
+ renderMain() {
+ const wizard = this.wizard;
+ if (wizard.isStartStep) return ;
+ if (wizard.isInputStep) return ;
+ if (wizard.isSubmitStep) return ;
+ if (wizard.isCfnStep) return ;
+
+ return null;
+ }
+
+ renderTitle() {
+ return (
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(RegisterStudy, {
+ usersStore: computed,
+ accountsStore: computed,
+ wizard: computed,
+});
+
+export default inject(
+ 'dataSourceAccountsStore',
+ 'usersStore',
+ 'registerStudyWizard',
+)(withRouter(observer(RegisterStudy)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/StartStep.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/StartStep.js
new file mode 100644
index 0000000000..bdd8cc47d9
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/StartStep.js
@@ -0,0 +1,151 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+import { decorate, action, computed } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Header, Segment, List, Button } from 'semantic-ui-react';
+
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+
+// expected props
+// wizard (via props)
+class StartStep extends React.Component {
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ get wizard() {
+ return this.props.wizard;
+ }
+
+ handleNext = () => {
+ this.wizard.advanceToNextStep();
+ };
+
+ handleCancel = () => {
+ const goto = gotoFn(this);
+ this.wizard.reset();
+
+ goto('/data-sources');
+ };
+
+ render() {
+ return (
+ <>
+
+
+ {this.renderBeforeYouStart()}
+ {this.renderWhatIsNext()}
+ {this.renderLimitations()}
+
+
+
+
+
+
+ >
+ );
+ }
+
+ renderBeforeYouStart() {
+ return (
+ <>
+
+
+ You need to collect some information regarding the studies. The information that you need is:
+
+
+
+ The AWS account id of the account owning the studies and the region where the CloudFormation stack will be
+ deployed
+
+ The bucket name and region containing the studies
+
+ The KMS ARN used to encrypt the bucket (if one is used) or the KMS ARNs used to encrypt each study
+
+ The path of each study to be registered
+ The access level desired for each study, can be either read only or read and write
+
+ >
+ );
+ }
+
+ renderWhatIsNext() {
+ return (
+ <>
+
+
+
+ You will be asked to provide the information listed above
+
+ Some fields might be pre-populated for you if you had previously registered the account and/or the bucket
+
+ You will be asked to assign study admins for each study
+ Once you enter all the information requested, a CloudFormation template is generated
+ You will be able to create/update the stack using the generated CloudFormation template
+
+ >
+ );
+ }
+
+ renderLimitations() {
+ return (
+ <>
+
+
+
+ Studies can not contain other studies
+
+ Buckets that restrict access to specific VPC endpoints and/or specific external IP addresses are not
+ supported
+
+
+ Different studies can be encrypted using different KMS keys, however, objects within the same study must be
+ encrypted with the same key
+
+ Accessing buckets via fips endpoints is not supported
+ Buckets with requester pays are not supported
+
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(StartStep, {
+ wizard: computed,
+ handleCancel: action,
+ handleNext: action,
+});
+
+export default inject()(withRouter(observer(StartStep)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/SubmitStep.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/SubmitStep.js
new file mode 100644
index 0000000000..190bc056b9
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/data-sources/register/SubmitStep.js
@@ -0,0 +1,231 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import _ from 'lodash';
+import React from 'react';
+import { decorate, computed, action } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Segment, Button, Icon, Header, Progress, Message } from 'semantic-ui-react';
+
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+
+// expected props
+// - wizard (via prop)
+class SubmitStep extends React.Component {
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ get operations() {
+ return this.wizard.operations;
+ }
+
+ get running() {
+ return this.operations.running;
+ }
+
+ get success() {
+ return this.operations.success;
+ }
+
+ get failure() {
+ return this.operations.failure;
+ }
+
+ get allFailed() {
+ return this.operations.allFailed;
+ }
+
+ get wizard() {
+ return this.props.wizard;
+ }
+
+ handleCancel = () => {
+ const goto = gotoFn(this);
+ this.wizard.reset();
+
+ goto('/data-sources');
+ };
+
+ handleRetry = () => {
+ swallowError(this.wizard.retry());
+ };
+
+ handleNext = () => {
+ this.wizard.advanceToNextStep();
+ };
+
+ render() {
+ return (
+ <>
+
+
+ {this.renderContent()}
+
+ >
+ );
+ }
+
+ renderContent() {
+ const operations = this.operations;
+ const running = this.running;
+ const success = this.success;
+ const error = this.failure;
+
+ return (
+
+
+ {running && (
+
+ )}
+ {success && (
+
+ Successfully Registered Studies
+
+ )}
+ {error && (
+
+ Error Registering Studies
+
+ )}
+
+
+
+ {_.map(operations.ops, op => this.renderOperation(op))}
+ {this.renderButtons()}
+
+
+ );
+ }
+
+ renderFailedStepsWarning() {
+ return (
+
+ Failures have occurred
+ It seems that one or more steps have failed while registration. Please fix the errors and retry.
+
+ If you wish to proceed anyway with creating/updating the CloudFormation stack, resources corresponding to the
+ failed steps might not be reflected in the CloudFormation template.
+
+
+ );
+ }
+
+ renderOperation(op) {
+ const isError = op.failure;
+ const isSuccess = op.success;
+ const isSkipped = op.skipped;
+ const isRunning = op.running;
+ const color = isError ? 'red' : isSuccess ? 'green' : isRunning ? 'orange' : isSkipped ? 'orange' : 'grey';
+ const message = op.error || op.message;
+ const { name } = op;
+
+ return (
+
+
+
+ );
+ }
+
+ renderButtons() {
+ // Show retry button if allFailed or some failure
+ // Show next button if failure or success
+ // Show cancel button if allFailed
+ const running = this.running;
+ const disabled = this.running;
+ const success = this.success;
+ const failure = this.failure;
+ const allFailed = this.allFailed;
+ const showNext = !allFailed && (failure || success);
+ const showRetry = allFailed || failure;
+ const showCancel = showRetry;
+
+ return (
+
+ {this.failure && this.renderFailedStepsWarning()}
+ {showNext && (
+
+ )}
+
+ {showRetry && (
+
+ )}
+
+ {showCancel && (
+
+ )}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(SubmitStep, {
+ wizard: computed,
+ operations: computed,
+ running: computed,
+ success: computed,
+ failure: computed,
+ allFailed: computed,
+ handleCancel: action,
+ handleRetry: action,
+ handleNext: action,
+});
+
+export default inject()(withRouter(observer(SubmitStep)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudiesTab.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudiesTab.js
index 543ecff649..51de430a46 100644
--- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudiesTab.js
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudiesTab.js
@@ -20,7 +20,13 @@ import { computed, decorate } from 'mobx';
import { observer, inject } from 'mobx-react';
import { Header, Icon, Segment } from 'semantic-ui-react';
import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
-import { isStoreLoading, isStoreError, isStoreReady, isStoreEmpty } from '@aws-ee/base-ui/dist/models/BaseStore';
+import {
+ isStoreLoading,
+ isStoreError,
+ isStoreReady,
+ isStoreEmpty,
+ stopHeartbeat,
+} from '@aws-ee/base-ui/dist/models/BaseStore';
import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
@@ -44,9 +50,7 @@ class StudiesTab extends React.Component {
}
componentWillUnmount() {
- const store = this.studiesStore;
- if (!store) return;
- store.stopHeartbeat();
+ stopHeartbeat(this.studiesStore);
}
get canCreateStudy() {
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyFilesTable.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyFilesTable.js
index cd9619ec8c..63462b94dd 100644
--- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyFilesTable.js
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyFilesTable.js
@@ -19,7 +19,7 @@ import { observer } from 'mobx-react';
import { Table, Segment, Header, Icon } from 'semantic-ui-react';
import { formatBytes, swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
-import { isStoreError, isStoreLoading, isStoreEmpty } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { isStoreError, isStoreLoading, isStoreEmpty, stopHeartbeat } from '@aws-ee/base-ui/dist/models/BaseStore';
import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
@@ -39,7 +39,7 @@ class StudyFilesTable extends React.Component {
}
componentWillUnmount() {
- this.filesStore.stopHeartbeat();
+ stopHeartbeat(this.filesStore);
}
render() {
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyPermissionsTable.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyPermissionsTable.js
index 54729d93c2..176cdbdbc9 100644
--- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyPermissionsTable.js
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyPermissionsTable.js
@@ -21,7 +21,7 @@ import { Button, Dimmer, Dropdown, Loader, Icon, Table } from 'semantic-ui-react
import { displayError, displaySuccess } from '@aws-ee/base-ui/dist/helpers/notification';
import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
-import { isStoreError, isStoreLoading, isStoreNew } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { isStoreError, isStoreLoading, isStoreNew, stopHeartbeat } from '@aws-ee/base-ui/dist/models/BaseStore';
import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
import UserLabels from '@aws-ee/base-ui/dist/parts/helpers/UserLabels';
@@ -42,18 +42,22 @@ class StudyPermissionsTable extends React.Component {
});
}
+ get study() {
+ return this.props.study;
+ }
+
componentDidMount() {
swallowError(this.permissionsStore.load());
this.permissionsStore.startHeartbeat();
}
componentWillUnmount() {
- this.permissionsStore.stopHeartbeat();
+ stopHeartbeat(this.permissionsStore);
}
enableEditMode = () => {
// Set users who currently have permission to the study as the selected users
- this.permissionsStore.studyPermissions.userTypes.forEach(userType => {
+ this.study.userTypes.forEach(userType => {
this.selectedUserIds[userType] = this.permissionsStore.studyPermissions[`${userType}Users`];
});
@@ -103,7 +107,7 @@ class StudyPermissionsTable extends React.Component {
renderTable() {
const studyPermissions = this.permissionsStore.studyPermissions;
- const isEditable = studyPermissions.adminUsers.some(uid => uid === this.currUser.uid);
+ const isEditable = studyPermissions.isStudyAdmin(this.currUser.uid) && this.study.state.canChangePermission;
return (
<>
@@ -125,7 +129,7 @@ class StudyPermissionsTable extends React.Component {
- {this.permissionsStore.studyPermissions.userTypes.map(userType => {
+ {this.study.userTypes.map(userType => {
const uids = studyPermissions[`${userType}Users`];
const userIdentifiers = _.map(uids, uid => ({ uid }));
const users = this.usersStore.asUserObjects(userIdentifiers);
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyRow.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyRow.js
index 2a85b3d7bb..53f4b9b130 100644
--- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyRow.js
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyRow.js
@@ -16,7 +16,7 @@
import React from 'react';
import { decorate, action, computed, runInAction, observable } from 'mobx';
import { inject, observer } from 'mobx-react';
-import { Header, Checkbox, Segment, Accordion, Icon } from 'semantic-ui-react';
+import { Header, Checkbox, Segment, Accordion, Icon, Popup, Label } from 'semantic-ui-react';
import c from 'classnames';
import StudyFilesTable from './StudyFilesTable';
@@ -41,7 +41,7 @@ class StudyRow extends React.Component {
}
get isSelectable() {
- return this.props.isSelectable;
+ return this.props.isSelectable && this.study.state.canSelect;
}
handleFileSelection = study => {
@@ -78,9 +78,10 @@ class StudyRow extends React.Component {
- {isSelectable && }
+ {isSelectable && }
+ {this.renderStatus(study.state)}
{this.renderHeader(study)}
{this.renderDescription(study)}
{this.renderFilesAccordion(study)}
@@ -98,16 +99,16 @@ class StudyRow extends React.Component {
if (isSelectable) onClickAttr.onClick = () => this.handleFileSelection(study);
return (
- <>
- {study.uploadLocationEnabled && study.canUpload &&
}
-