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 ( +
+ + {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 ; + }; + + 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 ( + + ); + } + + 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 ( +
+
+ {({ processing, /* onSubmit, */ onCancel }) => ( + <> + +