diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..c1a403648c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+**/.class
+**/.DS_Store
+**/node_modules
+**/npm-debug.log
+**/pnpm-debug.log
+
+.build
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/*.example.json
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history
+
+# IntelliJ Idea Module Files
+*.iml
+**/.idea
+
+# User-config files
+main/config/settings/*.yml
+main/solution/post-deployment/config/settings/*.yml
+main/solution/prepare-master-acc/config/settings/*.yml
+main/cicd/cicd-pipeline/config/settings/*.yml
+main/cicd/cicd-source/config/settings/*.yml
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000000..6a9bbf8ea4
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,53 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Launch sls offline",
+ "program": "${workspaceFolder}/main/solution/backend/node_modules/serverless/bin/serverless",
+ "cwd": "${workspaceFolder}/main/solution/backend",
+ "args": [
+ "offline",
+ "-s",
+ "${env:USER}",
+ "--noTimeout",
+ "--dontPrintOutput"
+ ],
+ "outFiles": ["${workspaceFolder}/main/solution/backend/.webpack/**/*.js"],
+ "sourceMaps": true,
+ "protocol": "inspector",
+ "runtimeExecutable": "node",
+ "runtimeArgs": ["--lazy"]
+ },
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Launch sls post deployment",
+ "program": "${workspaceFolder}/main/solution/backend/node_modules/serverless/bin/serverless",
+ "cwd": "${workspaceFolder}/main/solution/post-deployment",
+ "args": [
+ "invoke",
+ "local",
+ "-f",
+ "postDeployment",
+ "-s",
+ "${env:USER}",
+ "--noTimeout",
+ "--dontPrintOutput",
+ "--data",
+ "{}"
+ ],
+ "outFiles": [
+ "${workspaceFolder}/main/solution/post-deployment/.webpack/**/*.js"
+ ],
+ "sourceMaps": true,
+ "protocol": "inspector",
+ "runtimeExecutable": "node",
+ "runtimeArgs": ["--lazy"]
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000..0532f4db2a
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,23 @@
+{
+ "cSpell.words": [
+ "Cognito",
+ "Dropdown",
+ "Errored",
+ "Overrider",
+ "autorun",
+ "basedl",
+ "cloudfront",
+ "lifecycle",
+ "mobx",
+ "overridables",
+ "raas",
+ "sagemaker",
+ "skippable",
+ "stackable",
+ "webapp"
+ ],
+ "editor.formatOnSave": true,
+ "eslint.packageManager": "pnpm",
+ "prettier.packageManager": "pnpm",
+ "eslint.workingDirectories": [{ "mode": "auto" }]
+}
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000000..33f3f11bf0
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1 @@
+* @awslabs/aws-go-research
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..5b627cfa60
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,4 @@
+## Code of Conduct
+This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
+For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
+opensource-codeofconduct@amazon.com with any additional questions or comments.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..914e0741d7
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,61 @@
+# Contributing Guidelines
+
+Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
+documentation, we greatly value feedback and contributions from our community.
+
+Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
+information to effectively respond to your bug report or contribution.
+
+
+## Reporting Bugs/Feature Requests
+
+We welcome you to use the GitHub issue tracker to report bugs or suggest features.
+
+When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
+reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
+
+* A reproducible test case or series of steps
+* The version of our code being used
+* Any modifications you've made relevant to the bug
+* Anything unusual about your environment or deployment
+
+
+## Contributing via Pull Requests
+Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
+
+1. You are working against the latest source on the *master* branch.
+2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
+3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
+
+To send us a pull request, please:
+
+1. Fork the repository.
+2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
+3. Ensure local tests pass.
+4. Commit to your fork using clear commit messages.
+5. Send us a pull request, answering any default questions in the pull request interface.
+6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
+
+GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
+[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
+
+
+## Finding contributions to work on
+Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
+
+
+## Code of Conduct
+This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
+For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
+opensource-codeofconduct@amazon.com with any additional questions or comments.
+
+
+## Security issue notifications
+If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
+
+
+## Licensing
+
+See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
+
+We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000..67db858821
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,175 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
diff --git a/LICENSE-LAMBDA b/LICENSE-LAMBDA
new file mode 100644
index 0000000000..6aa0c458f3
--- /dev/null
+++ b/LICENSE-LAMBDA
@@ -0,0 +1,14 @@
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/LICENSE-SUMMARY b/LICENSE-SUMMARY
new file mode 100644
index 0000000000..578dbab990
--- /dev/null
+++ b/LICENSE-SUMMARY
@@ -0,0 +1,2 @@
+This project is licensed under the terms of the Apache 2.0 license. See LICENSE.
+Included AWS Lambda functions are licensed under the MIT-0 license. See LICENSE-LAMBDA.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000000..616fc58894
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1 @@
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..dbd8255987
--- /dev/null
+++ b/README.md
@@ -0,0 +1,121 @@
+# AWS Galileo Gateway
+
+A platform that provides researchers with one-click access to collaborative workspace environments operating across teams, universities, and datasets while enabling university IT stakeholders to manage, monitor, and control spending, apply security best practices, and comply with corporate governance.
+
+Platform provides one-click option to admins for easier creation (vending) of new AWS accounts specific to researchers' teams for easier governance.
+
+For more information about various AWS accounts see [aws-accounts-readme.md](main/documentation/aws-accounts-readme.md).
+
+The solution contains the following components:
+
+- solution/infrastructure/
+- solution/backend/
+- solution/edge-lambda
+- solution/machine-images/
+- solution/prepare-master-acc
+- solution/post-deployment/
+- solution/ui/
+
+The solution also includes a Continuous Integration/Continuous Delivery feature:
+
+- main/cicd/cicd-pipeline
+- main/cicd/cicd-source
+
+---
+
+## Getting Started
+
+Node.js v12.x or later is required.
+
+Before you can build this project, you need to install [pnpm](https://pnpm.js.org/en/). Run the following command:
+
+```bash
+$ npm install -g pnpm
+```
+
+To create the initial settings files, take a look at the example.yaml settings file in main/config/example.yaml and create your own copy.
+The stage is either 'example' or your username. This method should be used only for the very first time you install this solution.
+In the rest of this README, \$STAGE is used to designate the stage.
+
+Now, let's perform an initial deployment:
+
+```bash
+$ scripts/environment-deploy.sh
+```
+
+Following an initial successful deployment, you can subsequently deploy updates to the infrastructure, backend, and post-deployment components as follows:
+
+```bash
+$ cd main/solution/
+$ pnpx sls deploy -s $STAGE
+$ cd -
+```
+
+To run (rerun) the post-deployment steps:
+
+```bash
+$ cd main/solution/post-deployment
+$ pnpx sls invoke -f postDeployment -s $STAGE
+$ cd -
+```
+
+To re-deploy the UI
+
+```bash
+$ cd main/solution/ui
+$ pnpx sls package-ui --stage $STAGE --local=true
+$ pnpx sls package-ui --stage $STAGE
+$ pnpx sls deploy-ui --stage $STAGE --invalidate-cache=true
+$ cd -
+```
+
+To view information about the deployed components (e.g. CloudFront URL, root password), run the
+following, where `[stage]` is the name of the environment (defaults to `$STAGE` if not provided):
+
+```bash
+scripts/get-info.sh [stage]
+```
+
+Once you have deployed the app and the UI, you can start developing locally on your computer.
+You will be running a local server that uses the same lambda functions code. To start local development, run the following commands to run a local server:
+
+```bash
+$ cd main/solution/backend
+$ pnpx sls offline -s $STAGE
+$ cd -
+```
+
+Then, in a separate terminal, run the following commands to start the ui server and open up a browser:
+
+```bash
+$ cd main/solution/ui
+$ pnpx sls start-ui -s $STAGE
+$ cd -
+```
+
+---
+
+## Audits
+
+To audit the installed NPM packages, run the following commands:
+
+```bash
+$ cd
+$ pnpm audit
+```
+
+Please follow prevailing best practices for auditing your NPM dependencies and fixing them as needed.
+
+---
+
+## Recommended Reading
+
+- [Serverless Framework for AWS](https://serverless.com/framework/docs/providers/aws/)
+- [Serverless Stack](https://serverless-stack.com/)
+- [Configure Multiple AWS Profiles](https://serverless-stack.com/chapters/configure-multiple-aws-profiles.html)
+- [Serverless Offline](https://github.com/dherault/serverless-offline)
+
+## License
+
+This project is licensed under the terms of the Apache 2.0 license. See [LICENSE](LICENSE).
+Included AWS Lambda functions are licensed under the MIT-0 license. See [LICENSE-LAMBDA](LICENSE-LAMBDA).
diff --git a/THIRD-PARTY-LICENSES.txt b/THIRD-PARTY-LICENSES.txt
new file mode 100644
index 0000000000..62b007ff14
--- /dev/null
+++ b/THIRD-PARTY-LICENSES.txt
@@ -0,0 +1,130 @@
+** semantic-ui-css; version 2.4.2 -- https://semantic-ui.com/
+Copyright (c) 2015 Semantic Org
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Semantic Org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+------
+
+** semantic-ui; version 2.4.2 -- https://github.com/Semantic-Org/Semantic-UI
+Copyright (c) 2015 Semantic Org
+
+The MIT License
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the 'Software'), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+------
+
+** animate; version 3.7.0 -- http://daneden.me/animate
+Copyright (c) 2018 Daniel Eden
+
+The MIT License (MIT)
+
+Copyright (c) 2018 Daniel Eden
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+------
+
+** basscss; version 8.0.4 -- https://github.com/basscss/basscss
+Copyright (c) 2013 – 2016 Brent Jackson
+
+The MIT License (MIT)
+
+Copyright (c) 2013 – 2016 Brent Jackson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+------
+
+** semantic-ui-react; version 0.88.2 --
+https://github.com/Semantic-Org/Semantic-UI-React
+Copyright (c) 2016 TechnologyAdvice
+
+MIT License
+
+Copyright (c) 2016 TechnologyAdvice
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/.eslintrc.json b/addons/addon-base-post-deployment/packages/base-post-deployment/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/.gitignore b/addons/addon-base-post-deployment/packages/base-post-deployment/.gitignore
new file mode 100644
index 0000000000..f15d856fe2
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/.gitignore
@@ -0,0 +1,22 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# Webpack generated directories
+.webpack
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# IntelliJ Idea Module Files
+*.iml
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/.prettierrc.json b/addons/addon-base-post-deployment/packages/base-post-deployment/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/README.md b/addons/addon-base-post-deployment/packages/base-post-deployment/README.md
new file mode 100644
index 0000000000..644b0e6905
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/README.md
@@ -0,0 +1,13 @@
+## To invoke post deployment locally
+
+After you run
+
+```
+$ pnpx sls deploy -s
+```
+
+You can invoke lambda locally
+
+```
+$ pnpx sls invoke local -f postDeployment -s
+```
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/jest.config.js b/addons/addon-base-post-deployment/packages/base-post-deployment/jest.config.js
new file mode 100644
index 0000000000..3f7ffc8068
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/jest.config.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/jsconfig.json b/addons/addon-base-post-deployment/packages/base-post-deployment/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/deployment-store-service.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/deployment-store-service.js
new file mode 100644
index 0000000000..0652f2e20a
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/deployment-store-service.js
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const createOrUpdateSchema = require('./schema/deployment-item');
+
+const settingKeys = {
+ tableName: 'dbTableDeploymentStore',
+};
+
+class DeploymentStoreService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'dbService']);
+ }
+
+ async init() {
+ await super.init();
+ const [dbService] = await this.service(['dbService']);
+ const table = this.settings.get(settingKeys.tableName);
+
+ this._getter = () => dbService.helper.getter().table(table);
+ this._updater = () => dbService.helper.updater().table(table);
+ this._query = () => dbService.helper.query().table(table);
+ this._deleter = () => dbService.helper.deleter().table(table);
+ }
+
+ async find({ type, id, fields = [] }) {
+ return this._getter()
+ .key({ type, id })
+ .projection(fields)
+ .get();
+ }
+
+ async mustFind({ type, id, fields = [] }) {
+ const result = await this.find({ type, id, fields });
+ if (!result) throw this.boom.notFound(`deployment item of type "${type}" and id "${id}" does not exist`, true);
+ return result;
+ }
+
+ async createOrUpdate(rawData) {
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+
+ // Validate input
+ await validationService.ensureValid(rawData, createOrUpdateSchema);
+
+ const { type, id } = rawData;
+
+ // Time to save the the db object
+ return this._updater()
+ .key({ type, id })
+ .item(rawData)
+ .update();
+ }
+
+ async delete({ type, id }) {
+ // Lets now remove the item from the database
+ const result = await runAndCatch(
+ async () => {
+ return this._deleter()
+ .condition('attribute_exists(type) and attribute_exists(id)') // yes we need this
+ .key({ type, id })
+ .delete();
+ },
+ async () => {
+ throw this.boom.notFound(`deployment item of type "${type}" and id "${id}" does not exist`, true);
+ },
+ );
+
+ return result;
+ }
+}
+
+module.exports = DeploymentStoreService;
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/plugins/services-plugin.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/plugins/services-plugin.js
new file mode 100644
index 0000000000..619b43e630
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/plugins/services-plugin.js
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+
+const AwsService = require('@aws-ee/base-services/lib/aws/aws-service');
+const S3Service = require('@aws-ee/base-services/lib/s3-service');
+const DbService = require('@aws-ee/base-services/lib/db-service');
+const JsonSchemaValidationService = require('@aws-ee/base-services/lib/json-schema-validation-service');
+const InputManifestValidationService = require('@aws-ee/base-services/lib/input-manifest/input-manifest-validation-service');
+const LockService = require('@aws-ee/base-services/lib/lock/lock-service');
+const PluginRegistryService = require('@aws-ee/base-services/lib/plugin-registry/plugin-registry-service');
+const AuditWriterService = require('@aws-ee/base-services/lib/audit/audit-writer-service');
+const AuthorizationService = require('@aws-ee/base-services/lib/authorization/authorization-service');
+const UserAuthzService = require('@aws-ee/base-services/lib/user/user-authz-service');
+const UserService = require('@aws-ee/base-services/lib/user/user-service');
+const DbPasswordService = require('@aws-ee/base-services/lib/db-password/db-password-service');
+const AuthenticationProviderTypeService = require('@aws-ee/base-api-services/lib/authentication-providers/authentication-provider-type-service');
+const AuthenticationProviderConfigService = require('@aws-ee/base-api-services/lib/authentication-providers/authentication-provider-config-service');
+const registerBuiltInAuthProvisioners = require('@aws-ee/base-api-services/lib/authentication-providers/register-built-in-provisioner-services.js');
+
+const DeploymentStoreService = require('../deployment-store-service');
+
+const settingKeys = {
+ tablePrefix: 'dbTablePrefix',
+};
+
+/**
+ * A function that registers base services required by the base addon for post-deployment lambda handler
+ * @param container An instance of ServicesContainer to register services to
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerServices(container, pluginRegistry) {
+ container.register('aws', new AwsService(), { lazy: false });
+ container.register('jsonSchemaValidationService', new JsonSchemaValidationService());
+ container.register('authenticationProviderTypeService', new AuthenticationProviderTypeService());
+ container.register('authenticationProviderConfigService', new AuthenticationProviderConfigService());
+ container.register('lockService', new LockService());
+ container.register('s3Service', new S3Service());
+ container.register('dbService', new DbService(), { lazy: false });
+ container.register('deploymentStoreService', new DeploymentStoreService());
+ container.register('dbPasswordService', new DbPasswordService());
+ container.register('userService', new UserService());
+ container.register('inputManifestValidationService', new InputManifestValidationService());
+ container.register('auditWriterService', new AuditWriterService());
+ container.register('pluginRegistryService', new PluginRegistryService(pluginRegistry), { lazy: false });
+
+ // Authorization Services from base addon
+ container.register('authorizationService', new AuthorizationService());
+ container.register('userAuthzService', new UserAuthzService());
+
+ registerBuiltInAuthProvisioners(container);
+}
+
+/**
+ * A function that registers base static settings required by the base addon for api handler lambda function
+ * @param existingStaticSettings An existing static settings plain javascript object containing settings as key/value contributed by other plugins
+ * @param settings Default instance of settings service that resolves settings from environment variables
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point
+ *
+ * @returns {Promise<*>} A promise that resolves to static settings object
+ */
+// eslint-disable-next-line no-unused-vars
+function getStaticSettings(existingStaticSettings, settings, pluginRegistry) {
+ const staticSettings = {
+ ...existingStaticSettings,
+ };
+
+ // Register all dynamodb table names used by the base rest api addon
+ const tablePrefix = settings.get(settingKeys.tablePrefix);
+ const table = (key, suffix) => {
+ staticSettings[key] = `${tablePrefix}-${suffix}`;
+ };
+ table('dbTableAuthenticationProviderTypes', 'DbAuthenticationProviderTypes');
+ table('dbTableAuthenticationProviderConfigs', 'DbAuthenticationProviderConfigs');
+ table('dbTablePasswords', 'DbPasswords');
+ table('dbTableUserApiKeys', 'DbUserApiKeys');
+ table('dbTableRevokedTokens', 'DbRevokedTokens');
+ table('dbTableUsers', 'DbUsers');
+ table('dbTableLocks', 'DbLocks');
+
+ return staticSettings;
+}
+
+const plugin = {
+ getStaticSettings,
+ // getLoggingContext, // not implemented, the default behavior provided by addon-base is sufficient
+ // getLoggingContext, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerSettingsService, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerLoggerService, // not implemented, the default behavior provided by addon-base is sufficient
+ registerServices,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/plugins/steps-plugin.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/plugins/steps-plugin.js
new file mode 100644
index 0000000000..b0ea14fd50
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/plugins/steps-plugin.js
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+const CreateRootUserService = require('../steps/create-root-user-service');
+const AddAuthProviders = require('../steps/add-auth-providers');
+const CreateJwtKeyService = require('../steps/create-jwt-key-service');
+
+/**
+ * Returns a map of post deployment steps
+ *
+ * @param existingStepsMap Map of existing post deployment steps
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>}
+ */
+// eslint-disable-next-line no-unused-vars
+async function getSteps(existingStepsMap, pluginRegistry) {
+ const stepsMap = new Map([
+ ...existingStepsMap,
+ ['createRootUser', new CreateRootUserService()],
+ ['createJwtKeyService', new CreateJwtKeyService()],
+ ['addAuthProviders', new AddAuthProviders()],
+ ]);
+
+ return stepsMap;
+}
+
+const plugin = {
+ getSteps,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/schema/deployment-item.json b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/schema/deployment-item.json
new file mode 100644
index 0000000000..8ccee66fd6
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/schema/deployment-item.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "required": ["type", "id"]
+}
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps-registration-util.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps-registration-util.js
new file mode 100644
index 0000000000..42cf137707
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps-registration-util.js
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+/**
+ * Utility function to register post-deployment steps by calling each post-deployment step registration plugin in order.
+ *
+ * @param {*} container An instance of ServicesContainer
+ * @param {getPlugins} pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ * Each 'postDeploymentStep' plugin in the returned array is an object containing "getSteps" method.
+ *
+ * @returns {Promise>}
+ */
+async function registerSteps(container, pluginRegistry) {
+ const plugins = await pluginRegistry.getPlugins('postDeploymentStep');
+
+ // 1. Collect steps from all plugins
+ //
+ // Ask each plugin to return their steps. Each plugin is passed a Map containing the post deployment steps collected
+ // so far from other plugins. The plugins are called in the same order as returned by the registry.
+ // Each plugin gets a chance to add, remove, update, or delete steps by mutating the provided stepsMap object.
+ // This stepsMap is a Map that has step service names as keys and an instance of step implementation service containing
+ // "execute" method as value.
+ //
+ const stepsMap = await _.reduce(
+ plugins,
+ async (stepsSoFarPromise, plugin) => plugin.getSteps(await stepsSoFarPromise, pluginRegistry),
+ Promise.resolve(new Map()),
+ );
+
+ // 2. Register all steps to the container
+ stepsMap.forEach((stepService, stepName) => {
+ container.register(stepName, stepService);
+ });
+
+ return stepsMap;
+}
+
+module.exports = { registerSteps };
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/add-auth-providers.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/add-auth-providers.js
new file mode 100644
index 0000000000..7009deb7fc
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/add-auth-providers.js
@@ -0,0 +1,182 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context');
+const authProviderConstants = require('@aws-ee/base-api-services/lib/authentication-providers/constants')
+ .authenticationProviders;
+
+const settingKeys = {
+ awsRegion: 'awsRegion',
+ envName: 'envName',
+ solutionName: 'solutionName',
+ enableNativeUserPoolUsers: 'enableNativeUserPoolUsers',
+ fedIdpIds: 'fedIdpIds',
+ fedIdpNames: 'fedIdpNames',
+ fedIdpDisplayNames: 'fedIdpDisplayNames',
+ fedIdpMetadatas: 'fedIdpMetadatas',
+ defaultAuthNProviderTitle: 'defaultAuthNProviderTitle',
+ cognitoAuthNProviderTitle: 'cognitoAuthNProviderTitle',
+};
+
+class AddAuthProviders extends Service {
+ constructor() {
+ super();
+ this.dependency([
+ 'aws',
+ 'authenticationProviderConfigService',
+ 'authenticationProviderTypeService',
+ 'cognitoUserPoolAuthenticationProvisionerService',
+ 'internalAuthenticationProvisionerService',
+ ]);
+ }
+
+ async addDefaultAuthenticationProviderConfig() {
+ const authenticationProviderTypeService = await this.service('authenticationProviderTypeService');
+ const authenticationProviderTypes = await authenticationProviderTypeService.getAuthenticationProviderTypes(
+ getSystemRequestContext(),
+ );
+
+ const internalAuthProviderTypeConfig = _.find(authenticationProviderTypes, {
+ type: authProviderConstants.internalAuthProviderTypeId,
+ });
+ // Each provider can ask for their specific config at the time of registering the provider
+ // The config below for "internal" provider can be hard-coded.
+ const defaultAuthProviderConfig = {
+ id: authProviderConstants.internalAuthProviderId,
+ title: this.settings.get(settingKeys.defaultAuthNProviderTitle),
+ signInUri: 'api/authentication/id-tokens',
+ // signOutUri: '',
+ };
+
+ const internalAuthenticationProvisionerService = await this.service('internalAuthenticationProvisionerService');
+ await internalAuthenticationProvisionerService.provision({
+ providerTypeConfig: internalAuthProviderTypeConfig,
+ providerConfig: defaultAuthProviderConfig,
+ });
+ }
+
+ /**
+ * Configure Cognito Authentication Provider. The step method below invokes the cognito auth provider "Provisioner" service.
+ * The service will do the followings
+ * 1. Create cognito user pool, if it doesn't exist
+ * 2. Create and configure application client for this solution in the cognito user pool
+ * 3. Configure identity providers in the cognito user pool
+ * 4. Configure cognito user pool domain for the client application
+ */
+ async addCognitoAuthenticationProviderWithSamlFederation() {
+ // Get settings
+ const envName = this.settings.get(settingKeys.envName);
+ const solutionName = this.settings.get(settingKeys.solutionName);
+
+ const enableNativeUserPoolUsers = this.settings.getBoolean(settingKeys.enableNativeUserPoolUsers);
+
+ const fedIdpIds = this.settings.optionalObject(settingKeys.fedIdpIds, []);
+ const fedIdpNames = this.settings.optionalObject(settingKeys.fedIdpNames, []);
+ const fedIdpDisplayNames = this.settings.optionalObject(settingKeys.fedIdpDisplayNames, []);
+ const fedIdpMetadatas = this.settings.optionalObject(settingKeys.fedIdpMetadatas, []);
+
+ // If user pools aren't enabled and no IdPs are configured, skip user pool creation
+ const idpsConfigured = [fedIdpIds, fedIdpNames, fedIdpDisplayNames, fedIdpMetadatas].some(
+ array => array.length === 0,
+ );
+ if (!enableNativeUserPoolUsers && idpsConfigured) {
+ this.log.info('Cognito user pool not enabled in settings; skipping creation');
+ return;
+ }
+
+ // Construct base auth provider config
+ const federatedIdentityProviders = await Promise.all(
+ fedIdpIds.map(async (idpId, idx) => {
+ return {
+ id: idpId,
+ name: fedIdpNames[idx],
+ displayName: fedIdpDisplayNames[idx],
+ metadata: fedIdpMetadatas[idx],
+ };
+ }),
+ );
+
+ const userPoolName = `${envName}-${solutionName}-userPool`;
+ const cognitoAuthProviderConfig = {
+ title: this.settings.get(settingKeys.cognitoAuthNProviderTitle),
+ userPoolName,
+ clientName: `${envName}-${solutionName}-client`,
+ userPoolDomain: `${envName}-${solutionName}`,
+ enableNativeUserPoolUsers,
+ federatedIdentityProviders,
+ };
+
+ // Define auth provider type config
+ const authenticationProviderTypeService = await this.service('authenticationProviderTypeService');
+ const authenticationProviderTypes = await authenticationProviderTypeService.getAuthenticationProviderTypes(
+ getSystemRequestContext(),
+ );
+
+ const cognitoAuthProviderTypeConfig = _.find(authenticationProviderTypes, {
+ type: authProviderConstants.cognitoAuthProviderTypeId,
+ });
+
+ // Check whether user pool already exists
+ const aws = await this.service('aws');
+ const cognitoIdentityServiceProvider = new aws.sdk.CognitoIdentityServiceProvider();
+ // TODO: Handle pagination (hopefully there aren't more than 1000 user pools)
+ const result = await cognitoIdentityServiceProvider.listUserPools({ MaxResults: '60' }).promise();
+ const userPool = _.find(result.UserPools, { Name: userPoolName });
+
+ let authProviderExists = false;
+ if (userPool) {
+ // If pool exists, set its ID in the config so it can be updated
+ cognitoAuthProviderConfig.userPoolId = userPool.Id;
+
+ // Verify that the stored auth provider config also exists
+ const awsRegion = this.settings.get(settingKeys.awsRegion);
+ const authProviderId = `https://cognito-idp.${awsRegion}.amazonaws.com/${userPool.Id}`;
+
+ const authenticationProviderConfigService = await this.service('authenticationProviderConfigService');
+ authProviderExists = !!(await authenticationProviderConfigService.getAuthenticationProviderConfig(
+ authProviderId,
+ ));
+
+ if (authProviderExists) {
+ cognitoAuthProviderConfig.id = authProviderId;
+ }
+ }
+
+ // Create or update user pool
+ const action = authProviderExists
+ ? authProviderConstants.provisioningAction.update
+ : authProviderConstants.provisioningAction.create;
+
+ const cognitoAuthenticationProvisionerService = await this.service(
+ 'cognitoUserPoolAuthenticationProvisionerService',
+ );
+ await cognitoAuthenticationProvisionerService.provision({
+ providerTypeConfig: cognitoAuthProviderTypeConfig,
+ providerConfig: cognitoAuthProviderConfig,
+ action,
+ });
+ }
+
+ async execute() {
+ // Setup both the default (internal) auth provider as well as a Cognito
+ // auth provider (if configured)
+ await this.addDefaultAuthenticationProviderConfig();
+ await this.addCognitoAuthenticationProviderWithSamlFederation();
+ }
+}
+
+module.exports = AddAuthProviders;
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/create-jwt-key-service.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/create-jwt-key-service.js
new file mode 100644
index 0000000000..4eb8202b9d
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/create-jwt-key-service.js
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+const passwordGenerator = require('generate-password');
+
+const settingKeys = {
+ paramStoreJwtSecret: 'paramStoreJwtSecret',
+ solutionName: 'solutionName',
+};
+
+class CreateJwtKeyService extends Service {
+ constructor() {
+ super();
+ this.dependency(['aws']);
+ }
+
+ generatePassword() {
+ return passwordGenerator.generate({
+ length: 12, // 12 characters in password
+ numbers: true, // include numbers in password
+ symbols: true, // include symbols
+ uppercase: true, // include uppercase
+ strict: true, // make sure to include at least one character from each pool
+ });
+ }
+
+ async createJwtSigningKey() {
+ const aws = await this.service('aws');
+ const ssm = new aws.sdk.SSM({ apiVersion: '2014-11-06' });
+ const solutionName = this.settings.get(settingKeys.solutionName);
+ const paramStoreJwtSecretName = this.settings.get(settingKeys.paramStoreJwtSecret);
+
+ let doesKeyExist = false;
+ try {
+ await ssm.getParameter({ Name: paramStoreJwtSecretName, WithDecryption: true }).promise();
+ doesKeyExist = true;
+ } catch (err) {
+ if (err.code !== 'ParameterNotFound') {
+ // Swallow "ParameterNotFound" and let all other errors bubble up
+ throw err;
+ }
+ }
+
+ if (doesKeyExist) {
+ this.log.info(
+ `JWT signing key already exists in parameter store at ${paramStoreJwtSecretName}. Did not reset it.`,
+ );
+ // TODO: Support resetting JWT key
+ } else {
+ // Auto-generate signing key for the jwt tokens
+ const jwtSigningKey = this.generatePassword();
+
+ await ssm
+ .putParameter({
+ Name: paramStoreJwtSecretName,
+ Type: 'SecureString',
+ Value: jwtSigningKey,
+ Description: `JWT signing key for ${solutionName}`,
+ Overwrite: true,
+ })
+ .promise();
+
+ this.log.info(`Created JWT signing key and saved it to parameter store at ${paramStoreJwtSecretName}`);
+ }
+ }
+
+ async execute() {
+ // The following will create new JWT signing key every time it is executed
+ // TODO: Do not re-create JWT keys if they already exists, at the same time support for rotating the key
+ return this.createJwtSigningKey();
+ }
+}
+
+module.exports = CreateJwtKeyService;
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/create-root-user-service.js b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/create-root-user-service.js
new file mode 100644
index 0000000000..8c31047562
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/lib/steps/create-root-user-service.js
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+const passwordGenerator = require('generate-password');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const authProviderConstants = require('@aws-ee/base-api-services/lib/authentication-providers/constants')
+ .authenticationProviders;
+const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context');
+
+const settingKeys = {
+ paramStoreJwtSecret: 'paramStoreJwtSecret',
+ rootUserName: 'rootUserName',
+ rootUserEmail: 'rootUserEmail',
+ rootUserFirstName: 'rootUserFirstName',
+ rootUserLastName: 'rootUserLastName',
+ rootUserPasswordParamName: 'rootUserPasswordParamName',
+ solutionName: 'solutionName',
+};
+
+class CreateRootUserService extends Service {
+ constructor() {
+ super();
+ this.dependency(['userService', 'dbPasswordService', 'aws']);
+ }
+
+ async createRootUser() {
+ const rootUserName = this.settings.get(settingKeys.rootUserName);
+ const rootUserEmail = this.settings.get(settingKeys.rootUserEmail);
+ const rootUserFirstName = this.settings.get(settingKeys.rootUserFirstName);
+ const rootUserLastName = this.settings.get(settingKeys.rootUserLastName);
+ const rootUserPasswordParamName = this.settings.get(settingKeys.rootUserPasswordParamName);
+ const solutionName = this.settings.get(settingKeys.solutionName);
+
+ // Auto-generate password for the root user
+ const rootUserPassword = this.generatePassword();
+
+ const [userService, dbPasswordService, aws] = await this.service(['userService', 'dbPasswordService', 'aws']);
+
+ try {
+ await userService.createUser(getSystemRequestContext(), {
+ username: rootUserName,
+ authenticationProviderId: authProviderConstants.internalAuthProviderId,
+ firstName: rootUserFirstName,
+ lastName: rootUserLastName,
+ email: rootUserEmail,
+ isAdmin: true,
+ userType: 'root',
+ });
+ this.log.info('Created root user in the data lake');
+
+ await dbPasswordService.savePassword(getSystemRequestContext(), {
+ username: rootUserName,
+ password: rootUserPassword,
+ });
+ this.log.info("Created root user's password");
+
+ const ssm = new aws.sdk.SSM({ apiVersion: '2014-11-06' });
+ await ssm
+ .putParameter({
+ Name: rootUserPasswordParamName,
+ Type: 'SecureString',
+ Value: rootUserPassword,
+ Description: `root user password for the ${solutionName}`,
+ Overwrite: true,
+ })
+ .promise();
+ this.log.info(`Created root user with user name = ${rootUserName}`);
+ this.log.info(`Please find the root user's password in parameter store at = ${rootUserPasswordParamName}`);
+ } catch (err) {
+ if (err.code === 'alreadyExists') {
+ // TODO: Allow updating root users information in post-deployment
+ // The root user already exists. Nothing to do.
+ this.log.info(
+ `The root user with user name = ${rootUserName} already exists. Did NOT overwrite that user's information.`,
+ );
+ } else {
+ // In case of any other error let it bubble up
+ throw err;
+ }
+ }
+ }
+
+ generatePassword() {
+ return passwordGenerator.generate({
+ length: 12, // 12 characters in password
+ numbers: true, // include numbers in password
+ symbols: true, // include symbols
+ uppercase: true, // include uppercase
+ strict: true, // make sure to include at least one character from each pool
+ });
+ }
+
+ async execute() {
+ return this.createRootUser();
+ }
+}
+
+module.exports = CreateRootUserService;
diff --git a/addons/addon-base-post-deployment/packages/base-post-deployment/package.json b/addons/addon-base-post-deployment/packages/base-post-deployment/package.json
new file mode 100644
index 0000000000..b8c191bb84
--- /dev/null
+++ b/addons/addon-base-post-deployment/packages/base-post-deployment/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@aws-ee/base-post-deployment",
+ "version": "1.0.0",
+ "private": true,
+ "description": "A library containing base set of post-deployment steps to be run with solutions based on addons",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-api-services": "workspace:*",
+ "@aws-ee/base-services": "workspace:*",
+ "@aws-ee/base-services-container": "workspace:*",
+ "aws-sdk": "^2.647.0",
+ "generate-password": "^1.5.0",
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/.babelrc b/addons/addon-base-raas-ui/packages/base-raas-ui/.babelrc
new file mode 100644
index 0000000000..83064a1e60
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/.babelrc
@@ -0,0 +1,4 @@
+{
+ "plugins": [["@babel/plugin-proposal-class-properties", { "loose": true }]],
+ "presets": ["@babel/preset-react"]
+}
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/.eslintrc.json b/addons/addon-base-raas-ui/packages/base-raas-ui/.eslintrc.json
new file mode 100644
index 0000000000..90ac399dcf
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/.eslintrc.json
@@ -0,0 +1,42 @@
+{
+ "extends": ["airbnb", "prettier", "prettier/react", "plugin:jest/recommended", "eslint-config-prettier"],
+ "parser": "babel-eslint",
+ "plugins": ["jest", "prettier", "react"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "no-console": 0,
+ "no-plusplus": 0,
+ "no-nested-ternary": 0,
+ "import/no-named-as-default": 0,
+ "import/no-named-as-default-member": 0,
+ "react/jsx-props-no-spreading": 0,
+ "react/jsx-one-expression-per-line": 0,
+ "react/jsx-filename-extension": 0,
+ "react/destructuring-assignment": 0,
+ "react/sort-comp": 0,
+ "react/prop-types": 0,
+ "jsx-a11y/no-static-element-interactions": 0,
+ "jsx-a11y/click-events-have-key-events": 0
+ },
+ "env": {
+ "browser": true,
+ "node": true,
+ "es6": true,
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/.gitignore b/addons/addon-base-raas-ui/packages/base-raas-ui/.gitignore
new file mode 100644
index 0000000000..49cc63674e
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/.gitignore
@@ -0,0 +1,45 @@
+# See https://help.github.com/ignore-files/ for more about ignoring files.
+
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+**/npm-debug.log*
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# dependencies
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# transpiled code
+dist
+
+# misc
+.env.local
+.env.development
+.env.development.local
+.env.test.local
+.env.production
+.env.production.local
+
+yarn-debug.log*
+yarn-error.log*
+
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/.prettierrc.json b/addons/addon-base-raas-ui/packages/base-raas-ui/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/images/aws-icons/s3.svg b/addons/addon-base-raas-ui/packages/base-raas-ui/images/aws-icons/s3.svg
new file mode 100644
index 0000000000..8fde615e23
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/images/aws-icons/s3.svg
@@ -0,0 +1 @@
+Amazon-Simple-Storage-Service-S3_Bucket-with-Objects_dark-bg
\ No newline at end of file
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/custom-icon.png b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/custom-icon.png
new file mode 100644
index 0000000000..721b1bb2e2
Binary files /dev/null and b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/custom-icon.png differ
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/ec2-icon.svg b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/ec2-icon.svg
new file mode 100644
index 0000000000..526fe3d883
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/ec2-icon.svg
@@ -0,0 +1 @@
+ec2-icon
\ No newline at end of file
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/ecs-icon.svg b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/ecs-icon.svg
new file mode 100644
index 0000000000..d6901800e2
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/ecs-icon.svg
@@ -0,0 +1 @@
+ecs-icon
\ No newline at end of file
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/emr-icon.svg b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/emr-icon.svg
new file mode 100644
index 0000000000..f870e48465
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/emr-icon.svg
@@ -0,0 +1 @@
+emr-icon
\ No newline at end of file
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/sagemaker-notebook-icon.svg b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/sagemaker-notebook-icon.svg
new file mode 100644
index 0000000000..a9e54a5032
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/images/marketplace/sagemaker-notebook-icon.svg
@@ -0,0 +1 @@
+Amazon-SageMaker_Notebook_light-bg
\ No newline at end of file
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/jest.config.js b/addons/addon-base-raas-ui/packages/base-raas-ui/jest.config.js
new file mode 100644
index 0000000000..60c221fc1d
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/jest.config.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/jsconfig.json b/addons/addon-base-raas-ui/packages/base-raas-ui/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/package.json b/addons/addon-base-raas-ui/packages/base-raas-ui/package.json
new file mode 100644
index 0000000000..332cd3593d
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/package.json
@@ -0,0 +1,98 @@
+{
+ "name": "@aws-ee/base-raas-ui",
+ "version": "0.1.0",
+ "private": true,
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@auth0/auth0-spa-js": "^1.2.3",
+ "@aws-ee/base-ui": "workspace:*",
+ "aws-sdk": "^2.647.0",
+ "chart.js": "^2.9.3",
+ "classnames": "^2.2.6",
+ "crypto-browserify": "^3.12.0",
+ "csvtojson": "^2.0.10",
+ "is-cidr": "^3.1.0",
+ "jwt-decode": "^2.2.0",
+ "lodash": "^4.17.15",
+ "mobx": "^5.15.4",
+ "mobx-react": "^6.1.7",
+ "mobx-react-form": "^2.0.8",
+ "mobx-state-tree": "^3.15.0",
+ "numeral": "^2.0.6",
+ "pretty-bytes": "^5.3.0",
+ "prop-types": "^15.7.2",
+ "react": "^16.12.0",
+ "react-avatar": "^3.9.0",
+ "react-chartjs-2": "^2.9.0",
+ "react-copy-to-clipboard": "^5.0.2",
+ "react-dom": "^16.12.0",
+ "react-dotdotdot": "^1.3.1",
+ "react-dropzone": "^10.1.9",
+ "react-router-dom": "^5.1.2",
+ "react-select": "^3.0.8",
+ "react-sparklines": "^1.7.0",
+ "react-syntax-highlighter": "^11.0.2",
+ "react-table": "^6.11.5",
+ "react-timeago": "^4.4.0",
+ "semantic-ui-react": "^0.88.2",
+ "showdown": "^1.9.1",
+ "toastr": "^2.1.4",
+ "typeface-lato": "0.0.75",
+ "uuid": "^3.4.0",
+ "validatorjs": "^3.18.1",
+ "request": "^2.34"
+ },
+ "devDependencies": {
+ "@babel/cli": "^7.8.4",
+ "@babel/core": "^7.8.6",
+ "@babel/plugin-proposal-class-properties": "^7.8.3",
+ "@babel/plugin-transform-react-jsx": "^7.8.3",
+ "@babel/preset-env": "^7.8.6",
+ "@babel/preset-react": "^7.8.3",
+ "babel-eslint": "^10.0.3",
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.0.1",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-jsx-a11y": "^6.2.3",
+ "eslint-plugin-prettier": "^3.1.2",
+ "eslint-plugin-react": "^7.18.3",
+ "eslint-plugin-react-hooks": "^1.7.0",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "prop-types": "^15.7.2",
+ "serverless": "^1.63.0",
+ "serverless-deployment-bucket": "^1.1.0",
+ "typescript": "^3.7.5",
+ "webpack": "4.41.2"
+ },
+ "scripts": {
+ "babel": "babel src/ --out-dir dist/ --source-maps",
+ "babel:watch": "babel src/ --out-dir dist/ --source-maps --watch",
+ "build": "pnpm run babel",
+ "build:watch": "pnpm run babel:watch",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "prepare": "pnpm run build"
+ },
+ "files": [
+ "README.md",
+ "dist/",
+ "src/"
+ ],
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
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
new file mode 100644
index 0000000000..2f36cd4c62
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/api.js
@@ -0,0 +1,247 @@
+/*
+ * 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 { httpApiGet, httpApiPost, httpApiPut, httpApiDelete } from '@aws-ee/base-ui/dist/helpers/api';
+
+function getUserRoles() {
+ return httpApiGet('api/user-roles');
+}
+
+function getAwsAccounts() {
+ return httpApiGet('api/aws-accounts');
+}
+
+function getAwsAccount(accountId) {
+ return httpApiGet(`api/aws-accounts/${accountId}`);
+}
+
+function addUsers(users) {
+ return httpApiPost('api/users/bulk', { data: users });
+}
+
+function addAwsAccount(awsAccount) {
+ return httpApiPost('api/aws-accounts', { data: awsAccount });
+}
+
+function createAwsAccount(awsAccount) {
+ return httpApiPost('api/aws-accounts/provision', { data: awsAccount });
+}
+
+function addIndex(index) {
+ return httpApiPost('api/indexes', { data: index });
+}
+
+function updateUserApplication(user) {
+ // const params = {};
+ // if (user.authenticationProviderId) {
+ // params.authenticationProviderId = user.authenticationProviderId;
+ // }
+ // if (user.identityProviderName) {
+ // params.identityProviderName = user.identityProviderName;
+ // }
+ // return httpApiPut(`api/users/${user.username}/userself`, { data: user, params });
+ return httpApiPut(`api/user`, { data: user });
+}
+
+async function deleteUser(user) {
+ const data = {};
+ if (user.authenticationProviderId) {
+ data.authenticationProviderId = user.authenticationProviderId;
+ }
+ if (user.identityProviderName) {
+ data.identityProviderName = user.identityProviderName;
+ }
+ return httpApiDelete(`api/users/${user.username}`, { data });
+}
+
+function getStudies(category) {
+ return httpApiGet('api/studies', { params: { category } });
+}
+
+function getStudy(id) {
+ return httpApiGet(`api/studies/${id}`);
+}
+
+function createStudy(body) {
+ return httpApiPost('api/studies', { data: body });
+}
+
+function listStudyFiles(studyId) {
+ return httpApiGet(`api/studies/${studyId}/files`);
+}
+
+function getPresignedStudyUploadRequests(studyId, filenames) {
+ if (Array.isArray(filenames)) {
+ filenames = filenames.join(',');
+ }
+ return httpApiGet(`api/studies/${studyId}/upload-requests`, { params: { filenames } });
+}
+
+function getStudyPermissions(studyId) {
+ return httpApiGet(`api/studies/${studyId}/permissions`);
+}
+
+function updateStudyPermissions(studyId, updateRequest) {
+ return httpApiPut(`api/studies/${studyId}/permissions`, { data: updateRequest });
+}
+
+async function getStepTemplates() {
+ return httpApiGet('api/step-templates');
+}
+
+function getEnvironments() {
+ return httpApiGet('api/workspaces');
+}
+
+function getEnvironmentCost(id, numberDaysInPast, groupByService = true, groupByUser = false) {
+ return httpApiGet(
+ `api/costs?env=${id}&numberOfDaysInPast=${numberDaysInPast}&groupByService=${groupByService}&groupByUser=${groupByUser}`,
+ );
+}
+
+function getAllProjCostGroupByUser(numberDaysInPast) {
+ return httpApiGet(`api/costs?proj=ALL&groupByUser=true&numberOfDaysInPast=${numberDaysInPast}`);
+}
+
+function getAllProjCostGroupByEnv(numberDaysInPast) {
+ return httpApiGet(`api/costs?proj=ALL&groupByEnv=true&numberOfDaysInPast=${numberDaysInPast}`);
+}
+
+function getEnvironment(id) {
+ return httpApiGet(`api/workspaces/${id}`);
+}
+
+function deleteEnvironment(id) {
+ return httpApiDelete(`api/workspaces/${id}`);
+}
+
+function createEnvironment(body) {
+ return httpApiPost('api/workspaces', { data: body });
+}
+
+function updateEnvironment(body) {
+ return httpApiPut('api/workspaces', { data: body });
+}
+
+function getEnvironmentKeypair(id) {
+ return httpApiGet(`api/workspaces/${id}/keypair`);
+}
+
+function getEnvironmentPasswordData(id) {
+ return httpApiGet(`api/workspaces/${id}/password`);
+}
+
+function getEnvironmentNotebookUrl(id) {
+ return httpApiGet(`api/workspaces/${id}/url`);
+}
+
+function getEnvironmentSpotPriceHistory(type) {
+ return httpApiGet(`api/workspaces/pricing/${type}`);
+}
+
+function getExternalTemplate(key) {
+ return httpApiGet(`api/template/${key}`);
+}
+
+function getIndexes() {
+ return httpApiGet('api/indexes');
+}
+
+function getIndex(indexId) {
+ return httpApiGet(`api/indexes/${indexId}`);
+}
+
+function getProjects() {
+ return httpApiGet('api/projects');
+}
+
+function getProject(id) {
+ return httpApiGet(`api/projects/${id}`);
+}
+
+function addProject(project) {
+ return httpApiPost('api/projects', { data: project });
+}
+
+function getAccounts() {
+ return httpApiGet('api/accounts');
+}
+
+function getAccount(id) {
+ return httpApiGet(`api/accounts/${id}`);
+}
+
+function removeAccountInfo(id) {
+ return httpApiDelete(`api/accounts/${id}`);
+}
+
+function getComputePlatforms() {
+ return httpApiGet(`api/compute/platforms`);
+}
+
+function getComputeConfigurations(platformId) {
+ return httpApiGet(`api/compute/platforms/${platformId}/configurations`);
+}
+
+function getClientIpAddress() {
+ return httpApiGet(`api/ip`);
+}
+
+// API Functions Insertion Point (do not change this text, it is being used by hygen cli)
+
+export {
+ addIndex,
+ addUsers,
+ removeAccountInfo,
+ deleteUser,
+ getUserRoles,
+ getAwsAccounts,
+ getAwsAccount,
+ getStudies,
+ getStudy,
+ createStudy,
+ listStudyFiles,
+ getPresignedStudyUploadRequests,
+ getStudyPermissions,
+ updateStudyPermissions,
+ addAwsAccount,
+ createAwsAccount,
+ getStepTemplates,
+ getEnvironments,
+ getEnvironment,
+ getEnvironmentCost,
+ deleteEnvironment,
+ createEnvironment,
+ updateEnvironment,
+ getEnvironmentKeypair,
+ getEnvironmentPasswordData,
+ getEnvironmentNotebookUrl,
+ getEnvironmentSpotPriceHistory,
+ getExternalTemplate,
+ getAllProjCostGroupByUser,
+ getIndexes,
+ getIndex,
+ getAllProjCostGroupByEnv,
+ updateUserApplication,
+ getProjects,
+ getProject,
+ addProject,
+ getAccounts,
+ getAccount,
+ getComputePlatforms,
+ getComputeConfigurations,
+ getClientIpAddress,
+};
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/cfn-service.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/cfn-service.js
new file mode 100644
index 0000000000..15a5d2fa65
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/cfn-service.js
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const aws = require('aws-sdk');
+
+const STACK_FAIL = [
+ 'CREATE_FAILED',
+ 'ROLLBACK_FAILED',
+ 'DELETE_FAILED',
+ 'UPDATE_ROLLBACK_FAILED',
+ 'ROLLBACK_COMPLETE',
+ 'UPDATE_ROLLBACK_COMPLETE',
+];
+const STACK_SUCCESS = ['CREATE_COMPLETE', 'DELETE_COMPLETE', 'UPDATE_COMPLETE'];
+
+export default class CfnService {
+ constructor(accessKeyId, secretAccessKey, region = 'us-east-1') {
+ if (accessKeyId) {
+ this.cfn = new aws.CloudFormation({
+ accessKeyId,
+ secretAccessKey,
+ region,
+ sslEnabled: true,
+ });
+ } else {
+ this.cfn = new aws.CloudFormation({
+ sslEnabled: true,
+ });
+ }
+ }
+
+ isDone(status) {
+ return STACK_FAIL.includes(status) || STACK_SUCCESS.includes(status);
+ }
+
+ static async validateCredentials(accessKeyId, secretAccessKey) {
+ const sts = new aws.STS({
+ accessKeyId,
+ secretAccessKey,
+ sslEnabled: true,
+ });
+
+ return sts.getCallerIdentity().promise();
+ }
+
+ async describeStack(stackName) {
+ const params = { StackName: stackName };
+
+ try {
+ const response = await this.cfn.describeStacks(params).promise();
+ const stack = _.get(response, 'Stacks[0]');
+ const status = _.get(stack, 'StackStatus', 'Unknown');
+ const statusReason = _.get(stack, 'StackStatusReason', 'Unknown');
+ const outputs = _.get(stack, 'Outputs', []);
+ const outputsNormalized = _.map(outputs, item => ({
+ key: item.OutputKey,
+ value: item.OutputValue,
+ description: item.Description,
+ exportName: item.ExportName,
+ }));
+
+ return {
+ status,
+ statusReason,
+ isDone: this.isDone(status),
+ isFailed: STACK_FAIL.includes(status),
+ outputs: outputsNormalized,
+ };
+ } catch (e) {
+ throw new Error(`${e.code}: ${e.message}`);
+ }
+ }
+
+ async createStack(stackName, cfnParams, templateUrl, description = '') {
+ const input = {
+ StackName: stackName,
+ Parameters: cfnParams,
+ Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
+ TemplateURL: templateUrl,
+ Tags: [
+ {
+ Key: 'Description',
+ Value: description,
+ },
+ ],
+ };
+
+ return this.cfn.createStack(input).promise();
+ }
+
+ async deleteStack(stackName) {
+ const input = {
+ StackName: stackName,
+ };
+
+ const response = await this.cfn.deleteStack(input).promise();
+
+ return response;
+ }
+}
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/crypto.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/crypto.js
new file mode 100644
index 0000000000..65e04f18e5
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/crypto.js
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+/**
+ * Encrypts plaintext using AES-GCM with supplied password, for decryption with aesGcmDecrypt().
+ * https://gist.github.com/chrisveness/43bcda93af9f646d083fad678071b90a
+ * MIT lisence
+ *
+ * @param {String} plaintext - Plaintext to be encrypted.
+ * @param {String} password - Password to use to encrypt plaintext.
+ * @returns {String} Encrypted ciphertext.
+ *
+ * @example
+ * const ciphertext = await aesGcmEncrypt('my secret text', 'pw');
+ * aesGcmEncrypt('my secret text', 'pw').then(function(ciphertext) { console.log(ciphertext); });
+ */
+async function aesGcmEncrypt(plaintext, password) {
+ const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8
+ const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password
+
+ const iv = crypto.getRandomValues(new Uint8Array(12)); // get 96-bit random iv
+
+ const alg = { name: 'AES-GCM', iv }; // specify algorithm to use
+
+ const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['encrypt']); // generate key from pw
+
+ const ptUtf8 = new TextEncoder().encode(plaintext); // encode plaintext as UTF-8
+ const ctBuffer = await crypto.subtle.encrypt(alg, key, ptUtf8); // encrypt plaintext using key
+
+ const ctArray = Array.from(new Uint8Array(ctBuffer)); // ciphertext as byte array
+ const ctStr = ctArray.map(byte => String.fromCharCode(byte)).join(''); // ciphertext as string
+ const ctBase64 = btoa(ctStr); // encode ciphertext as base64
+
+ const ivHex = Array.from(iv)
+ .map(b => `00${b.toString(16)}`.slice(-2))
+ .join(''); // iv as hex string
+
+ return ivHex + ctBase64; // return iv+ciphertext
+}
+
+/**
+ * Decrypts ciphertext encrypted with aesGcmEncrypt() using supplied password.
+ * https://gist.github.com/chrisveness/43bcda93af9f646d083fad678071b90a
+ * MIT lisence
+ *
+ * @param {String} ciphertext - Ciphertext to be decrypted.
+ * @param {String} password - Password to use to decrypt ciphertext.
+ * @returns {String} Decrypted plaintext.
+ *
+ * @example
+ * const plaintext = await aesGcmDecrypt(ciphertext, 'pw');
+ * aesGcmDecrypt(ciphertext, 'pw').then(function(plaintext) { console.log(plaintext); });
+ */
+async function aesGcmDecrypt(ciphertext, password) {
+ const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8
+ const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password
+
+ const iv = ciphertext
+ .slice(0, 24)
+ .match(/.{2}/g)
+ .map(byte => parseInt(byte, 16)); // get iv from ciphertext
+
+ const alg = { name: 'AES-GCM', iv: new Uint8Array(iv) }; // specify algorithm to use
+
+ const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']); // use pw to generate key
+
+ const ctStr = atob(ciphertext.slice(24)); // decode base64 ciphertext
+ const ctUint8 = new Uint8Array(ctStr.match(/[\s\S]/g).map(ch => ch.charCodeAt(0))); // ciphertext as Uint8Array
+
+ const plainBuffer = await crypto.subtle.decrypt(alg, key, ctUint8); // decrypt ciphertext using key
+ const plaintext = new TextDecoder().decode(plainBuffer); // decode password from UTF-8
+
+ return plaintext; // return the plaintext
+}
+
+export { aesGcmEncrypt, aesGcmDecrypt };
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
new file mode 100644
index 0000000000..37c17c8abc
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/errors.js
@@ -0,0 +1,65 @@
+/*
+ * 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';
+
+const codes = ['apiError', 'notFound', 'badRequest', 'tokenExpired', 'incorrectImplementation', 'timeout'];
+
+const boom = {
+ error: (friendlyOrErr, code, friendly = '') => {
+ if (_.isString(friendlyOrErr)) {
+ const e = new Error(friendlyOrErr);
+ e.isBoom = true;
+ e.code = code;
+ e.friendly = friendlyOrErr; // the friendly argument is ignored and friendlyOrErr is used instead
+ return e;
+ }
+ if (_.isError(friendlyOrErr)) {
+ friendlyOrErr.code = code; // eslint-disable-line no-param-reassign
+ friendlyOrErr.isBoom = true; // eslint-disable-line no-param-reassign
+ friendlyOrErr.friendly = friendly || _.startCase(code);
+ return friendlyOrErr;
+ }
+
+ // if we are here, it means that the msgOrErr is an object
+ const err = new Error(JSON.stringify(friendlyOrErr));
+ err.isBoom = true;
+ err.code = code;
+ err.friendly = friendly || _.startCase(code);
+
+ return err;
+ },
+};
+
+// inject all the codes array elements as properties for the boom
+// example 'apiError' injected => produces boom.apiError(errOrFriendlyMsg, friendlyMsg)
+// then you can call boom.apiError(err, 'Error fetching user info')
+codes.forEach(code => {
+ boom[code] = (errOrFriendlyMsg, friendlyMsg) => boom.error(errOrFriendlyMsg, code, friendlyMsg);
+});
+
+const isNotFound = error => {
+ return _.get(error, 'code') === 'notFound';
+};
+
+const isTokenExpired = error => {
+ return _.get(error, 'code') === 'tokenExpired';
+};
+
+const isForbidden = error => {
+ return _.get(error, 'code') === 'forbidden';
+};
+
+export { boom, isNotFound, isTokenExpired, isForbidden };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalAccountDetails.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalAccountDetails.js
new file mode 100644
index 0000000000..2222c8d138
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalAccountDetails.js
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+const STS = require('aws-sdk/clients/sts');
+
+export default function getAccountDetails({ accessKeyId, secretAccessKey, region = 'us-east-1' }) {
+ const sts = accessKeyId
+ ? new STS({
+ accessKeyId,
+ secretAccessKey,
+ region,
+ sslEnabled: true,
+ })
+ : new STS({ sslEnabled: true });
+
+ return sts.getCallerIdentity().promise();
+}
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalCostUtil.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalCostUtil.js
new file mode 100644
index 0000000000..fa33d61a1c
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalCostUtil.js
@@ -0,0 +1,56 @@
+/*
+ * 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 { EnvironmentConfigurationsStore } from '../models/environments/EnvironmentConfigurationsStore';
+
+const getEstimatedCost = async (env, numberOfDaysToGetCostInfo) => {
+ const envConfig = EnvironmentConfigurationsStore.create();
+ await envConfig.load();
+ const allEnvConfigs = envConfig.list;
+ const config = _.find(allEnvConfigs, conf => {
+ // Hail EMR has spot pricing and on demand price. o we need to pick the correct EMR env config
+ if (env.instanceInfo.type === 'emr') {
+ if (env.instanceInfo.config.spotBidPrice) {
+ return (
+ conf.type === env.instanceInfo.type && conf.size === env.instanceInfo.size && conf.label.includes('Spot')
+ );
+ }
+ return (
+ conf.type === env.instanceInfo.type && conf.size === env.instanceInfo.size && conf.label.includes('On Demand')
+ );
+ }
+ return conf.type === env.instanceInfo.type && conf.size === env.instanceInfo.size;
+ });
+ const cost = {};
+ cost[config.type] = {
+ amount: config.totalPrice,
+ unit: 'USD',
+ };
+
+ const allCost = [];
+ for (let i = numberOfDaysToGetCostInfo; i > 0; i--) {
+ const day = new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
+ const costForDay = {
+ startDate: day,
+ cost,
+ };
+ allCost.push(costForDay);
+ }
+ return allCost;
+};
+
+// eslint-disable-next-line import/prefer-default-export
+export { getEstimatedCost };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalKeypairService.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalKeypairService.js
new file mode 100644
index 0000000000..8d61620fb4
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalKeypairService.js
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+const EC2 = require('aws-sdk/clients/ec2');
+const SSM = require('aws-sdk/clients/ssm');
+
+const paramStoreRoot = 'raas';
+
+export default class EnvironmentKeypairService {
+ constructor({ accessKeyId, secretAccessKey, region = 'us-east-1' }) {
+ if (accessKeyId) {
+ this.ec2 = new EC2({
+ accessKeyId,
+ secretAccessKey,
+ region,
+ sslEnabled: true,
+ });
+ this.ssm = new SSM({
+ accessKeyId,
+ secretAccessKey,
+ region,
+ sslEnabled: true,
+ });
+ } else {
+ this.ec2 = new EC2({
+ sslEnabled: true,
+ });
+ this.ssm = new SSM({
+ sslEnabled: true,
+ });
+ }
+ }
+
+ async create(id) {
+ const keyPair = await this.ec2.createKeyPair({ KeyName: id }).promise();
+
+ const parameterName = `/${paramStoreRoot}/environments/${id}`;
+ await this.ssm
+ .putParameter({
+ Name: parameterName,
+ Type: 'SecureString',
+ Value: keyPair.KeyMaterial,
+ Description: `ssh key for environment ${id}`,
+ Overwrite: true,
+ })
+ .promise();
+
+ return keyPair.KeyName;
+ }
+
+ async mustFind(id) {
+ const parameterName = `/${paramStoreRoot}/environments/${id}`;
+ const privateKey = await this.ssm
+ .getParameter({
+ Name: parameterName,
+ WithDecryption: true,
+ })
+ .promise();
+
+ return { privateKey: privateKey.Parameter.Value };
+ }
+
+ async delete(id) {
+ const parameterName = `/${paramStoreRoot}/environments/${id}`;
+
+ await this.ec2.deleteKeyPair({ KeyName: id }).promise();
+
+ await this.ssm
+ .deleteParameter({
+ Name: parameterName,
+ })
+ .promise();
+
+ return true;
+ }
+}
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalVpcService.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalVpcService.js
new file mode 100644
index 0000000000..bfc40188e7
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/externalVpcService.js
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+const EC2 = require('aws-sdk/clients/ec2');
+
+export default class SageMakerService {
+ constructor({ accessKeyId, secretAccessKey, region = 'us-east-1' }) {
+ if (accessKeyId) {
+ this.ec2 = new EC2({
+ accessKeyId,
+ secretAccessKey,
+ region,
+ sslEnabled: true,
+ });
+ } else {
+ this.ec2 = new EC2({
+ sslEnabled: true,
+ });
+ }
+ }
+
+ async defaultVPCInfo() {
+ const { Vpcs: vpcs } = await this.ec2
+ .describeVpcs({
+ Filters: [
+ {
+ Name: 'isDefault',
+ Values: ['true'],
+ },
+ ],
+ })
+ .promise();
+ const { VpcId: vpcId } = vpcs.find(({ IsDefault }) => IsDefault);
+
+ const { Subnets: subnets } = await this.ec2
+ .describeSubnets({
+ Filters: [
+ {
+ Name: 'vpc-id',
+ Values: [vpcId],
+ },
+ ],
+ })
+ .promise();
+
+ // Default subnets should be public, but just make sure
+ const publicSubnets = subnets.filter(({ MapPublicIpOnLaunch }) => MapPublicIpOnLaunch);
+ const { SubnetId: subnetId } = publicSubnets.reduce((result, subnet) =>
+ subnet.AvailableIpAddressCount > result.AvailableIpAddressCount ? subnet : result,
+ );
+ return { vpcId, subnetId };
+ }
+}
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/form.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/form.js
new file mode 100644
index 0000000000..125ae28dba
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/form.js
@@ -0,0 +1,53 @@
+/*
+ * 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 dvr from 'mobx-react-form/lib/validators/DVR';
+import validatorjs from 'validatorjs';
+import MobxReactForm from 'mobx-react-form';
+import isCidr from 'is-cidr';
+import * as baseFormHelper from '@aws-ee/base-ui/dist/helpers/form';
+
+const dvrRules = {
+ cidr: {
+ validatorFn: value => {
+ const result = isCidr(value);
+ return result === 4 || result === 6;
+ },
+ message: 'The :attribute is not in the CIDR format.',
+ },
+};
+
+// Extend base formPlugins and add support for "cidr" validation rule
+const formPlugins = {
+ ...baseFormHelper.formPlugins,
+ dvr: dvr({
+ package: validatorjs,
+ extend: ({ validator }) => {
+ Object.keys(dvrRules).forEach(key => validator.register(key, dvrRules[key].validatorFn, dvrRules[key].message));
+ },
+ }),
+};
+
+const formOptions = baseFormHelper.formOptions;
+
+function createForm(fields, pluginsParam, optionsParam) {
+ const plugins = pluginsParam || formPlugins;
+ const options = optionsParam || formOptions;
+ return new MobxReactForm({ fields }, { plugins, options });
+}
+
+const createSingleFieldForm = baseFormHelper.createSingleFieldForm;
+
+export { formPlugins, formOptions, createForm, createSingleFieldForm };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/sage-maker-service.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/sage-maker-service.js
new file mode 100644
index 0000000000..d427d3b739
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/sage-maker-service.js
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+const SageMaker = require('aws-sdk/clients/sagemaker');
+
+export default class SageMakerService {
+ constructor(accessKeyId, secretAccessKey, region = 'us-east-1') {
+ if (accessKeyId) {
+ this.sm = new SageMaker({
+ accessKeyId,
+ secretAccessKey,
+ region,
+ sslEnabled: true,
+ });
+ } else {
+ this.sm = new SageMaker({
+ sslEnabled: true,
+ });
+ }
+ }
+
+ async getPresignedNotebookInstanceUrl(notebookInstanceName) {
+ const params = {
+ NotebookInstanceName: notebookInstanceName,
+ };
+ return this.sm.createPresignedNotebookInstanceUrl(params).promise();
+ }
+}
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/xhr-upload.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/xhr-upload.js
new file mode 100644
index 0000000000..4ddd228836
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/helpers/xhr-upload.js
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+/**
+ * @typedef {Object} UploadHandle
+ * @property {Promise} done a Promise that resolves if the upload completes, or rejects if there is an upload error.
+ * @property {() => void} cancel cancels the upload by calling XMLHttpRequest.abort().
+ * @property {(callback: (uploadedBytes: number) => void) => void} onProgress used to register event listeners for upload progress events
+ */
+
+/**
+ * Uploads an HTML file object or a blob using XMLHttpRequest.
+ *
+ * @params {File|Blob} file
+ * @params {sring} url
+ * @params {Object} fields
+ * @returns {UploadHandle}
+ */
+const upload = (file, url, fields = {}) => {
+ const req = new XMLHttpRequest();
+ const uploadProgressListeners = [];
+ const uploadProgressCallback = uploadedBytes => {
+ uploadProgressListeners.forEach(fn => {
+ fn(uploadedBytes);
+ });
+ };
+
+ const done = new Promise((resolve, reject) => {
+ req.upload.addEventListener('progress', event => {
+ uploadProgressCallback(event.loaded || 0);
+ });
+ req.upload.addEventListener('error', () => {
+ reject(new Error('Network Error'));
+ });
+ req.onreadystatechange = () => {
+ if (req.readyState === 4) {
+ // Request is DONE
+ if (req.status === 0) {
+ // Request status is UNSENT
+ reject(new Error('Cancelled'));
+ } else if (req.status >= 400 && req.status <= 599) {
+ // Request received 4xx or 5xx error
+ reject(new Error(`Error: ${req.statusText}`));
+ } else {
+ resolve();
+ }
+ }
+ };
+ });
+
+ const formData = new FormData();
+ Object.entries(fields).forEach(([name, value]) => formData.append(name, value));
+ formData.append('file', file, file.name);
+
+ req.open('POST', url);
+ req.send(formData);
+
+ return {
+ done,
+ cancel() {
+ req.abort();
+ },
+ onProgress(cb) {
+ uploadProgressListeners.push(cb);
+ },
+ };
+};
+
+export default upload;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/App.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/App.js
new file mode 100644
index 0000000000..c50ed70ba7
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/App.js
@@ -0,0 +1,63 @@
+/*
+ * 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';
+import { getEnv, getType } from 'mobx-state-tree';
+
+function createAppType(appContext) {
+ const ParentApp = getType(appContext.app);
+
+ const AppType = ParentApp.named('RaasApp')
+ .props({
+ userRegistered: false,
+ })
+ .actions(self => {
+ // save the base implementations of the parent app
+ const superInit = self.init;
+ const superCleanup = self.cleanup;
+
+ return {
+ init: async payload => {
+ await superInit(payload);
+ self.runInAction(() => {
+ const userStore = getEnv(self).userStore;
+ if (_.get(userStore, 'user.status') === 'active') {
+ self.setUserRegistered(true);
+ }
+ });
+ },
+
+ setUserRegistered(flag) {
+ self.userRegistered = flag;
+ },
+
+ // this method is called by the Cleaner
+ cleanup() {
+ self.setUserRegistered(false);
+ superCleanup();
+ },
+ };
+ });
+
+ return AppType;
+}
+
+function registerContextItems(appContext) {
+ const App = createAppType(appContext);
+ appContext.app = App.create({}, appContext);
+}
+
+export { registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/Account.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/Account.js
new file mode 100644
index 0000000000..aac0e82c25
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/Account.js
@@ -0,0 +1,58 @@
+/*
+ * 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, applySnapshot } from 'mobx-state-tree';
+
+import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier';
+
+const CfnInfo = types.model({
+ crossAccountExecutionRoleArn: '',
+ subnetId: '',
+ vpcId: '',
+ stackId: '',
+});
+// ==================================================================
+// Account
+// ==================================================================
+const Account = types
+ .model('Account', {
+ id: types.identifier,
+ accountName: '',
+ status: '',
+ accountArn: '',
+ email: '',
+ cfnInfo: types.optional(CfnInfo, {}),
+ rev: types.maybe(types.number),
+ name: '',
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ })
+ .actions(self => ({
+ setAccount(rawAccount) {
+ // Note: if you have partial data vs full data, you need to replace the applySnapshot() with
+ // the appropriate logic
+ applySnapshot(self, rawAccount);
+ },
+ }))
+
+ // eslint-disable-next-line no-unused-vars
+ .views(self => ({
+ // add view methods here
+ }));
+
+// eslint-disable-next-line import/prefer-default-export
+export { Account };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/AccountStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/AccountStore.js
new file mode 100644
index 0000000000..2a34982d2b
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/AccountStore.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 { getAccount } from '../../helpers/api';
+
+// ==================================================================
+// AccountStore
+// ==================================================================
+const AccountStore = BaseStore.named('AccountStore')
+ .props({
+ accountId: '',
+ tickPeriod: 300 * 1000, // 5 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const parent = getParent(self, 2);
+ const rawAccount = await getAccount(self.accountId);
+ parent.addAccount(rawAccount);
+ return undefined;
+ },
+
+ cleanup: () => {
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get account() {
+ const parent = getParent(self, 2);
+ const w = parent.getAccount(self.accountId);
+ return w;
+ },
+ }));
+
+// Note: Do NOT register this in the global context, if you want to gain access to an instance
+// use accountsStore.getAccountStore()
+// eslint-disable-next-line import/prefer-default-export
+export { AccountStore };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/AccountsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/AccountsStore.js
new file mode 100644
index 0000000000..111edd5140
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/accounts/AccountsStore.js
@@ -0,0 +1,136 @@
+/*
+ * 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';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils';
+
+import { getAccounts, removeAccountInfo } from '../../helpers/api';
+import { Account } from './Account';
+import { AccountStore } from './AccountStore';
+
+// ==================================================================
+// AccountsStore
+// ==================================================================
+const AccountsStore = BaseStore.named('AccountsStore')
+ .props({
+ accounts: types.optional(types.map(Account), {}),
+ accountStores: types.optional(types.map(AccountStore), {}),
+ tickPeriod: 5 * 1000, // 10 sec
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const accounts = await getAccounts();
+ // We try to preserve existing accounts data and merge the new data instead
+ // We could have used self.accounts.replace(), but it will do clear() then merge()
+ self.runInAction(() => {
+ consolidateToMap(self.accounts, accounts, (exiting, newItem) => {
+ exiting.setAccount(newItem);
+ });
+ });
+ return undefined;
+ },
+
+ addAccount(rawAccount) {
+ const id = rawAccount.id;
+ const previous = self.accounts.get(id);
+
+ if (!previous) {
+ self.accounts.put(rawAccount);
+ } else {
+ previous.setAccount(rawAccount);
+ }
+ },
+
+ async removeItem(id) {
+ const account = self.accounts.get(id);
+ self.accounts.delete(account);
+ await removeAccountInfo(id);
+ },
+
+ getAccountStore: accountId => {
+ let entry = self.accountStores.get(accountId);
+ if (!entry) {
+ // Lazily create the store
+ self.accountStores.set(accountId, AccountStore.create({ accountId }));
+ entry = self.accountStores.get(accountId);
+ }
+
+ return entry;
+ },
+
+ cleanup: () => {
+ self.accounts.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get listCreatingAccount() {
+ const result = [];
+ self.accounts.forEach(account => {
+ if (account.status === 'PENDING') {
+ result.push(account);
+ }
+ });
+ return _.reverse(_.sortBy(result, ['createdAt', 'name']));
+ },
+
+ get listErrorAccount() {
+ const result = [];
+ self.accounts.forEach(account => {
+ if (account.status === 'FAILED') {
+ result.push(account);
+ }
+ });
+ return _.reverse(_.sortBy(result, ['createdAt', 'name']));
+ },
+
+ get empty() {
+ return self.accounts.size === 0;
+ },
+
+ get total() {
+ return self.accounts.size;
+ },
+
+ get list() {
+ const result = [];
+ self.accounts.forEach(account => result.push(account));
+
+ return _.reverse(_.sortBy(result, ['createdAt', 'name']));
+ },
+
+ hasAccount(id) {
+ return self.accounts.has(id);
+ },
+
+ getAccount(id) {
+ return self.accounts.get(id);
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.accountsStore = AccountsStore.create({}, appContext);
+}
+
+export { AccountsStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/aws-accounts/AwsAccount.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/aws-accounts/AwsAccount.js
new file mode 100644
index 0000000000..1e9cd88b35
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/aws-accounts/AwsAccount.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 { types } from 'mobx-state-tree';
+
+import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier';
+
+// ==================================================================
+// AwsAccounts
+// ==================================================================
+const AwsAccount = types
+ .model('AwsAccounts', {
+ id: types.identifier,
+ rev: types.maybe(types.number),
+ name: '',
+ description: '',
+ accountId: '',
+ externalId: '',
+ roleArn: '',
+ vpcId: '',
+ subnetId: '',
+ encryptionKeyArn: '',
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ })
+ .actions(self => ({
+ setAwsAccounts(rawAwsAccounts) {
+ self.id = rawAwsAccounts.id;
+ self.rev = rawAwsAccounts.rev || self.rev || 0;
+ self.name = rawAwsAccounts.name || self.name || '';
+ self.description = rawAwsAccounts.description || self.description;
+ self.accountId = rawAwsAccounts.accountId || rawAwsAccounts.accountId;
+ self.externalId = rawAwsAccounts.externalId || self.externalId;
+ self.roleArn = rawAwsAccounts.roleArn || self.roleArn;
+ self.vpcId = rawAwsAccounts.vpcId || self.vpcId;
+ self.subnetId = rawAwsAccounts.subnetId || self.subnetId;
+ self.encryptionKeyArn = rawAwsAccounts.encryptionKeyArn || self.encryptionKeyArn;
+ self.createdAt = rawAwsAccounts.createdAt || self.createdAt;
+ self.updatedAt = rawAwsAccounts.updatedAt || self.updatedAt;
+ self.createdBy = rawAwsAccounts.createdBy || self.createdBy;
+ self.updatedBy = rawAwsAccounts.updatedBy || self.updatedBy;
+ // we don't update the other fields because they are being populated by a separate store
+ },
+ }))
+
+ // eslint-disable-next-line no-unused-vars
+ .views(self => ({
+ // add view methods here
+ }));
+
+// eslint-disable-next-line import/prefer-default-export
+export { AwsAccount };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/aws-accounts/AwsAccountsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/aws-accounts/AwsAccountsStore.js
new file mode 100644
index 0000000000..63758a0514
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/aws-accounts/AwsAccountsStore.js
@@ -0,0 +1,121 @@
+/*
+ * 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 { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { getAwsAccounts, addAwsAccount, createAwsAccount } from '../../helpers/api';
+import { AwsAccount } from './AwsAccount';
+
+// ==================================================================
+// AwsAccountsStore
+// ==================================================================
+const AwsAccountsStore = BaseStore.named('AwsAccountsStore')
+ .props({
+ awsAccounts: types.optional(types.map(AwsAccount), {}),
+ tickPeriod: 10 * 1000, // 10 sec
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const awsAccounts = (await getAwsAccounts()) || [];
+ // We try to preserve existing accounts data and merge the new data instead
+ // We could have used self.accounts.replace(), but it will do clear() then merge()
+ self.runInAction(() => {
+ awsAccounts.forEach(awsAccount => {
+ const awsAccountsModel = AwsAccount.create(awsAccount);
+ const previous = self.awsAccounts.get(awsAccountsModel.id);
+ if (!previous) {
+ self.awsAccounts.set(awsAccountsModel.id, awsAccountsModel);
+ } else {
+ previous.setAwsAccounts(awsAccount);
+ }
+ });
+ });
+ return undefined;
+ },
+
+ cleanup: () => {
+ superCleanup();
+ },
+
+ addAwsAccount: async awsAccount => {
+ const addedAwsAccount = await addAwsAccount(awsAccount);
+ self.runInAction(() => {
+ const addedAwsAccountModel = AwsAccount.create(addedAwsAccount);
+ self.awsAccounts.set(addedAwsAccountModel.id, addedAwsAccountModel);
+ });
+ },
+
+ createAwsAccount: async awsAccount => {
+ await createAwsAccount(awsAccount);
+ },
+ };
+ })
+
+ .views(self => ({
+ get list() {
+ const result = [];
+ // converting map self.users to result array
+ self.awsAccounts.forEach(awsAccount => {
+ const res = {};
+ res.name = awsAccount.name;
+ res.accountId = awsAccount.accountId;
+ res.roleArn = awsAccount.roleArn;
+ res.description = awsAccount.description;
+ res.externalId = awsAccount.externalId;
+ res.vpcId = awsAccount.vpcId;
+ res.subnetId = awsAccount.subnetId;
+ res.encryptionKeyArn = awsAccount.encryptionKeyArn;
+ result.push(res);
+ });
+ return result;
+ },
+
+ get dropdownOptions() {
+ const result = [];
+ // converting map self.users to result array
+ self.awsAccounts.forEach(awsAccount => {
+ const account = {};
+ account.key = awsAccount.id;
+ account.value = awsAccount.id;
+ // For migration purposes fallback to id if there's no name
+ account.text = `${awsAccount.description} (${awsAccount.name || awsAccount.id})`;
+ result.push(account);
+ });
+ return result;
+ },
+
+ getNameForAccountId(id) {
+ const account = self.awsAccounts.get(id);
+
+ // For migration purposes fallback to id if there's no name
+ if (!account || !account.name) {
+ return id;
+ }
+
+ return `${account.name} (${account.accountId})`;
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.awsAccountsStore = AwsAccountsStore.create({}, appContext);
+}
+
+export { AwsAccountsStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/client-info/ClientInformation.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/client-info/ClientInformation.js
new file mode 100644
index 0000000000..8eb5385808
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/client-info/ClientInformation.js
@@ -0,0 +1,23 @@
+/*
+ * 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';
+
+const ClientInformation = types.model('ClientInformation', {
+ ipAddress: '',
+});
+
+// eslint-disable-next-line import/prefer-default-export
+export { ClientInformation };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/client-info/ClientInformationStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/client-info/ClientInformationStore.js
new file mode 100644
index 0000000000..ceae3e1a9e
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/client-info/ClientInformationStore.js
@@ -0,0 +1,63 @@
+/*
+ * 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 { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { getClientIpAddress } from '../../helpers/api';
+import { ClientInformation } from './ClientInformation';
+
+// There are situations in which it is useful for the UI to be able to
+// determine the IP address that it has, for example so that it can use that
+// IP address in a Security Group rule that later restricts access for a given
+// compute environment to the user that launched it. So, this store implements
+// a "what is my IP address?" feature.
+const ClientInformationStore = BaseStore.named('ClientInformationStore')
+ .props({
+ clientInformation: types.optional(ClientInformation, {}),
+ })
+ .actions(self => {
+ return {
+ async doLoad() {
+ const info = await getClientIpAddress();
+ const ipAddress = info.ipAddress;
+ if (ipAddress === '127.0.0.1') {
+ // Only for "local" development that we call http://httpbin.org/get
+ // otherwise for any other development modes including for production,
+ // we call our own api to get the ip address.
+ const answer = await fetch('http://httpbin.org/get').then(res => res.json());
+ self.runInAction(() => {
+ self.clientInformation = ClientInformation.create({ ipAddress: answer && answer.origin });
+ });
+ return;
+ }
+
+ self.runInAction(() => {
+ self.clientInformation = ClientInformation.create({ ipAddress });
+ });
+ },
+ };
+ })
+ .views(self => ({
+ get ipAddress() {
+ return self.clientInformation.ipAddress;
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.clientInformationStore = ClientInformationStore.create({}, appContext);
+}
+
+export { ClientInformationStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/component-states/component-session-state.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/component-states/component-session-state.js
new file mode 100644
index 0000000000..9f423788a7
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/component-states/component-session-state.js
@@ -0,0 +1,36 @@
+/*
+ * 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 { sessionStore } from '@aws-ee/base-ui/dist/models/SessionStore';
+
+/**
+ * A function that returns component's state (as MST model).
+ * The function creates the component's state MST model if it doesn't exist in the SessionStore.
+ *
+ * @param uiStateModel The MST model containing the component's UI state
+ * @param id The identifier string for the model
+ * @param componentStateCreatorFn The function to create the component's state MST model if it doesn't exist in the SessionStore.
+ * The default "componentStateCreatorFn" just uses the "create()" method of the given model to create initial state.
+ *
+ * @returns {*}
+ */
+function getComponentSessionState(uiStateModel, id, componentStateCreatorFn = model => model.create()) {
+ const stateId = `${uiStateModel.name}-${id}`;
+ const entry = sessionStore.get(stateId) || componentStateCreatorFn(uiStateModel);
+ sessionStore.set(stateId, entry);
+ return entry;
+}
+
+export default getComponentSessionState;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputeConfiguration.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputeConfiguration.js
new file mode 100644
index 0000000000..3c8e7ff520
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputeConfiguration.js
@@ -0,0 +1,79 @@
+/*
+ * 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';
+import { types, getEnv, applySnapshot } from 'mobx-state-tree';
+
+// This represents a specific configuration of a compute platform, such as a specific size of an ec2 setup
+const ComputeConfiguration = types
+ .model('ComputeConfiguration', {
+ id: types.identifier,
+ type: '',
+ title: '',
+ displayOrder: types.maybe(types.number),
+ priceInfo: types.frozen(),
+ desc: '',
+ displayProps: types.frozen(), // an array of objects, each object has a key and a value that are purely used for displaying purposes
+ params: types.frozen(),
+ })
+ .actions(self => ({
+ setComputeConfiguration(raw) {
+ // Note: if you have partial data vs full data, you need to replace the applySnapshot() with
+ // the appropriate logic
+ applySnapshot(self, raw);
+ },
+ }))
+ .views(self => ({
+ get descHtml() {
+ const showdown = getEnv(self).showdown;
+ return showdown.convert(self.desc);
+ },
+
+ // Returns true if the configuration supports changing the value of a given param
+ isMutable(param) {
+ return _.has(self.params, ['mutable', param]);
+ },
+
+ // Returns all mutable parameters that this configuration allow
+ get mutableParams() {
+ return _.get(self.params, 'mutable', {});
+ },
+
+ // If undefined is returned, it means that changing the cidr value is not supported
+ get defaultCidr() {
+ if (!self.isMutable('cidr')) return undefined;
+ return _.get(self.mutableParams, 'cidr', '');
+ },
+
+ get pricePerDay() {
+ const info = self.priceInfo || {};
+ if (info.timeUnit === 'hour') return info.value * 24;
+ if (info.timeUnit === 'day') return info.value;
+
+ return undefined;
+ },
+
+ // Use this method to get a value of a parameter, regardless whether it is immutable or not
+ // We first see if the parameter exists in the immutable params if so, it is returned,
+ // otherwise the one in the mutable params is returned if any
+ getParam(name) {
+ const value = _.get(self.params, ['immutable', name]);
+ if (!_.isUndefined(value)) return value;
+ return _.get(self.mutableParams, name);
+ },
+ }));
+
+export { ComputeConfiguration };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatform.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatform.js
new file mode 100644
index 0000000000..3410501598
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatform.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.
+ */
+
+/* eslint-disable import/prefer-default-export */
+import _ from 'lodash';
+import { values } from 'mobx';
+import { types, getEnv, applySnapshot } from 'mobx-state-tree';
+import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils';
+
+import { ComputeConfiguration } from './ComputeConfiguration';
+
+// This represents a compute platform information such as an emr or an ec2
+const ComputePlatform = types
+ .model('ComputePlatform', {
+ id: types.identifier,
+ type: '',
+ title: '',
+ desc: '',
+ displayOrder: types.maybe(types.number),
+ configurations: types.map(ComputeConfiguration),
+ })
+ .actions(self => ({
+ setComputePlatform(rawComputePlatform) {
+ // Note: if you have partial data vs full data, you need to replace the applySnapshot() with
+ // the appropriate logic
+
+ // Preserve configurations
+ const configurations = self.configurations || {};
+ applySnapshot(self, rawComputePlatform);
+ self.configurations = configurations;
+ },
+
+ setConfigurations(raw) {
+ consolidateToMap(self.configurations, raw, (exiting, newItem) => {
+ exiting.setComputeConfiguration(newItem);
+ });
+ },
+ }))
+ .views(self => ({
+ get descHtml() {
+ const showdown = getEnv(self).showdown;
+ return showdown.convert(self.desc);
+ },
+
+ get configurationsList() {
+ return _.sortBy(values(self.configurations), 'displayOrder');
+ },
+
+ getConfiguration(configurationId) {
+ return self.configurations.get(configurationId);
+ },
+ }));
+
+export { ComputePlatform };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatformStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatformStore.js
new file mode 100644
index 0000000000..c78842f288
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatformStore.js
@@ -0,0 +1,60 @@
+/*
+ * 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 { getParent } from 'mobx-state-tree';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { getComputeConfigurations } from '../../helpers/api';
+
+// ==================================================================
+// ComputePlatformStore
+// ==================================================================
+const ComputePlatformStore = BaseStore.named('ComputePlatformStore')
+ .props({
+ platformId: '',
+ tickPeriod: 300 * 1000, // 5 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const configurations = await getComputeConfigurations(self.platformId);
+ const platform = self.computePlatform;
+ if (!platform) return;
+ platform.setConfigurations(configurations);
+ },
+
+ cleanup: () => {
+ self.platformId = '';
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get computePlatform() {
+ const parent = getParent(self, 2);
+ const platform = parent.getComputePlatform(self.platformId);
+ return platform;
+ },
+ }));
+
+// Note: Do NOT register this in the app context, if you want to gain access to an instance
+// use computePlatformsStore.getComputePlatformStore()
+export { ComputePlatformStore };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatformsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatformsStore.js
new file mode 100644
index 0000000000..cfa81cce07
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/compute/ComputePlatformsStore.js
@@ -0,0 +1,88 @@
+/*
+ * 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 { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils';
+
+import { getComputePlatforms } from '../../helpers/api';
+import { ComputePlatform } from './ComputePlatform';
+import { ComputePlatformStore } from './ComputePlatformStore';
+
+// ==================================================================
+// ComputePlatformsStore
+// ==================================================================
+const ComputePlatformsStore = BaseStore.named('ComputePlatformsStore')
+ .props({
+ platforms: types.map(ComputePlatform),
+ platformsStores: types.map(ComputePlatformStore),
+ })
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const computePlatforms = await getComputePlatforms();
+ self.runInAction(() => {
+ consolidateToMap(self.platforms, computePlatforms, (exiting, newItem) => {
+ exiting.setComputePlatform(newItem);
+ });
+ });
+ },
+
+ getComputePlatformStore(platformId) {
+ let entry = self.platformsStores.get(platformId);
+ if (!entry) {
+ // Lazily create the store
+ self.platformsStores.set(platformId, ComputePlatformStore.create({ platformId }));
+ entry = self.platformsStores.get(platformId);
+ }
+
+ return entry;
+ },
+
+ cleanup() {
+ self.platforms.clear();
+ self.platformsStores.clear();
+ superCleanup();
+ },
+ };
+ })
+ .views(self => ({
+ get empty() {
+ return self.platforms.size === 0;
+ },
+
+ get total() {
+ return self.platforms.size;
+ },
+
+ get list() {
+ return _.sortBy(values(self.platforms), 'displayOrder');
+ },
+
+ getComputePlatform(id) {
+ return self.platforms.get(id);
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.computePlatformsStore = ComputePlatformsStore.create({}, appContext);
+}
+
+export { ComputePlatformsStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/constants/local-storage-keys.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/constants/local-storage-keys.js
new file mode 100644
index 0000000000..7e187ae0f8
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/constants/local-storage-keys.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+const localStorageKeys = {
+ // Name of the id token for Data Lake APIs in local storage
+ appIdToken: 'appIdToken',
+ pinToken: 'pin',
+};
+
+export default localStorageKeys;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/Environment.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/Environment.js
new file mode 100644
index 0000000000..8198876c00
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/Environment.js
@@ -0,0 +1,134 @@
+/*
+ * 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, applySnapshot } from 'mobx-state-tree';
+import { uiEventBus } from '@aws-ee/base-ui/dist/models/SessionStore';
+import { storage } from '@aws-ee/base-ui/dist/helpers/utils';
+import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier';
+
+import { InstanceInfo } from './InstanceInfo';
+import { getEnvironmentKeypair, getEnvironmentNotebookUrl, getEnvironmentPasswordData } from '../../helpers/api';
+import SageMakerService from '../../helpers/sage-maker-service';
+import localStorageKeys from '../constants/local-storage-keys';
+
+// ==================================================================
+// Environment
+// ==================================================================
+const serviceCost = types.model({
+ amount: types.number,
+ unit: types.string,
+});
+
+const environmentCost = types.model({
+ startDate: types.string,
+ cost: types.map(serviceCost),
+});
+
+const Environment = types
+ .model('Environment', {
+ id: types.identifier,
+ rev: types.maybe(types.number),
+ description: '',
+ instanceInfo: types.optional(InstanceInfo, {}),
+ name: '',
+ status: '',
+ indexId: '',
+ projectId: '',
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ costs: types.optional(types.array(environmentCost), []),
+ fetchingUrl: types.optional(types.boolean, false),
+ error: types.maybeNull(types.string),
+ isExternal: types.optional(types.boolean, false),
+ stackId: types.maybeNull(types.string),
+ sharedWithUsers: types.array(UserIdentifier, []),
+ })
+ .actions(self => ({
+ setEnvironment(rawEnvironment) {
+ // Note: if you have partial data vs full data, you need to replace the applySnapshot() with
+ // the appropriate logic
+
+ // Preserve the value of the fetchingUrl
+ const fetchingUrl = self.fetchingUrl;
+ applySnapshot(self, rawEnvironment);
+ self.fetchingUrl = fetchingUrl;
+ },
+
+ async getEnvironmentNotebookUrl(user) {
+ if (self.isExternal) {
+ if (!_.isEmpty(storage.getItem(localStorageKeys.pinToken))) {
+ const creds = await user.unencryptedCreds(storage.getItem(localStorageKeys.pinToken));
+ const sm = new SageMakerService(creds.accessKeyId, creds.secretAccessKey, creds.region);
+ return sm.getPresignedNotebookInstanceUrl(self.instanceInfo.NotebookInstanceName);
+ }
+ throw new Error('No PIN to decrypt User credientials');
+ } else {
+ self.setFetchingUrl(true);
+ return getEnvironmentNotebookUrl(self.id);
+ }
+ },
+
+ setFetchingUrl(value) {
+ self.fetchingUrl = value;
+ },
+
+ markAsTerminating() {
+ self.status = 'TERMINATING';
+ },
+
+ async getKeyPair() {
+ return getEnvironmentKeypair(self.id, `${self.id}.pem`);
+ },
+
+ async getWindowsPassword() {
+ return Promise.all([self.getKeyPair(), getEnvironmentPasswordData(self.id)]);
+ },
+ }))
+
+ // eslint-disable-next-line no-unused-vars
+ .views(self => ({
+ // add view methods here
+ get isTerminated() {
+ return _.includes(['TERMINATING', 'TERMINATED', 'TERMINATING_FAILED'], this.status);
+ },
+
+ get isCompleted() {
+ return _.includes(['COMPLETED'], this.status);
+ },
+
+ get isPending() {
+ return _.includes(['PENDING'], this.status);
+ },
+
+ get isError() {
+ return _.includes(['FAILED'], this.status);
+ },
+ }));
+
+// eslint-disable-next-line no-unused-vars
+function registerContextItems(appContext) {
+ uiEventBus.listenTo('environmentDeleted', {
+ id: 'Environment',
+ listener: async event => {
+ // event will be the environment object
+ event.markAsTerminating();
+ },
+ });
+}
+
+export { Environment, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentConfiguration.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentConfiguration.js
new file mode 100644
index 0000000000..71a95775b9
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentConfiguration.js
@@ -0,0 +1,109 @@
+/*
+ * 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 { applySnapshot, types } from 'mobx-state-tree';
+import { getEnvironmentSpotPriceHistory } from '../../helpers/api';
+
+const SpotPriceHistoryItem = types.model('SpotPriceHistoryItem', {
+ availabilityZone: '',
+ spotPrice: types.number,
+});
+
+const EnvironmentConfiguration = types
+ .model('EnvironmentConfiguration', {
+ id: types.identifier,
+ type: '',
+ size: '',
+ label: '',
+ price: types.number,
+ description: '',
+ defaultCidr: '',
+ properties: types.frozen(),
+ spotBidMultiplier: types.optional(types.number, 0),
+ spotPriceHistory: types.optional(types.array(SpotPriceHistoryItem), []),
+ emrConfiguration: types.frozen(),
+ })
+ .actions(self => ({
+ setEnvironmentConfiguration(configuration) {
+ // Note: if you have partial data vs full data, you need to replace the applySnapshot() with
+ // the appropriate logic
+ const fetchingSpotPriceHistory = self.fetchingSpotPriceHistory;
+ applySnapshot(self, configuration);
+ self.fetchingSpotPriceHistory = fetchingSpotPriceHistory;
+ },
+
+ async getSpotPriceHistory() {
+ const spotInstance = self.isEmrCluster ? self.emrConfiguration.workerInstanceSize : self.size;
+ const prices = await getEnvironmentSpotPriceHistory(spotInstance);
+
+ self.setSpotPriceHistory(prices);
+ },
+
+ setSpotPriceHistory(prices) {
+ self.spotPriceHistory = prices;
+ },
+ }))
+ .views(self => ({
+ get isOnDemandPricing() {
+ return !self.spotBidMultiplier;
+ },
+
+ get isEmrCluster() {
+ return !!self.emrConfiguration;
+ },
+
+ get hasSpotPriceHistory() {
+ return self.spotPriceHistory.length > 0;
+ },
+
+ get averageSpotPriceHistory() {
+ if (self.hasSpotPriceHistory) {
+ return self.spotPriceHistory.reduce(
+ (result, { spotPrice }) => result + spotPrice / self.spotPriceHistory.length,
+ 0,
+ );
+ }
+ self.getSpotPriceHistory();
+ return 0;
+ },
+
+ get spotBidPrice() {
+ return self.averageSpotPriceHistory * self.spotBidMultiplier;
+ },
+
+ get isLoadingPrice() {
+ return this.isOnDemandPricing ? false : self.averageSpotPriceHistory === 0;
+ },
+
+ get totalPrice() {
+ if (self.isOnDemandPricing && !self.isEmrCluster) {
+ return self.price * 24;
+ }
+ if (self.isOnDemandPricing && self.isEmrCluster) {
+ const { workerInstanceOnDemandPrice, workerInstanceCount } = self.emrConfiguration;
+ return (self.price + workerInstanceOnDemandPrice * workerInstanceCount) * 24;
+ }
+ // this is now a spot bid below the onDemand cost
+ if (self.isEmrCluster) {
+ const { workerInstanceCount } = self.emrConfiguration;
+ return (self.price + self.spotBidPrice * workerInstanceCount) * 24;
+ }
+ // last option is spot single node
+ return self.spotBidPrice * 24;
+ },
+ }));
+
+// eslint-disable-next-line import/prefer-default-export
+export { EnvironmentConfiguration };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentConfigurationsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentConfigurationsStore.js
new file mode 100644
index 0000000000..28ff0aca57
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentConfigurationsStore.js
@@ -0,0 +1,416 @@
+/*
+ * 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';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils';
+
+import { EnvironmentConfiguration } from './EnvironmentConfiguration';
+
+const EnvironmentConfigurationsStore = BaseStore.named('EnvironmentConfigurationsStore')
+ .props({
+ configurations: types.map(EnvironmentConfiguration),
+ heartbeatInterval: -1,
+ })
+ .actions(self => {
+ return {
+ async doLoad() {
+ const environmentConfigurations = await getEnvironmentConfigurations();
+
+ self.runInAction(() => {
+ consolidateToMap(self.configurations, environmentConfigurations, (exiting, newItem) => {
+ exiting.setEnvironmentConfiguration(newItem);
+ });
+ });
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.configurations.size === 0;
+ },
+
+ get total() {
+ return self.configurations.size;
+ },
+
+ get list() {
+ const result = [];
+ self.configurations.forEach(configuration => result.push(configuration));
+
+ return _.sortBy(result, ['id']);
+ },
+
+ getConfiguration(id) {
+ return self.configurations.get(id);
+ },
+ }));
+
+async function getEnvironmentConfigurations() {
+ let idCounter = 1;
+ return [
+ {
+ type: 'sagemaker',
+ // size: 'ml.t3.xlarge',
+ size: 'ml.t2.medium',
+ label: 'Small',
+ price: 0.0464,
+ defaultCidr: '0.0.0.0/0',
+ description:
+ 'A small research workspace meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '4',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '16',
+ },
+ ],
+ },
+ {
+ type: 'sagemaker',
+ size: 'ml.m5.4xlarge',
+ label: 'Medium',
+ price: 1.075,
+ defaultCidr: '0.0.0.0/0',
+ description: 'A medium research workspace meant for average sized problems.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '32',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '128',
+ },
+ ],
+ },
+ {
+ label: 'Large',
+ type: 'sagemaker',
+ size: 'ml.m5.24xlarge',
+ price: 6.451,
+ defaultCidr: '0.0.0.0/0',
+ description: 'A large research workspace meant for the largest of problems. It costs the most amount per hour.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '96',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '384',
+ },
+ ],
+ },
+ {
+ type: 'emr',
+ size: 'm5.xlarge',
+ label: 'Small - On Demand',
+ price: 0.192,
+ defaultCidr: '',
+ description:
+ 'A small research workspace meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour.',
+ emrConfiguration: {
+ workerInstanceSize: 'm5.xlarge',
+ workerInstanceCount: 1,
+ workerInstanceOnDemandPrice: 0.192,
+ diskSizeGb: 10,
+ },
+ properties: [
+ {
+ key: 'vCPU',
+ value: '4',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '16',
+ },
+ {
+ key: 'Worker nodes',
+ value: '1',
+ },
+ ],
+ },
+ {
+ type: 'emr',
+ size: 'm5.xlarge',
+ label: 'Small - Spot',
+ price: 0.192,
+ spotBidMultiplier: 1.3,
+ emrConfiguration: {
+ workerInstanceSize: 'm5.xlarge',
+ workerInstanceCount: 1,
+ workerInstanceOnDemandPrice: 0.192,
+ diskSizeGb: 10,
+ },
+ defaultCidr: '',
+ description:
+ 'A small research workspace meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour. This research workspace uses spot pricing for worker nodes with a maximum price of 1.3x the spot history price.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '4',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '16',
+ },
+ {
+ key: 'Worker nodes',
+ value: '1',
+ },
+ ],
+ },
+ {
+ type: 'emr',
+ size: 'm5.xlarge',
+ label: 'Medium - On Demand',
+ price: 0.192,
+ emrConfiguration: {
+ workerInstanceSize: 'm5.xlarge',
+ workerInstanceCount: 8,
+ workerInstanceOnDemandPrice: 0.192,
+ diskSizeGb: 10,
+ },
+ defaultCidr: '',
+ description:
+ 'A medium research workspace meant for average sized problems. This research workspace uses spot pricing for worker nodes with a maximum price of the on demand price.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '4',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '16',
+ },
+ {
+ key: 'Worker nodes',
+ value: '8',
+ },
+ ],
+ },
+ {
+ type: 'emr',
+ size: 'm5.xlarge',
+ label: 'Medium - Spot',
+ price: 0.192,
+ spotBidMultiplier: 1.3,
+ emrConfiguration: {
+ workerInstanceSize: 'm5.xlarge',
+ workerInstanceCount: 8,
+ workerInstanceOnDemandPrice: 0.192,
+ diskSizeGb: 10,
+ },
+ defaultCidr: '',
+ description:
+ 'A medium research workspace meant for average sized problems. This research workspace uses spot pricing for worker nodes with a maximum price of 1.3x the spot history price.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '4',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '16',
+ },
+ {
+ key: 'Worker nodes',
+ value: '8',
+ },
+ ],
+ },
+ {
+ label: 'Large - On Demand',
+ type: 'emr',
+ size: 'm5.xlarge',
+ price: 0.192,
+ emrConfiguration: {
+ workerInstanceSize: 'm5.24xlarge',
+ workerInstanceCount: 8,
+ workerInstanceOnDemandPrice: 4.608,
+ diskSizeGb: 10,
+ },
+ defaultCidr: '',
+ description:
+ 'A large research workspace meant for the largest of problems. It costs the most amount per hour. This research workspace uses spot pricing for worker nodes with a maximum price of the on demand price.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '96',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '384',
+ },
+ {
+ key: 'Worker nodes',
+ value: '8',
+ },
+ ],
+ },
+ {
+ label: 'Large - Spot',
+ type: 'emr',
+ size: 'm5.xlarge',
+ price: 0.192,
+ spotBidMultiplier: 1.3,
+ emrConfiguration: {
+ workerInstanceSize: 'm5.24xlarge',
+ workerInstanceCount: 8,
+ workerInstanceOnDemandPrice: 4.608,
+ diskSizeGb: 10,
+ },
+ defaultCidr: '',
+ description:
+ 'A large research workspace meant for the largest of problems. It costs the most amount per hour. This research workspace uses spot pricing for worker nodes with a maximum price of 1.3x the spot history price.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '96',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '384',
+ },
+ {
+ key: 'Worker nodes',
+ value: '8',
+ },
+ ],
+ },
+ {
+ size: 'r5.2xlarge',
+ type: 'ec2-linux',
+ label: 'Small',
+ defaultCidr: '',
+ price: 0.504,
+ description:
+ 'A small environment is meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '8',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '64',
+ },
+ ],
+ },
+ {
+ size: 'r5.8xlarge',
+ type: 'ec2-linux',
+ label: 'Medium',
+ defaultCidr: '',
+ price: 2.016,
+ description: 'A medium environment is meant for average sized problems.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '32',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '256',
+ },
+ ],
+ },
+ {
+ label: 'Large',
+ type: 'ec2-linux',
+ size: 'r5.16xlarge',
+ defaultCidr: '',
+ price: 4.032,
+ description: 'A large environment is meant for the largest of problems. It costs the most amount per hour.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '64',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '512',
+ },
+ ],
+ },
+ {
+ size: 'r5.2xlarge',
+ type: 'ec2-windows',
+ label: 'Small',
+ defaultCidr: '',
+ price: 0.872,
+ description:
+ 'A small environment is meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '8',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '64',
+ },
+ ],
+ },
+ {
+ size: 'r5.8xlarge',
+ type: 'ec2-windows',
+ label: 'Medium',
+ defaultCidr: '',
+ price: 3.488,
+ description: 'A medium environment is meant for average sized problems.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '32',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '256',
+ },
+ ],
+ },
+ {
+ label: 'Large',
+ type: 'ec2-windows',
+ size: 'r5.16xlarge',
+ defaultCidr: '',
+ price: 6.976,
+ description: 'A large environment is meant for the largest of problems. It costs the most amount per hour.',
+ properties: [
+ {
+ key: 'vCPU',
+ value: '64',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '512',
+ },
+ ],
+ },
+ ].map(config => ({ ...config, id: `${idCounter++}` }));
+}
+
+function registerContextItems(appContext) {
+ appContext.environmentConfigurationsStore = EnvironmentConfigurationsStore.create({}, appContext);
+}
+
+export { EnvironmentConfigurationsStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentStore.js
new file mode 100644
index 0000000000..90f13f4d3d
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentStore.js
@@ -0,0 +1,75 @@
+/*
+ * 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 { displayWarning } from '@aws-ee/base-ui/dist/helpers/notification';
+
+import { getEnvironment, getEnvironmentCost } from '../../helpers/api';
+import { getEstimatedCost } from '../../helpers/externalCostUtil';
+
+// ==================================================================
+// EnvironmentStore
+// ==================================================================
+const EnvironmentStore = BaseStore.named('EnvironmentStore')
+ .props({
+ environmentId: '',
+ tickPeriod: 300 * 1000, // 5 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const parent = getParent(self, 2);
+ const rawEnvironment = await getEnvironment(self.environmentId);
+ const envCreatedAt = new Date(rawEnvironment.createdAt);
+ const now = new Date();
+ const diffTime = Math.abs(now - envCreatedAt);
+ const numberOfDaysBetweenDateCreatedAndToday = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+
+ try {
+ const numberDaysInPast = Math.min(30, numberOfDaysBetweenDateCreatedAndToday);
+ const environmentCost = rawEnvironment.isExternal
+ ? await getEstimatedCost(rawEnvironment, numberDaysInPast)
+ : await getEnvironmentCost(self.environmentId, numberDaysInPast);
+ rawEnvironment.costs = environmentCost;
+ } catch (error) {
+ displayWarning('Error encountered retrieving cost data', error);
+ }
+
+ parent.addEnvironment(rawEnvironment);
+ },
+
+ cleanup: () => {
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get environment() {
+ const parent = getParent(self, 2);
+ const w = parent.getEnvironment(self.environmentId);
+ return w;
+ },
+ }));
+
+// Note: Do NOT register this in the global context, if you want to gain access to an instance
+// use environmentsStore.getEnvironmentStore()
+// eslint-disable-next-line import/prefer-default-export
+export { EnvironmentStore };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentsStore.js
new file mode 100644
index 0000000000..b564ff6a2d
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/EnvironmentsStore.js
@@ -0,0 +1,306 @@
+/*
+ * 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 { getEnv, types } from 'mobx-state-tree';
+import { displayWarning } from '@aws-ee/base-ui/dist/helpers/notification';
+import { consolidateToMap, storage } from '@aws-ee/base-ui/dist/helpers/utils';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { getEstimatedCost } from '../../helpers/externalCostUtil';
+import localStorageKeys from '../constants/local-storage-keys';
+import {
+ getEnvironments,
+ deleteEnvironment,
+ createEnvironment,
+ getEnvironmentCost,
+ getExternalTemplate,
+ updateEnvironment,
+} from '../../helpers/api';
+import { Environment } from './Environment';
+import { EnvironmentStore } from './EnvironmentStore';
+import CfnService from '../../helpers/cfn-service';
+import ExternalKeypairService from '../../helpers/externalKeypairService';
+import ExternalVpcService from '../../helpers/externalVpcService';
+import getExternalAccountDetails from '../../helpers/externalAccountDetails';
+
+// ==================================================================
+// EnvironmentsStore
+// ==================================================================
+const EnvironmentsStore = BaseStore.named('EnvironmentsStore')
+ .props({
+ environments: types.optional(types.map(Environment), {}),
+ environmentStores: types.optional(types.map(EnvironmentStore), {}),
+ tickPeriod: 30 * 1000, // 30 seconds
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const environments = await getEnvironments();
+
+ try {
+ const costPromises = environments.map(env => {
+ if (env.isExternal) {
+ return getEstimatedCost(env, 1);
+ }
+ return getEnvironmentCost(env.id, 1);
+ });
+
+ const costInfo = await Promise.all(costPromises);
+
+ for (let i = 0; i < environments.length; i++) {
+ environments[i].costs = costInfo[i];
+ }
+ } catch (error) {
+ displayWarning('Error encountered retrieving cost data', error);
+ }
+
+ self.runInAction(() => {
+ consolidateToMap(self.environments, environments, (exiting, newItem) => {
+ exiting.setEnvironment(newItem);
+ });
+ });
+ },
+
+ addEnvironment(rawEnvironment) {
+ const id = rawEnvironment.id;
+ const previous = self.environments.get(id);
+
+ if (!previous) {
+ self.environments.put(rawEnvironment);
+ } else {
+ previous.setEnvironment(rawEnvironment);
+ }
+ },
+
+ getEnvironmentStore: environmentId => {
+ let entry = self.environmentStores.get(environmentId);
+ if (!entry) {
+ // Lazily create the store
+ self.environmentStores.set(environmentId, EnvironmentStore.create({ environmentId }));
+ entry = self.environmentStores.get(environmentId);
+ }
+
+ return entry;
+ },
+
+ markAsTerminating: id => {
+ const previous = self.environments.get(id);
+ if (previous) {
+ previous.markAsTerminating();
+ }
+ },
+
+ async updateExternalEnvironment(environment, user, pin) {
+ if (!environment.isExternal || !user.isExternalUser || _.isEmpty(storage.getItem(localStorageKeys.pinToken))) {
+ return;
+ }
+ const creds = await user.unencryptedCreds(pin);
+ const cfn = new CfnService(creds.accessKeyId, creds.secretAccessKey, creds.region);
+ const request = { id: environment.id };
+ try {
+ const response = await cfn.describeStack(environment.stackId);
+ _.assign(request, this.convertCfnResponse(response, environment.instanceInfo));
+ } catch (e) {
+ _.assign(request, { status: 'FAILED', error: e.message });
+ } finally {
+ if (environment.status !== request.status) {
+ updateEnvironment(request);
+ }
+ }
+ },
+
+ convertCfnResponse(response, instanceInfo) {
+ if (!response.isDone) {
+ return { status: 'PENDING' };
+ }
+ response.outputs.forEach(output => {
+ _.assign(instanceInfo, { [output.key]: output.value });
+ });
+ instanceInfo = _.omitBy(instanceInfo, _.isEmpty);
+ return response.isFailed
+ ? { status: 'FAILED', error: response.statusReason, instanceInfo }
+ : response.status === 'DELETE_COMPLETE'
+ ? { status: 'TERMINATED', instanceInfo }
+ : { status: 'COMPLETED', instanceInfo };
+ },
+
+ async deleteEnvironment(environment, user, pin) {
+ if (environment.isExternal) {
+ await this.deleteExternalEnvironment(await user.unencryptedCreds(pin), environment);
+ }
+ const uiEventBus = getEnv(self).uiEventBus;
+ await deleteEnvironment(environment.id);
+ await uiEventBus.fireEvent('environmentDeleted', environment);
+ },
+
+ async deleteExternalEnvironment(creds, environment) {
+ const cfn = new CfnService(creds.accessKeyId, creds.secretAccessKey, creds.region);
+ await cfn.deleteStack(environment.stackId);
+
+ if (environment.instanceInfo.type !== 'sagemaker') {
+ const externalKeypairService = new ExternalKeypairService(creds);
+ await externalKeypairService.delete(environment.id);
+ }
+ },
+
+ async createEnvironment(environment) {
+ // environment = { platformId, configurationId, name, description, projectId, studyIds, params, pin }
+ // - projectId is only available if the user is not external
+ // - pin is only available if creation of the environment is done by an external researcher user role.
+ // and should never be sent to the server
+ const user = self.user;
+ const result = user.isExternalResearcher
+ ? await this.createExternalEnvironment(
+ await user.unencryptedCreds(environment.pin),
+ user.username,
+ _.omit(environment, ['pin']), // remove the pin, we don't want to send it to the server
+ )
+ : await createEnvironment(environment);
+ self.addEnvironment(result);
+ return self.getEnvironment(result.id);
+ },
+
+ async createExternalEnvironment(creds, username, rawEnvironment) {
+ const { platformId, configurationId } = rawEnvironment;
+ const configuration = self.getComputeConfiguration(platformId, configurationId);
+ const { type, title } = configuration;
+ const size = configuration.getParam('size');
+
+ // We need to get the external account details to pass the account Id to the api to allow ami access
+ const { Account: accountId } = await getExternalAccountDetails(creds);
+ // We first call the backend because it will enrich with id and the imageId if needed
+ const environment = await createEnvironment({ ...rawEnvironment, accountId });
+ const cfn = new CfnService(creds.accessKeyId, creds.secretAccessKey, creds.region);
+ const name = `analysis-${new Date().getTime()}`;
+ const params = await this.getExternalParams({ environment, name, creds });
+ const url = await getExternalTemplate(`${type}.cfn.yml`);
+ const response = await cfn.createStack(
+ name,
+ params,
+ url,
+ username,
+ `Created By ${username} - ${title} - ${type} - ${size}`,
+ );
+
+ return updateEnvironment({ id: environment.id, stackId: response.StackId });
+ },
+
+ async getExternalParams({
+ environment: {
+ id,
+ instanceInfo: { type, size, config, cidr, s3Mounts, iamPolicyDocument, environmentInstanceFiles },
+ amiImage,
+ },
+ name,
+ creds,
+ }) {
+ const cfnParams = [];
+ const addParam = (key, v) => cfnParams.push({ ParameterKey: key, ParameterValue: `${v}` });
+
+ addParam('Namespace', name);
+ addParam('S3Mounts', s3Mounts);
+ addParam('IamPolicyDocument', iamPolicyDocument);
+ addParam('EnvironmentInstanceFiles', environmentInstanceFiles);
+
+ const externalVpcService = new ExternalVpcService(creds);
+ const { vpcId, subnetId } = await externalVpcService.defaultVPCInfo();
+ addParam('VPC', vpcId);
+ addParam('Subnet', subnetId);
+
+ if (type === 'sagemaker') {
+ addParam('InstanceType', size); // Yes, size here is actually the instance type we want to send to cfn
+ }
+
+ if (type === 'emr') {
+ addParam('DiskSizeGB', config.diskSizeGb.toString());
+ addParam('MasterInstanceType', size);
+ addParam('WorkerInstanceType', config.workerInstanceSize);
+ addParam('CoreNodeCount', config.workerInstanceCount.toString());
+
+ // Add parameters to support spot instance pricing if specified
+ // TODO this needs to be parameterized
+ const isOnDemand = !config.spotBidPrice;
+ // The spot bid price can only have 3 decimal places maximum
+ const spotBidPrice = isOnDemand ? '0' : config.spotBidPrice.toFixed(3);
+
+ addParam('Market', isOnDemand ? 'ON_DEMAND' : 'SPOT');
+ addParam('WorkerBidPrice', spotBidPrice);
+
+ // These paramaters apply for types apart from sagemaker, but keep the logic simple for now
+ const externalKeypairService = new ExternalKeypairService(creds);
+ const keyName = await externalKeypairService.create(id);
+
+ addParam('AmiId', amiImage);
+ addParam('AccessFromCIDRBlock', cidr);
+ addParam('KeyName', keyName);
+ }
+
+ return cfnParams;
+ },
+
+ async updateEnvironment(environment) {
+ await updateEnvironment(environment);
+ },
+
+ cleanup: () => {
+ storage.removeItem(localStorageKeys.pinToken);
+ self.environments.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.environments.size === 0;
+ },
+
+ get total() {
+ return self.environments.size;
+ },
+
+ get list() {
+ const result = [];
+ self.environments.forEach(environment => result.push(environment));
+
+ return _.reverse(_.sortBy(result, ['createdAt', 'name']));
+ },
+
+ getEnvironment(id) {
+ return self.environments.get(id);
+ },
+
+ get user() {
+ return getEnv(self).userStore.user;
+ },
+
+ getComputeConfiguration(platformId, configurationId) {
+ const store = getEnv(self).computePlatformsStore;
+ const platform = store.getComputePlatform(platformId);
+ if (!platform) return undefined;
+ return platform.getConfiguration(configurationId);
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.environmentsStore = EnvironmentsStore.create({}, appContext);
+}
+
+export { EnvironmentsStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/InstanceInfo.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/InstanceInfo.js
new file mode 100644
index 0000000000..df67aa917c
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments/InstanceInfo.js
@@ -0,0 +1,42 @@
+/*
+ * 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 { getSnapshot, types } from 'mobx-state-tree';
+
+const InstanceInfo = types
+ .model('InstanceInfo', {
+ Ec2WorkspaceDnsName: '',
+ type: '',
+ size: '',
+ JupyterUrl: '',
+ NotebookInstanceName: '',
+ s3Mounts: '',
+ iamPolicyDocument: '',
+ environmentInstanceFiles: '',
+ })
+ .views(self => ({
+ get id() {
+ return self.identifierStr;
+ },
+ get identifier() {
+ return self;
+ },
+ get identifierStr() {
+ return JSON.stringify(getSnapshot(self));
+ },
+ }));
+
+// eslint-disable-next-line import/prefer-default-export
+export { InstanceInfo };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/files/FileUploadGroup.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/files/FileUploadGroup.js
new file mode 100644
index 0000000000..5515b1e031
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/files/FileUploadGroup.js
@@ -0,0 +1,131 @@
+/*
+ * 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 uuidv4 from 'uuid/v4';
+
+const FileUpload = types
+ .model('FileUpload', {
+ id: types.identifier,
+ status: types.union(
+ types.literal('PENDING'),
+ types.literal('UPLOADING'),
+ types.literal('COMPLETE'),
+ types.literal('FAILED'),
+ ),
+ uploaded: types.maybeNull(types.number),
+ error: types.maybeNull(types.string),
+ })
+ .volatile(() => ({
+ file: undefined,
+ cancel: undefined,
+ }))
+ .views(self => ({
+ get size() {
+ return self.file ? self.file.size : 0;
+ },
+ get name() {
+ return self.file ? self.file.name : '';
+ },
+ getFile() {
+ return self.file;
+ },
+ }))
+ .actions(self => ({
+ updateProgress(uploadedBytes) {
+ self.uploaded = uploadedBytes;
+ },
+ updateStatusToUploading() {
+ self.status = 'UPLOADING';
+ },
+ updateStatusToComplete() {
+ self.status = 'COMPLETE';
+ },
+ updateStatusToFailed(error) {
+ self.status = 'FAILED';
+ self.error = error;
+ },
+ setFile(file) {
+ self.file = file;
+ },
+ setCancel(cancel) {
+ self.cancel = cancel;
+ },
+ doCancel() {
+ if (self.cancel) {
+ self.cancel();
+ self.cancel = undefined;
+ }
+ },
+ }));
+
+const FileUploadGroup = types
+ .model('FileUploadGroup', {
+ resourceId: types.identifier,
+ fileUploads: types.map(FileUpload),
+ state: types.union(types.literal('PENDING'), types.literal('UPLOADING'), types.literal('COMPLETE')),
+ })
+ .views(self => ({
+ get fileUploadObjects() {
+ return Array.from(self.fileUploads.values());
+ },
+ getFileUpload(fileUploadId) {
+ return self.fileUploads.get(fileUploadId);
+ },
+ }))
+ .actions(self => ({
+ async start(fileUploadHandler) {
+ if (self.state !== 'PENDING') {
+ throw new Error(`Cannot transition state from ${self.state} -> UPLOADING`);
+ }
+ self.setStateToUploading();
+ const fileUploads = Array.from(self.fileUploads.values()).filter(fileUpload => fileUpload.status === 'PENDING');
+ await Promise.all(
+ fileUploads.map(async fileUpload => {
+ fileUpload.updateStatusToUploading();
+ try {
+ await fileUploadHandler(fileUpload);
+ } catch (error) {
+ fileUpload.updateStatusToFailed(error.message);
+ }
+ }),
+ );
+ self.setStateToComplete();
+ },
+ cancel() {
+ self.fileUploads.forEach(fileUpload => {
+ fileUpload.doCancel();
+ });
+ },
+ remove(id) {
+ self.fileUploads.delete(id);
+ },
+ add({ file }) {
+ const model = FileUpload.create({
+ id: uuidv4(),
+ status: 'PENDING',
+ });
+ model.setFile(file);
+ self.fileUploads.put(model);
+ },
+ setStateToComplete() {
+ self.state = 'COMPLETE';
+ },
+ setStateToUploading() {
+ self.state = 'UPLOADING';
+ },
+ }));
+
+export default FileUploadGroup;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/files/FileUploadsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/files/FileUploadsStore.js
new file mode 100644
index 0000000000..d7ec72a5a0
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/files/FileUploadsStore.js
@@ -0,0 +1,42 @@
+/*
+ * 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 FileUploadGroup from './FileUploadGroup';
+
+const FileUploadsStore = types
+ .model('FileUploadsStore', {
+ fileUploadGroups: types.map(FileUploadGroup),
+ })
+ .actions(self => ({
+ getFileUploadGroup(resourceId) {
+ let group = self.fileUploadGroups.get(resourceId);
+ if (!group) {
+ group = FileUploadGroup.create({ resourceId, state: 'PENDING' });
+ self.fileUploadGroups.put(group);
+ }
+ return group;
+ },
+ resetFileUploadGroup(resourceId) {
+ const group = FileUploadGroup.create({ resourceId, state: 'PENDING' });
+ self.fileUploadGroups.put(group);
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.fileUploadsStore = FileUploadsStore.create({}, appContext);
+}
+
+export { FileUploadsStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddAwsAccountForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddAwsAccountForm.js
new file mode 100644
index 0000000000..f57a2a7540
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddAwsAccountForm.js
@@ -0,0 +1,69 @@
+/*
+ * 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';
+
+const addAwsAccountFormFields = {
+ name: {
+ label: 'Account Name',
+ placeholder: 'Type the name of this account',
+ rules: 'required|string|between:1,100',
+ },
+ accountId: {
+ label: 'AWS Account ID',
+ placeholder: 'Type the 12-digit AWS account ID',
+ rules: 'required|string|size:12',
+ },
+ roleArn: {
+ label: 'Role Arn',
+ placeholder: 'Type Role ARN for launching resources into this AWS account',
+ rules: 'required|string|between:10,300',
+ },
+ externalId: {
+ label: 'External ID',
+ placeholder: 'Type external ID for this AWS account',
+ rules: 'required|string|between:1,300',
+ },
+ description: {
+ label: 'Description',
+ placeholder: 'Type description for this AWS account',
+ rules: 'required|string',
+ },
+ vpcId: {
+ label: 'VPC ID',
+ placeholder: 'Type the ID of the VPC where EMR clusters will be launched',
+ rules: 'required|string|min:12|max:21',
+ },
+ subnetId: {
+ label: 'Subnet ID',
+ placeholder: 'Type the ID of the subnet where the EMR clusters will be launched',
+ rules: 'required|string|min:15|max:24',
+ },
+ encryptionKeyArn: {
+ label: 'KMS Encryption Key ARN',
+ placeholder: 'Type the KMS Encryption Key ARN to use for this AWS account',
+ rules: 'required|string|between:1,100',
+ },
+};
+
+function getAddAwsAccountFormFields() {
+ return addAwsAccountFormFields;
+}
+
+function getAddAwsAccountForm() {
+ return createForm(addAwsAccountFormFields);
+}
+
+export { getAddAwsAccountFormFields, getAddAwsAccountForm };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddIndexForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddIndexForm.js
new file mode 100644
index 0000000000..a1aae5f39d
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddIndexForm.js
@@ -0,0 +1,42 @@
+/*
+ * 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';
+
+const addIndexFormFields = {
+ id: {
+ label: 'Index ID',
+ placeholder: 'Type id for this index',
+ rules: 'required|string|between:1,300',
+ },
+ awsAccountId: {
+ label: 'AWS Account ID',
+ },
+ description: {
+ label: 'Description',
+ placeholder: 'Type description for this index',
+ rules: 'string|between:1,3000',
+ },
+};
+
+function getAddIndexFormFields() {
+ return addIndexFormFields;
+}
+
+function getAddIndexForm() {
+ return createForm(addIndexFormFields);
+}
+
+export { getAddIndexFormFields, getAddIndexForm };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddLocalUserForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddLocalUserForm.js
new file mode 100644
index 0000000000..5e4af10087
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddLocalUserForm.js
@@ -0,0 +1,53 @@
+/* eslint-disable import/prefer-default-export */
+import { createForm } from '../../helpers/form';
+
+const addUserFormFields = {
+ email: {
+ label: 'Username',
+ placeholder: 'Type email address as username for the user',
+ extra: { explain: 'Username in email format' },
+ rules: 'required|email|string',
+ },
+ password: {
+ label: 'Password',
+ placeholder: 'Type default password for the user',
+ explain: `The password must be between 4 and 2048 characters long.`,
+ rules: 'required|string|between:4,2048',
+ },
+ firstName: {
+ label: 'First Name',
+ placeholder: 'Type first name of the user',
+ rules: 'required|string|between:1,500',
+ },
+ lastName: {
+ label: 'Last Name',
+ placeholder: 'Type last name of the user',
+ rules: 'required|string|between:1,500',
+ },
+ userRole: {
+ label: 'UserRole',
+ extra: { explain: "Select user's role" },
+ rules: 'required',
+ },
+ projectId: {
+ label: 'Projects',
+ extra: { explain: 'Select projects that this user are associated with' },
+ },
+ status: {
+ label: 'Status',
+ extra: {
+ explain: 'Active users can log into the Research Portal',
+ yesLabel: 'Active',
+ noLabel: 'Inactive',
+ yesValue: 'active',
+ noValue: 'inactive',
+ },
+ rules: 'required',
+ },
+};
+
+function getAddUserForm() {
+ return createForm(addUserFormFields);
+}
+
+export { getAddUserForm };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddProjectForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddProjectForm.js
new file mode 100644
index 0000000000..69a8015156
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddProjectForm.js
@@ -0,0 +1,42 @@
+/*
+ * 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';
+
+const addProjectFormFields = {
+ id: {
+ label: 'Project ID',
+ placeholder: 'Type id for this project',
+ rules: 'required|string|between:1,300',
+ },
+ indexId: {
+ label: 'Index ID',
+ },
+ description: {
+ label: 'Description',
+ placeholder: 'Type description for this project',
+ rules: 'string|between:1,3000',
+ },
+};
+
+function getAddProjectFormFields() {
+ return addProjectFormFields;
+}
+
+function getAddProjectForm() {
+ return createForm(addProjectFormFields);
+}
+
+export { getAddProjectFormFields, getAddProjectForm };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddUserApplicationForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddUserApplicationForm.js
new file mode 100644
index 0000000000..f046c2983e
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddUserApplicationForm.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 { createForm } from '../../helpers/form';
+
+const addUserApplicationFormFields = {
+ email: {
+ label: 'Username',
+ placeholder: 'Type email address as username for the user',
+ },
+ firstName: {
+ label: 'First Name',
+ placeholder: 'Your first name',
+ explain: 'Your current default first name is ',
+ rules: 'required|string',
+ },
+ lastName: {
+ label: 'Last Name',
+ placeholder: 'Your last name',
+ explain: 'Your current default last name is ',
+ rules: 'required|string',
+ },
+ applyReason: {
+ label: 'Describe Your Research',
+ explain: 'Please tell us why you are requesting access to the Research Portal',
+ placeholder: 'Why are you requesting access?',
+ rules: 'required|string',
+ },
+};
+
+function getAddUserApplicationFormFields() {
+ return addUserApplicationFormFields;
+}
+
+function getAddUserApplicationForm() {
+ return createForm(addUserApplicationFormFields);
+}
+
+export { getAddUserApplicationFormFields, getAddUserApplicationForm };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddUserForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddUserForm.js
new file mode 100644
index 0000000000..a8e2f675f8
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/AddUserForm.js
@@ -0,0 +1,57 @@
+/*
+ * 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';
+
+const addUserFormFields = {
+ email: {
+ label: 'Username',
+ placeholder: 'Type email address as username for the user',
+ extra: { explain: 'Username in email format' },
+ rules: 'required|email|string',
+ },
+ identityProviderName: {
+ label: 'Identity Provider',
+ extra: { explain: 'Identity Provider for this user' },
+ },
+ projectId: {
+ label: 'Project Id',
+ extra: { explain: 'Select Project for this user' },
+ },
+ userRole: {
+ label: 'UserRole',
+ extra: { explain: "Select user's role" },
+ },
+ status: {
+ label: 'Status',
+ extra: {
+ explain: 'Active users can log into the Research Portal',
+ yesLabel: 'Active',
+ noLabel: 'Inactive',
+ yesValue: 'active',
+ noValue: 'inactive',
+ },
+ },
+};
+
+function getAddUserFormFields() {
+ return addUserFormFields;
+}
+
+function getAddUserForm() {
+ return createForm(addUserFormFields);
+}
+
+export { getAddUserFormFields, getAddUserForm };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateAwsAccountForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateAwsAccountForm.js
new file mode 100644
index 0000000000..3a10d1feae
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateAwsAccountForm.js
@@ -0,0 +1,54 @@
+/*
+ * 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';
+
+const createAwsAccountFormFields = {
+ accountName: {
+ label: 'Account Name',
+ placeholder: 'Type the name of this account',
+ rules: 'required|string|between:1,100',
+ },
+ accountEmail: {
+ label: 'AWS Account Email',
+ placeholder: 'Type AWS account email',
+ rules: 'required|string|email',
+ },
+ masterRoleArn: {
+ label: 'Master Role Arn',
+ placeholder: 'Type configured Role ARN of master account of the Organization',
+ rules: 'required|string|between:10,300',
+ },
+ externalId: {
+ label: 'External ID',
+ placeholder: 'Type external ID for this AWS account',
+ rules: 'required|string|between:1,300',
+ },
+ description: {
+ label: 'Description',
+ placeholder: 'Type description for this AWS account',
+ rules: 'required|string',
+ },
+};
+
+function getCreateAwsAccountFormFields() {
+ return createAwsAccountFormFields;
+}
+
+function getCreateAwsAccountForm() {
+ return createForm(createAwsAccountFormFields);
+}
+
+export { getCreateAwsAccountFormFields, getCreateAwsAccountForm };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateExternalPlatformForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateExternalPlatformForm.js
new file mode 100644
index 0000000000..8d5168bbb6
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateExternalPlatformForm.js
@@ -0,0 +1,79 @@
+/*
+ * 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 { createForm } from '../../helpers/form';
+
+const getFields = ({ askForCredentials, cidr }) => {
+ const rulesPrefix = askForCredentials ? 'required|' : '';
+ const fields = {
+ name: {
+ label: 'Name',
+ placeholder: 'Type a name for this research workspace',
+ extra: {
+ explain:
+ 'Name can contain only alphanumeric characters (case sensitive) and hyphens. It must start with an alphabetic character and cannot be longer than 128 characters',
+ },
+ rules: 'required|string|between:3,128|regex:/^[A-Za-z][A-Za-z0-9-]+$/',
+ },
+ description: {
+ label: 'Description',
+ placeholder: 'The description of this research workspace',
+ rules: 'required|string|between:3,2048',
+ },
+ configurationId: {
+ label: 'Configuration',
+ placeholder: 'The configuration for the research workspace',
+ rules: 'required',
+ },
+ accessKeyId: {
+ label: 'IAM Access Key Id',
+ placeholder: 'Access key for your IAM user',
+ rules: `${rulesPrefix}string|between:16,128`,
+ },
+ secretAccessKey: {
+ label: 'IAM Secret Access Key',
+ placeholder: 'Secret access key for your IAM user',
+ rules: `${rulesPrefix}string|size:40`, // TODO - is this right?
+ },
+ pin: {
+ label: 'PIN',
+ placeholder: 'A PIN or password to secure your IAM credentials',
+ rules: 'required|string|between:4,16',
+ },
+ };
+
+ if (!_.isUndefined(cidr)) {
+ fields.cidr = {
+ label: 'Whitelisted CIDR',
+ extra: {
+ explain: `This research workspace will only be reachable from this CIDR. You can get your organization's CIDR range from your IT department. The provided default is the CIDR that restricts to your IP address.`,
+ },
+ placeholder: 'The CIDR range to restrict research workspace access to',
+ rules: 'required|cidr',
+ value: cidr,
+ };
+ }
+
+ return fields;
+};
+
+function getCreateExternalPlatformForm(...args) {
+ return createForm(getFields(...args));
+}
+
+// eslint-disable-next-line import/prefer-default-export
+export { getCreateExternalPlatformForm };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateInternalPlatformForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateInternalPlatformForm.js
new file mode 100644
index 0000000000..67b05b705e
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateInternalPlatformForm.js
@@ -0,0 +1,70 @@
+/*
+ * 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 { createForm } from '../../helpers/form';
+
+const getFields = ({ projectIdOptions, cidr }) => {
+ const fields = {
+ name: {
+ label: 'Name',
+ placeholder: 'Type a name for this research workspace',
+ extra: {
+ explain:
+ 'Name can contain only alphanumeric characters (case sensitive) and hyphens. It must start with an alphabetic character and cannot be longer than 128 characters',
+ },
+ rules: 'required|string|between:3,128|regex:/^[A-Za-z][A-Za-z0-9-]+$/',
+ },
+ description: {
+ label: 'Description',
+ placeholder: 'The description of this research workspace',
+ rules: 'required|string|between:3,2048',
+ },
+ projectId: {
+ label: 'Project ID',
+ placeholder: 'The project ID associated with this study',
+ rules: ['required', 'string', 'min:1', 'max:100'],
+ extra: {
+ options: projectIdOptions,
+ },
+ },
+ configurationId: {
+ label: 'Configuration',
+ placeholder: 'The configuration for the research workspace',
+ rules: 'required',
+ },
+ };
+
+ if (!_.isUndefined(cidr)) {
+ fields.cidr = {
+ label: 'Whitelisted CIDR',
+ extra: {
+ explain: `This research workspace will only be reachable from this CIDR. You can get your organization's CIDR range from your IT department. The provided default is the CIDR that restricts to your IP address.`,
+ },
+ placeholder: 'The CIDR range to restrict research workspace access to',
+ rules: 'required|cidr',
+ value: cidr,
+ };
+ }
+
+ return fields;
+};
+
+function getCreateInternalPlatformForm(...args) {
+ return createForm(getFields(...args));
+}
+
+// eslint-disable-next-line import/prefer-default-export
+export { getCreateInternalPlatformForm };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateStudy.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateStudy.js
new file mode 100644
index 0000000000..8ad930e4f4
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/CreateStudy.js
@@ -0,0 +1,63 @@
+/*
+ * 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';
+import { categories } from '../studies/categories';
+
+const createStudyFields = {
+ // General fields
+ id: {
+ label: 'ID',
+ placeholder: 'A unique ID used to reference the study',
+ extra: {
+ explain: 'Must be less than 100 characters long and only contain alphanumeric characters, "-", or "_"',
+ },
+ rules: ['required', 'string', 'between:1,100', 'regex:/^[A-Za-z0-9-_]+$/'],
+ },
+ categoryId: {
+ label: '', // not shown because extra.showHeader = false
+ extra: {
+ explain:
+ 'If you choose "My Study", only you can access it. If you choose "Organization Study", you get to decide who can access it.',
+ yesLabel: 'My Study',
+ noLabel: 'Organization Study',
+ yesValue: categories.myStudies.id,
+ noValue: categories.organization.id,
+ showHeader: false,
+ },
+ rules: ['required'],
+ },
+ name: {
+ label: 'Name',
+ placeholder: 'A name for the study',
+ rules: ['string', 'max:2048'],
+ },
+ description: {
+ label: 'Description',
+ placeholder: 'A description of the study',
+ rules: ['string', 'max:8192'],
+ },
+ projectId: {
+ label: 'Project ID',
+ placeholder: 'The project ID associated with this study',
+ rules: ['required', 'string', 'min:1', 'max:100'],
+ },
+};
+
+const getCreateStudyForm = () => {
+ return createForm(createStudyFields);
+};
+
+export { getCreateStudyForm }; // eslint-disable-line import/prefer-default-export
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/ExternalUserIAMForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/ExternalUserIAMForm.js
new file mode 100644
index 0000000000..519f956194
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/ExternalUserIAMForm.js
@@ -0,0 +1,52 @@
+/*
+ * 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 getIAMFormFields(isIAMRequired = true) {
+ const rulesPrefix = isIAMRequired ? 'required|' : '';
+ return [
+ {
+ name: 'accessKeyId',
+ label: 'IAM User Access Key Id',
+ placeholder: 'Access Key for your IAM user',
+ rules: `${rulesPrefix}string|size:20`,
+ },
+ {
+ name: 'secretAccessKey',
+ label: 'IAM User Secret Access Key',
+ placeholder: 'Secret access Key for your IAM user',
+ rules: `${rulesPrefix}string|size:40`,
+ },
+ {
+ name: 'region',
+ label: 'AWS Region',
+ placeholder: 'us-east-1',
+ rules: `${rulesPrefix}string|regex:/^[a-z]{2}(-gov)?-[a-z]*-\\d$/`,
+ },
+ {
+ name: 'pin',
+ label: 'PIN',
+ placeholder: 'A PIN or password to secure your IAM credentials',
+ rules: 'required|string|between:4,16',
+ },
+ ];
+}
+
+function getExternalUserIAMForm() {
+ return createForm(getIAMFormFields());
+}
+
+export { getExternalUserIAMForm, getIAMFormFields };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/ExternalUserPinForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/ExternalUserPinForm.js
new file mode 100644
index 0000000000..b13a0a3c8b
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/ExternalUserPinForm.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.
+ */
+
+import { createForm } from '../../helpers/form';
+
+function getExternalUserPinFormFields() {
+ return [
+ {
+ name: 'pin',
+ label: 'PIN',
+ placeholder: 'A PIN or password to secure your IAM credentials',
+ rules: 'required|string|between:4,16',
+ },
+ ];
+}
+
+function getExternalUserPinForm() {
+ return createForm(getExternalUserPinFormFields());
+}
+
+export { getExternalUserPinForm, getExternalUserPinFormFields };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UpdateUserConfig.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UpdateUserConfig.js
new file mode 100644
index 0000000000..e4f6d1b5b4
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UpdateUserConfig.js
@@ -0,0 +1,82 @@
+/*
+ * 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 { createForm } from '../../helpers/form';
+import { toValueFromIdp } from './UserFormUtils';
+
+function getUpdateUserConfigFormFields(existingUser) {
+ return {
+ username: {
+ label: 'Username',
+ value: _.get(existingUser, 'username', ''),
+ },
+ firstName: {
+ label: 'First Name',
+ placeholder: 'First name of this user',
+ value: _.get(existingUser, 'firstName', ''),
+ rules: 'required|string',
+ },
+ lastName: {
+ label: 'Last Name',
+ placeholder: 'Last name of this user',
+ value: _.get(existingUser, 'lastName', ''),
+ rules: 'required|string',
+ },
+ email: {
+ label: 'Email',
+ placeholder: 'email address',
+ value: _.get(existingUser, 'email', ''),
+ rules: 'required|email|string',
+ },
+ identityProviderName: {
+ label: 'Identity Provider Name',
+ value: toValueFromIdp({
+ authenticationProviderId: _.get(existingUser, 'authenticationProviderId', ''),
+ identityProviderName: _.get(existingUser, 'identityProviderName', ''),
+ }),
+ },
+ projectId: {
+ label: 'Project',
+ value: _.get(existingUser, 'projectId', ''),
+ },
+ userRole: {
+ label: 'User Role',
+ value: _.get(existingUser, 'userRole', ''),
+ },
+ applyReason: {
+ label: 'Reason for Applying',
+ explain: ' ',
+ value: _.get(existingUser, 'applyReason', ''),
+ },
+ status: {
+ label: 'User Status',
+ extra: {
+ explain: 'Active users can log into the Research Portal',
+ yesLabel: 'Active',
+ noLabel: 'Inactive',
+ yesValue: 'active',
+ noValue: 'inactive',
+ },
+ value: _.get(existingUser, 'status', ''),
+ },
+ };
+}
+
+function getUpdateUserConfigForm(existingUser) {
+ return createForm(getUpdateUserConfigFormFields(existingUser));
+}
+
+export { getUpdateUserConfigFormFields, getUpdateUserConfigForm };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UserFormUtils.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UserFormUtils.js
new file mode 100644
index 0000000000..71384227fc
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/forms/UserFormUtils.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';
+
+/**
+ * Returns identity provider options that can be used for displaying idp selection options
+ * @param providerConfigs An array of authentication provider configuration objects. For details about the shape of
+ * the object see "authenticationProviderConfigs" property of
+ * "addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderConfigsStore.js"
+ *
+ * @returns {[]}
+ */
+function toIdpOptions(providerConfigs) {
+ const options = [];
+
+ _.forEach(providerConfigs, providerConfig => {
+ const config = providerConfig.config;
+
+ // Each providerConfig (authentication provider) can have zero or more identity providers.
+ if (!_.isEmpty(config.federatedIdentityProviders)) {
+ _.forEach(config.federatedIdentityProviders, idp => {
+ options.push({
+ key: idp.id,
+ text: idp.name,
+
+ // Make sure the authentication provider's information is embedded in the value
+ // along with the idp name. This is required so disambiguate two idps with the same idp name based on which
+ // authentication provider they belong to
+ value: JSON.stringify({ authNProviderId: providerConfig.id, idpName: idp.name }),
+ });
+ });
+ }
+ });
+ return options;
+}
+
+// From string to object
+function toIdpFromValue(value) {
+ return JSON.parse(value);
+}
+
+// From object to string
+function toValueFromIdp({ authenticationProviderId, identityProviderName }) {
+ return JSON.stringify({ authNProviderId: authenticationProviderId, idpName: identityProviderName });
+}
+
+// eslint-disable-next-line import/prefer-default-export
+export { toIdpOptions, toIdpFromValue, toValueFromIdp };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/indexes/Index.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/indexes/Index.js
new file mode 100644
index 0000000000..76056ac31b
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/indexes/Index.js
@@ -0,0 +1,48 @@
+/*
+ * 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, applySnapshot } from 'mobx-state-tree';
+
+import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier';
+
+// ==================================================================
+// Index
+// ==================================================================
+const Index = types
+ .model('Index', {
+ id: types.identifier,
+ rev: types.maybe(types.number),
+ awsAccountId: '',
+ description: '',
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ })
+ .actions(self => ({
+ setIndex(rawIndex) {
+ // Note: if you have partial data vs full data, you need to replace the applySnapshot() with
+ // the appropriate logic
+ applySnapshot(self, rawIndex);
+ },
+ }))
+
+ // eslint-disable-next-line no-unused-vars
+ .views(self => ({
+ // add view methods here
+ }));
+
+// eslint-disable-next-line import/prefer-default-export
+export { Index };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/indexes/IndexesStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/indexes/IndexesStore.js
new file mode 100644
index 0000000000..2a406aaa1e
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/indexes/IndexesStore.js
@@ -0,0 +1,124 @@
+/*
+ * 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';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils';
+import { getIndexes, addIndex } from '../../helpers/api';
+import { Index } from './Index';
+
+// ==================================================================
+// IndexesStore
+// ==================================================================
+const IndexesStore = BaseStore.named('IndexesStore')
+ .props({
+ indexes: types.optional(types.map(Index), {}),
+ tickPeriod: 900 * 1000, // 15 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const indexes = (await getIndexes()) || [];
+ self.runInAction(() => {
+ consolidateToMap(self.indexes, indexes, (exiting, newItem) => {
+ exiting.setIndex(newItem);
+ });
+ });
+ },
+
+ addIndex: async index => {
+ const addedIndex = await addIndex(index);
+ self.runInAction(() => {
+ // Added newly created user to users map
+ const addedIndexModel = Index.create(addedIndex);
+ self.indexes.set(addedIndexModel.id, addedIndexModel);
+ });
+ },
+
+ getIndexesStore: indexesId => {
+ let entry = self.indexesStores.get(indexesId);
+ if (!entry) {
+ // Lazily create the store
+ self.indexesStores.set(indexesId, IndexesStore.create({ indexesId }));
+ entry = self.indexesStores.get(indexesId);
+ }
+
+ return entry;
+ },
+
+ getIndex: indexesId => {
+ let res = {};
+ self.indexes.forEach(index => {
+ if (index.id === indexesId) res = _.clone(index);
+ });
+ return res;
+ },
+
+ cleanup: () => {
+ self.indexes.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get dropdownOptions() {
+ const result = [];
+ // converting map self.users to result array
+ self.indexes.forEach(index => {
+ const proj = {};
+ proj.key = index.id;
+ proj.value = index.id;
+ proj.text = index.id;
+ result.push(proj);
+ });
+ return result;
+ },
+
+ get empty() {
+ return self.indexes.size === 0;
+ },
+
+ get total() {
+ return self.indexes.size;
+ },
+
+ get list() {
+ const result = [];
+ self.indexes.forEach(indexes => result.push(indexes));
+
+ return _.reverse(_.sortBy(result, ['createdAt', 'name']));
+ },
+
+ hasIndexes(id) {
+ return self.indexes.has(id);
+ },
+
+ getIndexes(id) {
+ return self.indexes.get(id);
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.indexesStore = IndexesStore.create({}, appContext);
+}
+
+export { IndexesStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/Project.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/Project.js
new file mode 100644
index 0000000000..872d31e408
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/Project.js
@@ -0,0 +1,48 @@
+/*
+ * 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, applySnapshot } from 'mobx-state-tree';
+
+import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier';
+
+// ==================================================================
+// Project
+// ==================================================================
+const Project = types
+ .model('Project', {
+ id: types.identifier,
+ rev: types.maybe(types.number),
+ description: '',
+ indexId: '',
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ })
+ .actions(self => ({
+ setProject(rawProject) {
+ // Note: if you have partial data vs full data, you need to replace the applySnapshot() with
+ // the appropriate logic
+ applySnapshot(self, rawProject);
+ },
+ }))
+
+ // eslint-disable-next-line no-unused-vars
+ .views(self => ({
+ // add view methods here
+ }));
+
+// eslint-disable-next-line import/prefer-default-export
+export { Project };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/ProjectStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/ProjectStore.js
new file mode 100644
index 0000000000..402a5a00a5
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/ProjectStore.js
@@ -0,0 +1,58 @@
+/*
+ * 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 { getProject } from '../../helpers/api';
+
+// ==================================================================
+// ProjectStore
+// ==================================================================
+const ProjectStore = BaseStore.named('ProjectStore')
+ .props({
+ projectId: '',
+ tickPeriod: 300 * 1000, // 5 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const parent = getParent(self, 2);
+ const rawProject = await getProject(self.projectId);
+ parent.addProject(rawProject);
+ },
+
+ cleanup: () => {
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get project() {
+ const parent = getParent(self, 2);
+ const w = parent.getProject(self.projectId);
+ return w;
+ },
+ }));
+
+// Note: Do NOT register this in the global context, if you want to gain access to an instance
+// use projectsStore.getProjectStore()
+// eslint-disable-next-line import/prefer-default-export
+export { ProjectStore };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/ProjectsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/ProjectsStore.js
new file mode 100644
index 0000000000..81e1669ad5
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/projects/ProjectsStore.js
@@ -0,0 +1,124 @@
+/*
+ * 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';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils';
+
+import { getProjects, addProject } from '../../helpers/api';
+import { Project } from './Project';
+import { ProjectStore } from './ProjectStore';
+
+// ==================================================================
+// ProjectsStore
+// ==================================================================
+const ProjectsStore = BaseStore.named('ProjectsStore')
+ .props({
+ projects: types.optional(types.map(Project), {}),
+ projectStores: types.optional(types.map(ProjectStore), {}),
+ tickPeriod: 900 * 1000, // 15 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const projects = await getProjects();
+ // We try to preserve existing projects data and merge the new data instead
+ // We could have used self.projects.replace(), but it will do clear() then merge()
+ self.runInAction(() => {
+ consolidateToMap(self.projects, projects, (exiting, newItem) => {
+ exiting.setProject(newItem);
+ });
+ });
+ },
+
+ async addProject(rawProject) {
+ const id = rawProject.id;
+ const previous = self.projects.get(id);
+
+ if (!previous) {
+ self.projects.put(rawProject);
+ await addProject(rawProject);
+ } else {
+ previous.setProject(rawProject);
+ }
+ },
+
+ getProjectStore: projectId => {
+ let entry = self.projectStores.get(projectId);
+ if (!entry) {
+ // Lazily create the store
+ self.projectStores.set(projectId, ProjectStore.create({ projectId }));
+ entry = self.projectStores.get(projectId);
+ }
+
+ return entry;
+ },
+
+ cleanup: () => {
+ self.projects.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.projects.size === 0;
+ },
+
+ get total() {
+ return self.projects.size;
+ },
+
+ get list() {
+ const result = [];
+ self.projects.forEach(project => result.push(project));
+
+ return _.reverse(_.sortBy(result, ['createdAt', 'id']));
+ },
+
+ get dropdownOptions() {
+ const result = [];
+ // converting map self.users to result array
+ self.projects.forEach(project => {
+ const res = {};
+ res.key = project.id;
+ res.value = project.id;
+ res.text = project.id;
+ result.push(res);
+ });
+
+ return result;
+ },
+
+ hasProject(id) {
+ return self.projects.has(id);
+ },
+
+ getProject(id) {
+ return self.projects.get(id);
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.projectsStore = ProjectsStore.create({}, appContext);
+}
+
+export { ProjectsStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/resources/Session.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/resources/Session.js
new file mode 100644
index 0000000000..a0520d9178
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/resources/Session.js
@@ -0,0 +1,69 @@
+/*
+ * 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 { createSession } from '../../helpers/api';
+
+const File = types.model('File', {
+ name: '', // the extension of the file determines its type such as cram or crai
+ size: types.optional(types.number, 0),
+});
+
+const Run = types.model('Run', {
+ id: '',
+ sample: '',
+ alignment: '',
+ sex: '',
+ center: '',
+ release: '',
+ files: types.optional(types.array(File), []),
+});
+
+const Consent = types.model('Consent', {
+ id: '', // such as 'phs00001.v1.p1.c1
+ name: '', // 'code --- qualifier'
+ runs: types.optional(types.array(Run), []),
+});
+
+const Token = types.model('Token', {
+ id: '',
+ expireAt: '',
+ sessionId: '',
+ username: '',
+});
+
+const Study = types.model('Study', {
+ id: '',
+ name: '',
+ consents: types.optional(types.array(Consent), []),
+});
+
+const Session = types.model('Session', {
+ id: types.identifier,
+ title: '',
+ studies: types.optional(types.array(Study), []),
+ tokens: types.optional(types.array(Token), []),
+});
+
+// eslint-disable-next-line no-unused-vars
+function createNewSession(raw) {
+ return (
+ Promise.resolve()
+ // .then(() => createSession(raw))
+ .then(result => Session.create(result))
+ );
+}
+
+export { Session, createNewSession };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/selections/FilesSelection.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/selections/FilesSelection.js
new file mode 100644
index 0000000000..d81e02adbf
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/selections/FilesSelection.js
@@ -0,0 +1,135 @@
+/*
+ * 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, applySnapshot } from 'mobx-state-tree';
+
+// TODO: Improve file model
+// const File2 = types.model('File2', {
+// name: '',
+// size: types.optional(types.number, 0),
+// });
+
+// TODO this should have been named 'Run'
+const File = types.model('File', {
+ id: types.identifier,
+ name: '',
+ description: types.maybeNull(types.string),
+ accessStatus: '',
+});
+
+// TODO this should have been named 'RunsSelection'
+const FilesSelection = types
+ .model('FilesSelection', {
+ files: types.optional(types.map(File), {}),
+ })
+ .actions(self => ({
+ setFile(file) {
+ self.files.set(file.id, file);
+ },
+ deleteFile(id) {
+ self.files.delete(id);
+ },
+ cleanup() {
+ self.files.clear();
+ },
+ setFiles(filesMapSnapshot) {
+ applySnapshot(self.files, filesMapSnapshot);
+ },
+ }))
+ .views(self => ({
+ hasFile(id) {
+ return self.files.has(id);
+ },
+ get empty() {
+ return self.files.size === 0;
+ },
+ get count() {
+ return self.files.size;
+ },
+ get studiesCount() {
+ const studyIdMap = {};
+ self.files.forEach(entry => {
+ studyIdMap[entry.studyId] = true;
+ });
+
+ return _.size(studyIdMap);
+ },
+ studiesCountByStatus: state => {
+ const studyIdMap = {};
+ self.files.forEach(entry => {
+ if (entry.accessStatus === state) studyIdMap[entry.studyId] = true;
+ });
+
+ return _.size(studyIdMap);
+ },
+ studiesCountByNotStatus: state => {
+ const studyIdMap = {};
+ self.files.forEach(entry => {
+ if (entry.accessStatus !== state) studyIdMap[entry.studyId] = true;
+ });
+
+ return _.size(studyIdMap);
+ },
+ get fileNames() {
+ const names = [];
+ self.files.forEach(entry => {
+ names.push(entry.id);
+ });
+
+ return names;
+ },
+ groupByStudy: () => {
+ const studyIdMap = {};
+ self.files.forEach(entry => {
+ const values = studyIdMap[entry.studyId];
+ if (_.isArray(values)) {
+ values.push(entry);
+ } else {
+ studyIdMap[entry.studyId] = [entry];
+ }
+ });
+
+ return studyIdMap;
+ },
+ groupNotApprovedByStudy: () => {
+ const studyIdMap = {};
+ self.files.forEach(entry => {
+ if (entry.accessStatus === 'approved') return;
+ const values = studyIdMap[entry.studyId];
+ if (_.isArray(values)) {
+ values.push(entry);
+ } else {
+ studyIdMap[entry.studyId] = [entry];
+ }
+ });
+
+ return studyIdMap;
+ },
+ countByStatus: state => {
+ let counter = 0;
+ self.files.forEach(file => {
+ if (file.accessStatus === state) counter += 1;
+ });
+
+ return counter;
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.filesSelection = FilesSelection.create({}, appContext);
+}
+
+export { FilesSelection, registerContextItems };
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
new file mode 100644
index 0000000000..05c69766ee
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudiesStore.js
@@ -0,0 +1,127 @@
+/*
+ * 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';
+import { types } from 'mobx-state-tree';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils';
+
+import { getStudies, createStudy } from '../../helpers/api';
+import { categories } from './categories';
+import { Study } from './Study';
+import { StudyStore } from './StudyStore';
+
+// ==================================================================
+// StudiesStore
+// ==================================================================
+const StudiesStore = BaseStore.named('StudiesStore')
+ .props({
+ category: '',
+ studies: types.optional(types.map(Study), {}),
+ studyStores: types.optional(types.map(StudyStore), {}),
+ tickPeriod: 300 * 1000, // 5 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const studies = await getStudies(self.category);
+ // We try to preserve existing studies data and merge the new data instead
+ // We could have used self.studies.replace(), but it will do clear() then merge()
+ self.runInAction(() => {
+ consolidateToMap(self.studies, studies, (exiting, newItem) => {
+ exiting.setStudy(newItem);
+ });
+ });
+ },
+
+ addStudy(rawStudy) {
+ const id = rawStudy.id;
+ const previous = self.studies.get(id);
+
+ if (!previous) {
+ self.studies.put(rawStudy);
+ } else {
+ previous.setStudy(rawStudy);
+ }
+ },
+
+ getStudyStore: studyId => {
+ let entry = self.studyStores.get(studyId);
+ if (!entry) {
+ // Lazily create the store
+ self.studyStores.set(studyId, StudyStore.create({ studyId }));
+ entry = self.studyStores.get(studyId);
+ }
+
+ return entry;
+ },
+
+ async createStudy(study) {
+ const result = await createStudy({ ...study, uploadLocationEnabled: true });
+ self.runInAction(() => {
+ self.addStudy(result);
+ });
+ const resultStudy = self.getStudy(result.id);
+
+ return resultStudy;
+ },
+
+ cleanup: () => {
+ self.studies.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.studies.size === 0;
+ },
+
+ get total() {
+ return self.studies.size;
+ },
+
+ get list() {
+ const result = [];
+ self.studies.forEach(study => result.push(study));
+
+ return _.reverse(_.sortBy(result, ['createdAt', 'name']));
+ },
+
+ hasStudy(id) {
+ return self.studies.has(id);
+ },
+
+ getStudy(id) {
+ return self.studies.get(id);
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.studiesStoresMap = {
+ // TODO - we should be using ids when calling the backend but the backend needs fixing too since it does not support ids yet
+ [categories.myStudies.id]: StudiesStore.create({ category: categories.myStudies.name }),
+ [categories.organization.id]: StudiesStore.create({ category: categories.organization.name }),
+ [categories.openData.id]: StudiesStore.create({ category: categories.openData.name }),
+ };
+}
+
+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
new file mode 100644
index 0000000000..1fd24be105
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/Study.js
@@ -0,0 +1,79 @@
+/*
+ * 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, applySnapshot } from 'mobx-state-tree';
+
+import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier';
+
+import { StudyFilesStore } from './StudyFilesStore';
+import { StudyPermissionsStore } from './StudyPermissionsStore';
+import { categories } from './categories';
+
+// ==================================================================
+// Study
+// ==================================================================
+const Study = types
+ .model('Study', {
+ id: types.identifier,
+ rev: types.maybe(types.number),
+ name: '',
+ category: '',
+ projectId: '',
+ access: types.maybe(types.string),
+ resources: types.optional(types.array(types.model({ arn: types.string })), []),
+ description: types.maybeNull(types.string),
+ uploadLocationEnabled: false,
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ filesStore: types.maybe(StudyFilesStore),
+ permissionsStore: types.maybe(StudyPermissionsStore),
+ })
+ .actions(self => ({
+ setStudy(rawStudy) {
+ // Note: if you have partial data vs full data, you need to replace the applySnapshot() with
+ // the appropriate logic
+ applySnapshot(self, rawStudy);
+ },
+
+ getFilesStore() {
+ if (!self.filesStore) {
+ self.filesStore = StudyFilesStore.create({ studyId: self.id });
+ }
+ return self.filesStore;
+ },
+
+ getPermissionsStore() {
+ if (!self.permissionsStore) {
+ self.permissionsStore = StudyPermissionsStore.create({ studyId: self.id });
+ }
+ return self.permissionsStore;
+ },
+ }))
+
+ // eslint-disable-next-line no-unused-vars
+ .views(self => ({
+ // add view methods here
+ get isOpenDataStudy() {
+ return self.category === categories.openData.name; // TODO the backend should really send an id and not a name
+ },
+
+ get isOrganizationStudy() {
+ return self.category === categories.organization.name; // TODO the backend should really send an id and not a name
+ },
+ }));
+
+export { Study }; // eslint-disable-line import/prefer-default-export
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyFilesStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyFilesStore.js
new file mode 100644
index 0000000000..904921f879
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyFilesStore.js
@@ -0,0 +1,87 @@
+/*
+ * 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';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { listStudyFiles } from '../../helpers/api';
+
+// ==================================================================
+// StudyFile
+// ==================================================================
+const StudyFile = types.model('StudyFile', {
+ filename: types.string,
+ size: types.integer,
+ lastModified: types.Date,
+});
+
+// ==================================================================
+// StudyFiles
+// ==================================================================
+const StudyFilesStore = BaseStore.named('StudyFilesStore')
+ .props({
+ studyId: '',
+ files: types.array(StudyFile),
+ tickPeriod: 5 * 1000, // 5 seconds
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ // Retrieve files
+ let files = await listStudyFiles(self.studyId);
+
+ // Determine which files were added or removed
+ const comparator = (fileA, fileB) => fileA.filename === fileB.filename;
+ const removed = _.differenceWith(self.files, files, comparator);
+ const added = _.differenceWith(files, self.files, comparator);
+
+ // Only update store when needed to avoid unnecessary re-rendering
+ if (removed.length !== 0 || added.length !== 0) {
+ // Sort files by name and cast lastModified as Date()
+ files = files
+ .sort((fileA, fileB) => fileA.filename.localeCompare(fileB.filename))
+ .map(file => ({
+ ...file,
+ lastModified: new Date(file.lastModified),
+ }));
+
+ // Update store
+ self.runInAction(() => {
+ self.files.replace(files);
+ });
+ }
+ },
+
+ cleanup: () => {
+ self.files.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.files.length === 0;
+ },
+ }));
+
+// Note: Do NOT register this in the global context, if you want to gain access to an instance
+// use study.getFilesStore()
+export { StudyFilesStore }; // eslint-disable-line import/prefer-default-export
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
new file mode 100644
index 0000000000..2a401f263f
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissions.js
@@ -0,0 +1,40 @@
+/*
+ * 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 { types } from 'mobx-state-tree';
+
+import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier';
+
+// ==================================================================
+// Study Permissions
+// ==================================================================
+const StudyPermissions = types
+ .model('StudyPermissions', {
+ id: types.identifier,
+ adminUsers: types.array(UserIdentifier),
+ readonlyUsers: types.array(UserIdentifier),
+ createdAt: '',
+ createdBy: UserIdentifier,
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ })
+ .views(_self => ({
+ get userTypes() {
+ return ['admin', 'readonly'];
+ },
+ }));
+
+export { StudyPermissions };
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
new file mode 100644
index 0000000000..8934dc93ce
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyPermissionsStore.js
@@ -0,0 +1,76 @@
+/*
+ * 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';
+import { types } from 'mobx-state-tree';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { getStudyPermissions, updateStudyPermissions } from '../../helpers/api';
+import { StudyPermissions } from './StudyPermissions';
+
+// ==================================================================
+// StudyStore
+// ==================================================================
+const StudyPermissionsStore = BaseStore.named('StudyPermissionsStore')
+ .props({
+ studyId: types.identifier,
+ studyPermissions: types.maybe(StudyPermissions),
+ tickPeriod: 300 * 1000, // 5 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ doLoad: async () => {
+ const newPermissions = await getStudyPermissions(self.studyId);
+ if (!self.studyPermissions || !_.isEqual(self.studyPermissions, newPermissions)) {
+ self.runInAction(() => {
+ self.studyPermissions = newPermissions;
+ });
+ }
+ },
+
+ cleanup: () => {
+ superCleanup();
+ },
+
+ update: async selectedUsers => {
+ const updateRequest = { usersToAdd: [], usersToRemove: [] };
+
+ self.studyPermissions.userTypes.forEach(type => {
+ const userToRequestFormat = user => ({ principalIdentifier: user, permissionLevel: type });
+
+ // Set selected users as "usersToAdd" (API is idempotent)
+ updateRequest.usersToAdd.push(...selectedUsers[type].map(userToRequestFormat));
+
+ // Set removed users as "usersToRemove"
+ updateRequest.usersToRemove.push(
+ ..._.differenceWith(self.studyPermissions[`${type}Users`], selectedUsers[type], _.isEqual).map(
+ userToRequestFormat,
+ ),
+ );
+ });
+
+ // Perform update and reload store
+ await updateStudyPermissions(self.studyId, updateRequest);
+ await self.load();
+ },
+ };
+ });
+
+export { StudyPermissionsStore };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyStore.js
new file mode 100644
index 0000000000..dc01377bed
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/StudyStore.js
@@ -0,0 +1,58 @@
+/*
+ * 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 { getStudy } from '../../helpers/api';
+
+// ==================================================================
+// StudyStore
+// ==================================================================
+const StudyStore = BaseStore.named('StudyStore')
+ .props({
+ studyId: '',
+ tickPeriod: 300 * 1000, // 5 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const parent = getParent(self, 2);
+ const rawStudy = await getStudy(self.studyId);
+ parent.addStudy(rawStudy);
+ },
+
+ cleanup: () => {
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get study() {
+ const parent = getParent(self, 2);
+ const w = parent.getStudy(self.studyId);
+ return w;
+ },
+ }));
+
+// Note: Do NOT register this in the global context, if you want to gain access to an instance
+// use studiesStore.getStudyStore()
+// eslint-disable-next-line import/prefer-default-export
+export { StudyStore };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/categories.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/categories.js
new file mode 100644
index 0000000000..5417116307
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/studies/categories.js
@@ -0,0 +1,28 @@
+/*
+ * 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';
+
+const categories = {
+ myStudies: { name: 'My Studies', id: 'my-studies' },
+ organization: { name: 'Organization', id: 'organization' },
+ openData: { name: 'Open Data', id: 'open-data' },
+};
+
+function getCategoryById(id) {
+ return _.find(categories, ['id', id]);
+}
+
+export { categories, getCategoryById }; // eslint-disable-line import/prefer-default-export
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/user-roles/UserRole.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/user-roles/UserRole.js
new file mode 100644
index 0000000000..8759c38932
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/user-roles/UserRole.js
@@ -0,0 +1,48 @@
+/*
+ * 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, applySnapshot } from 'mobx-state-tree';
+
+import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier';
+
+// ==================================================================
+// UserRole
+// ==================================================================
+const UserRole = types
+ .model('UserRole', {
+ id: types.identifier,
+ rev: types.maybe(types.number),
+ description: '',
+ userType: '',
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ })
+ .actions(self => ({
+ setUserRole(rawUserRole) {
+ // Note: if you have partial data vs full data, you need to replace the applySnapshot() with
+ // the appropriate logic
+ applySnapshot(self, rawUserRole);
+ },
+ }))
+
+ // eslint-disable-next-line no-unused-vars
+ .views(self => ({
+ // add view methods here
+ }));
+
+// eslint-disable-next-line import/prefer-default-export
+export { UserRole };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/user-roles/UserRolesStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/user-roles/UserRolesStore.js
new file mode 100644
index 0000000000..d3f78ada76
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/user-roles/UserRolesStore.js
@@ -0,0 +1,90 @@
+/*
+ * 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';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils';
+import { getUserRoles } from '../../helpers/api';
+import { UserRole } from './UserRole';
+
+// ==================================================================
+// UserRolesStore
+// ==================================================================
+const UserRolesStore = BaseStore.named('UserRolesStore')
+ .props({
+ userRoles: types.optional(types.map(UserRole), {}),
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const userRoles = (await getUserRoles()) || [];
+ self.runInAction(() => {
+ consolidateToMap(self.userRoles, userRoles, (exiting, newItem) => {
+ exiting.setUserRole(newItem);
+ });
+ });
+ },
+
+ cleanup: () => {
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get list() {
+ const result = [];
+ // converting map self.users to result array
+ self.userRoles.forEach(userRole => result.push(userRole));
+ return result;
+ },
+ get dropdownOptions() {
+ const result = [];
+ // converting map self.users to result array
+ self.userRoles.forEach(userRole => {
+ const role = {};
+ role.key = userRole.id;
+ role.value = userRole.id;
+ role.text = userRole.id;
+ result.push(role);
+ });
+ return result;
+ },
+
+ isInternalUser(userRoleId) {
+ return _.toLower(self.getUserType(userRoleId)) === 'internal';
+ },
+
+ isInternalGuest(userRoleId) {
+ return _.toLower(userRoleId) === 'internal-guest';
+ },
+
+ getUserType(userRoleId) {
+ const found = self.userRoles.get(userRoleId);
+ return found ? found.userType : '';
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.userRolesStore = UserRolesStore.create({}, appContext);
+}
+
+export { UserRolesStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/User.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/User.js
new file mode 100644
index 0000000000..c41808b29a
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/User.js
@@ -0,0 +1,195 @@
+/*
+ * 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 _ from 'lodash';
+import { storage, removeNulls } from '@aws-ee/base-ui/dist/helpers/utils';
+import { aesGcmEncrypt, aesGcmDecrypt } from '../../helpers/crypto';
+import localStorageKeys from '../constants/local-storage-keys';
+
+const User = types
+ .model('User', {
+ firstName: types.maybeNull(types.optional(types.string, '')),
+ lastName: types.maybeNull(types.optional(types.string, '')),
+ isAdmin: types.optional(types.boolean, false),
+ username: '',
+ ns: types.maybeNull(types.optional(types.string, '')),
+ email: '',
+ userType: '',
+ authenticationProviderId: '', // Id of the authentication provider this user is authenticated against (such as internal, cognito auth provider id etc)
+ identityProviderName: types.maybeNull(types.optional(types.string, '')), // Name of the identity provider this user belongs to (such as Identity Provider Id in cognito user pool in case of Federation etc)
+ status: 'active',
+ rev: 0,
+ userRole: '',
+ projectId: types.array(types.string, []), // TODO this property should be named projectIds
+ isExternalUser: types.optional(types.boolean, false), // TODO we need to consider have this a derived property
+ encryptedCreds: types.maybeNull(types.string),
+ applyReason: '',
+ })
+ .actions(self => ({
+ runInAction(fn) {
+ return fn();
+ },
+ async setEncryptedCreds(unencryptedCreds, pin) {
+ unencryptedCreds.region = unencryptedCreds.region || 'us-east-1';
+ const encryptedCreds = await aesGcmEncrypt(JSON.stringify(unencryptedCreds), pin);
+ // TODO Should we store the pin in the session?
+ storage.setItem(localStorageKeys.pinToken, pin);
+ self.runInAction(() => {
+ self.encryptedCreds = encryptedCreds;
+ });
+ },
+ async clearEncryptedCreds() {
+ self.runInAction(() => {
+ self.encryptedCreds = undefined;
+ });
+ },
+ setUser(rawUser) {
+ removeNulls(rawUser);
+ self.firstName = rawUser.firstName || self.firstName || '';
+ self.lastName = rawUser.lastName || self.lastName || '';
+ self.isAdmin = rawUser.isAdmin || self.isAdmin;
+ self.isExternalUser = rawUser.isExternalUser || self.isExternalUser;
+ self.username = rawUser.username || self.username;
+ self.ns = rawUser.ns || self.ns;
+ self.email = rawUser.email || self.email;
+ self.authenticationProviderId = rawUser.authenticationProviderId || self.authenticationProviderId;
+ self.identityProviderName = rawUser.identityProviderName || self.identityProviderName;
+ self.status = rawUser.status || self.status || 'active';
+ self.createdBy = rawUser.createdBy || self.createdBy;
+ self.rev = rawUser.rev || self.rev || 0;
+ self.userRole = rawUser.userRole || self.userRole;
+ self.projectId = rawUser.projectId || self.projectId || [];
+ self.encryptedCreds = rawUser.encryptedCreds || self.encryptedCreds;
+ self.applyReason = rawUser.applyReason || self.applyReason || '';
+ // we don't update the other fields because they are being populated by a separate store
+ },
+ }))
+ .views(self => ({
+ get displayName() {
+ return `${self.firstName}`;
+ },
+
+ get longDisplayName() {
+ if (self.unknown) {
+ return `${self.username}??`;
+ }
+ const fullName = `${self.firstName} ${self.lastName}`;
+ if (self.email) {
+ return `${fullName} (${self.email})`;
+ }
+ return fullName;
+ },
+
+ get unknown() {
+ return !self.firstName && !self.lastName;
+ },
+
+ get isRootUser() {
+ return _.toLower(self.userType) === 'root';
+ },
+
+ get isActive() {
+ return _.toLower(self.status) === 'active';
+ },
+
+ get isInternalGuest() {
+ return self.userRole === 'internal-guest';
+ },
+
+ get isExternalGuest() {
+ return self.userRole === 'guest';
+ },
+
+ get isInternalResearcher() {
+ return self.userRole === 'researcher';
+ },
+
+ get isExternalResearcher() {
+ return self.userRole === 'external-researcher';
+ },
+
+ get isSystem() {
+ const identifier = self.identifier;
+ return identifier.username === '_system_';
+ },
+
+ isSame({ username, ns }) {
+ return self.username === username && self.ns === ns;
+ },
+
+ get id() {
+ return self.identifierStr;
+ },
+
+ get identifier() {
+ return { username: self.username, ns: self.ns };
+ },
+
+ get identifierStr() {
+ return JSON.stringify(self.identifier);
+ },
+
+ get hasProjects() {
+ return !_.isEmpty(self.projectId);
+ },
+
+ get hasCredentials() {
+ return self.isExternalResearcher && !_.isEmpty(self.encryptedCreds) && self.encryptedCreds !== 'N/A';
+ },
+
+ // TODO - this should not be a view, it should be moved to the actions section
+ // - a better approach is to do unencryptedCreds as a view but then
+ // have the call to store the pin in a separate method that is in the action
+ async unencryptedCreds(pin) {
+ try {
+ const creds = JSON.parse(await aesGcmDecrypt(self.encryptedCreds, pin));
+ // TODO Should we store the pin in the session?
+ storage.setItem(localStorageKeys.pinToken, pin);
+ return creds;
+ } catch (e) {
+ throw new Error('Invalid PIN. Please try again');
+ }
+ },
+
+ // A map of high level actions that the user is allowed to perform.
+ // Example: { 'canCreateStudy': true/false, 'canCreateWorkspace': true/false }
+ //
+ // Note: actions that require a resource before the permission is determined, are NOT captured in this capability matrix.
+ get capabilities() {
+ const active = self.isActive;
+ const external = self.isExternalUser; // Either external guest or external user
+ const externalGuest = self.isExternalGuest;
+ const internalGuest = self.isInternalGuest;
+
+ const canCreateStudy = active && !external && !internalGuest;
+ const canCreateWorkspace = active && !externalGuest && !internalGuest;
+ const canSelectStudy = active && !externalGuest && !internalGuest;
+ const canViewDashboard = active && !external && !internalGuest;
+
+ return {
+ canCreateStudy,
+ canCreateWorkspace,
+ canSelectStudy,
+ canViewDashboard,
+ };
+ },
+ }));
+
+function getIdentifierObjFromId(identifierStr) {
+ return JSON.parse(identifierStr);
+}
+
+export { User, getIdentifierObjFromId };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UserStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UserStore.js
new file mode 100644
index 0000000000..6a64c56095
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UserStore.js
@@ -0,0 +1,93 @@
+/*
+ * 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';
+
+import { getUser } from '@aws-ee/base-ui/dist/helpers/api';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { User } from './User';
+
+const UserStore = BaseStore.named('UserStore')
+ .props({
+ user: types.maybe(User),
+ })
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const user = await getUser();
+ self.runInAction(() => {
+ self.user = User.create(user);
+ });
+ },
+ cleanup: () => {
+ self.user = undefined;
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return _.isEmpty(self.user);
+ },
+
+ // TODO this method should really be moved to the User model and renamed to something like projectIdOptions
+ get projectIdDropdown() {
+ const result = _.map(self.user.projectId, id => ({ key: id, value: id, text: id }));
+ return result;
+ },
+
+ get cloneUser() {
+ let result = {};
+ const {
+ username,
+ authenticationProviderId,
+ identityProviderName,
+ firstName,
+ lastName,
+ email,
+ isAdmin,
+ status,
+ userRole,
+ rev,
+ projectId,
+ } = self.user;
+ result = {
+ username,
+ authenticationProviderId,
+ identityProviderName,
+ firstName,
+ lastName,
+ email,
+ isAdmin,
+ status,
+ rev,
+ userRole,
+ projectId,
+ };
+ return result;
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.userStore = UserStore.create({}, appContext);
+}
+
+export { UserStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UsersStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UsersStore.js
new file mode 100644
index 0000000000..bb516567fc
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/users/UsersStore.js
@@ -0,0 +1,236 @@
+/*
+ * 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 { applySnapshot, detach, getSnapshot, types } from 'mobx-state-tree';
+import { addUser, updateUser, getUsers } from '@aws-ee/base-ui/dist/helpers/api';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { deleteUser, addUsers, updateUserApplication } from '../../helpers/api';
+import { User } from './User';
+
+const UsersStore = BaseStore.named('UsersStore')
+ .props({
+ users: types.optional(types.map(User), {}),
+ tickPeriod: 60 * 1000, // 1 minute
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const users = (await getUsers()) || [];
+ self.runInAction(() => {
+ users.forEach(user => {
+ const userModel = User.create(user);
+ const previous = self.users.get(userModel.id);
+ if (!previous) {
+ self.users.set(userModel.id, userModel);
+ } else {
+ applySnapshot(previous, user);
+ }
+ });
+ });
+ },
+
+ cleanup: () => {
+ self.users.clear();
+ superCleanup();
+ },
+ addUser: async user => {
+ // if username is not specified then pass email as username
+ const username = user.username || user.email;
+ const addedUser = await addUser({ ...user, username });
+ self.runInAction(() => {
+ // Added newly created user to users map
+ const addedUserModel = User.create(addedUser);
+ self.users.set(addedUserModel.id, addedUserModel);
+ });
+ },
+ addUsers: async users => {
+ await addUsers(users);
+ },
+ updateUser: async user => {
+ const updatedUser = await updateUser(user);
+ const userModel = User.create(updatedUser);
+ const previousUser = self.users.get(userModel.id);
+ applySnapshot(previousUser, updatedUser);
+ },
+ updateUserApplication: async user => {
+ const res = await updateUserApplication(user);
+ return res;
+ },
+ deleteUser: async user => {
+ const id = user && user.id ? user.id : User.create(user).id;
+ await deleteUser(user);
+ const deletedUser = self.users.get(id);
+ self.runInAction(() => {
+ // Detaching here instead of deleting because the ReactTable component in UsersList somehow still fires
+ // "Cell" component rendering after the user is deleted from the map
+ // That results in the following error
+ // "You are trying to read or write to an object that is no longer part of a state tree. (Object type: 'User', Path upon death: "
+ detach(deletedUser);
+ // self.users.delete(id);
+ });
+ // return deletedUser;
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.users.size === 0;
+ },
+
+ get hasNonRootAdmins() {
+ const nonRootAdmins = _.filter(self.list, user => user.isAdmin && !user.isRootUser);
+ return !_.isEmpty(nonRootAdmins);
+ },
+
+ get hasNonRootUsers() {
+ return !_.isEmpty(self.nonRootUsers);
+ },
+
+ get nonRootUsers() {
+ return _.filter(self.list, user => !user.isRootUser);
+ },
+
+ get list() {
+ const result = [];
+ // converting map self.users to result array
+ self.users.forEach(user => result.push(user));
+ return result;
+ },
+
+ asSelectOptions({ nonClearables = [] } = {}) {
+ const result = [];
+ self.users.forEach(user =>
+ result.push({
+ value: user.id,
+ label: user.longDisplayName,
+ clearableValue: !nonClearables.includes(user.id),
+ }),
+ );
+ return result;
+ },
+
+ asDropDownOptions({ status = 'active' } = {}) {
+ const result = [];
+ self.users.forEach(user => {
+ if (user.status === status) {
+ result.push({
+ key: user.id,
+ value: user.id,
+ text: user.longDisplayName,
+ });
+ }
+ });
+ return result;
+ },
+
+ asUserObject(userIdentifier) {
+ if (userIdentifier) {
+ const user = self.users.get(userIdentifier.id);
+ return user || User.create({ username: userIdentifier.username, ns: userIdentifier.ns }); // this could happen in the employee is no longer active or with the company
+ }
+ return undefined;
+ },
+
+ asUserObjects(userIdentifiers = []) {
+ const result = [];
+ userIdentifiers.forEach(userIdentifier => {
+ if (userIdentifier) {
+ const user = self.users.get(userIdentifier.id);
+ if (user) {
+ result.push(user);
+ } else {
+ result.push(User.create(getSnapshot(userIdentifier)));
+ } // this could happen in the employee is no longer active or with the company
+ }
+ });
+
+ return result;
+ },
+ }));
+
+// function registerModels(globals) {
+// globals.usersStore = UsersStore.create({}, globals);
+// }
+
+// export { UsersStore, toUserIds, toLongNames, toLongName, registerModels };
+
+// const UsersStore = BaseUsersStore.named('UsersStore')
+// .actions(self => {
+// return {
+// addUser: async user => {
+// const addedUser = await addUser(user);
+// self.runInAction(() => {
+// // Added newly created user to users map
+// const addedUserModel = User.create(addedUser);
+// self.users.set(addedUserModel.id, addedUserModel);
+// });
+// },
+// updateUser: async user => {
+// const updatedUser = await updateUser(user);
+// const userModel = User.create(updatedUser);
+// const previousUser = self.users.get(userModel.id);
+// applySnapshot(previousUser, updatedUser);
+// },
+// addUsers: async users => {
+// await addUsers(users);
+// },
+// updateUserApplication: async user => {
+// const res = await updateUserApplication(user);
+// return res;
+// },
+// deleteUser: async user => {
+// await deleteUser(user);
+// },
+// };
+// })
+
+// .views(self => ({
+// asUserObject(userIdentifier) {
+// if (userIdentifier) {
+// const user = self.users.get(userIdentifier.id);
+// return user || User.create({ username: userIdentifier.username, ns: userIdentifier.ns });
+// }
+// return undefined;
+// },
+
+// asUserObjects(userIdentifiers = []) {
+// const result = [];
+// userIdentifiers.forEach(userIdentifier => {
+// if (userIdentifier) {
+// const user = self.users.get(userIdentifier.id);
+// if (user) {
+// result.push(user);
+// } else {
+// result.push(User.create(getSnapshot(userIdentifier)));
+// }
+// }
+// });
+
+// return result;
+// },
+// }));
+
+function registerContextItems(appContext) {
+ appContext.usersStore = UsersStore.create({}, appContext);
+}
+
+export { UsersStore, registerContextItems };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/UserApplication.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/UserApplication.js
new file mode 100644
index 0000000000..0addb655ee
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/UserApplication.js
@@ -0,0 +1,165 @@
+/*
+ * 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 { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { decorate, observable, action, runInAction } from 'mobx';
+import { Button, Header, Container, Message } from 'semantic-ui-react';
+import { 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 { getAddUserApplicationForm } from '../models/forms/AddUserApplicationForm';
+
+class UserApplication extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+
+ runInAction(() => {
+ this.logoutProcessing = false;
+ this.currentUser = props.userStore.cloneUser;
+ });
+ this.form = getAddUserApplicationForm();
+ this.form.$('email').value = this.currentUser.username;
+ }
+
+ handleLogout = action(async event => {
+ this.logoutProcessing = true;
+ event.preventDefault();
+ event.stopPropagation();
+
+ try {
+ await this.props.authentication.logout();
+ runInAction(() => {
+ this.logoutProcessing = false;
+ });
+ } catch (error) {
+ displayError(error);
+ runInAction(() => {
+ this.logoutProcessing = false;
+ });
+ }
+ });
+
+ render() {
+ let content = null;
+ if (this.currentUser.status === 'pending') {
+ content = this.renderFormSubmittedMessage();
+ } else {
+ content = this.renderAddUserPage();
+ }
+ return content;
+ }
+
+ renderAddUserPage() {
+ return (
+
+
+
+ Research Portal Application
+
+
{this.renderAddUserForm()}
+
+
+ );
+ }
+
+ renderFormSubmittedMessage() {
+ const processing = this.logoutProcessing;
+ return (
+
+
+
+ We have received your application
+ You will not have access to the portal until an administrator reviews and approves your application.
+
+ We recommend you logout and login when you have received access.
+
+ Logout
+
+
+
+
+
+ );
+ }
+
+ renderAddUserForm() {
+ const form = this.form;
+
+ return (
+
+ );
+ }
+
+ handleSubmit = async form => {
+ try {
+ const user = form.values();
+
+ this.currentUser.firstName = user.firstName;
+ this.currentUser.lastName = user.lastName;
+ this.currentUser.applyReason = user.applyReason;
+ this.currentUser.status = 'pending';
+ const updatedUser = await this.getStore().updateUserApplication(this.currentUser);
+ runInAction(() => {
+ this.currentUser.status = updatedUser.status;
+ });
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ getStore() {
+ return this.props.usersStore;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(UserApplication, {
+ logoutProcessing: observable,
+ currentUser: observable,
+});
+
+export default inject(
+ 'userStore',
+ 'usersStore',
+ 'userRolesStore',
+ 'awsAccountsStore',
+ 'indexesStore',
+ 'authentication',
+)(withRouter(observer(UserApplication)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/Accounts.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/Accounts.js
new file mode 100644
index 0000000000..d7609f8fec
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/Accounts.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 React from 'react';
+import { Tab, Segment, Container } from 'semantic-ui-react';
+import { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import AwsAccountsList from './AwsAccountsList';
+import IndexesList from './IndexesList';
+import ProjectsList from '../projects/ProjectsList';
+
+const panes = [
+ { menuItem: 'Projects', render: () => },
+ { menuItem: 'Indexes', render: () => },
+ { menuItem: 'AWS Accounts', render: () => },
+];
+
+// eslint-disable-next-line react/prefer-stateless-function
+class Accounts extends React.Component {
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+export default inject('userRolesStore', 'indexesStore', 'awsAccountsStore')(withRouter(observer(Accounts)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AddAwsAccount.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AddAwsAccount.js
new file mode 100644
index 0000000000..a7b1fbe116
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AddAwsAccount.js
@@ -0,0 +1,197 @@
+/*
+ * 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 { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { decorate, observable, action, runInAction } from 'mobx';
+import { Button, Dimmer, Header, List, Loader, Segment } from 'semantic-ui-react';
+import _ from 'lodash';
+
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
+import validate from '@aws-ee/base-ui/dist/models/forms/Validate';
+
+import { getAddAwsAccountForm, getAddAwsAccountFormFields } from '../../models/forms/AddAwsAccountForm';
+
+class AddAwsAccount extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ // eslint-disable-next-line react/no-unused-state
+ role: 'guest',
+ // eslint-disable-next-line react/no-unused-state
+ status: 'active',
+ };
+
+ runInAction(() => {
+ this.formProcessing = false;
+ this.validationErrors = new Map();
+ this.awsAccount = {};
+ });
+ this.form = getAddAwsAccountForm();
+ this.addAwsAccountFormFields = getAddAwsAccountFormFields();
+ }
+
+ render() {
+ return (
+
+
+
{this.renderAddAwsAccountForm()}
+
+ );
+ }
+
+ // eslint-disable-next-line react/no-unused-state
+ handleRoleChange = (e, { value }) => this.setState({ role: value });
+
+ // eslint-disable-next-line react/no-unused-state
+ handleStatusChange = (e, { value }) => this.setState({ status: value });
+
+ renderAddAwsAccountForm() {
+ const processing = this.formProcessing;
+ const fields = this.addAwsAccountFormFields;
+ const toEditableInput = (attributeName, type = 'text') => {
+ const handleChange = action(event => {
+ event.preventDefault();
+ this.awsAccount[attributeName] = event.target.value;
+ });
+ return (
+
+
+
+ );
+ };
+
+ return (
+
+
+ Checking
+
+ {Object.keys(fields).map(field => (
+
+ {this.renderField(field, toEditableInput(field))}
+
+
+ ))}
+ {this.renderButtons()}
+
+ );
+ }
+
+ renderButtons() {
+ const processing = this.formProcessing;
+ return (
+
+
+ Add AWS Account
+
+
+ Cancel
+
+
+ );
+ }
+
+ renderField(name, component) {
+ const fields = this.addAwsAccountFormFields;
+ const explain = fields[name].explain;
+ const label = fields[name].label;
+ const hasExplain = !_.isEmpty(explain);
+ const fieldErrors = this.validationErrors.get(name);
+ const hasError = !_.isEmpty(fieldErrors);
+
+ return (
+
+
+ {hasExplain &&
{explain}
}
+
{component}
+ {hasError && (
+
+
+ {_.map(fieldErrors, fieldError => (
+
+ {fieldError}
+
+ ))}
+
+
+ )}
+
+ );
+ }
+
+ goto(pathname) {
+ const location = this.props.location;
+ const link = createLink({ location, pathname });
+ this.props.history.push(link);
+ }
+
+ handleCancel = action(event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.formProcessing = false;
+ this.goto('/accounts');
+ });
+
+ handleSubmit = action(async () => {
+ this.formProcessing = true;
+ try {
+ // Perform client side validations first
+ const validationResult = await validate(this.awsAccount, this.addAwsAccountFormFields);
+ // if there are any client side validation errors then do not attempt to make API call
+ if (validationResult.fails()) {
+ runInAction(() => {
+ this.validationErrors = validationResult.errors;
+ this.formProcessing = false;
+ });
+ } else {
+ // There are no client side validation errors so ask the store to add user (which will make API call to server to add the user)
+
+ await this.props.awsAccountsStore.addAwsAccount(this.awsAccount);
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ this.goto('/accounts');
+ }
+ } catch (error) {
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ displayError(error);
+ }
+ });
+
+ getStore() {
+ return this.props.usersStore;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(AddAwsAccount, {
+ formProcessing: observable,
+ user: observable,
+ validationErrors: observable,
+});
+export default inject('usersStore', 'awsAccountsStore')(withRouter(observer(AddAwsAccount)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AddIndex.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AddIndex.js
new file mode 100644
index 0000000000..5b8d801769
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AddIndex.js
@@ -0,0 +1,213 @@
+/*
+ * 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 { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { decorate, observable, action, runInAction } from 'mobx';
+import { Button, Dimmer, Header, List, Loader, Dropdown, Segment } from 'semantic-ui-react';
+import _ from 'lodash';
+
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
+import validate from '@aws-ee/base-ui/dist/models/forms/Validate';
+
+import { getAddIndexForm, getAddIndexFormFields } from '../../models/forms/AddIndexForm';
+
+class AddIndex extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ // eslint-disable-next-line react/no-unused-state
+ role: 'guest',
+ // eslint-disable-next-line react/no-unused-state
+ status: 'active',
+ awsAccountId: '',
+ };
+
+ runInAction(() => {
+ this.formProcessing = false;
+ this.validationErrors = new Map();
+ this.index = {};
+ });
+ this.form = getAddIndexForm();
+ this.addIndexFormFields = getAddIndexFormFields();
+ }
+
+ goto(pathname) {
+ const location = this.props.location;
+ const link = createLink({ location, pathname });
+ this.props.history.push(link);
+ }
+
+ render() {
+ return (
+
+
+
{this.renderAddIndexForm()}
+
+ );
+ }
+
+ // eslint-disable-next-line react/no-unused-state
+ handleRoleChange = (e, { value }) => this.setState({ role: value });
+
+ // eslint-disable-next-line react/no-unused-state
+ handleStatusChange = (e, { value }) => this.setState({ status: value });
+
+ renderAddIndexForm() {
+ const processing = this.formProcessing;
+ const fields = this.addIndexFormFields;
+ const toEditableInput = (attributeName, type = 'text') => {
+ const handleChange = action(event => {
+ event.preventDefault();
+ this.index[attributeName] = event.target.value;
+ });
+ return (
+
+
+
+ );
+ };
+
+ return (
+
+
+ Checking
+
+ {this.renderField('id', toEditableInput('id', 'id'))}
+
+ {this.renderField('awsAccountId')}
+ {this.renderAwsAccountSelection()}
+
+ {this.renderField('description', toEditableInput('description', 'description'))}
+ {this.renderButtons()}
+
+ );
+ }
+
+ renderButtons() {
+ const processing = this.formProcessing;
+ return (
+
+
+ Add Index
+
+
+ Cancel
+
+
+ );
+ }
+
+ renderAwsAccountSelection() {
+ const awsAccountIdOption = this.props.awsAccountsStore.dropdownOptions;
+ return (
+
+ );
+ }
+
+ handleAwsAccountSelection = (e, { value }) => this.setState({ awsAccountId: value });
+
+ renderField(name, component) {
+ const fields = this.addIndexFormFields;
+ const explain = fields[name].explain;
+ const label = fields[name].label;
+ const hasExplain = !_.isEmpty(explain);
+ const fieldErrors = this.validationErrors.get(name);
+ const hasError = !_.isEmpty(fieldErrors);
+
+ return (
+
+
+ {hasExplain &&
{explain}
}
+
{component}
+ {hasError && (
+
+
+ {_.map(fieldErrors, fieldError => (
+
+ {fieldError}
+
+ ))}
+
+
+ )}
+
+ );
+ }
+
+ handleCancel = action(event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.formProcessing = false;
+ this.goto('/accounts');
+ });
+
+ handleSubmit = action(async () => {
+ this.formProcessing = true;
+ try {
+ // Perform client side validations first
+ const validationResult = await validate(this.index, this.addIndexFormFields);
+ // if there are any client side validation errors then do not attempt to make API call
+ if (validationResult.fails()) {
+ runInAction(() => {
+ this.validationErrors = validationResult.errors;
+ this.formProcessing = false;
+ });
+ } else {
+ // There are no client side validation errors so ask the store to add user (which will make API call to server to add the user)
+ this.index.awsAccountId = this.state.awsAccountId;
+ await this.props.indexesStore.addIndex(this.index);
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ this.goto('/accounts');
+ }
+ } catch (error) {
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ displayError(error);
+ }
+ });
+
+ getStore() {
+ return this.props.usersStore;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(AddIndex, {
+ formProcessing: observable,
+ user: observable,
+ validationErrors: observable,
+});
+export default inject('usersStore', 'indexesStore', 'awsAccountsStore')(withRouter(observer(AddIndex)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AwsAccountsList.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AwsAccountsList.js
new file mode 100644
index 0000000000..d69a37208b
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/AwsAccountsList.js
@@ -0,0 +1,224 @@
+/*
+ * 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 { Button, Container, Header, Icon, Label, Message } from 'semantic-ui-react';
+import { withRouter } from 'react-router-dom';
+import { decorate, observable, runInAction } from 'mobx';
+import { inject, observer } from 'mobx-react';
+import ReactTable from 'react-table';
+
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import { isStoreError, isStoreLoading } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
+import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+class AwsAccountsList extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ runInAction(() => {
+ // An object that keeps track of which user is being edited
+ // Each key in the object below has key as user's unique id (/)
+ // and value as flag indicating whether to show the editor for the user
+ this.mapOfUsersBeingEdited = {};
+ this.formProcessing = false;
+ });
+ }
+
+ componentDidMount() {
+ const accountsStore = this.props.accountsStore;
+ const awsAccountsStore = this.props.awsAccountsStore;
+ swallowError(accountsStore.load());
+ swallowError(awsAccountsStore.load());
+ accountsStore.startHeartbeat();
+ awsAccountsStore.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const accountsStore = this.props.accountsStore;
+ const awsAccountsStore = this.props.awsAccountsStore;
+ accountsStore.stopHeartbeat();
+ awsAccountsStore.stopHeartbeat();
+ }
+
+ getAwsAccountsStore() {
+ const store = this.props.awsAccountsStore;
+ return store;
+ }
+
+ getAwsAccounts() {
+ const store = this.getAwsAccountsStore();
+ return store.list;
+ }
+
+ renderMain() {
+ const awsAccountsData = this.getAwsAccounts();
+ const pageSize = 5;
+ const showPagination = awsAccountsData.length > pageSize;
+ return (
+
+ {
+ const columnValue = String(row[filter.id]).toLowerCase();
+ const filterValue = filter.value.toLowerCase();
+ return columnValue.indexOf(filterValue) >= 0;
+ }}
+ columns={[
+ {
+ Header: 'Account Name',
+ accessor: 'name',
+ },
+ {
+ Header: 'AWS Account ID',
+ accessor: 'accountId',
+ },
+ {
+ Header: 'Description',
+ accessor: 'description',
+ },
+ {
+ Header: 'Role ARN',
+ accessor: 'roleArn',
+ },
+ {
+ Header: 'External ID',
+ accessor: 'externalId',
+ },
+ {
+ Header: 'VPC ID',
+ accessor: 'vpcId',
+ },
+ {
+ Header: 'Subnet ID',
+ accessor: 'subnetId',
+ },
+ {
+ Header: 'Encryption Key Arn',
+ accessor: 'encryptionKeyArn',
+ },
+ ]}
+ />
+
+ );
+ }
+
+ goto(pathname) {
+ const location = this.props.location;
+ const link = createLink({ location, pathname });
+ this.props.history.push(link);
+ }
+
+ handleAddAwsAccount = () => {
+ this.goto('/aws-accounts/add');
+ };
+
+ handleCreateAwsAccount = () => {
+ this.goto('/aws-accounts/create');
+ };
+
+ renderHeader() {
+ return (
+
+
+
+
+ AWS Accounts
+ {this.renderTotal()}
+
+
+
+ Create AWS Account
+
+
+ Add AWS Account
+
+
+ );
+ }
+
+ renderTotal() {
+ return {this.getAwsAccounts().length} ;
+ }
+
+ handleDismiss(id) {
+ const accountsStore = this.props.accountsStore;
+ accountsStore.removeItem(id);
+ this.componentDidMount();
+ }
+
+ renderCreatingAccountNotification() {
+ const accountsStore = this.props.accountsStore;
+ const pendingAccount = accountsStore.listCreatingAccount;
+ const errorAccounts = accountsStore.listErrorAccount;
+ return (
+
+ {pendingAccount.map(account => (
+
+
+ Trying to create accountID: {account.id}
+
+ ))}
+ {errorAccounts.map(account => (
+
this.handleDismiss(account.id)}>
+
+ Error happended in creating accountID: {account.id}. If the account is created, please contact follow{' '}
+
+ instruction
+ {' '}
+ to remove it. You may close this message after you make sure the account is removed.
+
+ ))}
+
+ );
+ }
+
+ render() {
+ const store = this.getAwsAccountsStore();
+ let content;
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else {
+ content = this.renderMain();
+ }
+ return (
+
+ {this.renderHeader()}
+ {this.renderCreatingAccountNotification()}
+ {content}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(AwsAccountsList, {
+ mapOfUsersBeingEdited: observable,
+ formProcessing: observable,
+});
+
+export default inject('awsAccountsStore', 'accountsStore')(withRouter(observer(AwsAccountsList)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/CreateAwsAccount.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/CreateAwsAccount.js
new file mode 100644
index 0000000000..75d464d75c
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/CreateAwsAccount.js
@@ -0,0 +1,189 @@
+/*
+ * 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 { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { decorate, observable, action, runInAction } from 'mobx';
+import { Button, Dimmer, Header, List, Loader, Segment } from 'semantic-ui-react';
+import _ from 'lodash';
+
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
+import validate from '@aws-ee/base-ui/dist/models/forms/Validate';
+
+import { getCreateAwsAccountForm, getCreateAwsAccountFormFields } from '../../models/forms/CreateAwsAccountForm';
+
+class CreateAwsAccount extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.formProcessing = false;
+ this.validationErrors = new Map();
+ this.awsAccount = {};
+ });
+ this.form = getCreateAwsAccountForm();
+ this.createAwsAccountFormFields = getCreateAwsAccountFormFields();
+ }
+
+ render() {
+ return (
+
+
+
{this.renderCreateAwsAccountForm()}
+
+ );
+ }
+
+ // eslint-disable-next-line react/no-unused-state
+ handleRoleChange = (e, { value }) => this.setState({ role: value });
+
+ // eslint-disable-next-line react/no-unused-state
+ handleStatusChange = (e, { value }) => this.setState({ status: value });
+
+ renderCreateAwsAccountForm() {
+ const processing = this.formProcessing;
+ const fields = this.createAwsAccountFormFields;
+ const toEditableInput = (attributeName, type = 'text') => {
+ const handleChange = action(event => {
+ event.preventDefault();
+ this.awsAccount[attributeName] = event.target.value;
+ });
+ return (
+
+
+
+ );
+ };
+
+ return (
+
+
+ Checking
+
+ {Object.keys(fields).map(field => (
+
+ {this.renderField(field, toEditableInput(field))}
+
+
+ ))}
+ {this.renderButtons()}
+
+ );
+ }
+
+ renderButtons() {
+ const processing = this.formProcessing;
+ return (
+
+
+ Create AWS Account
+
+
+ Cancel
+
+
+ );
+ }
+
+ renderField(name, component) {
+ const fields = this.createAwsAccountFormFields;
+ const explain = fields[name].explain;
+ const label = fields[name].label;
+ const hasExplain = !_.isEmpty(explain);
+ const fieldErrors = this.validationErrors.get(name);
+ const hasError = !_.isEmpty(fieldErrors);
+
+ return (
+
+
+ {hasExplain &&
{explain}
}
+
{component}
+ {hasError && (
+
+
+ {_.map(fieldErrors, fieldError => (
+
+ {fieldError}
+
+ ))}
+
+
+ )}
+
+ );
+ }
+
+ goto(pathname) {
+ const location = this.props.location;
+ const link = createLink({ location, pathname });
+ this.props.history.push(link);
+ }
+
+ handleCancel = action(event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.formProcessing = false;
+ this.goto('/accounts');
+ });
+
+ handleSubmit = action(async () => {
+ this.formProcessing = true;
+ try {
+ // Perform client side validations first
+ const validationResult = await validate(this.awsAccount, this.createAwsAccountFormFields);
+ // if there are any client side validation errors then do not attempt to make API call
+ if (validationResult.fails()) {
+ runInAction(() => {
+ this.validationErrors = validationResult.errors;
+ this.formProcessing = false;
+ });
+ } else {
+ // There are no client side validation errors so ask the store to add user (which will make API call to server to add the user)
+ await this.props.awsAccountsStore.createAwsAccount(this.awsAccount);
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ this.goto('/accounts');
+ }
+ } catch (error) {
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ displayError(error);
+ }
+ });
+
+ getStore() {
+ return this.props.usersStore;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(CreateAwsAccount, {
+ formProcessing: observable,
+ user: observable,
+ validationErrors: observable,
+});
+export default inject('usersStore', 'awsAccountsStore')(withRouter(observer(CreateAwsAccount)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/IndexesList.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/IndexesList.js
new file mode 100644
index 0000000000..5e4c3e535c
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/accounts/IndexesList.js
@@ -0,0 +1,132 @@
+/*
+ * 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 { Button, Container, Header, Icon, Label } from 'semantic-ui-react';
+import { withRouter } from 'react-router-dom';
+import { inject, observer } from 'mobx-react';
+import ReactTable from 'react-table';
+
+import { isStoreError, isStoreLoading } from '@aws-ee/base-ui/dist/models/BaseStore';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
+import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+
+class IndexesList extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ }
+
+ getIndexesStore() {
+ const store = this.props.indexesStore;
+ store.load();
+ return store;
+ }
+
+ getIndexes() {
+ const store = this.getIndexesStore();
+ return store.list;
+ }
+
+ renderMain() {
+ const indexesData = this.getIndexes();
+ const pageSize = indexesData.length;
+ const pagination = indexesData.length > pageSize;
+ return (
+
+ {
+ const columnValue = String(row[filter.id]).toLowerCase();
+ const filterValue = filter.value.toLowerCase();
+ return columnValue.indexOf(filterValue) >= 0;
+ }}
+ columns={[
+ {
+ Header: 'Index Name',
+ accessor: 'id',
+ },
+ {
+ Header: 'AWS Account',
+ id: 'awsAccountId',
+ accessor: row => this.props.awsAccountsStore.getNameForAccountId(row.awsAccountId),
+ },
+ {
+ Header: 'Description',
+ accessor: 'description',
+ },
+ ]}
+ />
+
+
+ );
+ }
+
+ goto(pathname) {
+ const location = this.props.location;
+ const link = createLink({ location, pathname });
+ this.props.history.push(link);
+ }
+
+ handleAddIndex = () => {
+ this.goto('/indexes/add');
+ };
+
+ renderHeader() {
+ return (
+
+
+
+
+ Indexes
+ {this.renderTotal()}
+
+
+
+ Add Index
+
+
+ );
+ }
+
+ renderTotal() {
+ return {this.getIndexes().length} ;
+ }
+
+ render() {
+ const store = this.getIndexesStore();
+ let content;
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else {
+ content = this.renderMain();
+ }
+ return (
+
+ {this.renderHeader()}
+ {content}
+
+ );
+ }
+}
+
+export default inject('awsAccountsStore', 'indexesStore')(withRouter(observer(IndexesList)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/ComputePlatformSetup.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/ComputePlatformSetup.js
new file mode 100644
index 0000000000..29e57a097f
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/ComputePlatformSetup.js
@@ -0,0 +1,293 @@
+/*
+ * 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, action } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { Icon, Header, Segment, Button } from 'semantic-ui-react';
+import { isStoreLoading, isStoreError, isStoreEmpty } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+import UserOnboarding from '../users/UserOnboarding';
+import SelectComputePlatformStep from './SelectComputePlatformStep';
+import ConfigureComputePlatformStep from './ConfigureComputePlatformStep';
+
+// expected props
+// - onPrevious (via props)
+// - onCompleted (via props) a function is called after a call to create an environment is performed
+// - studyIds (via props) (optional) an array of the selected study ids
+// - currentStep (via props) an instance of the CurrentStep model
+// - computePlatformsStore (via injection)
+// - clientInformationStore (via injection)
+class ComputePlatformSetup extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.selectedPlatformId = undefined;
+ this.onboardingOpen = false;
+ });
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ swallowError(this.computePlatformsStore.load());
+ }
+
+ get userStore() {
+ return this.props.userStore;
+ }
+
+ get computePlatformsStore() {
+ return this.props.computePlatformsStore;
+ }
+
+ get clientInformationStore() {
+ return this.props.clientInformationStore;
+ }
+
+ get currentStep() {
+ return this.props.currentStep;
+ }
+
+ setOnboarding = value => {
+ this.onboardingOpen = value;
+ };
+
+ handleConfigureCredentials = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.setOnboarding(true);
+ };
+
+ handleSelectComputePlatform = async platformId => {
+ this.selectedPlatformId = platformId;
+ const platformsStore = this.computePlatformsStore;
+ if (!platformsStore) return;
+
+ // We start the loading of the configurations for the selected platform
+ const platformStore = platformsStore.getComputePlatformStore(platformId);
+ await platformStore.load();
+
+ // We also try to figure out the ip address and if there is an error,
+ // then that is okay, we show an empty string for the cidr field
+ const clientInformationStore = this.clientInformationStore;
+ try {
+ await clientInformationStore.load();
+ } catch (error) {
+ // ignore intentionally
+ }
+
+ window.scrollTo(0, 0);
+ runInAction(() => {
+ this.currentStep.setStep('selectComputeConfiguration');
+ });
+ };
+
+ handlePrevious = () => {
+ const currentStep = this.currentStep;
+ if (currentStep.step === 'selectComputePlatform') {
+ this.props.onPrevious();
+ return;
+ }
+
+ this.currentStep.setStep('selectComputePlatform');
+ };
+
+ handleCompleted = async environment => {
+ return this.props.onCompleted(environment);
+ };
+
+ get studyIds() {
+ return this.props.studyIds;
+ }
+
+ render() {
+ const store = this.computePlatformsStore;
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = this.renderLoadingError();
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreEmpty(store)) {
+ content = this.renderEmpty();
+ } else {
+ content = this.renderContent();
+ }
+
+ return (
+ <>
+ {content} {this.onboardingOpen && this.setOnboarding(false)} />}
+ >
+ );
+ }
+
+ renderContent() {
+ const step = this.currentStep.step;
+ const platformId = this.selectedPlatformId;
+ const studyIds = this.studyIds;
+ const user = this.userStore.user;
+ const hasProjects = user.hasProjects;
+ const isExternalResearcher = user.isExternalResearcher;
+ const canCreateWorkspace = user.capabilities.canCreateWorkspace;
+ const hasCredentials = user.hasCredentials;
+ let content = null;
+
+ if (!canCreateWorkspace) {
+ return this.renderEmpty();
+ }
+
+ if (!isExternalResearcher && !hasProjects) {
+ return this.renderMissingProjects();
+ }
+
+ // Check if external and no credentials
+ if (isExternalResearcher && !hasCredentials) {
+ return this.renderMissingCredentials();
+ }
+
+ if (step === 'selectComputePlatform') {
+ content = (
+
+ );
+ } else if (step === 'selectComputeConfiguration') {
+ content = (
+
+ );
+ }
+
+ return content;
+ }
+
+ renderLoadingError() {
+ const store = this.computePlatformsStore;
+ return (
+ <>
+
+ {this.renderButtons()}
+ >
+ );
+ }
+
+ renderEmpty() {
+ return (
+ <>
+
+
+
+ No compute platform
+
+ There are no compute platform to choose from. Your role might be restricted. Please contact your
+ administrator.
+
+
+
+ {this.renderButtons()}
+ >
+ );
+ }
+
+ renderMissingProjects() {
+ return (
+ <>
+
+
+
+ Missing association with projects
+
+ You currently do not have permissions to use any projects for the workspace. please contact your
+ administrator.
+
+
+
+ {this.renderButtons()}
+ >
+ );
+ }
+
+ renderMissingCredentials() {
+ return (
+ <>
+
+
+
+ No AWS credentials
+ To manage research workspaces, click Configure AWS Credentials.
+
+
+
+ Configure AWS Credentials
+
+
+
+ {this.renderButtons()}
+ >
+ );
+ }
+
+ renderButtons() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(ComputePlatformSetup, {
+ handleSelectComputePlatform: action,
+ handlePrevious: action,
+ handleCompleted: action,
+ setOnboarding: action,
+ studyIds: computed,
+ userStore: computed,
+ computePlatformsStore: computed,
+ clientInformationStore: computed,
+ currentStep: computed,
+ selectedPlatformId: observable,
+ onboardingOpen: observable,
+});
+
+export default inject('userStore', 'computePlatformsStore', 'clientInformationStore')(observer(ComputePlatformSetup));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/ConfigureComputePlatformStep.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/ConfigureComputePlatformStep.js
new file mode 100644
index 0000000000..f489b7f3e4
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/ConfigureComputePlatformStep.js
@@ -0,0 +1,201 @@
+/*
+ * 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 { Icon, Header, Segment, Button } from 'semantic-ui-react';
+
+import CreateInternalPlatformForm from './parts/CreateInternalPlatformForm';
+import CreateExternalPlatformForm from './parts/CreateExternalPlatformForm';
+
+// expected props
+// - onPrevious (via props)
+// - onCompleted (via props) a function is called after a call to create a workspace is performed
+// - platformId (via props)
+// - studyIds (via props)
+// - computePlatformsStore (via injection)
+// - clientInformationStore (via injection)
+// - userStore (via injection)
+// - environmentsStore (via injection)
+class ConfigureComputePlatformStep extends React.Component {
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ get platformId() {
+ return this.props.platformId;
+ }
+
+ get platformTitle() {
+ return _.get(this.computePlatformsStore.getComputePlatform(this.platformId), 'title');
+ }
+
+ get userStore() {
+ return this.props.userStore;
+ }
+
+ get clientInformationStore() {
+ return this.props.clientInformationStore;
+ }
+
+ get computePlatformsStore() {
+ return this.props.computePlatformsStore;
+ }
+
+ get environmentsStore() {
+ return this.props.environmentsStore;
+ }
+
+ get configurations() {
+ const platform = this.computePlatformsStore.getComputePlatform(this.platformId);
+ if (!platform) return [];
+ return platform.configurationsList;
+ }
+
+ get defaultCidr() {
+ // We pick the first one
+ const value = _.get(_.first(this.configurations), 'defaultCidr');
+ if (_.isUndefined(value)) return undefined; // This means that cidr should not be treated as an input
+ if (!_.isEmpty(value)) return value;
+ if (_.isEmpty(this.clientInformationStore.ipAddress)) return '';
+
+ return `${this.clientInformationStore.ipAddress}/32`;
+ }
+
+ get studyIds() {
+ return this.props.studyIds;
+ }
+
+ handlePrevious = () => {
+ if (_.isFunction(this.props.onPrevious)) this.props.onPrevious();
+ };
+
+ // eslint-disable-next-line consistent-return
+ handleCreate = async data => {
+ const studyIds = this.studyIds || [];
+ const store = this.environmentsStore;
+ const environment = await store.createEnvironment({ ...data, studyIds });
+ return this.props.onCompleted(environment);
+ };
+
+ render() {
+ // Note: we assume that whatever component that is including this component, has
+ // already loaded and verified that the computePlatformsStore has no errors
+ const configurations = this.configurations;
+ let content = null;
+
+ if (_.isEmpty(configurations)) {
+ content = this.renderEmpty();
+ } else {
+ content = this.renderContent();
+ }
+
+ return content;
+ }
+
+ renderContent() {
+ const platformId = this.platformId;
+ const configurations = this.configurations;
+ const title = this.platformTitle;
+ const defaultCidr = this.defaultCidr;
+ const isExternal = this.userStore.user.isExternalUser;
+
+ return (
+
+ {!isExternal && (
+
+ )}
+ {isExternal && (
+
+ )}
+
+ );
+ }
+
+ renderEmpty() {
+ const title = this.platformTitle;
+ return (
+ <>
+
+
+
+
+ No compute platform configurations
+
+ There are no compute platform configurations to choose from. Your role might be restricted. Please contact
+ your administrator.
+
+
+
+ {this.renderButtons()}
+ >
+ );
+ }
+
+ renderButtons({ nextDisabled = true } = {}) {
+ return (
+
+
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(ConfigureComputePlatformStep, {
+ handlePrevious: action,
+ handleCreate: action,
+ userStore: computed,
+ computePlatformsStore: computed,
+ clientInformationStore: computed,
+ platformId: computed,
+ defaultCidr: computed,
+ platformTitle: computed,
+ environmentsStore: computed,
+ studyIds: computed,
+});
+
+export default inject(
+ 'userStore',
+ 'computePlatformsStore',
+ 'clientInformationStore',
+ 'environmentsStore',
+)(observer(ConfigureComputePlatformStep));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/SelectComputePlatformStep.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/SelectComputePlatformStep.js
new file mode 100644
index 0000000000..70505e0d64
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/SelectComputePlatformStep.js
@@ -0,0 +1,226 @@
+/*
+ * 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 { Icon, Header, Segment, Button, Card, Radio, Divider } from 'semantic-ui-react';
+import c from 'classnames';
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import { isStoreLoading, isStoreError, isStoreEmpty } from '@aws-ee/base-ui/dist/models/BaseStore';
+import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+// expected props
+// - onPrevious (via props)
+// - onNext (via props) a function is called with the selected computeTypeId
+// - computePlatformsStore (via injection)
+// - userStore (via injection)
+class SelectComputePlatformStep extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.selectedPlatformId = undefined;
+ this.processing = false;
+ });
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ swallowError(this.computePlatformsStore.load());
+ }
+
+ goto(pathname) {
+ const goto = gotoFn(this);
+ goto(pathname);
+ }
+
+ get userStore() {
+ return this.props.userStore;
+ }
+
+ get computePlatformsStore() {
+ return this.props.computePlatformsStore;
+ }
+
+ handleSelectedComputeType = typeId => {
+ this.selectedPlatformId = typeId;
+ };
+
+ handlePrevious = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (_.isFunction(this.props.onPrevious)) this.props.onPrevious();
+ };
+
+ handleNext = async event => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (_.isFunction(this.props.onNext)) {
+ try {
+ this.processing = true;
+ await this.props.onNext(this.selectedPlatformId);
+ } catch (error) {
+ displayError(error);
+ } finally {
+ runInAction(() => {
+ this.processing = false;
+ });
+ }
+ }
+ };
+
+ render() {
+ const store = this.computePlatformsStore;
+ if (!store) return null;
+
+ let content;
+ if (isStoreError(store)) {
+ content = this.renderLoadingError();
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreEmpty(store)) {
+ content = this.renderEmpty();
+ } else {
+ content = this.renderContent();
+ }
+
+ return content;
+ }
+
+ renderContent() {
+ // Logic:
+ // - if external researcher and no aws creds configured yet, show a message
+ // - if guest (internal/external) show a message
+ // - if internal researcher and no project ids, show a message
+ // - else show compute types card
+ const nextDisabled = _.isUndefined(this.selectedPlatformId);
+
+ return (
+
+ {this.renderCards()}
+ {this.renderButtons({ nextDisabled })}
+
+ );
+ }
+
+ renderCards() {
+ const processing = this.processing;
+ const computeTypes = this.computePlatformsStore.list || [];
+ const isSelected = type => type.id === this.selectedPlatformId;
+ const getAttrs = type => {
+ const attrs = {};
+ if (isSelected(type)) attrs.color = 'blue';
+ if (!processing) attrs.onClick = () => this.handleSelectedComputeType(type.id);
+ return attrs;
+ };
+
+ return (
+
+ {_.map(computeTypes, type => (
+
+
+
+
+ {type.title}
+
+
+
+
+ {/* Yes, we are doing dangerouslySetInnerHTML, the content was already sanitized by showdownjs */}
+ {/* eslint-disable-next-line react/no-danger */}
+
+
+
+
+
+ ))}
+
+ );
+ }
+
+ renderLoadingError() {
+ const store = this.computePlatformsStore;
+ return (
+ <>
+
+ {this.renderButtons()}
+ >
+ );
+ }
+
+ renderEmpty() {
+ return (
+ <>
+
+
+
+ No compute platform
+
+ There are no compute platform to choose from. Your role might be restricted. Please contact your
+ administrator.
+
+
+
+ {this.renderButtons()}
+ >
+ );
+ }
+
+ renderButtons({ nextDisabled = true } = {}) {
+ const processing = this.processing;
+ return (
+
+
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(SelectComputePlatformStep, {
+ handlePrevious: action,
+ handleNext: action,
+ handleSelectedComputeType: action,
+ userStore: computed,
+ computePlatformsStore: computed,
+ processing: observable,
+ selectedPlatformId: observable,
+});
+
+export default inject('userStore', 'computePlatformsStore')(withRouter(observer(SelectComputePlatformStep)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/helpers/CurrentStep.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/helpers/CurrentStep.js
new file mode 100644
index 0000000000..890fdf4ede
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/helpers/CurrentStep.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 */
+import { types } from 'mobx-state-tree';
+
+// ==================================================================
+// CurrentStep
+// ==================================================================
+const CurrentStep = types
+ .model('CurrentStep', {
+ step: '',
+ })
+
+ .actions(self => ({
+ setStep(step) {
+ self.step = step;
+ },
+ }));
+
+export { CurrentStep };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/parts/CreateExternalPlatformForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/parts/CreateExternalPlatformForm.js
new file mode 100644
index 0000000000..200a55b18f
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/parts/CreateExternalPlatformForm.js
@@ -0,0 +1,204 @@
+/*
+ * 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 { Segment, Button, Header } from 'semantic-ui-react';
+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 { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import { awsRegion } from '@aws-ee/base-ui/dist/helpers/settings';
+
+import { getCreateExternalPlatformForm } from '../../../models/forms/CreateExternalPlatformForm';
+import SelectConfigurationCards from './SelectConfigurationCards';
+
+// expected props
+// - onPrevious (via props)
+// - onNext (via props) a function is called with the form data
+// - platformId (via props)
+// - configurations (via props)
+// - title (via props)
+// - defaultCidr (via props)
+// - clientInformationStore (via injection)
+// - userStore (via injection)
+// - usersStore (via injection)
+class CreateExternalPlatformForm extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.askForCredentials = !this.userStore.user.hasCredentials;
+ this.form = getCreateExternalPlatformForm({
+ askForCredentials: this.askForCredentials,
+ cidr: this.props.defaultCidr,
+ });
+ });
+ }
+
+ get platformId() {
+ return this.props.platformId;
+ }
+
+ get configurations() {
+ return this.props.configurations;
+ }
+
+ get userStore() {
+ return this.props.userStore;
+ }
+
+ get usersStore() {
+ return this.props.usersStore;
+ }
+
+ // eslint-disable-next-line consistent-return
+ handlePrevious = () => {
+ if (_.isFunction(this.props.onPrevious)) return this.props.onPrevious();
+ };
+
+ // eslint-disable-next-line consistent-return
+ handleNext = async form => {
+ const data = { ...form.values(), params: {}, platformId: this.platformId };
+
+ // We pick the mutable parameters and put them in params object
+ const configuration = _.find(this.configurations, ['id', data.configurationId]);
+ _.forEach(_.keys(configuration.mutableParams), key => {
+ if (!_.has(data, key)) return;
+ data.params[key] = data[key];
+ delete data[key];
+ });
+
+ // Next, we need to encrypt the credentials if they are provided
+ const askForCredentials = this.askForCredentials;
+ const user = this.userStore.user;
+ const props = ['accessKeyId', 'secretAccessKey'];
+
+ try {
+ if (askForCredentials) {
+ const credentials = _.pick(data, props);
+ credentials.region = awsRegion;
+ const usersStore = this.usersStore;
+ await user.setEncryptedCreds(credentials, data.pin);
+ await usersStore.updateUser(user);
+ }
+
+ // We remove any access key information from data
+ // but we keep pin in data, and the environment store will remove it
+ const adjustedData = _.omit(data, props);
+
+ await this.props.onNext(adjustedData);
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ handleForgotPin = event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const form = this.form;
+ const addRequired = field => {
+ const rules = field.rules;
+ if (_.startsWith('required')) return;
+ field.set('rules', `required|${rules}`);
+ };
+
+ addRequired(form.$('accessKeyId'));
+ addRequired(form.$('secretAccessKey'));
+ this.askForCredentials = true;
+ };
+
+ render() {
+ const title = this.props.title || '';
+ return (
+
+
+ {this.renderForm()}
+
+ );
+ }
+
+ renderForm() {
+ const form = this.form;
+ const askForCidr = !_.isUndefined(this.props.defaultCidr);
+ const configurations = this.configurations;
+ const askForCredentials = this.askForCredentials;
+
+ return (
+
+
+ {({ processing, /* onSubmit, */ onCancel }) => (
+ <>
+
+ {askForCidr && }
+
+
+ {askForCredentials && (
+ <>
+
+
+ >
+ )}
+
+
+ {!askForCredentials && (
+
+ Forgot PIN?
+
+ )}
+
+
+
+
+ >
+ )}
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(CreateExternalPlatformForm, {
+ form: observable,
+ platformId: computed,
+ askForCredentials: observable,
+ configurations: computed,
+ userStore: computed,
+ usersStore: computed,
+ handlePrevious: action,
+ handleForgotPin: action,
+});
+
+export default inject('userStore', 'usersStore', 'clientInformationStore')(observer(CreateExternalPlatformForm));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/parts/CreateInternalPlatformForm.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/parts/CreateInternalPlatformForm.js
new file mode 100644
index 0000000000..34be2f1912
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/parts/CreateInternalPlatformForm.js
@@ -0,0 +1,155 @@
+/*
+ * 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 { Segment, Button, Header } from 'semantic-ui-react';
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import Dropdown from '@aws-ee/base-ui/dist/parts/helpers/fields/DropDown';
+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 { getCreateInternalPlatformForm } from '../../../models/forms/CreateInternalPlatformForm';
+import SelectConfigurationCards from './SelectConfigurationCards';
+
+// expected props
+// - onPrevious (via props)
+// - onNext (via props) a function is called with the form data
+// - platformId (via props)
+// - configurations (via props)
+// - title (via props)
+// - defaultCidr (via props)
+// - clientInformationStore (via injection)
+// - userStore (via injection)
+class CreateInternalPlatformForm extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.form = getCreateInternalPlatformForm({
+ projectIdOptions: this.projectIdOptions,
+ cidr: this.props.defaultCidr,
+ });
+ });
+ }
+
+ get projectIdOptions() {
+ const store = this.userStore;
+ return store.projectIdDropdown;
+ }
+
+ get platformId() {
+ return this.props.platformId;
+ }
+
+ get configurations() {
+ return this.props.configurations;
+ }
+
+ get userStore() {
+ return this.props.userStore;
+ }
+
+ // eslint-disable-next-line consistent-return
+ handlePrevious = () => {
+ if (_.isFunction(this.props.onPrevious)) return this.props.onPrevious();
+ };
+
+ // eslint-disable-next-line consistent-return
+ handleNext = async form => {
+ const data = { ...form.values(), params: {}, platformId: this.platformId };
+
+ // We pick the mutable parameters and put them in params object
+ const configuration = _.find(this.configurations, ['id', data.configurationId]);
+ _.forEach(_.keys(configuration.mutableParams), key => {
+ if (!_.has(data, key)) return;
+ data.params[key] = data[key];
+ delete data[key];
+ });
+
+ try {
+ await this.props.onNext(data);
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ render() {
+ const title = this.props.title || '';
+ return (
+
+
+ {this.renderForm()}
+
+ );
+ }
+
+ renderForm() {
+ const form = this.form;
+ const askForCidr = !_.isUndefined(this.props.defaultCidr);
+ const configurations = this.configurations;
+
+ return (
+
+
+ {({ processing, /* onSubmit, */ onCancel }) => (
+ <>
+
+ {askForCidr && }
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(CreateInternalPlatformForm, {
+ form: observable,
+ platformId: computed,
+ configurations: computed,
+ userStore: computed,
+ projectIdOptions: computed,
+ handlePrevious: action,
+});
+
+export default inject('userStore', 'clientInformationStore')(observer(CreateInternalPlatformForm));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/parts/SelectConfigurationCards.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/parts/SelectConfigurationCards.js
new file mode 100644
index 0000000000..aac6f78ece
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/compute/parts/SelectConfigurationCards.js
@@ -0,0 +1,148 @@
+/*
+ * 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 { Card, Radio, Divider, Table } from 'semantic-ui-react';
+import c from 'classnames';
+import Header from '@aws-ee/base-ui/dist/parts/helpers/fields/Header';
+import Description from '@aws-ee/base-ui/dist/parts/helpers/fields/Description';
+import ErrorPointer from '@aws-ee/base-ui/dist/parts/helpers/fields/ErrorPointer';
+import { nicePrice } from '@aws-ee/base-ui/dist/helpers/utils';
+
+// expected props
+// - configurations (via props) and array of the compute configurations MST
+// - formField (via props) an instance of the mobx form field
+class SelectConfigurationCards extends React.Component {
+ get configurations() {
+ return this.props.configurations;
+ }
+
+ get configurationId() {
+ return this.formField.value;
+ }
+
+ get formField() {
+ return this.props.formField;
+ }
+
+ handleSelectConfigurationId = configId => {
+ this.formField.sync(configId);
+ this.formField.resetValidation();
+ };
+
+ render() {
+ const field = this.formField;
+ const { error = '' } = field;
+ const hasError = !_.isEmpty(error); // IMPORTANT do NOT use field.hasError
+ const isDisabled = field.disabled;
+ const disabledClass = isDisabled ? 'disabled' : '';
+ const errorClass = hasError ? 'error' : '';
+
+ return (
+
+
+
+
+ {this.renderCards()}
+
+ );
+ }
+
+ renderCards() {
+ const disabled = this.formField.disabled;
+ const configurations = this.configurations;
+ const isSelected = config => config.id === this.configurationId;
+ const getAttrs = config => {
+ const attrs = {};
+ if (isSelected(config)) attrs.color = 'blue';
+ if (!disabled) attrs.onClick = () => this.handleSelectConfigurationId(config.id);
+
+ return attrs;
+ };
+
+ return (
+
+ {_.map(configurations, config => (
+
+
+
+
+ {config.title}
+
+
+
+
+ {/* Yes, we are doing dangerouslySetInnerHTML, the content was already sanitized by showdownjs */}
+ {/* eslint-disable-next-line react/no-danger */}
+
+
+
+
+ {this.renderTableInfo(config)}
+
+ ))}
+
+ );
+ }
+
+ renderTableInfo(config) {
+ const priceTitle = item => {
+ const isSpot = _.get(item, 'priceInfo.type') === 'spot';
+ return isSpot ? 'Maximum price per day' : 'Price per day';
+ };
+ const region = item => _.get(item, 'priceInfo.region');
+ const price = item => {
+ const perDay = item.pricePerDay;
+ if (_.isUndefined(perDay) || (_.isString(perDay) && _.isEmpty(perDay))) return 'N/A';
+ return `$${nicePrice(perDay)}`;
+ };
+
+ return (
+
+
+ {_.map(config.displayProps).map((property, propertyIndex) => {
+ return (
+ // eslint-disable-next-line react/no-array-index-key
+
+ {property.key}
+ {property.value}
+
+ );
+ })}
+
+
+ {priceTitle(config)}
+ {region(config)}
+
+ {price(config)}
+
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(SelectConfigurationCards, {
+ configurations: computed,
+ configurationId: computed,
+ formField: computed,
+ handleSelectConfigurationId: action,
+});
+
+export default inject()(observer(SelectConfigurationCards));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/dashboard/Dashboard.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/dashboard/Dashboard.js
new file mode 100644
index 0000000000..4d8e272c54
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/dashboard/Dashboard.js
@@ -0,0 +1,283 @@
+/*
+ * 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 _ from 'lodash';
+import { decorate } from 'mobx';
+import { observer } from 'mobx-react';
+import { Pie } from 'react-chartjs-2';
+import { Container, Header, Segment, Icon } from 'semantic-ui-react';
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import ProgressPlaceHolder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+
+import { getEnvironments, getEnvironmentCost } from '../../helpers/api';
+
+import { blueDatasets } from './graphs/graph-options';
+import BarGraph from './graphs/BarGraph';
+
+class Dashboard extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ totalCost: 0,
+ projNameToTotalCost: {},
+ projNameToUserTotalCost: {},
+ envNameToCostInfo: {},
+ isLoading: true,
+ };
+ }
+
+ async componentDidMount() {
+ window.scrollTo(0, 0);
+ try {
+ const { totalCost, projNameToTotalCost, projNameToUserTotalCost, envNameToCostInfo } = await this.getCosts();
+ this.setState({
+ totalCost,
+ projNameToTotalCost,
+ projNameToUserTotalCost,
+ envNameToCostInfo,
+ isLoading: false,
+ });
+ } catch (error) {
+ displayError('Error encountered retrieving cost data. Please refresh the page or try again later.');
+ }
+ }
+
+ render() {
+ return (
+
+ {this.renderTitle()}
+ {this.renderContent()}
+
+ );
+ }
+
+ renderTitle() {
+ return (
+
+
+
+ );
+ }
+
+ renderContent() {
+ return (
+
+ {this.state.isLoading === false && this.state.totalCost === 0 ? (
+ No cost data to show
+ ) : (
+ <>
+ {this.renderCostPerProj()}
+ {this.renderPastMonthCostPerEnv()}
+ {this.renderYesterdayCostPerEnv()}
+ {this.renderPastMonthCostPerProjPerUser()}
+
+ Total cost of all research workspaces for the past 30 days: $
+ {Math.round(this.state.totalCost * 100) / 100}
+
+ >
+ )}
+
+ );
+ }
+
+ async getCosts() {
+ const { envNameToCostInfo, envNameToIndex } = await this.getAccumulatedEnvCost();
+
+ const projNameToUserTotalCost = {};
+ Object.keys(envNameToCostInfo).forEach(envName => {
+ const projName = envNameToIndex[envName];
+ if (projNameToUserTotalCost[projName] === undefined) {
+ projNameToUserTotalCost[projName] = {};
+ }
+ Object.keys(envNameToCostInfo[envName].pastMonthCostByUser).forEach(user => {
+ const currentUserCost = _.get(projNameToUserTotalCost, `${projName}.${user}`, 0);
+ projNameToUserTotalCost[projName][user] =
+ currentUserCost + envNameToCostInfo[envName].pastMonthCostByUser[user];
+ });
+ });
+
+ const projNameToTotalCost = {};
+ let totalCost = 0;
+ Object.keys(projNameToUserTotalCost).forEach(projName => {
+ let indexCost = 0;
+ Object.keys(projNameToUserTotalCost[projName]).forEach(user => {
+ indexCost += projNameToUserTotalCost[projName][user];
+ });
+ totalCost += indexCost;
+ projNameToTotalCost[projName] = indexCost;
+ });
+
+ return { totalCost, projNameToTotalCost, projNameToUserTotalCost, envNameToCostInfo };
+ }
+
+ async getAccumulatedEnvCost() {
+ const environments = await getEnvironments();
+ const envIdToName = {};
+
+ const envNameToIndex = {};
+ environments.forEach(env => {
+ if (env.isExternal) return;
+ envIdToName[env.id] = env.name;
+ envNameToIndex[env.name] = env.indexId;
+ });
+
+ const envIds = Object.keys(envIdToName);
+ const envCostPromises = envIds.map(envId => {
+ return getEnvironmentCost(envId, 30, false, true);
+ });
+
+ const envCostResults = await Promise.all(envCostPromises);
+ const pastMonthCostByUserArray = envCostResults.map(costResult => {
+ const createdByToCost = {};
+ costResult.forEach(costDate => {
+ const cost = costDate.cost;
+ Object.keys(cost).forEach(group => {
+ let createdBy = group.split('$')[1];
+ createdBy = createdBy || 'None';
+ const currentUserCost = _.get(createdByToCost, createdBy, 0);
+ createdByToCost[createdBy] = currentUserCost + cost[group].amount;
+ });
+ });
+ return createdByToCost;
+ });
+
+ const yesterdayCostArray = envCostResults.map(costResult => {
+ const yesterdayCost = costResult[costResult.length - 1];
+ let totalCost = 0;
+ const arrayOfCosts = _.flatMapDeep(yesterdayCost.cost);
+ arrayOfCosts.forEach(cost => {
+ totalCost += cost.amount;
+ });
+ return totalCost;
+ });
+
+ const envNameToCostInfo = {};
+ for (let i = 0; i < envIds.length; i++) {
+ const key = envIdToName[envIds[i]];
+ envNameToCostInfo[key] = {
+ pastMonthCostByUser: pastMonthCostByUserArray[i],
+ yesterdayCost: yesterdayCostArray[i],
+ };
+ }
+ return { envNameToCostInfo, envNameToIndex };
+ }
+
+ renderCostPerProj() {
+ if (_.isEmpty(this.state.projNameToTotalCost)) {
+ return ;
+ }
+ const title = 'Index Costs for Past 30 Days';
+ const labels = Object.keys(this.state.projNameToTotalCost);
+ const dataPoints = Object.keys(this.state.projNameToTotalCost).map(projName => {
+ return this.state.projNameToTotalCost[projName];
+ });
+ const data = {
+ labels,
+ datasets: blueDatasets(title, dataPoints),
+ };
+
+ return ;
+ }
+
+ renderPastMonthCostPerEnv() {
+ if (_.isEmpty(this.state.envNameToCostInfo)) {
+ return ;
+ }
+
+ const pastMonthCostTotalArray = [];
+ Object.keys(this.state.envNameToCostInfo).forEach(envName => {
+ let total = 0;
+ Object.keys(this.state.envNameToCostInfo[envName].pastMonthCostByUser).forEach(user => {
+ total += this.state.envNameToCostInfo[envName].pastMonthCostByUser[user];
+ });
+ pastMonthCostTotalArray.push(total);
+ });
+ const title = 'Env Cost for Past 30 Days';
+ const labels = Object.keys(this.state.envNameToCostInfo);
+ const dataPoints = pastMonthCostTotalArray;
+ const data = {
+ labels,
+ datasets: blueDatasets(title, dataPoints),
+ };
+
+ return ;
+ }
+
+ renderYesterdayCostPerEnv() {
+ if (_.isEmpty(this.state.envNameToCostInfo)) {
+ return ;
+ }
+ const title = "Yesterday's Env Cost";
+ const labels = Object.keys(this.state.envNameToCostInfo);
+ const dataPoints = Object.keys(this.state.envNameToCostInfo).map(envName => {
+ return this.state.envNameToCostInfo[envName].yesterdayCost;
+ });
+ const data = {
+ labels,
+ datasets: blueDatasets(title, dataPoints),
+ };
+
+ return ;
+ }
+
+ renderPastMonthCostPerProjPerUser() {
+ if (_.isEmpty(this.state.projNameToUserTotalCost)) {
+ return ;
+ }
+ const results = [];
+ Object.keys(this.state.projNameToUserTotalCost).forEach(projName => {
+ const projCostData = this.state.projNameToUserTotalCost[projName];
+ const labels = Object.keys(projCostData);
+ // NOTE: We need a color for each user
+ const colors = ['#FF6384', '#36A2EB', '#FFCE56', '#CDDC39', '#4527a0', '#f4511e'];
+ const datasets = [
+ {
+ data: Object.keys(projCostData).map(user => {
+ return projCostData[user];
+ }),
+ backgroundColor: colors,
+ hoverBackgroundColor: colors,
+ },
+ ];
+
+ const data = {
+ labels,
+ datasets,
+ };
+
+ results.push(
+ ,
+ );
+ });
+ return (
+ <>
+ Index Cost Breakdowns for Past 30 Days
+ {results}
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(Dashboard, {});
+
+export default observer(Dashboard);
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/dashboard/graphs/BarGraph.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/dashboard/graphs/BarGraph.js
new file mode 100644
index 0000000000..76a4b7c8c3
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/dashboard/graphs/BarGraph.js
@@ -0,0 +1,32 @@
+/*
+ * 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 { Bar } from 'react-chartjs-2';
+
+import { barOptions } from './graph-options';
+
+const BarGraph = ({ className, data, title, width = 250, height = 120 }) => {
+ return (
+
+ );
+};
+
+export default BarGraph;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/dashboard/graphs/graph-options.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/dashboard/graphs/graph-options.js
new file mode 100644
index 0000000000..0576b02cd3
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/dashboard/graphs/graph-options.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.
+ */
+
+const barOptions = {
+ // see https://www.chartjs.org/docs/latest/configuration/legend.html
+ // see https://github.com/jerairrest/react-chartjs-2/blob/master/example/src/components/bar.js
+ // see https://stackoverflow.com/questions/36676263/chart-js-v2-hiding-grid-lines
+ // see https://github.com/jerairrest/react-chartjs-2
+ legend: {
+ display: false,
+ },
+ maintainAspectRatio: false,
+ scales: {
+ // see https://www.chartjs.org/docs/latest/charts/bar.html
+ xAxes: [
+ {
+ // barPercentage: 1,
+ // categoryPercentage: 1,
+ gridLines: {
+ display: false,
+ },
+ },
+ ],
+ yAxes: [
+ {
+ gridLines: {
+ display: false,
+ },
+ },
+ ],
+ },
+};
+
+function blueDatasets(label, data) {
+ return [
+ {
+ label: label || 'Label',
+ backgroundColor: 'rgba(33, 133, 208,0.2)',
+ borderColor: 'rgba(33, 133, 208,1)',
+ borderWidth: 1,
+ // hoverBackgroundColor: 'rgba(255,99,132,0.4)',
+ // hoverBorderColor: 'rgba(255,99,132,1)',
+ data: data || [1, 8, 5, 6, 3],
+ },
+ ];
+}
+export { barOptions, blueDatasets };
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentCard.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentCard.js
new file mode 100644
index 0000000000..000cdb2d91
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentCard.js
@@ -0,0 +1,242 @@
+/*
+ * 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 _ from 'lodash';
+import { observer, inject } from 'mobx-react';
+import { decorate, runInAction } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import TimeAgo from 'react-timeago';
+import { Header, Icon, Label, Image } from 'semantic-ui-react';
+import Dotdotdot from 'react-dotdotdot';
+
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import { storage } from '@aws-ee/base-ui/dist/helpers/utils';
+
+import EnvironmentStatusIcon from './EnvironmentStatusIcon';
+import By from '../helpers/By';
+import EnvironmentConnectButton from './EnvironmentConnectButton';
+import localStorageKeys from '../../models/constants/local-storage-keys';
+import sagemakerNotebookIcon from '../../../images/marketplace/sagemaker-notebook-icon.svg';
+import emrIcon from '../../../images/marketplace/emr-icon.svg';
+import ec2Icon from '../../../images/marketplace/ec2-icon.svg';
+
+const UPDATE_INTERVAL_MS = 20000;
+
+// expected props
+// - environment - a Environment model instance (via props)
+// - userDisplayName (via injection)
+// - location (from react router)
+class EnvironmentCard extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ // add any state changing initialization logic if needed
+ });
+ }
+
+ componentDidMount() {
+ const environment = this.getEnvironment();
+
+ environment.setFetchingUrl(false);
+ if (environment.isExternal && environment.isPending && this.props.user.isExternalUser) {
+ // TODO abstract this workflow to be used elsewhere
+ // Call checkExternalUpdate every minute
+ this.intervalId = setInterval(this.checkExternalUpdate, UPDATE_INTERVAL_MS, environment, this.props.user);
+ }
+ }
+
+ componentWillUnmount() {
+ clearInterval(this.intervalId);
+ }
+
+ checkExternalUpdate = (environment, user) => {
+ const pin = storage.getItem(localStorageKeys.pinToken);
+ // Confirm if the stack still needs to be checked
+ if (!(environment.isExternal && environment.isPending && user.isExternalUser)) {
+ clearInterval(this.intervalId);
+ return;
+ }
+ if (!_.isEmpty(pin)) {
+ this.getStore().updateExternalEnvironment(environment, user, pin);
+ }
+ };
+
+ handleTerminateEnvironment = async event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ try {
+ const store = this.getStore();
+ await store.deleteEnvironment(this.getEnvironment(), this.props.user, storage.getItem(localStorageKeys.pinToken));
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ getEnvironment() {
+ return this.props.environment;
+ }
+
+ getStore() {
+ return this.props.environmentsStore;
+ }
+
+ getUserDisplayNameService() {
+ return this.props.userDisplayName;
+ }
+
+ getIcon(type) {
+ switch (type) {
+ case 'sagemaker':
+ return sagemakerNotebookIcon;
+ case 'emr':
+ return emrIcon;
+ case 'ec2-linux':
+ case 'ec2-windows':
+ return ec2Icon;
+ default:
+ return null;
+ }
+ }
+
+ render() {
+ const item = this.getEnvironment();
+ return (
+
+ {this.renderLeftCard(item)}
+ {this.renderRightCard(item)}
+
+ );
+ }
+
+ renderLeftCard(env) {
+ const { name, description, createdAt, createdBy, fetchingUrl, status, instanceInfo } = env;
+ return (
+
+
+
+
+
+
+ {status === 'COMPLETED' && (instanceInfo.type === 'sagemaker' || instanceInfo.type === 'emr') && (
+
+ {fetchingUrl ? (
+ <>
+ Connecting
+
+ >
+ ) : (
+ <>Connect>
+ )}
+
+ )}
+
+
+
+
+
+
+
+ created
+
+
+ {description}
+
+
+ Yesterday's Research Workspace Cost: ${this.getCostInPastDay(env.costs)}
+
+
+ );
+ }
+
+ getCostInPastDay(costInfo) {
+ if (_.isEmpty(costInfo)) {
+ return 0;
+ }
+ const costsForLatestDate = costInfo[costInfo.length - 1].cost;
+ let total = 0;
+ costsForLatestDate.forEach(service => {
+ total += service.amount;
+ });
+ return total.toFixed(2);
+ }
+
+ renderRightCard(environment) {
+ const displayNameService = this.getUserDisplayNameService();
+ const sharedWithUsernames = _.map(environment.sharedWithUsers, 'username');
+
+ return (
+
+
+ Research Workspace Owners {' '}
+
+ {1}
+
+
+
+ {displayNameService.getLongDisplayName(environment.createdBy)}
+
+
+ Research Workspace Shared Users {' '}
+
+ {sharedWithUsernames.length}
+
+
+
+ {sharedWithUsernames.join(', ')}
+
+
+
+ Project {' '}
+
+
+ {environment.projectId}
+
+
{this.renderTerminateButton(environment)}
+
+ );
+ }
+
+ renderTerminateButton(environment) {
+ let terminateButton;
+ if (environment.isCompleted) {
+ terminateButton = (
+
+
+ Terminate
+
+ );
+ }
+
+ return <>{terminateButton}>;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(EnvironmentCard, {});
+
+export default inject('userDisplayName')(withRouter(observer(EnvironmentCard)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentConnectButton.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentConnectButton.js
new file mode 100644
index 0000000000..609f96a5c0
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentConnectButton.js
@@ -0,0 +1,63 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { action, computed, decorate } from 'mobx';
+
+class EnvironmentConnectButton extends React.Component {
+ getUrl = async environment => {
+ switch (environment.instanceInfo.type) {
+ case 'sagemaker': {
+ const { AuthorizedUrl } = await environment.getEnvironmentNotebookUrl(this.user);
+ return `${AuthorizedUrl}&view=lab`;
+ }
+ case 'emr':
+ return environment.instanceInfo.JupyterUrl;
+ default:
+ return '';
+ }
+ };
+
+ handleConnectClick = async event => {
+ event.preventDefault();
+ event.stopPropagation();
+ const newTab = window.open('about:blank', '_blank');
+
+ const environment = this.props.environment;
+
+ const url = await this.getUrl(environment);
+ // Change to the notebook
+ newTab.location = url;
+ environment.setFetchingUrl(false);
+ };
+
+ render() {
+ const { as: As, userStore, user, environment, ...props } = this.props;
+ return ;
+ }
+
+ get user() {
+ return this.props.user || this.props.userStore.user;
+ }
+}
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(EnvironmentConnectButton, {
+ user: computed,
+ handleConnectClick: action,
+ getUrl: action,
+});
+
+export default inject('userStore')(observer(EnvironmentConnectButton));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentDetailPage.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentDetailPage.js
new file mode 100644
index 0000000000..c8cd6ac2eb
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentDetailPage.js
@@ -0,0 +1,622 @@
+/*
+ * 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 { observer, inject, Observer } from 'mobx-react';
+import { decorate, observable, action, runInAction } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import TimeAgo from 'react-timeago';
+import {
+ Accordion,
+ Breadcrumb,
+ Button,
+ Container,
+ Dropdown,
+ Header,
+ Icon,
+ Label,
+ Popup,
+ Reveal,
+ Segment,
+ Tab,
+ Table,
+} from 'semantic-ui-react';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
+
+import crypto from 'crypto';
+
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import { isStoreLoading, isStoreReady, isStoreError } 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 EnvironmentStatusIcon from './EnvironmentStatusIcon';
+import By from '../helpers/By';
+import EnvironmentConnectButton from './EnvironmentConnectButton';
+
+const ErrorInfo = ({ environment }) => {
+ const [visible, setVisible] = React.useState(() => false);
+ // if (!environment.error) {
+ // environment.getEnvironmentError();
+ // }
+
+ return (
+
+ This research workspace encountered an {environment.error ? 'error' : 'unknown error'}.
+ {environment.error ? (
+
+ setVisible(s => !s)}>
+
+ Detailed error information
+
+
+ {environment.error}
+
+
+ ) : null}
+
+ );
+};
+
+// expected props
+// - environmentsStore (via injection)
+// - userStore (via injection)
+// - location (from react router)
+class EnvironmentDetailPage extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.updateSharedWithUsers = [];
+ this.formProcessing = false;
+ });
+ }
+
+ componentDidMount() {
+ const store = this.getInstanceStore();
+ swallowError(store.load());
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getInstanceStore();
+ store.stopHeartbeat();
+ }
+
+ getInstanceStore() {
+ const instanceId = this.getInstanceId();
+ return this.props.environmentsStore.getEnvironmentStore(instanceId);
+ }
+
+ getUserStore() {
+ return this.props.userStore;
+ }
+
+ getUser() {
+ const store = this.getUserStore();
+ if (!isStoreReady(store)) return {};
+ return store.user;
+ }
+
+ getInstanceId() {
+ return (this.props.match.params || {}).instanceId;
+ }
+
+ getEnvironment() {
+ const store = this.getInstanceStore();
+ if (!isStoreReady(store)) return {};
+ return store.environment;
+ }
+
+ render() {
+ const store = this.getInstanceStore();
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreReady(store)) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderBreadcrumb()}
+ {content}
+
+ );
+ }
+
+ renderBreadcrumb() {
+ const instanceId = this.getInstanceId();
+ const goto = gotoFn(this);
+
+ return (
+
+ goto('/workspaces')}>
+ Research Workspaces
+
+
+ {instanceId}
+
+ );
+ }
+
+ renderMain() {
+ const instance = this.getEnvironment();
+ const { id, name, updatedAt, updatedBy } = instance;
+
+ return (
+ <>
+
+ {this.renderTabs()}
+ >
+ );
+ }
+
+ renderTabs() {
+ const environment = this.getEnvironment();
+ if (environment.isCompleted) {
+ return this.renderCompletedTabs();
+ }
+ if (environment.isStopped) {
+ return this.renderStoppedTabs();
+ }
+ if (environment.isError) {
+ return ;
+ }
+ if (environment.isTerminated) {
+ return this.renderTerminateInfo();
+ }
+ return this.renderPendingInfo();
+ }
+
+ renderCompletedTabs() {
+ const environment = this.getEnvironment();
+
+ const panes = [
+ {
+ menuItem: 'Security',
+ render: () => (
+
+
+ {() => {
+ switch (environment.instanceInfo.type) {
+ case 'ec2-linux':
+ return this.renderEc2LinuxSecurity();
+ case 'ec2-windows':
+ return this.renderEc2WindowsSecurity();
+ case 'sagemaker':
+ return this.renderSagemakerSecurity();
+ case 'emr':
+ return this.renderEmrSecurity();
+ default:
+ return (
+
+
+
+ );
+ }
+ }}
+
+
+ ),
+ },
+ {
+ menuItem: 'Research Workspace Details',
+ render: () => (
+
+ {() => this.renderInstanceDetails()}
+
+ ),
+ },
+ this.renderUserShareTabPane(),
+ ];
+
+ return ;
+ }
+
+ renderInstanceDetails() {
+ return this.renderCostInfo();
+ }
+
+ renderCostInfo() {
+ return (
+
+ Daily Costs
+ {this.renderCostTable()}
+
+ );
+ }
+
+ renderUserShareTabPane() {
+ const environment = this.getEnvironment();
+ const user = this.getUser();
+ const { username: envUsername, ns: envNs } = environment.createdBy;
+ const isOwner = user.username === envUsername && user.ns === envNs;
+ const { isAdmin } = user;
+ const sharedWithUsersDropDownOptions = this.props.usersStore.asDropDownOptions().filter(item => {
+ const value = JSON.parse(item.value);
+ return !(value.username === envUsername && value.ns === envNs);
+ });
+
+ return {
+ menuItem: 'Sharing',
+ render: () => (
+
+
+ {() => {
+ return (
+
+ Share with Users
+ item.id)}
+ fluid
+ multiple
+ selection
+ search
+ placeholder={
+ isOwner
+ ? 'Select other users you want to share this environment'
+ : 'Only the owner can share the environment'
+ }
+ disabled={!(isOwner || isAdmin) || this.formProcessing}
+ onChange={this.handleSharedWithUsersSelection}
+ />
+
+
+ Update
+
+
+ );
+ }}
+
+
+ ),
+ };
+ }
+
+ renderStoppedTabs() {
+ const panes = [this.renderUserShareTabPane(), this.renderCostDetailsTabPane()];
+
+ return ;
+ }
+
+ handleSharedWithUsersSelection = (e, { value }) => {
+ this.updateSharedWithUsers = value.map(item => JSON.parse(item));
+ };
+
+ handleSubmitSharedWithUsersClick = async event => {
+ event.preventDefault();
+ event.stopPropagation();
+ const environment = this.getEnvironment();
+
+ runInAction(() => {
+ this.formProcessing = true;
+ });
+
+ const updateEnvironment = {
+ id: environment.id,
+ sharedWithUsers: this.updateSharedWithUsers,
+ };
+
+ try {
+ await this.props.environmentsStore.updateEnvironment(updateEnvironment);
+ } catch (error) {
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ displayError(error);
+ }
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ };
+
+ renderCostTable() {
+ // Convert from mobx obj to normal obj
+ const environment = JSON.parse(JSON.stringify(this.getEnvironment()));
+
+ let costHeadings = [];
+ const rows = [];
+ environment.costs.forEach(costItemGivenADate => {
+ const cost = costItemGivenADate.cost;
+ const headings = Object.keys(cost);
+ costHeadings.push(headings);
+ const rowValues = {};
+ rowValues.date = costItemGivenADate.startDate;
+ let total = 0;
+ headings.forEach(heading => {
+ const amount = cost[heading].amount;
+ rowValues[heading] = amount.toFixed(2);
+ total += amount;
+ });
+ rowValues.total = total.toFixed(2);
+ rows.push(rowValues);
+ });
+
+ costHeadings = _.flatten(costHeadings);
+ costHeadings = _.uniq(costHeadings);
+
+ return (
+
+
+
+ Date
+ {costHeadings.map(header => {
+ return {header} ;
+ })}
+ Total
+
+
+
+ {rows.map(row => {
+ return (
+
+ {row.date}
+ {costHeadings.map(header => {
+ return ${_.get(row, header, 0)} ;
+ })}
+ ${row.total}
+
+ );
+ })}
+
+
+ );
+ }
+
+ renderTerminateInfo() {
+ const environment = this.getEnvironment();
+ return (
+ <>
+
+ This research workspace was terminated {' '}
+ .
+
+ {this.renderCostInfo()}
+ >
+ );
+ }
+
+ renderPendingInfo() {
+ const environment = this.getEnvironment();
+ return (
+ <>
+
+ This research workspace was started {' '}
+ .
+
+ >
+ );
+ }
+
+ renderCopyToClipboard(text) {
+ return (
+
+
+
+
+ }
+ />
+ );
+ }
+
+ handleKeyPairRequest = async event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const environment = this.getEnvironment();
+ const keyPair = await environment.getKeyPair();
+
+ const downloadLink = document.createElement('a');
+ downloadLink.setAttribute('href', `data:application/octet-stream,${encodeURIComponent(keyPair.privateKey)}`);
+ downloadLink.setAttribute('download', `${environment.id}.pem`);
+ downloadLink.click();
+ };
+
+ handleWindowsPasswordRequest = async event => {
+ event.preventDefault();
+ runInAction(() => {
+ this.windowsPassword = 'loading';
+ });
+
+ const environment = this.getEnvironment();
+ const [{ privateKey }, { passwordData }] = await environment.getWindowsPassword();
+
+ const password = crypto
+ .privateDecrypt(
+ { key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING },
+ Buffer.from(passwordData, 'base64'),
+ )
+ .toString('utf8');
+
+ runInAction(() => {
+ this.windowsPassword = password;
+ });
+ };
+
+ renderEc2LinuxSecurity() {
+ const environment = this.getEnvironment();
+ return (
+
+ You'll need two pieces of information to connect to this research workspace.
+
+
+ The IP Address or DNS of the instance, for this research workspace it is{' '}
+ {environment.instanceInfo.Ec2WorkspaceDnsName}
+
+ The ssh key
+
+ Connecting to your research workspace depends on the operating system you are connecting from.
+
+ Example:
+
{`ssh -i ${environment.id}.pem ec2-user@${environment.instanceInfo.Ec2WorkspaceDnsName}`}
+
+ Download SSH Key
+
+
+ );
+ }
+
+ renderEc2WindowsSecurity() {
+ const environment = this.getEnvironment();
+ const passRevealDisabled = !this.windowsPassword || this.windowsPassword === 'loading';
+ const passRevealStyle = {
+ width: '27em',
+ height: '5em',
+ };
+
+ return (
+ <>
+
+ Your Windows workspace can be accessed via an RDP client by using the DNS host name and credentials defined
+ defined below.
+
+
+
+ Host {environment.instanceInfo.Ec2WorkspaceDnsName}
+ {this.renderCopyToClipboard(environment.instanceInfo.Ec2WorkspaceDnsName)}
+
+
+
+
+
+
+ {passRevealDisabled ? 'Get ' : 'Show '} Windows Credentials
+
+
+
+
+
+ Username Administrator
+
+
+
+ Password
+ {this.windowsPassword}
+ {this.renderCopyToClipboard(this.windowsPassword)}
+
+
+
+
+
+
+ Additional information about connecting via RDP can be found in the documentation below:
+
+
+ >
+ );
+ }
+
+ renderEmrSecurity() {
+ return this.renderConnectBtn(
+ 'To connect to this EMR Jupyter notebook instance simply click the launch button below.',
+ );
+ }
+
+ renderSagemakerSecurity() {
+ return this.renderConnectBtn(
+ 'To connect to this SageMaker notebook instance simply click the launch button below.',
+ );
+ }
+
+ renderConnectBtn(msg) {
+ const environment = this.getEnvironment();
+
+ return (
+
+
{msg}
+
+ {environment.fetchingUrl ? (
+ <>
+ Connecting
+
+ >
+ ) : (
+ <>Connect>
+ )}
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(EnvironmentDetailPage, {
+ windowsPassword: observable,
+ updateSharedWithUsers: observable,
+ formProcessing: observable,
+ handleSharedWithUsersSelection: action,
+});
+
+export default inject('environmentsStore', 'userStore', 'usersStore')(withRouter(observer(EnvironmentDetailPage)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentSetup.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentSetup.js
new file mode 100644
index 0000000000..c2f3fd06d4
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentSetup.js
@@ -0,0 +1,99 @@
+/*
+ * 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, runInAction, observable, action } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Icon, Container, Header } from 'semantic-ui-react';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import { displaySuccess } from '@aws-ee/base-ui/dist/helpers/notification';
+
+import { CurrentStep } from '../compute/helpers/CurrentStep';
+import ComputePlatformSetup from '../compute/ComputePlatformSetup';
+import SetupStepsProgress from './SetupStepsProgress';
+
+// expected props
+class EnvironmentSetup extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.currentStep = CurrentStep.create({ step: 'selectComputePlatform' });
+ });
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ goto(pathname) {
+ const goto = gotoFn(this);
+ goto(pathname);
+ }
+
+ handlePrevious = () => {
+ this.goto('/workspaces');
+ };
+
+ // eslint-disable-next-line no-unused-vars
+ handleCompleted = async environment => {
+ displaySuccess('The research workspace is being provisioned');
+ this.goto('/workspaces');
+ };
+
+ render() {
+ return (
+
+ {this.renderTitle()}
+ {this.renderStepsProgress()}
+ {this.renderContent()}
+
+ );
+ }
+
+ renderTitle() {
+ return (
+
+
+
+ Research Workspaces
+
+
+ );
+ }
+
+ renderStepsProgress() {
+ return ;
+ }
+
+ renderContent() {
+ return (
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(EnvironmentSetup, {
+ handlePrevious: action,
+ handleCompleted: action,
+ currentStep: observable,
+});
+
+export default inject()(withRouter(observer(EnvironmentSetup)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentStatusIcon.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentStatusIcon.js
new file mode 100644
index 0000000000..bf6aeb2bfc
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentStatusIcon.js
@@ -0,0 +1,74 @@
+/*
+ * 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 { Icon, Label } from 'semantic-ui-react';
+import { observer } from 'mobx-react';
+
+class EnvironmentStatusIcon extends React.Component {
+ getEnvironment() {
+ return this.props.environment;
+ }
+
+ render() {
+ const status = this.getEnvironment().status;
+ if (status === 'COMPLETED') {
+ return (
+
+ Ready
+
+ );
+ }
+ if (status === 'TERMINATED') {
+ return (
+
+ Terminated
+
+ );
+ }
+ if (status === 'FAILED') {
+ return (
+
+ Error
+
+ );
+ }
+ if (status === 'TERMINATING_FAILED') {
+ return (
+
+ Failed to terminate
+
+ );
+ }
+ if (status === 'TERMINATING') {
+ return (
+
+
+ Terminating
+
+
+
+ );
+ }
+ return (
+
+ Starting
+
+
+ );
+ }
+}
+
+export default observer(EnvironmentStatusIcon);
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentsList.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentsList.js
new file mode 100644
index 0000000000..46c3f10aba
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/EnvironmentsList.js
@@ -0,0 +1,218 @@
+/*
+ * 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, action, observable, runInAction } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Header, Icon, Segment, Container, Label, Button } from 'semantic-ui-react';
+
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import { storage, swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import { isStoreLoading, isStoreEmpty, isStoreNotEmpty, isStoreError } 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 EnvironmentCard from './EnvironmentCard';
+import UserOnboarding from '../users/UserOnboarding';
+import UserPinModal from '../users/UserPinModal';
+import localStorageKeys from '../../models/constants/local-storage-keys';
+
+// expected props
+// - environmentsStore (via injection)
+// - location (from react router)
+class EnvironmentsList extends React.Component {
+ constructor(props) {
+ super(props);
+ const user = this.getUserStore.user;
+
+ runInAction(() => {
+ this.user = user;
+ this.onboardingOpen = false;
+ this.pinModalOpen = user.isExternalUser && _.isEmpty(storage.getItem(localStorageKeys.pinToken));
+ });
+ }
+
+ componentDidMount() {
+ const store = this.getStore();
+ swallowError(store.load());
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getStore();
+ store.stopHeartbeat();
+ }
+
+ getStore() {
+ return this.props.environmentsStore;
+ }
+
+ get getUserStore() {
+ return this.props.userStore;
+ }
+
+ setOnboarding = value => {
+ this.onboardingOpen = value;
+ };
+
+ hidePinModal = () => {
+ this.pinModalOpen = false;
+ };
+
+ handleDetailClick = event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ // see https://reactjs.org/docs/events.html and https://github.com/facebook/react/issues/5733
+ const instanceId = event.currentTarget.dataset.instance;
+ const goto = gotoFn(this);
+ goto(`/workspaces/id/${instanceId}`);
+ };
+
+ handleCreateEnvironment = event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const goto = gotoFn(this);
+ goto(`/workspaces/create`);
+ };
+
+ handleConfigureCredentials = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.setOnboarding(true);
+ };
+
+ needsAWSCredentials = () => this.user.isExternalResearcher && !this.user.hasCredentials;
+
+ render() {
+ const store = this.getStore();
+ let content = null;
+
+ if (this.needsAWSCredentials()) {
+ content = this.renderConfigureAWS();
+ } else 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}
+ {this.onboardingOpen && this.setOnboarding(false)} />}
+
+ );
+ }
+
+ renderEmpty() {
+ return (
+
+
+
+ No research workspaces
+ To create a research workspace, click Create Research Workspace.
+
+
+ );
+ }
+
+ renderConfigureAWS() {
+ return (
+
+
+
+ No AWS credentials
+ To manage research workspaces, click Configure AWS Credentials.
+
+
+ );
+ }
+
+ renderTitle() {
+ return (
+
+
+
+ Research Workspaces {this.renderTotal()}
+
+ {this.needsAWSCredentials() ? (
+
+ Configure AWS Credentials
+
+ ) : (
+
+ Create Research Workspace
+
+ )}
+
+ );
+ }
+
+ renderTotal() {
+ const store = this.getStore();
+ if (isStoreError(store) || isStoreLoading(store)) return null;
+
+ return {store.total} ;
+ }
+
+ renderMain() {
+ const store = this.getStore();
+ const list = store.list;
+
+ return (
+
+
+ {_.map(list, item => (
+
+
+
+ ))}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(EnvironmentsList, {
+ handleDetailClick: action,
+ setOnboarding: action,
+ hidePinModal: action,
+ user: observable,
+ onboardingOpen: observable,
+ pinModalOpen: observable,
+});
+
+export default inject('environmentsStore', 'userStore')(withRouter(observer(EnvironmentsList)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/SetupStepsProgress.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/SetupStepsProgress.js
new file mode 100644
index 0000000000..181f87ebfc
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments/SetupStepsProgress.js
@@ -0,0 +1,57 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Step, Icon } from 'semantic-ui-react';
+
+// expected props
+// currentStep an instance of the CurrentStep model
+const Component = observer(({ currentStep = {} }) => {
+ let activeIndex;
+ const step = currentStep.step;
+
+ switch (step) {
+ case 'selectComputePlatform':
+ activeIndex = 0;
+ break;
+ case 'selectComputeConfiguration':
+ activeIndex = 1;
+ break;
+ default:
+ activeIndex = 0;
+ }
+
+ return (
+
+
+
+
+ Select Compute
+ Select a compute platform
+
+
+
+
+
+ Create Workspace
+ Create the workspace
+
+
+
+ );
+});
+
+export default Component;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/files/FileDropZone.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/files/FileDropZone.js
new file mode 100644
index 0000000000..9c701a8f43
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/files/FileDropZone.js
@@ -0,0 +1,155 @@
+/*
+ * 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, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import PropTypes from 'prop-types';
+
+import { Segment, Header, Divider, Button, Icon } from 'semantic-ui-react';
+import uuidv4 from 'uuid/v4';
+
+/**
+ * A reusable file input component.
+ * Motivation: components are stateful and behave unexpectedly
+ * when attempting to reuse them to upload multiple files.
+ */
+const ReusableFileInput = React.forwardRef(({ onChange, ...props }, ref) => {
+ const [inputKey, setInputKey] = React.useState(uuidv4());
+ return (
+ {
+ onChange(event);
+ setInputKey(uuidv4());
+ }}
+ {...props}
+ />
+ );
+});
+
+class FileDropZone extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.highlighted = false;
+ });
+ }
+
+ setHighlight(isHighlighted) {
+ runInAction(() => {
+ this.highlighted = isHighlighted;
+ });
+ }
+
+ render() {
+ const fileInputRef = React.createRef();
+ const enabled = this.props.state === 'PENDING';
+ return (
+ {
+ if (enabled) {
+ if (event.dataTransfer.types.includes('Files')) {
+ event.preventDefault();
+ this.setHighlight(true);
+ }
+ }
+ }}
+ onDragOver={event => {
+ if (enabled) {
+ if (event.dataTransfer.types.includes('Files')) {
+ event.preventDefault();
+ this.setHighlight(true);
+ }
+ }
+ }}
+ onDragLeave={() => {
+ this.setHighlight(false);
+ }}
+ onDragEnd={() => {
+ this.setHighlight(false);
+ }}
+ onDrop={event => {
+ if (enabled) {
+ if (event.dataTransfer.types.includes('Files')) {
+ event.preventDefault();
+ this.setHighlight(false);
+ const fileList = event.dataTransfer.files || [];
+ this.props.onSelectFiles([...fileList]);
+ }
+ }
+ }}
+ >
+
+ {
+ if (this.props.onSelectFiles) {
+ const fileList = event.currentTarget.files || [];
+ this.props.onSelectFiles([...fileList]);
+ }
+ }}
+ />
+ {this.props.state === 'PENDING' ? (
+ <>
+
+ Drag and drop
+ Or
+ {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ }}
+ >
+ Browse Files
+
+ >
+ ) : this.props.state === 'UPLOADING' ? (
+ <>
+
+ Uploading
+ >
+ ) : this.props.state === 'COMPLETE' ? (
+ <>
+
+ Upload Complete
+ >
+ ) : null}
+
+
+ );
+ }
+}
+FileDropZone.propTypes = {
+ state: PropTypes.oneOf(['PENDING', 'UPLOADING', 'COMPLETE']).isRequired,
+ onSelectFiles: PropTypes.func,
+};
+FileDropZone.defaultProps = {
+ onSelectFiles: null,
+};
+
+decorate(FileDropZone, {
+ setHighlight: observable,
+});
+export default observer(FileDropZone);
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/files/FileUpload.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/files/FileUpload.js
new file mode 100644
index 0000000000..c62b022f0f
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/files/FileUpload.js
@@ -0,0 +1,211 @@
+/*
+ * 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, observable, runInAction } from 'mobx';
+import { inject, observer, PropTypes as MobXPropTypes } from 'mobx-react';
+import PropTypes from 'prop-types';
+
+import React from 'react';
+import { Button, Grid, Header, Segment } from 'semantic-ui-react';
+
+import { displayError, displaySuccess, displayWarning } from '@aws-ee/base-ui/dist/helpers/notification';
+
+import StudyFileDropZone from './FileDropZone';
+import FileUploadTable from './FileUploadTable';
+
+const FileUpload = observer(
+ ({
+ files = [],
+ state = 'PENDING',
+ onCancelSelectFiles,
+ onCancelUpload,
+ onClickStartUpload,
+ onClickUploadMore,
+ onSelectFiles,
+ onClickRemoveFile,
+ onClickCancelFile,
+ }) => {
+ return (
+
+
+
+ {files.length > 0 && (
+
+
+
+
+
+
+
+
+
+ {state === 'PENDING' ? (
+
+ Upload Files
+
+ ) : state === 'UPLOADING' ? (
+
+ Uploading
+
+ ) : state === 'COMPLETE' ? (
+
+ Upload More Files
+
+ ) : null}
+ {state === 'PENDING' ? (
+
+ Cancel
+
+ ) : state === 'UPLOADING' ? (
+
+ Cancel
+
+ ) : null}
+
+
+
+
+ )}
+
+ );
+ },
+);
+FileUpload.propTypes = {
+ files: PropTypes.arrayOf(
+ PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ size: PropTypes.number.isRequired,
+ status: PropTypes.oneOf(['PENDING', 'UPLOADING', 'COMPLETE', 'FAILED']).isRequired,
+ uploaded: PropTypes.number,
+ error: PropTypes.string,
+ }),
+ ),
+ state: PropTypes.oneOf(['PENDING', 'UPLOADING', 'COMPLETE']).isRequired,
+ onCancelSelectFiles: PropTypes.func,
+ onCancelUpload: PropTypes.func,
+ onClickStartUpload: PropTypes.func,
+ onClickUploadMore: PropTypes.func,
+ onSelectFiles: PropTypes.func,
+ onClickRemoveFile: PropTypes.func,
+ onClickCancelFile: PropTypes.func,
+};
+
+class ConnectedFileUpload extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.fileUploadGroup = props.fileUploadsStore.getFileUploadGroup(this.props.resourceId);
+ });
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.resourceId !== this.props.resourceId) {
+ runInAction(() => {
+ this.fileUploadGroup = this.props.fileUploadsStore.getFileUploadGroup(this.props.resourceId);
+ });
+ }
+ }
+
+ handleResetFileUploadGroup = () => {
+ runInAction(() => {
+ this.props.fileUploadsStore.resetFileUploadGroup(this.props.resourceId);
+ this.fileUploadGroup = this.props.fileUploadsStore.getFileUploadGroup(this.props.resourceId);
+ });
+ };
+
+ handleCancel = () => {
+ this.fileUploadGroup.cancel();
+ };
+
+ handleStart = async () => {
+ const group = this.fileUploadGroup;
+ try {
+ await group.start(this.props.fileUploadHandler);
+ let success = 0;
+ let errors = 0;
+ group.fileUploadObjects.forEach(fileUpload => {
+ // eslint-disable-next-line default-case
+ switch (fileUpload.status) {
+ case 'COMPLETE':
+ success++;
+ break;
+ case 'FAILED':
+ if (fileUpload.error !== 'Cancelled') {
+ errors++;
+ }
+ break;
+ }
+ });
+ if (errors > 0 && success > 0) {
+ displayWarning(`File uploads completed with ${errors} errors`);
+ } else if (errors > 0) {
+ displayError(`File uploads failed`);
+ } else if (success > 0) {
+ displaySuccess(`File uploads completed successfully!`);
+ }
+ } catch (err) {
+ displayError(`File uploads failed: ${err}`);
+ }
+ };
+
+ handleCancelFileUpload = id => {
+ const fileUpload = this.fileUploadGroup.getFileUpload(id);
+ if (fileUpload) {
+ fileUpload.doCancel();
+ }
+ };
+
+ handleRemoveFileUpload = id => {
+ this.fileUploadGroup.remove(id);
+ };
+
+ handleSelectFiles = files => {
+ const group = this.fileUploadGroup;
+ files.forEach(file => {
+ group.add({ file });
+ });
+ };
+
+ render() {
+ return (
+
+ );
+ }
+}
+ConnectedFileUpload.propTypes = {
+ resourceId: PropTypes.string.isRequired,
+ fileUploadHandler: PropTypes.func.isRequired,
+ fileUploadsStore: MobXPropTypes.observableObject.isRequired,
+};
+
+decorate(ConnectedFileUpload, {
+ fileUploadGroup: observable,
+});
+export default inject('fileUploadsStore')(observer(ConnectedFileUpload));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/files/FileUploadTable.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/files/FileUploadTable.js
new file mode 100644
index 0000000000..63a9c30700
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/files/FileUploadTable.js
@@ -0,0 +1,99 @@
+/*
+ * 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 prettyBytes from 'pretty-bytes';
+import { observer } from 'mobx-react';
+import { Button, Icon, Progress, Table } from 'semantic-ui-react';
+
+const FileUploadStatus = observer(({ file }) =>
+ file.status === 'PENDING' ? (
+ 'Pending'
+ ) : file.status === 'UPLOADING' ? (
+
+ ) : file.status === 'FAILED' ? (
+ <>
+ {`${file.error || 'Error'}`}
+ >
+ ) : file.status === 'COMPLETE' ? (
+ <>
+ Complete
+ >
+ ) : (
+ 'Unknown'
+ ),
+);
+
+const FileUploadToolbar = observer(({ file, state, onClickRemove, onClickCancel }) =>
+ file.status === 'PENDING' ? (
+
+ ) : file.status === 'UPLOADING' ? (
+
+ ) : null,
+);
+
+const FileUploadRow = observer(({ file, state, onClickRemove, onClickCancel }) => (
+
+ {file.name}
+ {prettyBytes(file.size)}
+
+
+
+
+
+
+
+));
+
+const FileUploadTable = observer(({ files = [], state, onClickRemoveFile, onClickCancelFile }) => (
+
+
+
+ Filename
+ Size
+ Status
+
+
+
+
+ {files.map(file => (
+ onClickRemoveFile(file.id)}
+ onClickCancel={() => onClickCancelFile(file.id)}
+ />
+ ))}
+
+
+));
+
+export default FileUploadTable;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/Age.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/Age.js
new file mode 100644
index 0000000000..075be03abe
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/Age.js
@@ -0,0 +1,37 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import c from 'classnames';
+import TimeAgo from 'react-timeago';
+
+// expected props
+// - date (via props)
+// - emptyMessage (via props) (a message to display when the date is empty)
+// - className (via props)
+const Component = observer(({ date, className, emptyMessage = 'Not Provided' }) => {
+ if (_.isEmpty(date)) return {emptyMessage} ;
+ const formatter = (_value, _unit, _suffix, _epochSeconds, nextFormatter) =>
+ (nextFormatter() || '').replace(/ago$/, 'old');
+ return (
+
+
+
+ );
+});
+
+export default Component;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/By.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/By.js
new file mode 100644
index 0000000000..430d1a26bb
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/By.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 React from 'react';
+import { observer, inject } from 'mobx-react';
+import { decorate } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import c from 'classnames';
+
+// expected props
+// - user (via props)
+// - userDisplayName (via injection)
+// - className (via props)
+class By extends React.Component {
+ get user() {
+ return this.props.user;
+ }
+
+ get userDisplayNameService() {
+ return this.props.userDisplayName;
+ }
+
+ render() {
+ const user = this.user;
+ const displayNameService = this.userDisplayNameService;
+ const isSystem = displayNameService.isSystem(user);
+ return isSystem ? (
+ ''
+ ) : (
+ by {displayNameService.getDisplayName(user)}
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(By, {});
+
+export default inject('userDisplayName')(withRouter(observer(By)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/CfnStackOutputContainer/CfnStackOutput.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/CfnStackOutputContainer/CfnStackOutput.js
new file mode 100644
index 0000000000..efa56a6459
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/CfnStackOutputContainer/CfnStackOutput.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 React, { PureComponent } from 'react';
+import { Message, Icon, List } from 'semantic-ui-react';
+import PropTypes from 'prop-types';
+
+const LOADING_ICON = 'circle notched';
+const MESSAGE_POS = 'positive';
+const MESSAGE_NEG = 'negative';
+const MESSAGE_INFO = 'info';
+
+export default class CfnStackOutput extends PureComponent {
+ render() {
+ let messageType = MESSAGE_POS;
+ let iconType = 'check';
+ if (this.props.isStackExecuting) {
+ messageType = MESSAGE_INFO;
+ iconType = LOADING_ICON;
+ } else if (this.props.errorMessage) {
+ messageType = MESSAGE_NEG;
+ iconType = 'dont';
+ }
+
+ return (
+
+
+
+ Results from the creating the stack
+
+ {this.props.outputs.map((item, index) => {
+ // eslint-disable-next-line react/no-array-index-key
+ return {item} ;
+ })}
+
+ {this.props.errorMessage}
+
+
+ );
+ }
+}
+CfnStackOutput.propTypes = {
+ isStackExecuting: PropTypes.bool.isRequired,
+ outputs: PropTypes.arrayOf(PropTypes.string).isRequired,
+ errorMessage: PropTypes.string,
+};
+CfnStackOutput.defaultProps = {
+ errorMessage: '',
+};
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/CfnStackOutputContainer/index.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/CfnStackOutputContainer/index.js
new file mode 100644
index 0000000000..41c9777fca
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/CfnStackOutputContainer/index.js
@@ -0,0 +1,63 @@
+/*
+ * 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, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import CfnStackOutput from './CfnStackOutput';
+
+export default class CfnStackOutputContainer extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isStackExecuting: true,
+ errorMessage: '',
+ outputs: [],
+ };
+ }
+
+ componentDidMount() {
+ this.timer = setInterval(this.describeCfnStack, 15000);
+ }
+
+ componentWillUnmount() {
+ clearInterval(this.timer);
+ }
+
+ describeCfnStack = async () => {
+ if (this.state.isStackExecuting === false) {
+ clearInterval(this.timer);
+ return;
+ }
+ this.setState({
+ isStackExecuting: true,
+ });
+ const results = await this.props.cfn.describeStack(this.props.stackName);
+
+ this.setState({
+ // eslint-disable-next-line react/no-access-state-in-setstate
+ outputs: [...this.state.outputs, JSON.stringify(results)],
+ isStackExecuting: !results.isDone,
+ });
+ };
+
+ render() {
+ return ;
+ }
+}
+CfnStackOutputContainer.propTypes = {
+ stackName: PropTypes.string.isRequired,
+ // eslint-disable-next-line react/forbid-prop-types
+ cfn: PropTypes.object.isRequired,
+};
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/PinInput.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/PinInput.js
new file mode 100644
index 0000000000..3c1f1c6c5d
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/PinInput.js
@@ -0,0 +1,24 @@
+/*
+ * 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 Input from '@aws-ee/base-ui/dist/parts/helpers/fields/Input';
+
+export default class PinInput extends React.PureComponent {
+ render() {
+ const pin = this.props.form.$('pin');
+ return ;
+ }
+}
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/projects/AddProject.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/projects/AddProject.js
new file mode 100644
index 0000000000..71afe57ef7
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/projects/AddProject.js
@@ -0,0 +1,213 @@
+/*
+ * 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 { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { decorate, observable, action, runInAction } from 'mobx';
+import { Button, Dimmer, Header, List, Loader, Dropdown, Segment } from 'semantic-ui-react';
+import _ from 'lodash';
+
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
+import validate from '@aws-ee/base-ui/dist/models/forms/Validate';
+
+import { getAddProjectForm, getAddProjectFormFields } from '../../models/forms/AddProjectForm';
+
+class AddProject extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ // eslint-disable-next-line react/no-unused-state
+ role: 'guest',
+ // eslint-disable-next-line react/no-unused-state
+ status: 'active',
+ indexId: '',
+ };
+
+ runInAction(() => {
+ this.formProcessing = false;
+ this.validationErrors = new Map();
+ this.project = {};
+ });
+ this.form = getAddProjectForm();
+ this.addProjectFormFields = getAddProjectFormFields();
+ }
+
+ goto(pathname) {
+ const location = this.props.location;
+ const link = createLink({ location, pathname });
+ this.props.history.push(link);
+ }
+
+ render() {
+ return (
+
+
+
{this.renderAddProjectForm()}
+
+ );
+ }
+
+ // eslint-disable-next-line react/no-unused-state
+ handleRoleChange = (e, { value }) => this.setState({ role: value });
+
+ // eslint-disable-next-line react/no-unused-state
+ handleStatusChange = (e, { value }) => this.setState({ status: value });
+
+ renderAddProjectForm() {
+ const processing = this.formProcessing;
+ const fields = this.addProjectFormFields;
+ const toEditableInput = (attributeName, type = 'text') => {
+ const handleChange = action(event => {
+ event.preventDefault();
+ this.project[attributeName] = event.target.value;
+ });
+ return (
+
+
+
+ );
+ };
+
+ return (
+
+
+ Checking
+
+ {this.renderField('id', toEditableInput('id', 'id'))}
+
+ {this.renderField('indexId')}
+ {this.renderIndexSelection()}
+
+ {this.renderField('description', toEditableInput('description', 'description'))}
+ {this.renderButtons()}
+
+ );
+ }
+
+ renderButtons() {
+ const processing = this.formProcessing;
+ return (
+
+
+ Add Project
+
+
+ Cancel
+
+
+ );
+ }
+
+ renderIndexSelection() {
+ const indexIdOption = this.props.indexesStore.dropdownOptions;
+ return (
+
+ );
+ }
+
+ handleIndexSelection = (e, { value }) => this.setState({ indexId: value });
+
+ renderField(name, component) {
+ const fields = this.addProjectFormFields;
+ const explain = fields[name].explain;
+ const label = fields[name].label;
+ const hasExplain = !_.isEmpty(explain);
+ const fieldErrors = this.validationErrors.get(name);
+ const hasError = !_.isEmpty(fieldErrors);
+
+ return (
+
+
+ {hasExplain &&
{explain}
}
+
{component}
+ {hasError && (
+
+
+ {_.map(fieldErrors, fieldError => (
+
+ {fieldError}
+
+ ))}
+
+
+ )}
+
+ );
+ }
+
+ handleCancel = action(event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.formProcessing = false;
+ this.goto('/accounts');
+ });
+
+ handleSubmit = action(async () => {
+ this.formProcessing = true;
+ try {
+ // Perform client side validations first
+ const validationResult = await validate(this.project, this.addProjectFormFields);
+ // if there are any client side validation errors then do not attempt to make API call
+ if (validationResult.fails()) {
+ runInAction(() => {
+ this.validationErrors = validationResult.errors;
+ this.formProcessing = false;
+ });
+ } else {
+ // There are no client side validation errors so ask the store to add user (which will make API call to server to add the user)
+ this.project.indexId = this.state.indexId;
+ await this.props.projectsStore.addProject(this.project);
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ this.goto('/accounts');
+ }
+ } catch (error) {
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ displayError(error);
+ }
+ });
+
+ getStore() {
+ return this.props.usersStore;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(AddProject, {
+ formProcessing: observable,
+ user: observable,
+ validationErrors: observable,
+});
+export default inject('usersStore', 'indexesStore', 'projectsStore')(withRouter(observer(AddProject)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/projects/ProjectsList.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/projects/ProjectsList.js
new file mode 100644
index 0000000000..78ea7956c2
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/projects/ProjectsList.js
@@ -0,0 +1,131 @@
+/*
+ * 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 { Button, Container, Header, Icon, Label } from 'semantic-ui-react';
+import { withRouter } from 'react-router-dom';
+import { inject, observer } from 'mobx-react';
+import ReactTable from 'react-table';
+
+import { isStoreError, isStoreLoading } from '@aws-ee/base-ui/dist/models/BaseStore';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
+import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+
+class ProjectsList extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ }
+
+ getProjectsStore() {
+ const store = this.props.projectsStore;
+ store.load();
+ return store;
+ }
+
+ getProjects() {
+ const store = this.getProjectsStore();
+ return store.list;
+ }
+
+ renderMain() {
+ const projectsData = this.getProjects();
+ const pageSize = projectsData.length;
+ const pagination = projectsData.length > pageSize;
+ return (
+
+ {
+ const columnValue = String(row[filter.id]).toLowerCase();
+ const filterValue = filter.value.toLowerCase();
+ return columnValue.indexOf(filterValue) >= 0;
+ }}
+ columns={[
+ {
+ Header: 'Project Name',
+ accessor: 'id',
+ },
+ {
+ Header: 'Index Id',
+ accessor: 'indexId',
+ },
+ {
+ Header: 'Description',
+ accessor: 'description',
+ },
+ ]}
+ />
+
+
+ );
+ }
+
+ goto(pathname) {
+ const location = this.props.location;
+ const link = createLink({ location, pathname });
+ this.props.history.push(link);
+ }
+
+ handleAddProject = () => {
+ this.goto('/projects/add');
+ };
+
+ renderHeader() {
+ return (
+
+
+
+
+ Projects
+ {this.renderTotal()}
+
+
+
+ Add Project
+
+
+ );
+ }
+
+ renderTotal() {
+ return {this.getProjects().length} ;
+ }
+
+ render() {
+ const store = this.getProjectsStore();
+ let content;
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else {
+ content = this.renderMain();
+ }
+ return (
+
+ {this.renderHeader()}
+ {content}
+
+ );
+ }
+}
+
+export default inject('awsAccountsStore', 'projectsStore')(withRouter(observer(ProjectsList)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/CreateStudy.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/CreateStudy.js
new file mode 100644
index 0000000000..a9bc45c999
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/CreateStudy.js
@@ -0,0 +1,141 @@
+/*
+ * 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 { inject, observer } from 'mobx-react';
+import { decorate, observable, action, runInAction } from 'mobx';
+import { Button, Header, Modal, Segment } from 'semantic-ui-react';
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import Dropdown from '@aws-ee/base-ui/dist/parts/helpers/fields/DropDown';
+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 YesNo from '@aws-ee/base-ui/dist/parts/helpers/fields/YesNo';
+
+import { getCreateStudyForm } from '../../models/forms/CreateStudy';
+import { getCategoryById } from '../../models/studies/categories';
+
+// expected props
+// - userStore (via injection)
+// - studiesStoresMap (via injection)
+class CreateStudy extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.cleanModal();
+ this.form = getCreateStudyForm();
+ });
+ }
+
+ getStudiesStore(categoryId) {
+ return this.props.studiesStoresMap[categoryId];
+ }
+
+ cleanModal = () => {
+ runInAction(() => {
+ this.modalOpen = false;
+ });
+ };
+
+ handleFormCancel = form => {
+ form.clear();
+ this.cleanModal();
+ };
+
+ handleFormError = (_form, _errors) => {};
+
+ handleFormSubmission = async form => {
+ try {
+ const studyValues = form.values();
+ const categoryId = studyValues.categoryId; // Type here is the category id
+ const categoryName = (getCategoryById(categoryId) || {}).name;
+ const studiesStore = this.getStudiesStore(categoryId);
+
+ delete studyValues.categoryId;
+
+ // Create study, clear form, and close modal
+ await studiesStore.createStudy({ ...studyValues, category: categoryName }); // TODO the backend should really accept category id not the category name
+ form.clear();
+ this.cleanModal();
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ render() {
+ return (
+
+
+
+
{this.renderCreateStudyForm()}
+
+
+ );
+ }
+
+ renderTrigger() {
+ return (
+ {
+ this.modalOpen = true;
+ })}
+ >
+ Create Study
+
+ );
+ }
+
+ renderCreateStudyForm() {
+ const form = this.form;
+ const projectIds = this.props.userStore.projectIdDropdown;
+
+ return (
+
+
+ {({ processing, /* onSubmit, */ onCancel }) => (
+ <>
+
+
+
+
+
+
+
+ Create Study
+
+
+ Cancel
+
+ >
+ )}
+
+
+ );
+ }
+}
+
+decorate(CreateStudy, {
+ form: observable,
+ modalOpen: observable,
+ getStudiesStore: observable,
+ cleanModal: action,
+ handleFormSubmission: action,
+});
+
+export default inject('userStore', 'studiesStoresMap')(observer(CreateStudy));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudiesPage.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudiesPage.js
new file mode 100644
index 0000000000..226c7f8c35
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudiesPage.js
@@ -0,0 +1,239 @@
+/*
+ * 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 React from 'react';
+import _ from 'lodash';
+import { decorate, action, observable, computed } from 'mobx';
+import { observer, inject, Observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Container, Icon, Button, Label, Header, Tab, Message, Menu } from 'semantic-ui-react';
+import { niceNumber } from '@aws-ee/base-ui/dist/helpers/utils';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+
+import { isStoreError, isStoreNew, isStoreLoading } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { categories } from '../../models/studies/categories';
+import StudiesTab from './StudiesTab';
+import CreateStudy from './CreateStudy';
+import StudyStepsProgress from './StudyStepsProgress';
+
+// 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
+// - filesSelection (via injection)
+// - studiesStoresMap (via injection)
+// - userStore (via injection)
+class StudiesPage extends React.Component {
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ getStudiesStore(category) {
+ return this.props.studiesStoresMap[category.id];
+ }
+
+ goto(pathname) {
+ const goto = gotoFn(this);
+ goto(pathname);
+ }
+
+ get canCreateStudy() {
+ // Note, this does not cover the case if you can create a study but don't have any project linked with you yet.
+ return _.get(this.props.userStore, 'user.capabilities.canCreateStudy', true);
+ }
+
+ get canSelectStudy() {
+ return _.get(this.props.userStore, 'user.capabilities.canSelectStudy', true);
+ }
+
+ get isExternalUser() {
+ // Both external guests and external researchers are considered external users
+ return _.get(this.props.userStore, 'user.isExternalUser', true);
+ }
+
+ get hasProjects() {
+ return _.get(this.props.userStore, 'user.hasProjects', true);
+ }
+
+ handleNext = () => {
+ this.goto('/studies/setup-workspace');
+ };
+
+ render() {
+ const canSelectStudy = this.canSelectStudy;
+
+ return (
+
+ {this.renderTitle()}
+ {canSelectStudy && this.renderStepsProgress()}
+ {this.renderSelection()}
+ {this.renderStudyTabs()}
+
+ );
+ }
+
+ renderTitle() {
+ const canCreateStudy = this.canCreateStudy;
+ const hasProjects = this.hasProjects;
+ return (
+
+
+ {canCreateStudy && hasProjects && }
+
+ );
+ }
+
+ renderStepsProgress() {
+ return ;
+ }
+
+ renderStudyTabs() {
+ const isExternalUser = this.isExternalUser;
+ const getMenuItemLabel = category => {
+ const store = this.getStudiesStore(category);
+ 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.total)} ;
+ };
+
+ // Create tab panes for each study category. If the user is not external user, then myStudies pane should not be shown
+ const applicableCategories = _.filter(categories, category => {
+ if (category.id === 'my-studies' && isExternalUser) return false;
+ return true;
+ });
+
+ const studyPanes = _.map(applicableCategories, category => ({
+ menuItem: (
+
+ {category.name} {getMenuItemLabel(category)}
+
+ ),
+ render: () => (
+
+ {() => }
+
+ ),
+ }));
+
+ return ;
+ }
+
+ renderSelection() {
+ const selection = this.props.filesSelection;
+ const empty = selection.empty;
+ const count = selection.count;
+ const canCreateStudy = this.canCreateStudy;
+ const canSelectStudy = this.canSelectStudy;
+ const hasProjects = this.hasProjects;
+
+ if (empty && canCreateStudy && canSelectStudy && hasProjects) {
+ return this.renderWarningWithButton({
+ content: (
+ <>
+ Select one or more studies to proceed to the next step or create a study by clicking on Create Study {' '}
+ button at the top.
+ >
+ ),
+ });
+ }
+
+ if (empty && canCreateStudy && canSelectStudy && !hasProjects) {
+ return this.renderWarning({
+ header: 'Missing association with one or more projects!',
+ content:
+ "You won't be able to select or create studies because you currently don't have any association with one or more projects, please contact your administrator.",
+ });
+ }
+
+ if (empty && canSelectStudy && !canCreateStudy) {
+ return this.renderWarningWithButton({
+ content: 'Select one or more studies to proceed to the next step.',
+ });
+ }
+
+ if (empty) {
+ return this.renderWarning({
+ header: 'Limited access',
+ content:
+ 'You currently have limited access and will not be able to select studies to proceed to the next step.',
+ });
+ }
+
+ return (
+
+
+ Next
+
+
+
+
+ Selected studies
+
+ {niceNumber(count)}
+ {' '}
+
+
+
+ );
+ }
+
+ renderWarning({ header, content }) {
+ return (
+
+
+
+ {header}
+ {content}
+
+
+ );
+ }
+
+ renderWarningWithButton({ content }) {
+ return (
+
+
+ Next
+
+
+ {content}
+
+ );
+ }
+}
+
+decorate(StudiesPage, {
+ getStudiesStore: observable,
+ canCreateStudy: computed,
+ canSelectStudy: computed,
+ hasProjects: computed,
+ isExternalUser: computed,
+ handleNext: action,
+});
+
+export default inject('filesSelection', 'studiesStoresMap', 'userStore')(withRouter(observer(StudiesPage)));
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
new file mode 100644
index 0000000000..543ecff649
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudiesTab.js
@@ -0,0 +1,165 @@
+/*
+ * 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 React from 'react';
+import _ from 'lodash';
+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 BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+import StudyRow from './StudyRow';
+import { categories } from '../../models/studies/categories';
+
+// expected props
+// - category (an object with the shape { name, id})
+// - studiesStoresMap (via injection)
+// - userStore (via injection)
+class StudiesTab extends React.Component {
+ get studiesStore() {
+ return this.props.studiesStoresMap[this.props.category.id];
+ }
+
+ componentDidMount() {
+ const store = this.studiesStore;
+ if (!store) return;
+ if (!isStoreReady(store)) swallowError(store.load());
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.studiesStore;
+ if (!store) return;
+ store.stopHeartbeat();
+ }
+
+ get canCreateStudy() {
+ return _.get(this.props.userStore, 'user.capabilities.canCreateStudy', true) && this.hasProjects;
+ }
+
+ get canSelectStudy() {
+ const can = _.get(this.props.userStore, 'user.capabilities.canSelectStudy', true);
+ if (!can) return can; // If can't select study, then return early, no need to examine if the user is internal and does not have projects
+ if (!this.isExternalUser) return this.hasProjects;
+
+ return can;
+ }
+
+ get isExternalUser() {
+ // Both external guests and external researchers are considered external users
+ return _.get(this.props.userStore, 'user.isExternalUser', true);
+ }
+
+ get hasProjects() {
+ return _.get(this.props.userStore, 'user.hasProjects', true);
+ }
+
+ render() {
+ const studiesStore = this.studiesStore;
+ if (!studiesStore) return null;
+
+ // Render loading, error, or tab content
+ let content;
+ if (isStoreError(studiesStore)) {
+ content = ;
+ } else if (isStoreLoading(studiesStore)) {
+ content = ;
+ } else if (isStoreEmpty(studiesStore)) {
+ content = this.renderEmpty();
+ } else {
+ content = this.renderContent();
+ }
+
+ return content;
+ }
+
+ renderContent() {
+ const studiesStore = this.studiesStore;
+ const isSelectable = this.canSelectStudy;
+ return (
+
+ {studiesStore.list.map(study => (
+
+ ))}
+
+ );
+ }
+
+ renderEmpty() {
+ const categoryId = this.props.category.id;
+ const isOpenData = categoryId === categories.openData.id;
+ const isOrgData = categoryId === categories.organization.id;
+ const canCreateStudy = this.canCreateStudy;
+
+ let header = 'No studies';
+ let subheader = canCreateStudy ? (
+ <>
+ To create a study, click on the Create Study button at the top.
+ >
+ ) : (
+ ''
+ );
+
+ if (isOpenData) {
+ header = 'No studies from the Open Data project';
+ subheader = 'The information in this page is updated once a day, please come back later.';
+ }
+
+ if (isOrgData) {
+ header = 'No studies shared with you';
+ subheader = (
+ <>
+
+ Studies created at the organization level can be shared but you don't have any that is shared with you.
+
+ {canCreateStudy && (
+
+ You can create one yourself by clicking on the Create Study button at the top.
+
+ )}
+ {!canCreateStudy && (
+
+ Consider viewing the Open Data studies by clicking on the Open Data tab above.
+
+ )}
+ >
+ );
+ }
+
+ return (
+
+
+
+ {header}
+ {subheader}
+
+
+ );
+ }
+}
+
+decorate(StudiesTab, {
+ studiesStore: computed,
+ canCreateStudy: computed,
+ canSelectStudy: computed,
+ hasProjects: computed,
+ isExternalUser: computed,
+});
+
+export default inject('studiesStoresMap', 'userStore')(observer(StudiesTab));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyEnvironmentSetup.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyEnvironmentSetup.js
new file mode 100644
index 0000000000..9d4aedc9b8
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyEnvironmentSetup.js
@@ -0,0 +1,155 @@
+/*
+ * 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 { Icon, Container, Header, Segment, Button } from 'semantic-ui-react';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import { displaySuccess } from '@aws-ee/base-ui/dist/helpers/notification';
+
+import { CurrentStep } from '../compute/helpers/CurrentStep';
+import ComputePlatformSetup from '../compute/ComputePlatformSetup';
+import StudyStepsProgress from './StudyStepsProgress';
+
+// expected props
+// - filesSelection (via injection)
+class StudyEnvironmentSetup extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.currentStep = CurrentStep.create({ step: 'selectComputePlatform' });
+ });
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ goto(pathname) {
+ const goto = gotoFn(this);
+ goto(pathname);
+ }
+
+ handlePrevious = () => {
+ this.goto('/studies');
+ };
+
+ // eslint-disable-next-line no-unused-vars
+ handleCompleted = async environment => {
+ this.props.filesSelection.cleanup();
+ displaySuccess('The research workspace is being provisioned');
+ this.goto('/workspaces');
+ };
+
+ get studyIds() {
+ return this.props.filesSelection.fileNames; // TODO - yes this is confusing, we should refactor the filesSelection to studiesSelection
+ }
+
+ render() {
+ return (
+
+ {this.renderTitle()}
+ {this.renderStepsProgress()}
+ {this.renderContent()}
+
+ );
+ }
+
+ renderTitle() {
+ return (
+
+
+
+ );
+ }
+
+ renderStepsProgress() {
+ return ;
+ }
+
+ renderContent() {
+ const studyIds = this.studyIds;
+
+ if (_.isEmpty(studyIds)) {
+ return this.renderEmpty();
+ }
+
+ return (
+
+ );
+ }
+
+ renderEmpty() {
+ return (
+ <>
+
+
+
+ No studies selected
+
+ Before you can create a workspace, you need to select one or more studies.
+
+
+
+ {this.renderButtons()}
+ >
+ );
+ }
+
+ renderButtons() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(StudyEnvironmentSetup, {
+ handlePrevious: action,
+ handleCompleted: action,
+ studyIds: computed,
+ currentStep: observable,
+});
+
+export default inject('filesSelection')(withRouter(observer(StudyEnvironmentSetup)));
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
new file mode 100644
index 0000000000..cd9619ec8c
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyFilesTable.js
@@ -0,0 +1,105 @@
+/*
+ * 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 { observable, runInAction, decorate } from 'mobx';
+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 BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+// expected props
+// - study
+class StudyFilesTable extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.filesStore = props.study.getFilesStore();
+ });
+ }
+
+ componentDidMount() {
+ swallowError(this.filesStore.load());
+ this.filesStore.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ this.filesStore.stopHeartbeat();
+ }
+
+ render() {
+ const store = this.filesStore;
+ // Render loading, error, or tab content
+ let content;
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreEmpty(store)) {
+ content = this.renderEmpty();
+ } else {
+ content = this.renderTable();
+ }
+
+ return content;
+ }
+
+ renderTable() {
+ return (
+ <>
+
+
+
+ Filename
+ Size
+ Last Modified
+
+
+
+
+ {this.filesStore.files.map(file => (
+
+ {file.filename}
+ {formatBytes(file.size)}
+ {file.lastModified.toISOString()}
+
+ ))}
+
+
+ >
+ );
+ }
+
+ renderEmpty() {
+ return (
+
+
+
+ No files
+ No files are uploaded yet for this study
+
+
+ );
+ }
+}
+
+decorate(StudyFilesTable, {
+ filesStore: observable,
+});
+
+export default observer(StudyFilesTable);
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
new file mode 100644
index 0000000000..fea55aba9d
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyPermissionsTable.js
@@ -0,0 +1,203 @@
+/*
+ * 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 { action, decorate, observable, runInAction } from 'mobx';
+import { inject, observer } from 'mobx-react';
+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 { getIdentifierObjFromId } from '@aws-ee/base-ui/dist/models/users/User';
+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';
+
+// expected props
+// - study
+// - userStore (via injection)
+// - usersStore (via injection)
+class StudyPermissionsTable extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.permissionsStore = props.study.getPermissionsStore();
+ this.currUser = props.userStore.user;
+ this.usersStore = props.usersStore;
+
+ this.resetForm();
+ });
+ }
+
+ componentDidMount() {
+ swallowError(this.permissionsStore.load());
+ this.permissionsStore.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ this.permissionsStore.stopHeartbeat();
+ }
+
+ enableEditMode = () => {
+ // Set users who currently have permission to the study as the selected users
+ this.permissionsStore.studyPermissions.userTypes.forEach(userType => {
+ this.selectedUserIds[userType] = this.permissionsStore.studyPermissions[`${userType}Users`].map(user => user.id);
+ });
+
+ // Show edit dropdowns via observable
+ this.editModeOn = true;
+ };
+
+ resetForm = () => {
+ this.editModeOn = false;
+ this.isProcessing = false;
+ this.selectedUserIds = {};
+ };
+
+ submitUpdate = async () => {
+ runInAction(() => {
+ this.isProcessing = true;
+ });
+
+ // Convert user ID strings back into user objects
+ const selectedUsers = {};
+ this.permissionsStore.studyPermissions.userTypes.forEach(type => {
+ selectedUsers[type] = this.selectedUserIds[type].map(getIdentifierObjFromId);
+ });
+
+ // Perform update
+ try {
+ await this.permissionsStore.update(selectedUsers);
+ displaySuccess('Update Succeeded');
+ runInAction(() => {
+ this.resetForm();
+ });
+ } catch (error) {
+ displayError('Update Failed', error);
+ runInAction(() => {
+ this.isProcessing = false;
+ });
+ }
+ };
+
+ render() {
+ // Render loading, error, or permissions table
+ let content;
+ if (isStoreError(this.permissionsStore)) {
+ content = ;
+ } else if (isStoreLoading(this.permissionsStore) || isStoreNew(this.permissionsStore)) {
+ content = ;
+ } else {
+ content = this.renderTable();
+ }
+
+ return content;
+ }
+
+ renderTable() {
+ const studyPermissions = this.permissionsStore.studyPermissions;
+ const isEditable = studyPermissions.adminUsers.some(
+ adminUser => adminUser.ns === this.currUser.ns && adminUser.username === this.currUser.username,
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Permission Level
+
+ Users
+ {isEditable && !this.editModeOn && (
+
+ )}
+
+
+
+
+
+ {this.permissionsStore.studyPermissions.userTypes.map(userType => (
+
+ {userType}
+
+ {this.editModeOn ? (
+ this.renderUsersDropdown(userType)
+ ) : (
+
+ )}
+
+
+ ))}
+
+
+
+ {this.editModeOn && (
+ <>
+
+ Submit
+
+
+
+ Cancel
+
+ >
+ )}
+
+ >
+ );
+ }
+
+ renderUsersDropdown(userType) {
+ const dropdownOnChange = action((_event, data) => {
+ this.selectedUserIds[userType] = data.value;
+ });
+
+ return (
+
+ );
+ }
+}
+
+decorate(StudyPermissionsTable, {
+ editModeOn: observable,
+ isProcessing: observable,
+ selectedUserIds: observable,
+
+ enableEditMode: action,
+ resetForm: action,
+ submitUpdate: action,
+});
+export default inject('userStore', 'usersStore')(observer(StudyPermissionsTable));
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
new file mode 100644
index 0000000000..6a8255d68f
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyRow.js
@@ -0,0 +1,166 @@
+/*
+ * 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, runInAction, observable } from 'mobx';
+import { inject, observer } from 'mobx-react';
+import { Header, Checkbox, Segment, Accordion, Icon } from 'semantic-ui-react';
+import c from 'classnames';
+
+import StudyFilesTable from './StudyFilesTable';
+import StudyPermissionsTable from './StudyPermissionsTable';
+import UploadStudyFiles from './UploadStudyFiles';
+
+// expected props
+// - study (via props)
+// - isSelectable (via props)
+// - filesSelection (via injection)
+class StudyRow extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.filesExpanded = false;
+ this.permissionsExpanded = false;
+ });
+ }
+
+ get study() {
+ return this.props.study;
+ }
+
+ get isSelectable() {
+ return this.props.isSelectable;
+ }
+
+ handleFileSelection = study => {
+ const selection = this.props.filesSelection;
+ if (selection.hasFile(study.id)) {
+ selection.deleteFile(study.id);
+ } else {
+ const { id, name, description } = study;
+ // TODO: actually do different statuses?
+ selection.setFile({ id, name, description, accessStatus: 'approved' });
+ }
+ };
+
+ handleFilesExpanded = () => {
+ this.filesExpanded = !this.filesExpanded;
+ };
+
+ handlePermissionsExpanded = () => {
+ this.permissionsExpanded = !this.permissionsExpanded;
+ };
+
+ render() {
+ const isSelectable = this.isSelectable; // Internal and external guests can't select studies
+ const study = this.study;
+ const selection = this.props.filesSelection;
+ const isSelected = selection.hasFile(study.id);
+ const attrs = {};
+ const onClickAttr = {};
+
+ if (isSelected) attrs.color = 'blue';
+ if (isSelectable) onClickAttr.onClick = () => this.handleFileSelection(study);
+
+ return (
+
+
+
+ {isSelectable && }
+
+
+ {this.renderHeader(study)}
+ {this.renderDescription(study)}
+ {this.renderFilesAccordion(study)}
+ {this.renderPermissionsAccordion(study)}
+
+
+
+ );
+ }
+
+ renderHeader(study) {
+ const isSelectable = this.isSelectable; // Internal and external guests can't select studies
+ const onClickAttr = {};
+
+ if (isSelectable) onClickAttr.onClick = () => this.handleFileSelection(study);
+
+ return (
+ <>
+ {study.uploadLocationEnabled && study.access === 'admin' && }
+
+ {study.name}
+
+ {study.id}
+ {study.projectId && · {study.projectId} }
+
+
+ >
+ );
+ }
+
+ renderDescription(study) {
+ return {study.description}
;
+ }
+
+ renderFilesAccordion(study) {
+ if (study.isOpenDataStudy) return null;
+ if (!study.uploadLocationEnabled) return null;
+ const expanded = this.filesExpanded;
+
+ return (
+
+
+
+ Files
+
+
+ {expanded && study.uploadLocationEnabled && (
+
+
+
+ )}
+
+
+ );
+ }
+
+ renderPermissionsAccordion(study) {
+ if (!study.isOrganizationStudy) return null;
+ const expanded = this.permissionsExpanded;
+
+ return (
+
+
+
+ Permissions
+
+ {expanded && }
+
+ );
+ }
+}
+
+decorate(StudyRow, {
+ handleFileSelection: action,
+ handleFilesExpanded: action,
+ handlePermissionsExpanded: action,
+ study: computed,
+ filesExpanded: observable,
+ permissionsExpanded: observable,
+ isSelectable: computed,
+});
+
+export default inject('filesSelection')(observer(StudyRow));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyStepsProgress.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyStepsProgress.js
new file mode 100644
index 0000000000..ef82f70e80
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/StudyStepsProgress.js
@@ -0,0 +1,64 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Step, Icon } from 'semantic-ui-react';
+
+// expected props
+// currentStep an instance of the CurrentStep model
+const Component = observer(({ currentStep = {} }) => {
+ let activeIndex;
+ const step = currentStep.step;
+
+ switch (step) {
+ case 'selectComputePlatform':
+ activeIndex = 1;
+ break;
+ case 'selectComputeConfiguration':
+ activeIndex = 2;
+ break;
+ default:
+ activeIndex = 0;
+ }
+
+ return (
+
+
+
+
+ Find & Select Studies
+ Select the desired studies
+
+
+
+
+
+ Select Compute
+ Select a compute platform
+
+
+
+
+
+ Create Workspace
+ Create the workspace
+
+
+
+ );
+});
+
+export default Component;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/UploadStudyFiles.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/UploadStudyFiles.js
new file mode 100644
index 0000000000..ac2415fcd0
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/studies/UploadStudyFiles.js
@@ -0,0 +1,113 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { action, decorate, observable, runInAction } from 'mobx';
+import { Button, Header, Modal } from 'semantic-ui-react';
+
+import FileUpload from '../files/FileUpload';
+import { getPresignedStudyUploadRequests } from '../../helpers/api';
+import upload from '../../helpers/xhr-upload';
+
+// expected props
+// - studyId
+class UploadStudyFiles extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.modalOpen = false;
+ });
+ this.fileUploadHandler = this.fileUploadHandler.bind(this);
+ }
+
+ async fileUploadHandler(fileUpload) {
+ const file = fileUpload.getFile();
+ if (!file) {
+ throw new Error('No file');
+ }
+
+ // Get presigned POST request
+ let uploadRequest;
+ try {
+ const presignResult = await getPresignedStudyUploadRequests(this.props.studyId, fileUpload.name);
+ uploadRequest = presignResult[fileUpload.name];
+ } catch (error) {
+ const errMessage = 'Error occurred obtaining presigned request';
+ console.error(`${errMessage}:`, error);
+ throw new Error(errMessage);
+ }
+
+ if (!uploadRequest) {
+ throw new Error('Failed to obtain presigned request');
+ }
+
+ // Upload file
+ const uploadHandle = upload(file, uploadRequest.url, uploadRequest.fields);
+ fileUpload.setCancel(uploadHandle.cancel);
+ uploadHandle.onProgress(fileUpload.updateProgress);
+
+ try {
+ await uploadHandle.done;
+ } catch (error) {
+ const errMessage = 'Error encountered while uploading file';
+ console.error(`${errMessage}:`, error);
+ throw new Error(errMessage);
+ }
+ }
+
+ render() {
+ return (
+ {
+ this.modalOpen = false;
+ })}
+ >
+
+
+ );
+ }
+
+ renderTrigger() {
+ return (
+ {
+ this.modalOpen = true;
+ })}
+ >
+ Upload Files
+
+ );
+ }
+}
+
+decorate(UploadStudyFiles, {
+ modalOpen: observable,
+});
+
+export default observer(UploadStudyFiles);
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/AddSingleLocalUser.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/AddSingleLocalUser.js
new file mode 100644
index 0000000000..368b03312d
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/AddSingleLocalUser.js
@@ -0,0 +1,203 @@
+import _ from 'lodash';
+import React from 'react';
+import { computed, decorate, observable, runInAction, action } from 'mobx';
+import { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Container, Header, Icon, Message, Segment, Button } from 'semantic-ui-react';
+
+import { displaySuccess, displayError } from '@aws-ee/base-ui/dist//helpers/notification';
+import Stores from '@aws-ee/base-ui/dist/models/Stores';
+import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import Form from '@aws-ee/base-ui/dist/parts/helpers/fields/Form';
+import YesNo from '@aws-ee/base-ui/dist/parts/helpers/fields/YesNo';
+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 { getAddUserForm } from '../../models/forms/AddLocalUserForm';
+
+// expected props
+// - projectsStore (via injection)
+// - userRolesStore (via injection)
+// - usersStore (via injection)
+class AddSingleLocalUser extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.stores = new Stores([this.userRolesStore, this.projectsStore]);
+ this.form = getAddUserForm();
+ });
+ }
+
+ componentDidMount() {
+ swallowError(this.stores.load());
+ }
+
+ get projectsStore() {
+ return this.props.projectsStore;
+ }
+
+ get userRolesStore() {
+ return this.props.userRolesStore;
+ }
+
+ get usersStore() {
+ return this.props.usersStore;
+ }
+
+ // Private methods
+ handleCancel = () => {
+ const goto = gotoFn(this);
+ goto('/users');
+ };
+
+ handleFormSubmission = async form => {
+ const values = form.values();
+ const isInternalUser = this.userRolesStore.isInternalUser(values.userRole);
+ const isInternalGuest = this.userRolesStore.isInternalGuest(values.userRole);
+
+ let projectId = values.projectId || [];
+ if (!isInternalUser || isInternalGuest) {
+ // Pass projectId(s) only if it is internal user or a guest. Pass empty array otherwise.
+ projectId = [];
+ }
+
+ try {
+ await this.usersStore.addUser({ ...values, projectId });
+ runInAction(() => {
+ form.clear();
+ });
+ displaySuccess('Added local user successfully');
+
+ const goto = gotoFn(this);
+ goto('/users');
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ render() {
+ const stores = this.stores;
+ let content = null;
+ if (stores.hasError) {
+ content = ;
+ } else if (stores.loading) {
+ content = ;
+ } else if (stores.ready) {
+ content = this.renderContent();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderTitle()}
+ {this.renderWarning()}
+ {content}
+
+ );
+ }
+
+ renderTitle() {
+ return (
+
+
+
+ );
+ }
+
+ renderContent() {
+ const form = this.form;
+ const emailField = form.$('email');
+ const firstNameField = form.$('firstName');
+ const lastNameField = form.$('lastName');
+ const passwordField = form.$('password');
+ const userRoleField = form.$('userRole');
+ const projectIdField = form.$('projectId');
+ const statusField = form.$('status');
+
+ const userRoleOptions = this.userRolesStore.dropdownOptions;
+ const projectIdOptions = this.projectsStore.dropdownOptions;
+
+ const isInternalUser = this.userRolesStore.isInternalUser(userRoleField.value);
+ const isInternalGuest = this.userRolesStore.isInternalGuest(userRoleField.value);
+ const showProjectField = !_.isEmpty(projectIdOptions) && isInternalUser && !isInternalGuest;
+ const showProjectWarning = _.isEmpty(projectIdOptions) && isInternalUser && !isInternalGuest;
+
+ return (
+
+
+ {({ processing, onCancel }) => (
+ <>
+
+
+
+
+
+
+ {showProjectField && (
+
+ )}
+
+ {showProjectWarning && (
+
+ )}
+
+
+
+
+
+ Add Local User
+
+
+ Cancel
+
+
+ >
+ )}
+
+
+ );
+ }
+
+ renderWarning() {
+ return (
+
+ );
+ }
+}
+
+decorate(AddSingleLocalUser, {
+ projectsStore: computed,
+ userRolesStore: computed,
+ usersStore: computed,
+ stores: observable,
+ form: observable,
+ handleCancel: action,
+ handleFormSubmission: action,
+});
+
+export default inject('projectsStore', 'userRolesStore', 'usersStore')(withRouter(observer(AddSingleLocalUser)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/AddSingleUser.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/AddSingleUser.js
new file mode 100644
index 0000000000..7a22348b13
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/AddSingleUser.js
@@ -0,0 +1,245 @@
+/*
+ * 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 _ from 'lodash';
+import { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { decorate, computed, runInAction } from 'mobx';
+import { Segment, Button } from 'semantic-ui-react';
+
+import { displaySuccess, displayError } from '@aws-ee/base-ui/dist//helpers/notification';
+
+import Stores from '@aws-ee/base-ui/dist/models/Stores';
+import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import Form from '@aws-ee/base-ui/dist/parts/helpers/fields/Form';
+import YesNo from '@aws-ee/base-ui/dist/parts/helpers/fields/YesNo';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+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 { getAddUserForm, getAddUserFormFields } from '../../models/forms/AddUserForm';
+import { toIdpFromValue, toIdpOptions } from '../../models/forms/UserFormUtils';
+
+// expected props
+// - userStore (via injection)
+// - usersStore (via injection)
+// - userRolesStore (via injection)
+// - awsAccountsStore (via injection)
+// - projectsStore (via injection)
+// - authenticationProviderConfigsStore (via injection)
+class AddSingleUser extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.stores = new Stores([
+ this.userStore,
+ this.usersStore,
+ this.userRolesStore,
+ this.awsAccountsStore,
+ this.projectsStore,
+ this.authenticationProviderConfigsStore,
+ ]);
+ });
+ this.form = getAddUserForm();
+ this.addUserFormFields = getAddUserFormFields();
+ }
+
+ componentDidMount() {
+ swallowError(this.getStores().load());
+ }
+
+ render() {
+ const stores = this.getStores();
+ let content = null;
+ if (stores.hasError) {
+ content = ;
+ } else if (stores.loading) {
+ content = ;
+ } else if (stores.ready) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return content;
+ }
+
+ renderMain() {
+ const form = this.form;
+ const emailField = form.$('email');
+ const identityProviderNameField = form.$('identityProviderName');
+ const userRoleField = form.$('userRole');
+ const projectIdField = form.$('projectId');
+ const statusField = form.$('status');
+
+ const identityProviderOptions = this.getIdentityProviderOptions();
+ const userRoleOptions = this.getUserRoleOptions();
+ const projectIdOptions = this.getProjectOptions();
+
+ const isInternalUser = this.userRolesStore.isInternalUser(userRoleField.value);
+ const isInternalGuest = this.userRolesStore.isInternalGuest(userRoleField.value);
+ const showProjectField = !_.isEmpty(projectIdOptions) && isInternalUser && !isInternalGuest;
+
+ return (
+
+
+ {({ processing, _onSubmit, onCancel }) => (
+ <>
+
+
+
+
+ {showProjectField && (
+
+ )}
+
+
+
+
+
+ Add User
+
+
+ Cancel
+
+
+ >
+ )}
+
+
+ );
+ }
+
+ getIdentityProviderOptions() {
+ return toIdpOptions(this.authenticationProviderConfigsStore.list);
+ }
+
+ getUserRoleOptions() {
+ return this.userRolesStore.dropdownOptions;
+ }
+
+ getProjectOptions() {
+ return this.projectsStore.dropdownOptions;
+ }
+
+ // Private methods
+ handleCancel = () => {
+ const goto = gotoFn(this);
+ goto('/users');
+ };
+
+ handleFormSubmission = async form => {
+ const values = form.values();
+ const isInternalUser = this.userRolesStore.isInternalUser(values.userRole);
+ const isInternalGuest = this.userRolesStore.isInternalGuest(values.userRole);
+ let projectId = values.projectId || [];
+ if (!isInternalUser || isInternalGuest) {
+ // Pass projectId(s) only if the user's role is internal role and if the user is not a guest.
+ // Pass empty array otherwise.
+ projectId = [];
+ }
+
+ // The values.identityProviderName is in JSON string format
+ // containing authentication provider id as well as identity provider name
+ // See "src/models/forms/UserFormUtils.js" for more details.
+ const idpOptionValue = toIdpFromValue(values.identityProviderName);
+ const identityProviderName = idpOptionValue.idpName;
+ const authenticationProviderId = idpOptionValue.authNProviderId;
+
+ try {
+ await this.usersStore.addUser({ ...values, authenticationProviderId, identityProviderName, projectId });
+ form.clear();
+ displaySuccess('Added user successfully');
+
+ const goto = gotoFn(this);
+ goto('/users');
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ // eslint-disable-next-line no-unused-vars
+ handleFormError = (_form, _errors) => {
+ // We don't need to do anything here
+ };
+
+ getStores() {
+ return this.stores;
+ }
+
+ get userStore() {
+ return this.props.userStore;
+ }
+
+ get usersStore() {
+ return this.props.usersStore;
+ }
+
+ get userRolesStore() {
+ return this.props.userRolesStore;
+ }
+
+ get awsAccountsStore() {
+ return this.props.awsAccountsStore;
+ }
+
+ get projectsStore() {
+ return this.props.projectsStore;
+ }
+
+ get authenticationProviderConfigsStore() {
+ return this.props.authenticationProviderConfigsStore;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(AddSingleUser, {
+ userStore: computed,
+ usersStore: computed,
+ userRolesStore: computed,
+ awsAccountsStore: computed,
+ projectsStore: computed,
+ authenticationProviderConfigsStore: computed,
+});
+
+export default inject(
+ 'userStore',
+ 'usersStore',
+ 'userRolesStore',
+ 'awsAccountsStore',
+ 'projectsStore',
+ 'authenticationProviderConfigsStore',
+)(withRouter(observer(AddSingleUser)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/AddUser.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/AddUser.js
new file mode 100644
index 0000000000..2a6ecc9c0c
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/AddUser.js
@@ -0,0 +1,106 @@
+/*
+ * 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 { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Container, Header, Icon, Tab, Grid } from 'semantic-ui-react';
+
+import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import { isStoreError, isStoreLoading, isStoreReady } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { toIdpOptions } from '../../models/forms/UserFormUtils';
+import DragDrop from './DragDrop';
+import AddSingleUser from './AddSingleUser';
+
+// expected props
+// - authenticationProviderConfigsStore (via injection)
+class AddUser extends React.Component {
+ componentDidMount() {
+ swallowError(this.getStore().load());
+ }
+
+ render() {
+ const store = this.getStore();
+ let content = null;
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreReady(store)) {
+ content = this.renderMain();
+ }
+
+ return (
+
+ {this.renderTitle()}
+ {content}
+
+ );
+ }
+
+ renderTitle() {
+ return (
+
+
+
+ );
+ }
+
+ renderMain() {
+ const identityProviderOptions = this.getIdentityProviderOptions();
+ const panes = [
+ {
+ menuItem: 'Add Single User',
+ render: () => (
+
+
+
+ ),
+ },
+ {
+ menuItem: 'Add Multiple Users',
+ render: () => (
+
+
+
+ ),
+ },
+ ];
+ return (
+
+
+
+
+
+ );
+ }
+
+ getIdentityProviderOptions() {
+ return toIdpOptions(this.getStore().list);
+ }
+
+ getStore() {
+ return this.props.authenticationProviderConfigsStore;
+ }
+}
+
+export default inject('authenticationProviderConfigsStore')(withRouter(observer(AddUser)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/DragDrop.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/DragDrop.js
new file mode 100644
index 0000000000..7ae090612f
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/DragDrop.js
@@ -0,0 +1,249 @@
+/*
+ * 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, { Component } from 'react';
+import Dropzone from 'react-dropzone';
+import { withRouter } from 'react-router-dom';
+import { inject, observer } from 'mobx-react';
+import { Segment, Icon, Header, Button, Message, Container } from 'semantic-ui-react';
+import { computed, decorate, observable, action, runInAction } from 'mobx';
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
+
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+import Stores from '@aws-ee/base-ui/dist/models/Stores';
+import UserTable from './UserTable';
+
+class DragDrop extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ files: [],
+ fileContent: '',
+ jsonArrayContent: [],
+ };
+ runInAction(() => {
+ this.formProcessing = false;
+ this.validationErrors = new Map();
+ this.user = {};
+
+ this.stores = new Stores([
+ this.authenticationProviderConfigsStore,
+ this.userRolesStore,
+ this.usersStore,
+ this.userStore,
+ ]);
+ });
+ }
+
+ get userRolesStore() {
+ return this.props.userRolesStore;
+ }
+
+ get usersStore() {
+ return this.props.usersStore;
+ }
+
+ get userStore() {
+ return this.props.userStore;
+ }
+
+ get authenticationProviderConfigsStore() {
+ return this.props.authenticationProviderConfigsStore;
+ }
+
+ componentDidMount() {
+ swallowError(this.stores.load());
+ }
+
+ csvJSON(csv) {
+ const result = [];
+ const lines = csv.split(/\r?\n/);
+ const headers = lines[0].split(',');
+
+ for (let i = 1; i < lines.length; i++) {
+ const obj = {};
+ const currentline = lines[i].split(',');
+ for (let j = 0; j < headers.length; j++) {
+ obj[headers[j]] = currentline[j];
+ }
+ result.push(obj);
+ }
+
+ return result;
+ }
+
+ onDrop = files => {
+ const reader = new FileReader();
+ reader.onabort = () => console.log('file reading was aborted');
+ reader.onerror = () => console.log('file reading has failed');
+ reader.onload = () => {
+ const binaryStr = reader.result;
+ const jsonArray = this.csvJSON(binaryStr);
+ this.setState({ jsonArrayContent: jsonArray });
+ this.setState({ fileContent: binaryStr });
+ };
+ files.forEach(file => reader.readAsText(file));
+ this.setState({ files });
+ };
+
+ goto(pathname) {
+ const location = this.props.location;
+ const link = createLink({ location, pathname });
+ this.props.history.push(link);
+ }
+
+ renderButtons() {
+ const processing = this.formProcessing;
+ return (
+
+
+ Submit
+
+
+ Cancel
+
+
+ );
+ }
+
+ async addAuthProviderId(userArr) {
+ const promises = userArr.map(async user => {
+ const provider = this.authenticationProviderConfigsStore.getAuthenticationProviderConfigByIdpName(
+ user.identityProviderName,
+ );
+ const authenticationProviderId = _.get(provider, 'id');
+ user.authenticationProviderId = authenticationProviderId;
+ return user;
+ });
+ userArr = await Promise.all(promises);
+ return userArr;
+ }
+
+ handleSubmit = action(async () => {
+ this.formProcessing = true;
+ try {
+ // compose the content to users and invoke add user
+ const userArr = await this.addAuthProviderId(this.state.jsonArrayContent);
+ await this.usersStore.addUsers(userArr);
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ await this.usersStore.load();
+ this.goto('/users');
+ } catch (error) {
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ displayError(error);
+ }
+ });
+
+ handleCancel = action(event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.formProcessing = false;
+ this.goto('/users');
+ });
+
+ renderTable() {
+ let content;
+ if (this.state.fileContent) {
+ content = ;
+ } else {
+ content = '';
+ }
+ return content;
+ }
+
+ 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 content;
+ }
+
+ renderMain() {
+ const files = this.state.files.map(file => (
+
+ {file.name} - {file.size} bytes
+
+ ));
+ const maxSize = 512000;
+ return (
+
+
+
+
+ CSV Example
+ email,userRole,identityProviderName
+ user1@datalake.amazonaws.com,researcher,DataLake
+ user2@organization.onmicrosoft.com,researcher,AzureAD
+
+
+
+
+
+
+ Drag and drop files here
+
+
+ {({ getRootProps, getInputProps, rejectedFiles }) => {
+ const isFileTooLarge = rejectedFiles.length > 0 && rejectedFiles[0].size > maxSize;
+ return (
+
+
+ {
}
+ {isFileTooLarge &&
File is too large.
}
+
+ );
+ }}
+
+
+
+ {this.renderTable()}
+ {this.renderButtons()}
+
+ );
+ }
+}
+decorate(DragDrop, {
+ authenticationProviderConfigsStore: computed,
+ userRolesStore: computed,
+ usersStore: computed,
+ userStore: computed,
+ formProcessing: observable,
+});
+
+export default inject(
+ 'userStore',
+ 'usersStore',
+ 'userRolesStore',
+ 'authenticationProviderConfigsStore',
+)(withRouter(observer(DragDrop)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/RolesList.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/RolesList.js
new file mode 100644
index 0000000000..81c2f1d1b5
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/RolesList.js
@@ -0,0 +1,141 @@
+/*
+ * 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 { Container, Header, Icon, Label } from 'semantic-ui-react';
+import { withRouter } from 'react-router-dom';
+import { decorate, observable, runInAction } from 'mobx';
+import { inject, observer } from 'mobx-react';
+import ReactTable from 'react-table';
+
+import { isStoreError, isStoreLoading } from '@aws-ee/base-ui/dist/models/BaseStore';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
+import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+
+class RolesList extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ runInAction(() => {
+ // An object that keeps track of which user is being edited
+ // Each key in the object below has key as user's unique id (/)
+ // and value as flag indicating whether to show the editor for the user
+ this.mapOfUsersBeingEdited = {};
+ this.formProcessing = false;
+ });
+ }
+
+ getUserRolesStore() {
+ const store = this.props.userRolesStore;
+ return store;
+ }
+
+ getUserRoles() {
+ const store = this.getUserRolesStore();
+ return store.list;
+ }
+
+ renderMain() {
+ const userRolesData = this.getUserRoles();
+ const pageSize = userRolesData.length;
+ const showPagination = userRolesData.length > pageSize;
+ return (
+
+ {
+ const columnValue = String(row[filter.id]).toLowerCase();
+ const filterValue = filter.value.toLowerCase();
+ return columnValue.indexOf(filterValue) >= 0;
+ }}
+ columns={[
+ {
+ Header: 'User Role Name',
+ accessor: 'id',
+ },
+ {
+ Header: 'Description',
+ accessor: 'description',
+ },
+ {
+ Header: 'User Type',
+ accessor: 'userType',
+ },
+ ]}
+ />
+
+
+ );
+ }
+
+ goto(pathname) {
+ const location = this.props.location;
+ const link = createLink({ location, pathname });
+ this.props.history.push(link);
+ }
+
+ handleAddUserRole = () => {
+ this.goto('/user-roles/add');
+ };
+
+ renderHeader() {
+ return (
+
+
+
+
+ User Roles
+ {this.renderTotal()}
+
+
+
+ );
+ }
+
+ renderTotal() {
+ return {this.getUserRoles().length} ;
+ }
+
+ render() {
+ const store = this.getUserRolesStore();
+ let content;
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else {
+ content = this.renderMain();
+ }
+ return (
+
+ {this.renderHeader()}
+ {content}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(RolesList, {
+ mapOfUsersBeingEdited: observable,
+ formProcessing: observable,
+});
+
+export default inject('userRolesStore')(withRouter(observer(RolesList)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UpdateUser.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UpdateUser.js
new file mode 100644
index 0000000000..6d5ba0176d
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UpdateUser.js
@@ -0,0 +1,500 @@
+/*
+ * 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 { inject, observer } from 'mobx-react';
+import { decorate, observable, action, computed, runInAction } from 'mobx';
+import { Button, Header, Label, Segment, Modal, Menu, Icon, Table } from 'semantic-ui-react';
+import _ from 'lodash';
+
+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 BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+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 DropDown from '@aws-ee/base-ui/dist/parts/helpers/fields/DropDown';
+import YesNo from '@aws-ee/base-ui/dist/parts/helpers/fields/YesNo';
+import { displayError, displaySuccess } from '@aws-ee/base-ui/dist/helpers/notification';
+import { getUpdateUserConfigForm } from '../../models/forms/UpdateUserConfig';
+import { toIdpFromValue, toIdpOptions } from '../../models/forms/UserFormUtils';
+
+class UpdateUser extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.stores = new Stores([
+ this.userStore,
+ this.userRolesStore,
+ this.awsAccountsStore,
+ this.projectsStore,
+ this.authenticationProviderConfigsStore,
+ ]);
+ this.modalOpen = false;
+ this.processing = false;
+ this.view = 'detail'; // view mode or edit mode
+ });
+ this.form = getUpdateUserConfigForm(this.getCurrentUser());
+ }
+
+ componentDidMount() {
+ swallowError(this.getStores().load());
+ }
+
+ render() {
+ const stores = this.getStores();
+ let content = null;
+ if (stores.hasError) {
+ content = ;
+ } else if (stores.loading) {
+ content = ;
+ } else if (stores.ready) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+
+
+ );
+ }
+
+ renderMain() {
+ let content = null;
+ if (this.view === 'detail') {
+ content = this.renderDetailView();
+ } else if (this.view === 'edit') {
+ content = this.renderEditView();
+ }
+ return content;
+ }
+
+ renderDetailView() {
+ const getFieldLabel = fieldName => this.form.$(fieldName).label;
+ const toRow = fieldName => {
+ const value = _.get(this.getCurrentUser(), fieldName);
+ const displayValue = _.isArray(value) ? _.map(value, (v, k) => ) : value;
+ return (
+ <>
+
+ {getFieldLabel(fieldName)}
+
+ {displayValue}
+ >
+ );
+ };
+
+ return (
+
+
+
+ {toRow('username')}
+ {toRow('firstName')}
+ {toRow('lastName')}
+ {toRow('email')}
+ {this.getCurrentUser().isRootUser ? null : (
+ <>
+ {toRow('userRole')}
+ {toRow('identityProviderName')}
+ {toRow('projectId')}
+ {this.getCurrentUser().status === 'pending' && {toRow('applyReason')} }
+ {toRow('status')}
+ >
+ )}
+
+
+ {this.renderDetailViewButtons()}
+
+ );
+ }
+
+ renderDetailViewButtons() {
+ const makeButton = ({ label = '', color = 'blue', floated = 'left', disabled = false, ...props }) => {
+ const attrs = {};
+ if (color) attrs.color = color;
+ return (
+
+ {label}
+
+ );
+ };
+
+ const currentUser = this.getCurrentUser();
+
+ const cancelButton = makeButton({
+ label: 'Cancel',
+ floated: 'left',
+ color: '',
+ onClick: this.handleCancel,
+ disabled: this.processing,
+ });
+
+ const deleteButton =
+ // TODO: deletion actions should be confirmed by user first
+ this.view === 'detail'
+ ? makeButton({
+ label: 'Delete',
+ floated: 'right',
+ color: 'red',
+ onClick: this.handleDeleteClick,
+ disabled: currentUser.isRootUser || this.processing,
+ })
+ : '';
+
+ const activeButton =
+ this.props.user.status === 'pending' || this.props.user.status === 'inactive'
+ ? makeButton({
+ label: 'Activate User',
+ floated: 'right',
+ color: 'blue',
+ onClick: () => this.handleApproveDisapproveClick('active'),
+ disabled: this.processing,
+ })
+ : '';
+
+ const deactiveButton =
+ this.props.user.status === 'active' || this.props.user.status === 'pending'
+ ? makeButton({
+ label: 'Deactivate User',
+ floated: 'right',
+ disabled: currentUser.isRootUser || this.processing,
+ onClick: () => this.handleApproveDisapproveClick('inactive'),
+ })
+ : '';
+
+ const editButton =
+ currentUser.status === 'active' || currentUser.status === 'inactive' // do not show "edit" button for other status(es) such as "pending"
+ ? makeButton({ label: 'Edit', onClick: this.handleEditClick, floated: 'right', disabled: this.processing })
+ : '';
+
+ return this.props.adminMode ? (
+
+
+ {cancelButton}
+ {deleteButton}
+ {deactiveButton}
+ {activeButton}
+ {editButton}
+
+
+ ) : (
+
+
+ {cancelButton}
+ {editButton}
+
+
+ );
+ }
+
+ renderEditView() {
+ const form = this.form;
+
+ const firstNameField = form.$('firstName');
+ const lastNameField = form.$('lastName');
+ const emailField = form.$('email');
+ const identityProviderNameField = form.$('identityProviderName');
+ const userRoleField = form.$('userRole');
+ const projectIdField = form.$('projectId');
+ const statusField = form.$('status');
+
+ const identityProviderOptions = this.getIdentityProviderOptions();
+ const userRoleOptions = this.getUserRoleOptions();
+ const projectIdOptions = this.getProjectOptions();
+
+ const isInternalUser = this.userRolesStore.isInternalUser(userRoleField.value);
+ const isInternalGuest = this.userRolesStore.isInternalGuest(userRoleField.value);
+ const showProjectField = !_.isEmpty(projectIdOptions) && isInternalUser && !isInternalGuest;
+
+ const isAdminMode = this.props.adminMode;
+ return (
+
+
+ {({ processing, onCancel }) => (
+ <>
+
+
+
+ {this.getCurrentUser().isRootUser ? null : (
+ <>
+ {isAdminMode && (
+
+ )}
+ {isAdminMode && (
+
+ )}
+
+ {isAdminMode && showProjectField && (
+
+ )}
+
+
+ >
+ )}
+
+
+
+ Save
+
+
+ Cancel
+
+
+ >
+ )}
+
+
+ );
+ }
+
+ renderTrigger() {
+ let content = null;
+ if (this.props.adminMode) {
+ content = (
+
+ Detail
+
+ );
+ } else {
+ content = (
+
+ {this.props.userStore.user.displayName}
+
+ );
+ }
+ return content;
+ }
+
+ handleEditClick = () => {
+ this.view = 'edit';
+ };
+
+ handleCancel = () => {
+ this.form.clear();
+ if (this.view === 'edit') {
+ // if it's in edit mode then switch to detail view mode
+ this.view = 'detail';
+ } else {
+ // if not in edit mode then close
+ this.handleClose();
+ }
+ };
+
+ handleFormSubmission = async form => {
+ const values = form.values();
+ const isInternalUser = this.userRolesStore.isInternalUser(values.userRole);
+ const isInternalGuest = this.userRolesStore.isInternalGuest(values.userRole);
+ let projectId = values.projectId || [];
+ if (!isInternalUser || isInternalGuest) {
+ // Pass projectId(s) only if the user's role is internal role and if the user is not a guest.
+ // Pass empty array otherwise.
+ projectId = [];
+ }
+
+ const { firstName, lastName, email, userRole, status } = values;
+ const isAdmin = userRole === 'admin';
+ const identityProviderNameField = form.$('identityProviderName');
+
+ let userToUpdate = { ...this.getCurrentUser(), firstName, lastName, email };
+ if (this.props.adminMode && !this.getCurrentUser().isRootUser) {
+ userToUpdate = { ...userToUpdate, userRole, isAdmin, projectId, status };
+ }
+
+ try {
+ const usersStore = this.usersStore;
+ if (identityProviderNameField.isDirty) {
+ // Change in identityProviderName so delete existing user and add it again with new identityProviderName
+ if (this.props.adminMode) {
+ // clear out the user namespace as it will be re-derived based on authenticationProviderId and
+ // identityProviderName on server side
+ userToUpdate.ns = undefined;
+ // The values.identityProviderName is in JSON string format
+ // containing authentication provider id as well as identity provider name
+ // See "src/models/forms/UserFormUtils.js" for more details.
+ const idpOptionValue = toIdpFromValue(identityProviderNameField.value);
+ userToUpdate.identityProviderName = idpOptionValue.idpName;
+ userToUpdate.authenticationProviderId = idpOptionValue.authNProviderId;
+ await usersStore.addUser(userToUpdate);
+
+ // Delete existing user first
+ await usersStore.deleteUser(this.getCurrentUser());
+ } else {
+ displayError('Only admins can update identity provider information for the user');
+ }
+ } else {
+ // No change in identityProviderName so simply update the user
+
+ // allow updating only firstName, lastName and email in case self-service update (i.e., adminMode = false)
+ // or if the user being updated is a root user (i.e., this.getCurrentUser().isRootUser = true)
+ await usersStore.updateUser(userToUpdate);
+ }
+ form.clear();
+ displaySuccess('Updated user successfully');
+
+ // reload the current user's store after user updates, in case the currently
+ // logged in user is updated
+ await this.userStore.load();
+
+ this.handleClose();
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ handleDeleteClick = async () => {
+ try {
+ this.processing = true;
+ await this.usersStore.deleteUser(this.getCurrentUser());
+ } catch (error) {
+ displayError(error);
+ }
+ runInAction(() => {
+ this.processing = false;
+ });
+ this.handleClose();
+ };
+
+ handleApproveDisapproveClick = async status => {
+ try {
+ this.processing = true;
+ await this.usersStore.updateUser({ ...this.getCurrentUser(), status });
+
+ // reload the current user's store after user updates, in case the currently
+ // logged in user is updated
+ await this.userStore.load();
+ } catch (err) {
+ displayError(err);
+ }
+ runInAction(() => {
+ this.processing = false;
+ });
+ this.handleClose();
+ };
+
+ getStores() {
+ return this.stores;
+ }
+
+ handleOpen = () => {
+ this.usersStore.stopHeartbeat();
+
+ // Need to recreate form based on the user being passed (i.e., getCurrentUser) to make sure the form field values
+ // are updated as per the latest user information
+ this.form = getUpdateUserConfigForm(this.getCurrentUser());
+ this.modalOpen = true;
+ };
+
+ handleClose = () => {
+ this.usersStore.startHeartbeat();
+ this.modalOpen = false;
+ };
+
+ // eslint-disable-next-line no-unused-vars
+ handleFormError = (_form, _errors) => {
+ // We don't need to do anything here
+ };
+
+ getIdentityProviderOptions() {
+ return toIdpOptions(this.authenticationProviderConfigsStore.list);
+ }
+
+ getUserRoleOptions() {
+ return this.userRolesStore.dropdownOptions;
+ }
+
+ getProjectOptions() {
+ return this.projectsStore.dropdownOptions;
+ }
+
+ getCurrentUser() {
+ return this.props.user;
+ }
+
+ get userStore() {
+ return this.props.userStore;
+ }
+
+ get usersStore() {
+ return this.props.usersStore;
+ }
+
+ get userRolesStore() {
+ return this.props.userRolesStore;
+ }
+
+ get awsAccountsStore() {
+ return this.props.awsAccountsStore;
+ }
+
+ get projectsStore() {
+ return this.props.projectsStore;
+ }
+
+ get authenticationProviderConfigsStore() {
+ return this.props.authenticationProviderConfigsStore;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(UpdateUser, {
+ modalOpen: observable,
+ view: observable,
+ processing: observable,
+
+ authenticationProviderConfigsStore: computed,
+ projectsStore: computed,
+ awsAccountsStore: computed,
+ userRolesStore: computed,
+ usersStore: computed,
+ userStore: computed,
+
+ handleOpen: action,
+ handleClose: action,
+ handleCancel: action,
+
+ handleEditClick: action,
+ handleDeleteClick: action,
+ handleApproveDisapproveClick: action,
+
+ handleFormSubmission: action,
+});
+export default inject('authenticationProviderConfigsStore')(observer(UpdateUser));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/User.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/User.js
new file mode 100644
index 0000000000..eb82064dab
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/User.js
@@ -0,0 +1,41 @@
+/*
+ * 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 { Tab, Segment, Container } from 'semantic-ui-react';
+import { observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import RolesList from './RolesList';
+import UsersList from './UsersList';
+
+const panes = [
+ { menuItem: 'Users', render: () => },
+ { menuItem: 'Roles', render: () => },
+];
+
+// eslint-disable-next-line react/prefer-stateless-function
+class User extends React.Component {
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+export default withRouter(observer(User));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UserOnboarding.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UserOnboarding.js
new file mode 100644
index 0000000000..d9e5ca669e
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UserOnboarding.js
@@ -0,0 +1,648 @@
+/*
+ * 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 Dropzone from 'react-dropzone';
+import { decorate, observable, runInAction, action } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Accordion, Header, Icon, Segment, List, Modal, Button, Step, Table } from 'semantic-ui-react';
+import SyntaxHighlighter from 'react-syntax-highlighter';
+import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
+
+import Form from '@aws-ee/base-ui/dist/parts/helpers/fields/Form';
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import { awsRegion } from '@aws-ee/base-ui/dist/helpers/settings';
+
+import { getExternalUserPinForm } from '../../models/forms/ExternalUserPinForm';
+import CfnService from '../../helpers/cfn-service';
+import PinInput from '../helpers/PinInput';
+
+const steps = {
+ IAM_USER: 0,
+ CREDENTIALS: 1,
+ IAM_POLICY: 2,
+ ENCRYPT: 3,
+};
+
+const OnboardingSteps = [
+ {
+ key: 'user',
+ icon: 'user',
+ title: 'IAM User',
+ description: 'Create a user',
+ active: true,
+ },
+ {
+ key: 'credentials',
+ icon: 'upload',
+ title: 'Credentials',
+ description: 'Attach credentials',
+ },
+ {
+ key: 'policy',
+ icon: 'file alternate outline',
+ title: 'IAM Policy',
+ description: 'Add permissions',
+ },
+ {
+ key: 'encrypt',
+ icon: 'user secret',
+ title: 'Encrypt',
+ description: 'Encrypt credentials',
+ },
+];
+
+const generateDefaultIAMPolicy = accountId =>
+ `
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "ec2",
+ "Effect": "Allow",
+ "Action": "ec2:*",
+ "Resource": "*"
+ },
+ {
+ "Sid": "cloudformation",
+ "Effect": "Allow",
+ "Action": "cloudformation:*",
+ "Resource": "*"
+ },
+ {
+ "Sid": "emr",
+ "Effect": "Allow",
+ "Action": "elasticmapreduce:*",
+ "Resource": "*"
+ },
+ {
+ "Sid": "sagemaker",
+ "Effect": "Allow",
+ "Action": "sagemaker:*",
+ "Resource": "*"
+ },
+ {
+ "Sid": "iamRoleAccess",
+ "Effect": "Allow",
+ "Action": [
+ "iam:GetRole",
+ "iam:CreateRole",
+ "iam:TagRole",
+ "iam:GetRolePolicy",
+ "iam:PutRolePolicy",
+ "iam:DeleteRolePolicy",
+ "iam:DeleteRole",
+ "iam:PassRole"
+ ],
+ "Resource": "arn:aws:iam::${accountId}:role/analysis-*"
+ },
+ {
+ "Sid": "iamInstanceProfileAccess",
+ "Effect": "Allow",
+ "Action": [
+ "iam:AddRoleToInstanceProfile",
+ "iam:CreateInstanceProfile",
+ "iam:GetInstanceProfile",
+ "iam:DeleteInstanceProfile",
+ "iam:RemoveRoleFromInstanceProfile"
+ ],
+ "Resource": "arn:aws:iam::${accountId}:instance-profile/analysis-*"
+ },
+ {
+ "Sid": "iamRoleServicePolicyAccess",
+ "Effect": "Allow",
+ "Action": [
+ "iam:AttachRolePolicy",
+ "iam:DetachRolePolicy"
+ ],
+ "Resource": "arn:aws:iam::${accountId}:role/analysis-*",
+ "Condition": {
+ "ArnLike": {
+ "iam:PolicyARN": "arn:aws:iam::aws:policy/service-role/AmazonElasticMapReduceRole"
+ }
+ }
+ },
+ {
+ "Sid": "iamServiceLinkedRoleCreateAccess",
+ "Effect": "Allow",
+ "Action": [
+ "iam:CreateServiceLinkedRole",
+ "iam:PutRolePolicy"
+ ],
+ "Resource": "arn:aws:iam::*:role/aws-service-role/elasticmapreduce.amazonaws.com*/AWSServiceRoleForEMRCleanup*",
+ "Condition": {
+ "StringLike": {
+ "iam:AWSServiceName": [
+ "elasticmapreduce.amazonaws.com",
+ "elasticmapreduce.amazonaws.com.cn"
+ ]
+ }
+ }
+ },
+ {
+ "Sid": "s3",
+ "Effect": "Allow",
+ "Action": "s3:*",
+ "Resource": "*"
+ },
+ {
+ "Sid": "ssm",
+ "Effect": "Allow",
+ "Action": "ssm:*",
+ "Resource": "*"
+ },
+ {
+ "Sid": "kms",
+ "Effect": "Allow",
+ "Action": "kms:*",
+ "Resource": "*"
+ }
+ ]
+}
+`.trim();
+
+// Mapping from credentials.csv column names to preferred, short names
+const mapCredentialsColumns = new Map([
+ ['User name', 'username'],
+ ['Password', 'password'],
+ ['Access key ID', 'accessKeyId'],
+ ['Secret access key', 'secretAccessKey'],
+ ['Console login link', 'console'],
+]);
+
+const ListOnboardingIAMUser = (
+
+
+ Log into the{' '}
+
+ AWS Console
+ {' '}
+ and navigate to{' '}
+
+ IAM Users
+
+
+ Click 'Add User' and type a new, unique user name
+ Under 'Select AWS access type', check 'Programmatic access'
+
+ Click 'Next: Permissions', then 'Next: Tags', then 'Next: Review'
+
+ {/* Click 'Next: Tags'
+ Click 'Next: Review' */}
+ Click 'Create User'
+ On the 'Add User' success page, click 'Download .csv'
+ Click 'Close'
+
+);
+
+const ListOnboardingEncrypt = (
+
+ Enter a PIN which will be used to encrypt your AWS credentials
+ Remember this PIN because you will need it in future to launch workspaces
+
+);
+
+// expected props
+// - environmentsStore (via injection)
+// - location (from react router)
+class UserOnboarding extends React.Component {
+ constructor(props) {
+ super(props);
+ const user = this.getUserStore.user;
+ this.form = getExternalUserPinForm();
+
+ runInAction(() => {
+ this.user = user;
+ this.credentials = undefined;
+ this.credentialsValid = false;
+ this.accountId = '';
+ this.activePolicy = false;
+ this.copiedPolicy = '';
+ this.onboardingStep = 0;
+ this.onboardingSteps = OnboardingSteps.length;
+ });
+ }
+
+ get getUserStore() {
+ return this.props.userStore;
+ }
+
+ get getUsersStore() {
+ return this.props.usersStore;
+ }
+
+ onboardingNext = () => {
+ OnboardingSteps[this.onboardingStep].active = false;
+ OnboardingSteps[this.onboardingStep + 1].active = true;
+ this.onboardingStep += 1;
+ };
+
+ onboardingPrev = () => {
+ OnboardingSteps[this.onboardingStep].active = false;
+ OnboardingSteps[this.onboardingStep - 1].active = true;
+ this.onboardingStep -= 1;
+ };
+
+ onboardingCredentialsPut = async (creds, pin) => {
+ const usersStore = this.getUsersStore;
+ await this.user.setEncryptedCreds(creds, pin);
+ await usersStore.updateUser(this.user);
+ await usersStore.load();
+ };
+
+ onboardingSave = () => {
+ const pin = this.form.$('pin').value;
+ this.onboardingCredentialsPut(this.credentials, pin);
+ this.onboardingClose();
+ };
+
+ onboardingClose = () => {
+ this.resetOnboarding();
+ this.props.onclose();
+ };
+
+ shouldRenderOnboarding = () => this.user.isExternalUser;
+
+ resetOnboarding = () => {
+ OnboardingSteps.forEach(step => {
+ step.active = false;
+ });
+ OnboardingSteps[0].active = true;
+
+ this.credentials = undefined;
+ this.credentialsValid = false;
+ this.copiedPolicy = '';
+ this.onboardingStep = 0;
+ };
+
+ renderOnboardingButtons() {
+ const nextDisabled = this.onboardingStep === steps.CREDENTIALS && !this.credentialsValid;
+ return (
+ <>
+
+ {this.onboardingStep > 0 && (
+
+ )}
+ {this.onboardingStep < this.onboardingSteps - 1 && (
+
+ )}
+ {this.onboardingStep === this.onboardingSteps - 1 && (
+
+ )}
+ >
+ );
+ }
+
+ togglePolicy = () => {
+ this.activePolicy = !this.activePolicy;
+ };
+
+ testClipboardWrite = async () => {
+ return new Promise((resolve, _reject) => {
+ navigator.permissions.query({ name: 'clipboard-write' }).then(result => {
+ resolve(result.state === 'granted' || result.state === 'prompt');
+ });
+ });
+ };
+
+ handleCopyPolicy = () => {
+ navigator.clipboard.writeText(generateDefaultIAMPolicy(this.accountId)).then(
+ () => {
+ /* clipboard successfully set */
+ runInAction(() => {
+ this.copiedPolicy = 'copied!';
+ });
+ },
+ () => {
+ /* clipboard write failed */
+ runInAction(() => {
+ this.copiedPolicy = 'copy error!';
+ });
+ },
+ );
+ };
+
+ ListOnboardingIAMPolicy() {
+ return (
+
+
+ From the{' '}
+
+ IAM user list
+ {' '}
+ page, click the name of the new IAM user that you just created
+
+
+ In the 'Get started with permissions' block, click the 'Add inline policy' button
+
+ On the 'Create Policy' page, click the 'JSON' tab
+ Delete the default policy text
+
+ Click to copy a standard IAM policy{' '}
+
+ {this.copiedPolicy}
+
+ Paste the copied IAM policy into the IAM console
+ Click the 'Review Policy' button
+
+ Supply a name, for example 'compute-launch', and click 'Create Policy'
+
+
+ );
+ }
+
+ renderOnboardingIAMPolicyText() {
+ const showHide = this.activePolicy ? 'hide' : 'show';
+
+ return (
+
+
+
+ Click to {showHide} the standard IAM policy
+
+
+
+ {generateDefaultIAMPolicy(this.accountId)}
+
+
+
+ );
+ }
+
+ handleCredentialsReset = () => {
+ this.credentials = undefined;
+ this.credentialsValid = false;
+ };
+
+ renderOnboardingIAMCredentials() {
+ return (
+
+
+
+ Valid
+ IAM User Name
+ Access Key ID
+ Secret Key ID
+ Reset
+
+
+
+
+
+ {this.credentialsValid ? : }
+
+ {this.credentials.username}
+ {this.credentials.accessKeyId}
+ ********************
+
+
+
+
+
+
+ );
+ }
+
+ credentialsCSVtoJSON(csv) {
+ const obj = {};
+ const lines = csv.split(/\r?\n/);
+ const headers = lines[0].split(',');
+
+ // We only care about the first line of data following the header
+ for (let ii = 1; ii < lines.length; ii++) {
+ const currentline = lines[ii].split(',');
+
+ for (let jj = 0; jj < headers.length; jj++) {
+ const colname = mapCredentialsColumns.get(headers[jj]) || headers[jj];
+ obj[colname] = currentline[jj];
+ }
+ break;
+ }
+
+ return obj;
+ }
+
+ onCredentialsDrop = files => {
+ const reader = new FileReader();
+
+ reader.onabort = () => {
+ console.log('file reading was aborted');
+ displayError('File reading was aborted.');
+ };
+ reader.onerror = () => {
+ console.log('file reading has failed');
+ displayError('File reading has failed.');
+ };
+ reader.onload = () => {
+ const result = reader.result;
+ const credentials = this.credentialsCSVtoJSON(result);
+ runInAction(() => {
+ this.credentials = credentials;
+ this.credentials.region = awsRegion;
+ });
+
+ // Async IIFE
+ (async () => {
+ try {
+ const rc = await CfnService.validateCredentials(credentials.accessKeyId, credentials.secretAccessKey);
+ runInAction(() => {
+ this.credentialsValid = true;
+ this.accountId = rc.Account;
+ });
+ } catch (e) {
+ displayError(`Credential test failed: ${e}`);
+ runInAction(() => {
+ this.credentialsValid = false;
+ });
+ }
+ })();
+ };
+ files.forEach(file => reader.readAsText(file));
+ };
+
+ renderCredentialsDropzone() {
+ const maxSize = 1000;
+ return (
+
+
+ Drag and drop credentials file here
+
+ {/* Note: specifying single accept type causes uiploads to fail on Windows */}
+ {/* Was: accept="text/csv" */}
+
+ {({ getRootProps, getInputProps, rejectedFiles, isDragActive }) => {
+ const isFileTooLarge = rejectedFiles.length > 0 && rejectedFiles[0].size > maxSize;
+ return (
+
+
+ {isDragActive ? (
+
Drop the CSV file here ...
+ ) : (
+
Drag and drop a credentials.csv files here, or click to select a file
+ )}
+ {
}
+ {isFileTooLarge &&
File is too large.
}
+
+ );
+ }}
+
+
+ );
+ }
+
+ renderOnboardingIAMUser() {
+ return (
+
+
+ {/* */}
+
+
+
+ Please execute the following steps to create a new IAM User that you will use to supply credentials to the
+ Research Portal so that we can launch workspaces in your AWS account:
+
+ {ListOnboardingIAMUser}
+
+
+ );
+ }
+
+ renderOnboardingIAMPolicy() {
+ return (
+
+
+ {/* */}
+
+
+
+ Please execute the following steps to create a new IAM Policy that will provide permissions to the Research
+ Portal so that we can launch workspaces in your AWS account:
+
+ {this.ListOnboardingIAMPolicy()}
+ {this.renderOnboardingIAMPolicyText()}
+
+
+ );
+ }
+
+ renderOnboardingCredentials() {
+ return (
+
+
+ {/* */}
+
+
+
+ Please attach the credentials file that you downloaded earlier. This contains your new IAM User credentials.
+ The default name for this file is credentials.csv but your browser may have saved it under a different name.
+
+ {this.credentials && this.renderOnboardingIAMCredentials()}
+ {!this.credentials && this.renderCredentialsDropzone()}
+
+
+ );
+ }
+
+ renderOnboardingEncrypt() {
+ return (
+
+
+ {/* */}
+
+
+
+ Finally, we will locally encrypt your AWS credentials with a PIN and then store the encrypted credentials in
+ the Research Portal. This will allow you to launch research workspaces using just a PIN.
+
+ {ListOnboardingEncrypt}
+ {({ _processing, _onSubmit, _onCancel }) => }
+
+
+ );
+ }
+
+ renderOnboardingStep(step) {
+ switch (step) {
+ case steps.IAM_USER:
+ return this.renderOnboardingIAMUser();
+ case steps.CREDENTIALS:
+ return this.renderOnboardingCredentials();
+ case steps.IAM_POLICY:
+ return this.renderOnboardingIAMPolicy();
+ case steps.ENCRYPT:
+ return this.renderOnboardingEncrypt();
+ default:
+ return `Unexpected step: ${step}`;
+ }
+ }
+
+ render() {
+ return (
+ <>
+
+ Configure AWS Credentials
+
+
+
+ {this.renderOnboardingStep(this.onboardingStep)}
+ {this.renderOnboardingButtons()}
+
+ >
+ );
+ }
+}
+
+decorate(UserOnboarding, {
+ onboardingNext: action,
+ onboardingPrev: action,
+ resetOnboarding: action,
+ togglePolicy: action,
+ handleCopyPolicy: action,
+ handleCredentialsReset: action,
+ onCredentialsDrop: action,
+ onRegionChange: action,
+ user: observable,
+ credentials: observable,
+ credentialsValid: observable,
+ activePolicy: observable,
+ copiedPolicy: observable,
+ onboardingStep: observable,
+ onboardingSteps: observable,
+});
+export default inject('userStore', 'usersStore')(withRouter(observer(UserOnboarding)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UserPinModal.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UserPinModal.js
new file mode 100644
index 0000000000..5a6127925b
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UserPinModal.js
@@ -0,0 +1,94 @@
+/*
+ * 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, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { Modal, Form, Header, Button } from 'semantic-ui-react';
+import PropTypes from 'prop-types';
+
+class UserPinModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.errorMsg = undefined;
+ });
+ }
+
+ handlePinSubmission = async e => {
+ e.preventDefault();
+ e.persist();
+
+ // Will throw error if PIN is incorrect
+ try {
+ await this.props.user.unencryptedCreds(e.target.pin.value);
+ runInAction(() => {
+ this.errorMsg = undefined;
+ });
+ this.props.hideModal();
+ } catch (error) {
+ runInAction(() => {
+ this.errorMsg = error.message;
+ });
+ }
+ };
+
+ render() {
+ return (
+
+
+
+ {this.props.message}
+
+
+
+
+
+
+
+ );
+ }
+}
+UserPinModal.propTypes = {
+ show: PropTypes.bool.isRequired,
+ hideModal: PropTypes.func.isRequired,
+ // eslint-disable-next-line react/forbid-prop-types
+ user: PropTypes.object.isRequired,
+ message: PropTypes.string,
+};
+UserPinModal.defaultProps = {
+ message: '',
+};
+
+decorate(UserPinModal, {
+ errorMsg: observable,
+});
+
+export default observer(UserPinModal);
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UserTable.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UserTable.js
new file mode 100644
index 0000000000..ed11041c17
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UserTable.js
@@ -0,0 +1,53 @@
+/*
+ * 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 { withRouter } from 'react-router-dom';
+import { observer } from 'mobx-react';
+// Import React Table
+import ReactTable from 'react-table';
+
+// eslint-disable-next-line react/prefer-stateless-function
+class UserTable extends React.Component {
+ render() {
+ const { userData } = this.props;
+ return (
+
+
+
+
+ );
+ }
+}
+
+export default withRouter(observer(UserTable));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UsersList.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UsersList.js
new file mode 100644
index 0000000000..4343b2fe37
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/users/UsersList.js
@@ -0,0 +1,333 @@
+/*
+ * 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 { Button, Container, Header, Icon, Label, Dimmer, Loader, Segment, Popup } from 'semantic-ui-react';
+import { withRouter } from 'react-router-dom';
+import { decorate, observable, runInAction, action } from 'mobx';
+import { inject, observer } from 'mobx-react';
+import ReactTable from 'react-table';
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import { displayWarning } from '@aws-ee/base-ui/dist/helpers/notification';
+import { isStoreError, isStoreLoading, isStoreReady } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import BasicProgressPlaceholder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder';
+
+import UpdateUser from './UpdateUser';
+
+class UsersList extends React.Component {
+ constructor(props) {
+ super(props);
+ this.notifyNonRootUsers = true;
+ this.state = {
+ // eslint-disable-next-line react/no-unused-state
+ selectedRole: '',
+ // eslint-disable-next-line react/no-unused-state
+ projectId: [],
+ // eslint-disable-next-line react/no-unused-state
+ identityProviderName: '',
+ // eslint-disable-next-line react/no-unused-state
+ isIdentityProviderNameChanged: false,
+ // eslint-disable-next-line react/no-unused-state
+ unchangedIdentityProviderName: '',
+ };
+ runInAction(() => {
+ // An object that keeps track of which user is being edited
+ // Each key in the object below has key as user's unique id (/)
+ // and value as flag indicating whether to show the editor for the user
+ this.mapOfUsersBeingEdited = {};
+ this.formProcessing = false;
+ });
+ }
+
+ componentDidMount() {
+ const store = this.getStore();
+ swallowError(store.load());
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getStore();
+ store.stopHeartbeat();
+ }
+
+ getStore() {
+ return this.props.usersStore;
+ }
+
+ goto(pathname) {
+ const { location, history } = this.props;
+ const link = createLink({ location, pathname });
+ history.push(link);
+ }
+
+ handleAddUser = () => {
+ this.goto('/users/add');
+ };
+
+ handleAddLocalUser = () => {
+ this.goto('/users/add/local');
+ };
+
+ handleAddAuthenticationProvider = () => {
+ this.goto('/authentication-providers');
+ };
+
+ getAwsAccountOptions() {
+ const accountStore = this.props.awsAccountsStore;
+ return accountStore.dropdownOptions;
+ }
+
+ renderHeader() {
+ return (
+
+
+
+
+ Users
+ {this.renderTotal()}
+
+
+
+ {' '}
+ Add Local User{' '}
+
+
+ {' '}
+ Add Federated User{' '}
+
+
+ );
+ }
+
+ renderTotal() {
+ const store = this.getStore();
+ if (isStoreError(store) || isStoreLoading(store)) return null;
+ const nonRootUsers = store.nonRootUsers;
+ const count = nonRootUsers.length;
+
+ return {count} ;
+ }
+
+ renderMain() {
+ return this.renderUsers();
+ }
+
+ renderUsers() {
+ // Read "this.mapOfUsersBeingEdited" in the "render" method here
+ // The usersBeingEditedMap is then used in the ReactTable
+ // If we directly use this.mapOfUsersBeingEdited in the ReactTable's cell method, MobX does not
+ // realize that it is being used in the outer component's "render" method's scope
+ // Due to this, MobX does not re-render the component when observable state changes.
+ // To make this work correctly, we need to access "this.mapOfUsersBeingEdited" out side of ReactTable once
+
+ const store = this.getStore();
+ const usersList = store.list;
+ const pageSize = usersList.length;
+ const showPagination = usersList.length > pageSize;
+ const processing = this.formProcessing;
+
+ return (
+ // TODO: add api token stats and active flag here in the table
+
+
+ Updating
+
+ {
+ const columnValue = String(row[filter.id]).toLowerCase();
+ const filterValue = filter.value.toLowerCase();
+ return columnValue.indexOf(filterValue) >= 0;
+ }}
+ columns={[
+ {
+ Header: 'Name',
+ accessor: 'username',
+ width: 200,
+ },
+ {
+ Header: 'Email',
+ accessor: 'email',
+ width: 200,
+ },
+ {
+ Header: 'Identity Provider',
+ accessor: 'identityProviderName',
+ Cell: row => {
+ const user = row.original;
+ return user.identityProviderName || 'internal';
+ },
+ },
+ {
+ Header: 'Type',
+ accessor: 'isExternalUser',
+ width: 100,
+ Cell: row => {
+ const user = row.original;
+ return user.isExternalUser ? 'External' : 'Internal';
+ },
+ filterMethod: filter => {
+ if (filter.value.toLowerCase().includes('ex')) {
+ return false;
+ }
+ return true;
+ },
+ },
+ {
+ Header: 'Role',
+ accessor: 'userRole',
+ width: 100,
+ style: { whiteSpace: 'unset' },
+ Cell: row => {
+ const user = row.original;
+ return user.userRole || 'N/A';
+ },
+ },
+ {
+ Header: 'Project',
+ style: { whiteSpace: 'unset' },
+ Cell: row => {
+ const user = row.original;
+ return user.projectId.join(', ') || '<>';
+ },
+ },
+ {
+ Header: 'Status',
+ accessor: 'isActive',
+ width: 100,
+ Cell: row => {
+ const user = row.original;
+ let lable = null;
+ if (user.status === 'active') {
+ lable = (
+
+
+
+ Active
+
+
+ );
+ } else if (user.status === 'inactive') {
+ lable = (
+
+
+
+ Inactive
+
+
+ );
+ } else {
+ lable = (
+
+
+
+ Pending
+
+
+ );
+ }
+ return lable;
+ },
+ filterMethod: (filter, row) => {
+ if (row._original.status.indexOf(filter.value.toLowerCase()) >= 0) {
+ return true;
+ }
+ return false;
+ },
+ },
+ {
+ Header: '',
+ filterable: false,
+ Cell: cell => {
+ const user = cell.original;
+ return (
+
+ );
+ },
+ },
+ ]}
+ />
+
+ );
+ }
+
+ render() {
+ const store = this.getStore();
+ let content = null;
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreReady(store)) {
+ if (!store.hasNonRootUsers) {
+ if (this.notifyNonRootUsers) {
+ this.notifyNonRootUsers = false;
+ displayWarning(
+ 'Please add users in the Data Lake. May need to configure authentication provider in the Auth tab at left. Then login as a regular non-root User.',
+ );
+ }
+ }
+ content = this.renderMain();
+ }
+
+ return (
+
+ {this.renderHeader()}
+ {content}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(UsersList, {
+ mapOfUsersBeingEdited: observable,
+ formProcessing: observable,
+ handleAddUser: action,
+ handleAddAuthenticationProvider: action,
+ handleAddLocalUser: action,
+});
+
+export default inject(
+ 'userStore',
+ 'usersStore',
+ 'userRolesStore',
+ 'awsAccountsStore',
+ 'projectsStore',
+)(withRouter(observer(UsersList)));
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/app-component-plugin.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/app-component-plugin.js
new file mode 100644
index 0000000000..c286c6b0fb
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/app-component-plugin.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.
+ */
+
+import UserApplication from '../parts/UserApplication';
+
+// eslint-disable-next-line consistent-return, no-unused-vars
+function getAppComponent({ location, appContext }) {
+ const app = appContext.app || {};
+ // We are only going to return an App react component if the user is authenticated
+ // and not registered, otherwise we return undefined which means that the base ui
+ // plugin will provide its default App react component.
+ if (app.userAuthenticated && !app.userRegistered) {
+ return UserApplication;
+ }
+}
+
+const plugin = {
+ getAppComponent,
+};
+
+export default plugin;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/app-context-items-plugin.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/app-context-items-plugin.js
new file mode 100644
index 0000000000..0b6dd685b3
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/app-context-items-plugin.js
@@ -0,0 +1,63 @@
+/*
+ * 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 * as app from '../models/App';
+import * as userStore from '../models/users/UserStore';
+import * as usersStore from '../models/users/UsersStore';
+import * as accountsStore from '../models/accounts/AccountsStore';
+import * as awsAccountsStore from '../models/aws-accounts/AwsAccountsStore';
+import * as clientInformationStore from '../models/client-info/ClientInformationStore';
+import * as environment from '../models/environments/Environment';
+import * as environmentConfigurationsStore from '../models/environments/EnvironmentConfigurationsStore';
+import * as environmentsStore from '../models/environments/EnvironmentsStore';
+import * as fileUploadsStore from '../models/files/FileUploadsStore';
+import * as indexesStore from '../models/indexes/IndexesStore';
+import * as projectsStore from '../models/projects/ProjectsStore';
+import * as filesSelection from '../models/selections/FilesSelection';
+import * as studiesStore from '../models/studies/StudiesStore';
+import * as userRolesStore from '../models/user-roles/UserRolesStore';
+import * as computePlatformsStore from '../models/compute/ComputePlatformsStore';
+
+// eslint-disable-next-line no-unused-vars
+function registerAppContextItems(appContext) {
+ app.registerContextItems(appContext);
+ userStore.registerContextItems(appContext);
+ usersStore.registerContextItems(appContext);
+ accountsStore.registerContextItems(appContext);
+ awsAccountsStore.registerContextItems(appContext);
+ clientInformationStore.registerContextItems(appContext);
+ environment.registerContextItems(appContext);
+ environmentConfigurationsStore.registerContextItems(appContext);
+ environmentsStore.registerContextItems(appContext);
+ fileUploadsStore.registerContextItems(appContext);
+ indexesStore.registerContextItems(appContext);
+ projectsStore.registerContextItems(appContext);
+ filesSelection.registerContextItems(appContext);
+ studiesStore.registerContextItems(appContext);
+ userRolesStore.registerContextItems(appContext);
+ computePlatformsStore.registerContextItems(appContext);
+}
+
+// eslint-disable-next-line no-unused-vars
+function postRegisterAppContextItems(appContext) {
+ // No impl at this level
+}
+
+const plugin = {
+ registerAppContextItems,
+ postRegisterAppContextItems,
+};
+
+export default plugin;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/initialization-plugin.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/initialization-plugin.js
new file mode 100644
index 0000000000..663e93bff4
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/initialization-plugin.js
@@ -0,0 +1,48 @@
+/*
+ * 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';
+
+/**
+ * This is where we run the post initialization logic that is specific to RaaS.
+ *
+ * @param payload A free form object. This function expects a property named 'tokenInfo' to be available on the payload object.
+ * @param appContext An application context object containing various Mobx Stores, Models etc.
+ *
+ * @returns {Promise}
+ */
+async function postInit(payload, appContext) {
+ const tokenNotExpired = _.get(payload, 'tokenInfo.status') === 'notExpired';
+ if (!tokenNotExpired) return; // Continue only if we have a token that is not expired
+
+ const { userStore, usersStore, awsAccountsStore, userRolesStore, indexesStore, projectsStore } = appContext;
+
+ // TODO: Load these stores as needed instead of on startup
+ if (userStore.user.status === 'active') {
+ await Promise.all([
+ usersStore.load(),
+ awsAccountsStore.load(),
+ userRolesStore.load(),
+ indexesStore.load(),
+ projectsStore.load(),
+ ]);
+ }
+}
+
+const plugin = {
+ postInit,
+};
+
+export default plugin;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/menu-items-plugin.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/menu-items-plugin.js
new file mode 100644
index 0000000000..4ff6c1cbe9
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/menu-items-plugin.js
@@ -0,0 +1,54 @@
+/*
+ * 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';
+/**
+ * Adds navigation menu items to the given itemsMap.
+ *
+ * @param itemsMap A Map containing navigation items. This object is a Map that has route paths (urls) as
+ * keys and menu item object with the following shape
+ *
+ * {
+ * title: STRING, // Title for the navigation menu item
+ * icon: STRING, // semantic ui icon name fot the navigation menu item
+ * shouldShow: BOOLEAN || FUNCTION, // A flag or a function that returns a flag indicating whether to show the item or not (useful when showing menu items conditionally)
+ * render: OPTIONAL FUNCTION, // Optional function that returns rendered menu item component. Use this ONLY if you want to control full rendering of the menu item.
+ * }
+ *
+ * @param context A context object containing all various stores
+ *
+ * @returns Map<*> Returns A Map containing navigation menu items with the same shape as "itemsMap"
+ */
+// eslint-disable-next-line no-unused-vars
+function registerMenuItems(itemsMap, { location, appContext }) {
+ const isAdmin = _.get(appContext, 'userStore.user.isAdmin');
+ const canCreateWorkspaces = _.get(appContext, 'userStore.user.capabilities.canCreateWorkspace');
+ const canViewDashboard = _.get(appContext, 'userStore.user.capabilities.canViewDashboard');
+ const items = new Map([
+ ..._.filter([...itemsMap], item => {
+ if (item[0] === '/dashboard' && !canViewDashboard) return false;
+ return true;
+ }),
+ ['/accounts', { title: 'Accounts', icon: 'sitemap', shouldShow: isAdmin }],
+ ['/studies', { title: 'Studies', icon: 'book', shouldShow: true }],
+ ['/workspaces', { title: 'Workspaces', icon: 'server', shouldShow: canCreateWorkspaces }],
+ ]);
+
+ return items;
+}
+const plugin = {
+ registerMenuItems,
+};
+export default plugin;
diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/routes-plugin.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/routes-plugin.js
new file mode 100644
index 0000000000..cc90053005
--- /dev/null
+++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/plugins/routes-plugin.js
@@ -0,0 +1,100 @@
+/*
+ * 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 withAuth from '@aws-ee/base-ui/dist/withAuth';
+
+import User from '../parts/users/User';
+import Accounts from '../parts/accounts/Accounts';
+import AddUser from '../parts/users/AddUser';
+import AddIndex from '../parts/accounts/AddIndex';
+import Dashboard from '../parts/dashboard/Dashboard';
+import StudiesPage from '../parts/studies/StudiesPage';
+import StudyEnvironmentSetup from '../parts/studies/StudyEnvironmentSetup';
+import EnvironmentsList from '../parts/environments/EnvironmentsList';
+import EnvironmentDetailPage from '../parts/environments/EnvironmentDetailPage';
+import AddAwsAccount from '../parts/accounts/AddAwsAccount';
+import CreateAwsAccount from '../parts/accounts/CreateAwsAccount';
+import EnvironmentSetup from '../parts/environments/EnvironmentSetup';
+import AddProject from '../parts/projects/AddProject';
+import AddSingleLocalUser from '../parts/users/AddSingleLocalUser';
+
+/**
+ * Adds routes to the given routesMap.
+ * @param routesMap A Map containing routes. This object is a Map that has route paths as
+ * keys and React Component as value.
+ *
+ * @returns {Promise<*>} Returns a Map with the mapping of base routes vs React Component
+ */
+// eslint-disable-next-line no-unused-vars
+function registerRoutes(routesMap, { location, appContext }) {
+ // Temporary solution for the routes ordering issue
+ routesMap.delete('/users/add');
+ routesMap.delete('/users');
+
+ const routes = new Map([
+ ...routesMap,
+ ['/users/add/local', withAuth(AddSingleLocalUser)],
+ ['/users/add', withAuth(AddUser)],
+ ['/users', withAuth(User)],
+ ['/indexes/add', withAuth(AddIndex)],
+ ['/aws-accounts/add', withAuth(AddAwsAccount)],
+ ['/aws-accounts/create', withAuth(CreateAwsAccount)],
+ ['/accounts', withAuth(Accounts)],
+ ['/dashboard', withAuth(Dashboard)],
+ ['/studies/setup-workspace', withAuth(StudyEnvironmentSetup)],
+ ['/studies', withAuth(StudiesPage)],
+ ['/workspaces/create', withAuth(EnvironmentSetup)],
+ ['/workspaces/id/:instanceId', withAuth(EnvironmentDetailPage)],
+ ['/workspaces', withAuth(EnvironmentsList)],
+ ['/projects/add', withAuth(AddProject)],
+ ]);
+
+ return routes;
+}
+
+function getDefaultRouteLocation({ location, appContext }) {
+ const userStore = appContext.userStore;
+ let defaultRoute = '/dashboard';
+ const isRootUser = _.get(userStore, 'user.isRootUser');
+ const isExternalResearcher = _.get(userStore, 'user.isExternalResearcher');
+ const isInternalGuest = _.get(userStore, 'user.isInternalGuest');
+ const isExternalGuest = _.get(userStore, 'user.isExternalGuest');
+
+ if (isRootUser) {
+ defaultRoute = '/users';
+ } else if (isExternalResearcher) {
+ defaultRoute = '/workspaces';
+ } else if (isInternalGuest || isExternalGuest) {
+ defaultRoute = '/studies';
+ }
+
+ // See https://reacttraining.com/react-router/web/api/withRouter
+ const defaultLocation = {
+ pathname: defaultRoute,
+ search: location.search, // we want to keep any query parameters
+ hash: location.hash,
+ state: location.state,
+ };
+
+ return defaultLocation;
+}
+
+const plugin = {
+ registerRoutes,
+ getDefaultRouteLocation,
+};
+
+export default plugin;
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/.babelrc b/addons/addon-base-raas/packages/base-raas-cfn-templates/.babelrc
new file mode 100644
index 0000000000..ce25841fe7
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/.babelrc
@@ -0,0 +1,9 @@
+{
+ "plugins": [
+ ["babel-plugin-inline-import", {
+ "extensions": [
+ ".cfn.yml"
+ ]
+ }]
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/.eslintrc.json b/addons/addon-base-raas/packages/base-raas-cfn-templates/.eslintrc.json
new file mode 100644
index 0000000000..c700cf03a8
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/.eslintrc.json
@@ -0,0 +1,26 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "import/no-unresolved": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/.gitignore b/addons/addon-base-raas/packages/base-raas-cfn-templates/.gitignore
new file mode 100644
index 0000000000..dd5b3275fd
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/.gitignore
@@ -0,0 +1,19 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# transpiled code
+dist
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/.prettierrc.json b/addons/addon-base-raas/packages/base-raas-cfn-templates/.prettierrc.json
new file mode 100644
index 0000000000..4ee7b34147
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/.prettierrc.json
@@ -0,0 +1,8 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
+
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/jest.config.js b/addons/addon-base-raas/packages/base-raas-cfn-templates/jest.config.js
new file mode 100644
index 0000000000..3f7ffc8068
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/jest.config.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/jsconfig.json b/addons/addon-base-raas/packages/base-raas-cfn-templates/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/package.json b/addons/addon-base-raas/packages/base-raas-cfn-templates/package.json
new file mode 100644
index 0000000000..a0d21f682b
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@aws-ee/base-raas-cfn-templates",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Contains base RaaS CFN templates",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "@babel/cli": "^7.8.4",
+ "@babel/core": "^7.9.0",
+ "babel-plugin-inline-import": "^3.0.0",
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.1.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "babel": "babel src/ --out-dir dist/ --source-maps",
+ "babel:watch": "babel src/ --out-dir dist/ --source-maps --watch",
+ "build": "pnpm run babel",
+ "build:watch": "pnpm run babel:watch",
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; yarn run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "prepare": "pnpm run build"
+ }
+}
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/src/plugins/cfn-templates-plugin.js b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/plugins/cfn-templates-plugin.js
new file mode 100644
index 0000000000..f137ca8d9b
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/plugins/cfn-templates-plugin.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+// We are using Babel in this module to allow importing ".cfn.yml" files as plain text instead of parsing them webpack
+// "yaml-loader", because in this case we want the text value not the parsed yaml as an object.
+import ec2LinuxInstance from '../templates/ec2-linux-instance.cfn.yml';
+import ec2WindowsInstance from '../templates/ec2-windows-instance.cfn.yml';
+import sagemakerInstance from '../templates/sagemaker-notebook-instance.cfn.yml';
+import emrCluster from '../templates/emr-cluster.cfn.yml';
+import onboardAccount from '../templates/onboard-account.cfn.yml';
+
+const add = (name, yaml) => ({ name, yaml });
+
+// The order is important, add your templates here
+const templates = [
+ add('ec2-linux-instance', ec2LinuxInstance),
+ add('ec2-windows-instance', ec2WindowsInstance),
+ add('sagemaker-notebook-instance', sagemakerInstance),
+ add('emr-cluster', emrCluster),
+ add('onboard-account', onboardAccount),
+];
+
+async function registerCfnTemplates(registry) {
+ // eslint-disable-next-line no-restricted-syntax
+ for (const template of templates) {
+ await registry.add(template); // eslint-disable-line no-await-in-loop
+ }
+}
+
+module.exports = { registerCfnTemplates };
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/ec2-linux-instance.cfn.yml b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/ec2-linux-instance.cfn.yml
new file mode 100644
index 0000000000..b2a52e6f5e
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/ec2-linux-instance.cfn.yml
@@ -0,0 +1,182 @@
+AWSTemplateFormatVersion: 2010-09-09
+
+Description: Galileo-Gateway EC2-Linux
+
+Parameters:
+ Namespace:
+ Type: String
+ Description: An environment name that will be prefixed to resource names
+ AmiId:
+ Type: String
+ Description: Amazon Machine Image for the EC2 instance
+ InstanceType:
+ Type: String
+ Description: EC2 instance type to launch
+ Default: t3.xlarge
+ KeyName:
+ Type: String
+ Description: Keypair name for SSH access
+ AccessFromCIDRBlock:
+ Type: String
+ Description: The CIDR used to access the ec2 instances.
+ S3Mounts:
+ Type: String
+ Description: A JSON array of objects with name, bucket, and prefix properties used to mount data
+ IamPolicyDocument:
+ Type: String
+ Description: The IAM policy to be associated with the launched workstation
+ VPC:
+ Description: The VPC in which the EC2 instance will reside
+ Type: AWS::EC2::VPC::Id
+ Subnet:
+ Description: The VPC subnet in which the EC2 instance will reside
+ Type: AWS::EC2::Subnet::Id
+ EnvironmentInstanceFiles:
+ Type: String
+ Description: >-
+ An S3 URI (starting with "s3://") that specifies the location of files to be copied to
+ the environment instance, including any bootstrap scripts
+ EncryptionKeyArn:
+ Type: String
+ Description: The ARN of the KMS encryption Key used to encrypt data in the instance
+
+Conditions:
+ IamPolicyEmpty: !Equals [!Ref IamPolicyDocument, '{}']
+
+Resources:
+ IAMRole:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ RoleName: !Join ['-', [Ref: Namespace, 'ec2-role']]
+ Path: '/'
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Principal:
+ Service:
+ - 'ec2.amazonaws.com'
+ Action:
+ - 'sts:AssumeRole'
+ Policies:
+ - !If
+ - IamPolicyEmpty
+ - !Ref 'AWS::NoValue'
+ - PolicyName: !Join ['-', [Ref: Namespace, 's3-studydata-policy']]
+ PolicyDocument: !Ref IamPolicyDocument
+ - PolicyName: !Join ['-', [Ref: Namespace, 's3-bootstrap-script-policy']]
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Action: 's3:GetObject'
+ Resource: !Sub
+ - 'arn:aws:s3:::${S3Location}/*'
+ # Remove "s3://" prefix from EnvironmentInstanceFiles
+ - S3Location: !Select [1, !Split ['s3://', !Ref EnvironmentInstanceFiles]]
+ - Effect: 'Allow'
+ Action: 's3:ListBucket'
+ Resource: !Sub
+ - 'arn:aws:s3:::${S3Bucket}'
+ - S3Bucket: !Select [2, !Split ['/', !Ref EnvironmentInstanceFiles]]
+ Condition:
+ StringLike:
+ s3:prefix: !Sub
+ - '${S3Prefix}/*'
+ - S3Prefix: !Select [3, !Split ['/', !Ref EnvironmentInstanceFiles]]
+
+ InstanceProfile:
+ Type: 'AWS::IAM::InstanceProfile'
+ Properties:
+ InstanceProfileName: !Join ['-', [Ref: Namespace, 'ec2-profile']]
+ Path: '/'
+ Roles:
+ - Ref: IAMRole
+
+ SecurityGroup:
+ Type: 'AWS::EC2::SecurityGroup'
+ Properties:
+ GroupDescription: EC2 workspace security group
+ SecurityGroupEgress:
+ - IpProtocol: tcp
+ FromPort: 0
+ ToPort: 65535
+ CidrIp: 0.0.0.0/0
+ - IpProtocol: icmp
+ FromPort: -1
+ ToPort: -1
+ CidrIp: !Ref AccessFromCIDRBlock
+ SecurityGroupIngress:
+ - IpProtocol: tcp
+ FromPort: 22
+ ToPort: 22
+ CidrIp: !Ref AccessFromCIDRBlock
+ - IpProtocol: tcp
+ FromPort: 80
+ ToPort: 80
+ CidrIp: !Ref AccessFromCIDRBlock
+ - IpProtocol: tcp
+ FromPort: 443
+ ToPort: 443
+ CidrIp: !Ref AccessFromCIDRBlock
+ Tags:
+ - Key: Name
+ Value: !Join ['-', [Ref: Namespace, 'ec2-sg']]
+ - Key: Description
+ Value: EC2 workspace security group
+ VpcId: !Ref VPC
+
+ EC2Instance:
+ Type: 'AWS::EC2::Instance'
+ CreationPolicy:
+ ResourceSignal:
+ Timeout: 'PT20M'
+ Properties:
+ ImageId: !Ref AmiId
+ InstanceType: !Ref InstanceType
+ IamInstanceProfile: !Ref InstanceProfile
+ KeyName: !Ref KeyName
+ BlockDeviceMappings:
+ - DeviceName: /dev/xvda
+ Ebs:
+ VolumeSize: 8
+ Encrypted: true
+ KmsKeyId: !Ref EncryptionKeyArn
+ NetworkInterfaces:
+ - AssociatePublicIpAddress: 'true'
+ DeviceIndex: '0'
+ GroupSet:
+ - !Ref SecurityGroup
+ SubnetId: !Ref Subnet
+ Tags:
+ - Key: Name
+ Value: !Join ['-', [Ref: Namespace, 'ec2-linux']]
+ - Key: Description
+ Value: EC2 workspace instance
+ UserData:
+ Fn::Base64: !Sub |
+ #!/usr/bin/env bash
+ # Download and execute bootstrap script
+ aws s3 cp "${EnvironmentInstanceFiles}/get_bootstrap.sh" "/tmp"
+ chmod 500 "/tmp/get_bootstrap.sh"
+ /tmp/get_bootstrap.sh "${EnvironmentInstanceFiles}" '${S3Mounts}'
+
+ # Signal result to CloudFormation
+ /opt/aws/bin/cfn-signal -e $? --stack "${AWS::StackName}" --resource "EC2Instance" --region "${AWS::Region}"
+
+Outputs:
+ Ec2WorkspaceDnsName:
+ Description: Public DNS name of the EC2 workspace instance
+ Value: !GetAtt [EC2Instance, PublicDnsName]
+
+ Ec2WorkspacePublicIp:
+ Description: Public IP address of the EC2 workspace instance
+ Value: !GetAtt [EC2Instance, PublicIp]
+
+ Ec2WorkspaceInstanceId:
+ Description: Instance Id for the EC2 workspace instance
+ Value: !Ref EC2Instance
+
+ WorkspaceInstanceRoleArn:
+ Description: IAM role assumed by the EC2 workspace instance
+ Value: !GetAtt IAMRole.Arn
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/ec2-windows-instance.cfn.yml b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/ec2-windows-instance.cfn.yml
new file mode 100644
index 0000000000..c7ef80844b
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/ec2-windows-instance.cfn.yml
@@ -0,0 +1,148 @@
+AWSTemplateFormatVersion: 2010-09-09
+
+Description: Galileo-Gateway EC2-Windows
+
+Parameters:
+ Namespace:
+ Type: String
+ Description: An environment name that will be prefixed to resource names
+ AmiId:
+ Type: String
+ Description: Amazon Machine Image for the EC2 instance
+ InstanceType:
+ Type: String
+ Description: EC2 instance type to launch
+ Default: t3.xlarge
+ KeyName:
+ Type: String
+ Description: Keypair name for admin password encryption/decryption
+ AccessFromCIDRBlock:
+ Type: String
+ Description: The CIDR used to access the ec2 instances.
+ VPC:
+ Description: The VPC in which the EC2 instance will reside
+ Type: AWS::EC2::VPC::Id
+ Subnet:
+ Description: The VPC subnet in which the EC2 instance will reside
+ Type: AWS::EC2::Subnet::Id
+ EncryptionKeyArn:
+ Type: String
+ Description: The ARN of the KMS encryption Key used to encrypt data in the instance
+
+Resources:
+ IAMRole:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ RoleName: !Join ['-', [Ref: Namespace, 'ec2-role']]
+ Path: '/'
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Principal:
+ Service:
+ - 'ec2.amazonaws.com'
+ Action:
+ - 'sts:AssumeRole'
+ Policies:
+ - PolicyName: !Join ['-', [Ref: Namespace, 's3-policy']]
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Action:
+ - 's3:*'
+ Resource:
+ - '*'
+
+ InstanceProfile:
+ Type: 'AWS::IAM::InstanceProfile'
+ Properties:
+ InstanceProfileName: !Join ['-', [Ref: Namespace, 'ec2-profile']]
+ Path: '/'
+ Roles:
+ - Ref: IAMRole
+
+ SecurityGroup:
+ Type: 'AWS::EC2::SecurityGroup'
+ Properties:
+ GroupDescription: EC2 workspace security group
+ SecurityGroupEgress:
+ - IpProtocol: tcp
+ FromPort: 0
+ ToPort: 65535
+ CidrIp: 0.0.0.0/0
+ - IpProtocol: icmp
+ FromPort: -1
+ ToPort: -1
+ CidrIp: !Ref AccessFromCIDRBlock
+ SecurityGroupIngress:
+ - IpProtocol: tcp
+ FromPort: 3389
+ ToPort: 3389
+ CidrIp: !Ref AccessFromCIDRBlock
+ - IpProtocol: tcp
+ FromPort: 80
+ ToPort: 80
+ CidrIp: !Ref AccessFromCIDRBlock
+ - IpProtocol: tcp
+ FromPort: 443
+ ToPort: 443
+ CidrIp: !Ref AccessFromCIDRBlock
+ Tags:
+ - Key: Name
+ Value: !Join ['-', [Ref: Namespace, 'ec2-sg']]
+ - Key: Description
+ Value: EC2 workspace security group
+ VpcId: !Ref VPC
+
+ EC2Instance:
+ Type: 'AWS::EC2::Instance'
+ CreationPolicy:
+ ResourceSignal:
+ Timeout: 'PT20M'
+ Properties:
+ ImageId: !Ref AmiId
+ InstanceType: !Ref InstanceType
+ IamInstanceProfile: !Ref InstanceProfile
+ KeyName: !Ref KeyName
+ BlockDeviceMappings:
+ - DeviceName: /dev/sda1
+ Ebs:
+ VolumeSize: 30
+ Encrypted: true
+ KmsKeyId: !Ref EncryptionKeyArn
+ NetworkInterfaces:
+ - AssociatePublicIpAddress: 'true'
+ DeviceIndex: '0'
+ GroupSet:
+ - !Ref SecurityGroup
+ SubnetId: !Ref Subnet
+ Tags:
+ - Key: Name
+ Value: !Join ['-', [Ref: Namespace, 'ec2-windows']]
+ - Key: Description
+ Value: EC2 workspace instance
+ UserData:
+ Fn::Base64: !Sub |
+
+ cmd /c "exit 0" # Automatically return success; remove this line if actual bootstrapping logic is added
+ cfn-signal.exe -e $lastexitcode --stack ${AWS::StackId} --resource EC2Instance --region ${AWS::Region}
+
+
+Outputs:
+ Ec2WorkspaceDnsName:
+ Description: Public DNS name of the EC2 workspace instance
+ Value: !GetAtt [EC2Instance, PublicDnsName]
+
+ Ec2WorkspacePublicIp:
+ Description: Public IP address of the EC2 workspace instance
+ Value: !GetAtt [EC2Instance, PublicIp]
+
+ Ec2WorkspaceInstanceId:
+ Description: Instance Id for the EC2 workspace instance
+ Value: !Ref EC2Instance
+
+ WorkspaceInstanceRoleArn:
+ Description: IAM role assumed by the EC2 workspace instance
+ Value: !GetAtt IAMRole.Arn
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/emr-cluster.cfn.yml b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/emr-cluster.cfn.yml
new file mode 100644
index 0000000000..a3eeddf6ab
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/emr-cluster.cfn.yml
@@ -0,0 +1,317 @@
+AWSTemplateFormatVersion: 2010-09-09
+
+Description: Galileo-Gateway EMR-Hail-Jupyter
+
+Metadata:
+ AWS::CloudFormation::Interface:
+ ParameterGroups:
+ - Label:
+ default: EMR Options
+ Parameters:
+ - Namespace
+ - KeyName
+ - VPC
+ - Subnet
+ - CoreNodeCount
+ - DiskSizeGB
+ - MasterInstanceType
+ - WorkerInstanceType
+ - WorkerBidPrice
+ - AccessFromCIDRBlock
+ - AmiId
+ - Label:
+ default: Tags
+ Parameters:
+ - NameTag
+ - OwnerTag
+ - PurposeTag
+
+Parameters:
+ Namespace:
+ Type: String
+ Description: An environment name that will be prefixed to resource names
+ KeyName:
+ Description: SSH key pair to use for EMR node login
+ Type: AWS::EC2::KeyPair::KeyName
+ VPC:
+ Description: VPC for EMR nodes.
+ Type: AWS::EC2::VPC::Id
+ Subnet:
+ Description: Subnet for EMR nodes, from the VPC selected above
+ Type: AWS::EC2::Subnet::Id
+ CoreNodeCount:
+ Description: Number of core nodes to provision (1-80)
+ Type: Number
+ MinValue: '1'
+ MaxValue: '80'
+ Default: '5'
+ DiskSizeGB:
+ Description: EBS Volume size (GB) for each node
+ Type: Number
+ MinValue: '10'
+ MaxValue: '1000'
+ Default: '20'
+ MasterInstanceType:
+ Type: String
+ Default: m5.xlarge
+ Description: EMR node ec2 instance type.
+ WorkerInstanceType:
+ Type: String
+ Default: m5.xlarge
+ Description: EMR node ec2 instance type.
+ Market:
+ Type: String
+ Default: ON_DEMAND
+ Description: Which market to purchase workers on - ON_DEMAND or SPOT.
+ WorkerBidPrice:
+ Type: String
+ Description: Bid price for the worker spot nodes.
+ AccessFromCIDRBlock:
+ Type: String
+ MinLength: 9
+ Description: Restrict WebUI access to specified address or range
+ AmiId:
+ Type: String
+ Description: Ami Id to use for the cluster
+ EnvironmentInstanceFiles:
+ Type: String
+ Description: >-
+ An S3 URI (starting with "s3://") that specifies the location of files to be copied to
+ the environment instance, including any bootstrap scripts
+ S3Mounts:
+ Type: String
+ Description: A JSON array of objects with name, bucket and prefix properties used to mount data
+ IamPolicyDocument:
+ Type: String
+ Description: The IAM policy to be associated with the launched workstation
+ EncryptionKeyArn:
+ Type: String
+ Description: The ARN of the KMS encryption Key used to encrypt data in the cluster
+
+Conditions:
+ IamPolicyEmpty: !Equals [!Ref IamPolicyDocument, '{}']
+ IsOnDemandCondition:
+ !Equals [!Ref Market, ON_DEMAND]
+
+Resources:
+
+ # TODO: Use one bucket for EMR logs per account, so shift deployment to account on-boarding and pass here as param
+ LogBucket:
+ Type: AWS::S3::Bucket
+ DeletionPolicy: Retain
+ Properties:
+ PublicAccessBlockConfiguration: # Block all public access configuration for the S3 bucket
+ BlockPublicAcls: true
+ BlockPublicPolicy: true
+ IgnorePublicAcls: true
+ RestrictPublicBuckets: true
+
+ MasterSecurityGroup:
+ Type: AWS::EC2::SecurityGroup
+ Properties:
+ GroupDescription: Jupyter
+ VpcId:
+ Ref: VPC
+ SecurityGroupIngress:
+ - IpProtocol: tcp
+ FromPort: 8192
+ ToPort: 8192
+ CidrIp:
+ Ref: AccessFromCIDRBlock
+
+ InstanceProfile:
+ Properties:
+ Path: "/"
+ Roles:
+ - Ref: Ec2Role
+ Type: AWS::IAM::InstanceProfile
+
+ Ec2Role:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ RoleName: !Join ['-', [Ref: Namespace, 'ec2-role']]
+ Path: '/'
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Principal:
+ Service:
+ - 'ec2.amazonaws.com'
+ Action:
+ - 'sts:AssumeRole'
+ Policies:
+ - !If
+ - IamPolicyEmpty
+ - !Ref 'AWS::NoValue'
+ - PolicyName: !Join ['-', [Ref: Namespace, 's3-studydata-policy']]
+ PolicyDocument: !Ref IamPolicyDocument
+ - PolicyName: !Join ['-', [Ref: Namespace, 's3-bootstrap-script-policy']]
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Action: 's3:GetObject'
+ Resource:
+ - 'arn:aws:s3:::us-east-1.elasticmapreduce/bootstrap-actions/run-if'
+ - !Sub
+ - 'arn:aws:s3:::${S3Location}/*'
+ # Remove "s3://" prefix from EnvironmentInstanceFiles
+ - S3Location: !Select [ 1, !Split [ 's3://', !Ref EnvironmentInstanceFiles ] ]
+ - Effect: 'Allow'
+ Action: 's3:ListBucket'
+ Resource: !Sub
+ - 'arn:aws:s3:::${S3Bucket}'
+ - S3Bucket: !Select [ 2, !Split [ '/', !Ref EnvironmentInstanceFiles ]]
+ Condition:
+ StringLike:
+ s3:prefix: !Sub
+ - '${S3Prefix}/*'
+ - S3Prefix: !Select [ 3, !Split [ '/', !Ref EnvironmentInstanceFiles ]]
+
+ ServiceRole:
+ Type: AWS::IAM::Role
+ Properties:
+ Path: "/"
+ AssumeRolePolicyDocument:
+ Statement:
+ - Action:
+ - sts:AssumeRole
+ Effect: Allow
+ Principal:
+ Service:
+ - elasticmapreduce.amazonaws.com
+ Version: '2012-10-17'
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/service-role/AmazonElasticMapReduceRole
+
+
+ EmrSecurityConfiguration:
+ Type: AWS::EMR::SecurityConfiguration
+ Properties:
+ SecurityConfiguration: {
+ "EncryptionConfiguration": {
+ "AtRestEncryptionConfiguration": {
+ "LocalDiskEncryptionConfiguration": {
+ "EncryptionKeyProviderType": "AwsKms",
+ "AwsKmsKey": { "Ref": "EncryptionKeyArn" },
+ "EnableEbsEncryption": true
+ }
+ },
+ "EnableInTransitEncryption": false,
+ "EnableAtRestEncryption": true
+ }
+ }
+
+
+ # TODO: customise jupyter password from launch ui
+ # TODO: Add security configuration to cluster
+ # TODO: Can we make the jupyter use https?
+ # TODO: Also change notebook owner to hadoop on launch
+ EmrCluster:
+ Type: AWS::EMR::Cluster
+ Properties:
+ Applications:
+ - Name: Hadoop
+ - Name: Hive
+ - Name: Spark
+ BootstrapActions:
+ - Name: Run-Python-Jupyter
+ ScriptBootstrapAction:
+ Path: s3://us-east-1.elasticmapreduce/bootstrap-actions/run-if
+ Args:
+ - "instance.isMaster=true"
+ - "/opt/hail-on-AWS-spot-instances/src/jupyter_run.sh"
+ - Name: Mount-S3-Resources
+ ScriptBootstrapAction:
+ Path: !Sub "${EnvironmentInstanceFiles}/get_bootstrap.sh"
+ Args:
+ - !Ref EnvironmentInstanceFiles
+ - !Ref S3Mounts
+ CustomAmiId:
+ Ref: AmiId
+ Configurations:
+ - Classification: spark
+ ConfigurationProperties:
+ maximizeResourceAllocation: true
+ - Classification: yarn-site
+ ConfigurationProperties:
+ yarn.nodemanager.vmem-check-enabled: false
+ - Classification: spark-defaults
+ ConfigurationProperties:
+ spark.hadoop.io.compression.codecs: "org.apache.hadoop.io.compress.DefaultCodec,is.hail.io.compress.BGzipCodec,org.apache.hadoop.io.compress.GzipCodec"
+ spark.serializer: "org.apache.spark.serializer.KryoSerializer"
+ spark.hadoop.parquet.block.size: "1099511627776"
+ spark.sql.files.maxPartitionBytes: "1099511627776"
+ spark.sql.files.openCostInBytes: "1099511627776"
+ Configurations: []
+ Instances:
+ AdditionalMasterSecurityGroups:
+ - Fn::GetAtt:
+ - MasterSecurityGroup
+ - GroupId
+ Ec2KeyName:
+ Ref: KeyName
+ Ec2SubnetId:
+ Ref: Subnet
+ MasterInstanceGroup:
+ InstanceCount: 1
+ InstanceType:
+ Ref: MasterInstanceType
+ CoreInstanceGroup:
+ !If
+ - IsOnDemandCondition
+ -
+ InstanceCount:
+ Ref: CoreNodeCount
+ InstanceType:
+ Ref: WorkerInstanceType
+ Market:
+ Ref: Market
+ EbsConfiguration:
+ EbsOptimized: true
+ EbsBlockDeviceConfigs:
+ - VolumeSpecification:
+ SizeInGB:
+ Ref: DiskSizeGB
+ VolumeType: gp2
+ -
+ InstanceCount:
+ Ref: CoreNodeCount
+ InstanceType:
+ Ref: WorkerInstanceType
+ Market:
+ Ref: Market
+ BidPrice:
+ Ref: WorkerBidPrice
+ EbsConfiguration:
+ EbsOptimized: true
+ EbsBlockDeviceConfigs:
+ - VolumeSpecification:
+ SizeInGB:
+ Ref: DiskSizeGB
+ VolumeType: gp2
+ JobFlowRole:
+ Ref: InstanceProfile
+ Name: !Sub "${Namespace}-emr"
+ Tags: # Add Name tag so EC2 instances are easily identifiable
+ - Key: Name
+ Value: !Sub "${Namespace}-emr"
+ ServiceRole:
+ Ref: ServiceRole
+ ReleaseLabel: emr-5.27.0
+ # This has to be true because we assume a new user each time.
+ VisibleToAllUsers: true
+ SecurityConfiguration: !Ref EmrSecurityConfiguration
+ LogUri: !Sub "s3://${LogBucket}"
+
+Outputs:
+ JupyterUrl:
+ Description: Open Jupyter on your new EMR cluster
+ Value: !Sub "http://${EmrCluster.MasterPublicDNS}:8192"
+ LogBucket:
+ Description: EMR Scratch data and Logs bucket
+ Value: !Ref LogBucket
+ WorkspaceInstanceRoleArn:
+ Description: IAM role assumed by the EMR workspace instances
+ Value: !GetAtt Ec2Role.Arn
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/external/emr.cfn.yml b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/external/emr.cfn.yml
new file mode 100644
index 0000000000..2095e768d7
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/external/emr.cfn.yml
@@ -0,0 +1,336 @@
+AWSTemplateFormatVersion: 2010-09-09
+
+Description: Galileo-Gateway EMR-Hail-Jupyter
+
+Metadata:
+ AWS::CloudFormation::Interface:
+ ParameterGroups:
+ - Label:
+ default: EMR Options
+ Parameters:
+ - Namespace
+ - KeyName
+ - VPC
+ - Subnet
+ - CoreNodeCount
+ - DiskSizeGB
+ - MasterInstanceType
+ - WorkerInstanceType
+ - WorkerBidPrice
+ - AccessFromCIDRBlock
+ - AmiId
+ - Label:
+ default: Tags
+ Parameters:
+ - NameTag
+ - OwnerTag
+ - PurposeTag
+
+Parameters:
+ Namespace:
+ Type: String
+ Description: An environment name that will be prefixed to resource names
+ KeyName:
+ Description: SSH key pair to use for EMR node login
+ Type: AWS::EC2::KeyPair::KeyName
+ VPC:
+ Description: VPC for EMR nodes.
+ Type: AWS::EC2::VPC::Id
+ Subnet:
+ Description: Subnet for EMR nodes, from the VPC selected above
+ Type: AWS::EC2::Subnet::Id
+ CoreNodeCount:
+ Description: Number of core nodes to provision (1-80)
+ Type: Number
+ MinValue: '1'
+ MaxValue: '80'
+ Default: '5'
+ DiskSizeGB:
+ Description: EBS Volume size (GB) for each node
+ Type: Number
+ MinValue: '10'
+ MaxValue: '1000'
+ Default: '20'
+ MasterInstanceType:
+ Type: String
+ Default: m5.xlarge
+ Description: EMR node ec2 instance type.
+ WorkerInstanceType:
+ Type: String
+ Default: m5.xlarge
+ Description: EMR node ec2 instance type.
+ Market:
+ Type: String
+ Default: ON_DEMAND
+ Description: Which market to purchase workers on - ON_DEMAND or SPOT.
+ WorkerBidPrice:
+ Type: String
+ Description: Bid price for the worker spot nodes.
+ AccessFromCIDRBlock:
+ Type: String
+ MinLength: 9
+ Description: Restrict WebUI access to specified address or range
+ AmiId:
+ Type: String
+ Description: Ami Id to use for the cluster
+ EnvironmentInstanceFiles:
+ Type: String
+ Description: >-
+ An S3 URI (starting with "s3://") that specifies the location of files to be copied to
+ the environment instance, including any bootstrap scripts
+ S3Mounts:
+ Type: String
+ Description: A JSON array of objects with name, bucket and prefix properties used to mount data
+ IamPolicyDocument:
+ Type: String
+ Description: The IAM policy to be associated with the launched workstation
+
+Conditions:
+ IamPolicyEmpty: !Equals [!Ref IamPolicyDocument, '{}']
+ IsOnDemandCondition: !Equals [!Ref Market, ON_DEMAND]
+
+Resources:
+ # TODO: Use one bucket for EMR logs per account, so shift deployment to account on-boarding and pass here as param
+ LogBucket:
+ Type: AWS::S3::Bucket
+ DeletionPolicy: Retain
+ Properties:
+ PublicAccessBlockConfiguration: # Block all public access configuration for the S3 bucket
+ BlockPublicAcls: true
+ BlockPublicPolicy: true
+ IgnorePublicAcls: true
+ RestrictPublicBuckets: true
+
+ EncryptionKey:
+ Type: AWS::KMS::Key
+ Properties:
+ Description: 'This is the key used to secure resources in this account'
+ EnableKeyRotation: True
+ KeyPolicy:
+ Version: '2012-10-17'
+ Statement:
+ - Sid: Allow root access
+ Effect: 'Allow'
+ Principal:
+ AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
+ Action:
+ - 'kms:*'
+ Resource: '*'
+ - Sid: Allow use of the key by this account
+ Effect: 'Allow'
+ Principal:
+ AWS: '*'
+ Action:
+ - 'kms:DescribeKey'
+ - 'kms:Encrypt'
+ - 'kms:Decrypt'
+ - 'kms:ReEncrypt*'
+ - 'kms:GenerateDataKey'
+ - 'kms:GenerateDataKeyWithoutPlaintext'
+ - 'kms:CreateGrant'
+ - 'kms:RevokeGrant'
+ Resource: '*'
+ Condition:
+ StringEquals:
+ kms:CallerAccount: !Ref 'AWS::AccountId'
+
+ MasterSecurityGroup:
+ Type: AWS::EC2::SecurityGroup
+ Properties:
+ GroupDescription: Jupyter
+ VpcId:
+ Ref: VPC
+ SecurityGroupIngress:
+ - IpProtocol: tcp
+ FromPort: 8192
+ ToPort: 8192
+ CidrIp:
+ Ref: AccessFromCIDRBlock
+
+ InstanceProfile:
+ Properties:
+ Path: '/'
+ Roles:
+ - Ref: Ec2Role
+ Type: AWS::IAM::InstanceProfile
+
+ Ec2Role:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ RoleName: !Join ['-', [Ref: Namespace, 'ec2-role']]
+ Path: '/'
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Principal:
+ Service:
+ - 'ec2.amazonaws.com'
+ Action:
+ - 'sts:AssumeRole'
+ Policies:
+ - !If
+ - IamPolicyEmpty
+ - !Ref 'AWS::NoValue'
+ - PolicyName: !Join ['-', [Ref: Namespace, 's3-studydata-policy']]
+ PolicyDocument: !Ref IamPolicyDocument
+ - PolicyName: !Join ['-', [Ref: Namespace, 's3-bootstrap-script-policy']]
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Action: 's3:GetObject'
+ Resource:
+ - 'arn:aws:s3:::us-east-1.elasticmapreduce/bootstrap-actions/run-if'
+ - !Sub
+ - 'arn:aws:s3:::${S3Location}/*'
+ # Remove "s3://" prefix from EnvironmentInstanceFiles
+ - S3Location: !Select [1, !Split ['s3://', !Ref EnvironmentInstanceFiles]]
+ - Effect: 'Allow'
+ Action: 's3:ListBucket'
+ Resource: !Sub
+ - 'arn:aws:s3:::${S3Bucket}'
+ - S3Bucket: !Select [2, !Split ['/', !Ref EnvironmentInstanceFiles]]
+ Condition:
+ StringLike:
+ s3:prefix: !Sub
+ - '${S3Prefix}/*'
+ - S3Prefix: !Select [3, !Split ['/', !Ref EnvironmentInstanceFiles]]
+
+ ServiceRole:
+ Type: AWS::IAM::Role
+ Properties:
+ Path: '/'
+ AssumeRolePolicyDocument:
+ Statement:
+ - Action:
+ - sts:AssumeRole
+ Effect: Allow
+ Principal:
+ Service:
+ - elasticmapreduce.amazonaws.com
+ Version: '2012-10-17'
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/service-role/AmazonElasticMapReduceRole
+
+ EmrSecurityConfiguration:
+ Type: AWS::EMR::SecurityConfiguration
+ Properties:
+ SecurityConfiguration:
+ EncryptionConfiguration:
+ EnableInTransitEncryption: false
+ EnableAtRestEncryption: true
+ AtRestEncryptionConfiguration:
+ LocalDiskEncryptionConfiguration:
+ EncryptionKeyProviderType: AwsKms
+ AwsKmsKey: !GetAtt EncryptionKey.Arn
+ EnableEbsEncryption: true
+
+ # TODO: customise jupyter password from launch ui
+ # TODO: Add security configuration to cluster
+ # TODO: Can we make the jupyter use https?
+ # TODO: Also change notebook owner to hadoop on launch
+ EmrCluster:
+ Type: AWS::EMR::Cluster
+ Properties:
+ Applications:
+ - Name: Hadoop
+ - Name: Hive
+ - Name: Spark
+ BootstrapActions:
+ - Name: Run-Python-Jupyter
+ ScriptBootstrapAction:
+ Path: s3://us-east-1.elasticmapreduce/bootstrap-actions/run-if
+ Args:
+ - 'instance.isMaster=true'
+ - '/opt/hail-on-AWS-spot-instances/src/jupyter_run.sh'
+ - Name: Mount-S3-Resources
+ ScriptBootstrapAction:
+ Path: !Sub '${EnvironmentInstanceFiles}/get_bootstrap.sh'
+ Args:
+ - !Ref EnvironmentInstanceFiles
+ - !Ref S3Mounts
+ CustomAmiId:
+ Ref: AmiId
+ Configurations:
+ - Classification: spark
+ ConfigurationProperties:
+ maximizeResourceAllocation: true
+ - Classification: yarn-site
+ ConfigurationProperties:
+ yarn.nodemanager.vmem-check-enabled: false
+ - Classification: spark-defaults
+ ConfigurationProperties:
+ spark.hadoop.io.compression.codecs: 'org.apache.hadoop.io.compress.DefaultCodec,is.hail.io.compress.BGzipCodec,org.apache.hadoop.io.compress.GzipCodec'
+ spark.serializer: 'org.apache.spark.serializer.KryoSerializer'
+ spark.hadoop.parquet.block.size: '1099511627776'
+ spark.sql.files.maxPartitionBytes: '1099511627776'
+ spark.sql.files.openCostInBytes: '1099511627776'
+ Configurations: []
+ Instances:
+ AdditionalMasterSecurityGroups:
+ - Fn::GetAtt:
+ - MasterSecurityGroup
+ - GroupId
+ Ec2KeyName:
+ Ref: KeyName
+ Ec2SubnetId:
+ Ref: Subnet
+ MasterInstanceGroup:
+ InstanceCount: 1
+ InstanceType:
+ Ref: MasterInstanceType
+ CoreInstanceGroup: !If
+ - IsOnDemandCondition
+ - InstanceCount:
+ Ref: CoreNodeCount
+ InstanceType:
+ Ref: WorkerInstanceType
+ Market:
+ Ref: Market
+ EbsConfiguration:
+ EbsOptimized: true
+ EbsBlockDeviceConfigs:
+ - VolumeSpecification:
+ SizeInGB:
+ Ref: DiskSizeGB
+ VolumeType: gp2
+ - InstanceCount:
+ Ref: CoreNodeCount
+ InstanceType:
+ Ref: WorkerInstanceType
+ Market:
+ Ref: Market
+ BidPrice:
+ Ref: WorkerBidPrice
+ EbsConfiguration:
+ EbsOptimized: true
+ EbsBlockDeviceConfigs:
+ - VolumeSpecification:
+ SizeInGB:
+ Ref: DiskSizeGB
+ VolumeType: gp2
+ JobFlowRole:
+ Ref: InstanceProfile
+ Name: !Sub '${Namespace}-emr'
+ Tags: # Add Name tag so EC2 instances are easily identifiable
+ - Key: Name
+ Value: !Sub '${Namespace}-emr'
+ ServiceRole:
+ Ref: ServiceRole
+ ReleaseLabel: emr-5.27.0
+ # This has to be true because we assume a new user each time.
+ VisibleToAllUsers: true
+ SecurityConfiguration: !Ref EmrSecurityConfiguration
+ LogUri: !Sub 's3://${LogBucket}'
+
+Outputs:
+ JupyterUrl:
+ Description: Open Jupyter on your new EMR cluster
+ Value: !Sub 'http://${EmrCluster.MasterPublicDNS}:8192'
+ LogBucket:
+ Description: EMR Scratch data and Logs bucket
+ Value: !Ref LogBucket
+ WorkspaceInstanceRoleArn:
+ Description: IAM role assumed by the EMR workspace instances
+ Value: !GetAtt Ec2Role.Arn
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/external/sagemaker.cfn.yml b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/external/sagemaker.cfn.yml
new file mode 100644
index 0000000000..648a801a61
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/external/sagemaker.cfn.yml
@@ -0,0 +1,158 @@
+AWSTemplateFormatVersion: 2010-09-09
+
+Description: Galileo-Gateway SageMaker-Jupyter
+
+Parameters:
+ Namespace:
+ Type: String
+ Description: An environment name that will be prefixed to resource names
+ InstanceType:
+ Type: String
+ Description: EC2 instance type to launch
+ Default: ml.t3.xlarge
+ VPC:
+ Description: VPC for EMR nodes.
+ Type: AWS::EC2::VPC::Id
+ Subnet:
+ Description: Subnet for EMR nodes, from the VPC selected above
+ Type: AWS::EC2::Subnet::Id
+ S3Mounts:
+ Type: String
+ Description: A JSON array of objects with name, bucket and prefix properties used to mount data
+ IamPolicyDocument:
+ Type: String
+ Description: The IAM policy to be associated with the launched workstation
+ EnvironmentInstanceFiles:
+ Type: String
+ Description: >-
+ An S3 URI (starting with "s3://") that specifies the location of files to be copied to
+ the environment instance, including any bootstrap scripts
+
+Conditions:
+ IamPolicyEmpty: !Equals [!Ref IamPolicyDocument, '{}']
+
+Resources:
+ EncryptionKey:
+ Type: AWS::KMS::Key
+ Properties:
+ Description: 'This is the key used to secure resources in this account'
+ EnableKeyRotation: True
+ KeyPolicy:
+ Version: '2012-10-17'
+ Statement:
+ - Sid: Allow root access
+ Effect: 'Allow'
+ Principal:
+ AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
+ Action:
+ - 'kms:*'
+ Resource: '*'
+ - Sid: Allow use of the key by this account
+ Effect: 'Allow'
+ Principal:
+ AWS: '*'
+ Action:
+ - 'kms:DescribeKey'
+ - 'kms:Encrypt'
+ - 'kms:Decrypt'
+ - 'kms:ReEncrypt*'
+ - 'kms:GenerateDataKey'
+ - 'kms:GenerateDataKeyWithoutPlaintext'
+ - 'kms:CreateGrant'
+ - 'kms:RevokeGrant'
+ Resource: '*'
+ Condition:
+ StringEquals:
+ kms:CallerAccount: !Ref 'AWS::AccountId'
+
+ SecurityGroup:
+ Type: AWS::EC2::SecurityGroup
+ Properties:
+ GroupDescription: SageMaker Notebook Instance
+ VpcId:
+ Ref: VPC
+
+ IAMRole:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ RoleName: !Join ['-', [Ref: Namespace, 'sagemaker-notebook-role']]
+ Path: '/'
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Principal:
+ Service:
+ - 'sagemaker.amazonaws.com'
+ Action:
+ - 'sts:AssumeRole'
+ Policies:
+ - !If
+ - IamPolicyEmpty
+ - !Ref 'AWS::NoValue'
+ - PolicyName: !Join ['-', [Ref: Namespace, 's3-studydata-policy']]
+ PolicyDocument: !Ref IamPolicyDocument
+ - PolicyName: !Join ['-', [Ref: Namespace, 's3-bootstrap-script-policy']]
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Action: 's3:GetObject'
+ Resource: !Sub
+ - 'arn:aws:s3:::${S3Location}/*'
+ # Remove "s3://" prefix from EnvironmentInstanceFiles
+ - S3Location: !Select [1, !Split ['s3://', !Ref EnvironmentInstanceFiles]]
+ - Effect: 'Allow'
+ Action: 's3:ListBucket'
+ Resource: !Sub
+ - 'arn:aws:s3:::${S3Bucket}'
+ - S3Bucket: !Select [2, !Split ['/', !Ref EnvironmentInstanceFiles]]
+ Condition:
+ StringLike:
+ s3:prefix: !Sub
+ - '${S3Prefix}/*'
+ - S3Prefix: !Select [3, !Split ['/', !Ref EnvironmentInstanceFiles]]
+
+ - PolicyName: cw-logs
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - logs:CreateLogStream
+ - logs:DescribeLogStreams
+ - logs:PutLogEvents
+ - logs:CreateLogGroup
+ Resource:
+ - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/sagemaker/*
+ # TODO: Consider also passing DefaultCodeRepository to allow persisting notebook data
+ BasicNotebookInstance:
+ Type: 'AWS::SageMaker::NotebookInstance'
+ Properties:
+ InstanceType: !Ref InstanceType
+ RoleArn: !GetAtt IAMRole.Arn
+ SubnetId: !Ref Subnet
+ SecurityGroupIds:
+ - !Ref SecurityGroup
+ LifecycleConfigName: !GetAtt BasicNotebookInstanceLifecycleConfig.NotebookInstanceLifecycleConfigName
+ KmsKeyId: !GetAtt EncryptionKey.Arn
+
+ BasicNotebookInstanceLifecycleConfig:
+ Type: 'AWS::SageMaker::NotebookInstanceLifecycleConfig'
+ Properties:
+ OnCreate:
+ - Content:
+ Fn::Base64: !Sub |
+ #!/usr/bin/env bash
+ # Download and execute bootstrap script
+ aws s3 cp "${EnvironmentInstanceFiles}/get_bootstrap.sh" "/tmp"
+ chmod 500 "/tmp/get_bootstrap.sh"
+ /tmp/get_bootstrap.sh "${EnvironmentInstanceFiles}" '${S3Mounts}'
+
+Outputs:
+ NotebookInstanceName:
+ Description: The name of the SageMaker notebook instance.
+ Value: !GetAtt [BasicNotebookInstance, NotebookInstanceName]
+
+ WorkspaceInstanceRoleArn:
+ Description: IAM role assumed by the SageMaker workspace instance
+ Value: !GetAtt IAMRole.Arn
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/onboard-account.cfn.yml b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/onboard-account.cfn.yml
new file mode 100644
index 0000000000..a2706d45c7
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/onboard-account.cfn.yml
@@ -0,0 +1,296 @@
+AWSTemplateFormatVersion: 2010-09-09
+
+Description: Galileo-Gateway Research-Account
+
+Parameters:
+ Namespace:
+ Type: String
+ Description: An environment name that will be prefixed to resource names
+
+ CentralAccountId:
+ Type: String
+ Description: The account id of the newly created account.
+
+ ExternalId:
+ Type: String
+ Description: A unique ID used to identify this account
+
+ VpcCidr:
+ Description: Please enter the IP range (CIDR notation) for this VPC
+ Type: String
+ Default: 10.0.0.0/16
+
+ ApiHandlerArn:
+ Type: String
+ Description: The arn of apiHandler role
+
+ WorkflowRoleArn:
+ Type: String
+ Description: The arn of workflowRunner role
+
+ # Generous subnet allocation of 8192 addresses (ie room for a few concurrent EMR clusters)
+ # ending at 10.0.31.255
+ VpcPublicSubnet1Cidr:
+ Description: Please enter the IP range (CIDR notation) for the public subnet in the 1st Availability Zone
+ Type: String
+ Default: 10.0.0.0/19
+
+Metadata:
+ AWS::CloudFormation::Interface:
+ ParameterGroups:
+ - Label:
+ default: Shared Configuration
+ Parameters:
+ - Namespace
+ - Label:
+ default: Account Configuration
+ Parameters:
+ - CentralAccountId
+ - ExternalId
+ - Label:
+ default: Deployment Configuration
+ Parameters:
+ - VpcCidr
+ - VpcPublicSubnet1Cidr
+
+Resources:
+ # TODO lock these permissions down further
+ CrossAccountExecutionRole:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ RoleName: !Join ['-', [Ref: Namespace, 'cross-account-role']]
+ Path: '/'
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Principal:
+ AWS:
+ - !Join [':', ['arn:aws:iam:', Ref: CentralAccountId, 'root']]
+ - !Ref ApiHandlerArn
+ - !Ref WorkflowRoleArn
+ Action:
+ - 'sts:AssumeRole'
+ Condition:
+ StringEquals:
+ sts:ExternalId: !Ref ExternalId
+ Policies:
+ - PolicyName: cfn-access
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action:
+ - cloudformation:CreateStack
+ - cloudformation:DeleteStack
+ - cloudformation:DescribeStacks
+ - cloudformation:DescribeStackEvents
+ Resource: '*'
+ - PolicyName: sagemaker-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - sagemaker:*
+ Resource: '*'
+ - PolicyName: iam-role-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - iam:GetRole
+ - iam:CreateRole
+ - iam:TagRole
+ - iam:GetRolePolicy
+ - iam:PutRolePolicy
+ - iam:DeleteRolePolicy
+ - iam:DeleteRole
+ - iam:PassRole
+ Resource:
+ - !Sub 'arn:aws:iam::${AWS::AccountId}:role/analysis-*'
+ - PolicyName: iam-instance-profile-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - iam:AddRoleToInstanceProfile
+ - iam:CreateInstanceProfile
+ - iam:GetInstanceProfile
+ - iam:DeleteInstanceProfile
+ - iam:RemoveRoleFromInstanceProfile
+ Resource:
+ - !Sub 'arn:aws:iam::${AWS::AccountId}:instance-profile/analysis-*'
+ - PolicyName: iam-role-service-policy-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - iam:AttachRolePolicy
+ - iam:DetachRolePolicy
+ Resource:
+ - !Sub 'arn:aws:iam::${AWS::AccountId}:role/analysis-*'
+ Condition:
+ ArnLike:
+ iam:PolicyARN: arn:aws:iam::aws:policy/service-role/AmazonElasticMapReduceRole
+ - PolicyName: iam-service-linked-role-create-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - iam:CreateServiceLinkedRole
+ - iam:PutRolePolicy
+ Resource: arn:aws:iam::*:role/aws-service-role/elasticmapreduce.amazonaws.com*/AWSServiceRoleForEMRCleanup*
+ Condition:
+ StringLike:
+ iam:AWSServiceName:
+ - elasticmapreduce.amazonaws.com
+ - elasticmapreduce.amazonaws.com.cn
+ - PolicyName: cost-explorer-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - ce:*
+ Resource: '*'
+ - PolicyName: s3-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - s3:*
+ Resource: '*'
+ - PolicyName: ec2-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - ec2:*
+ Resource: '*'
+ - PolicyName: ssm-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - ssm:*
+ Resource: '*'
+ - PolicyName: emr-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - elasticmapreduce:*
+ Resource: '*'
+
+ # VPC for launching EMR clusters into
+ # Just one AZ as we're aiming for transient low-cost clusters rather than HA
+ VPC:
+ Type: AWS::EC2::VPC
+ Properties:
+ CidrBlock: !Ref VpcCidr
+ EnableDnsSupport: true
+ EnableDnsHostnames: true
+ Tags:
+ - Key: Name
+ Value: !Sub ${Namespace} vpc
+
+ InternetGateway:
+ Type: AWS::EC2::InternetGateway
+ Properties:
+ Tags:
+ - Key: Name
+ Value: !Sub ${Namespace} igw
+
+ InternetGatewayAttachment:
+ Type: AWS::EC2::VPCGatewayAttachment
+ Properties:
+ InternetGatewayId: !Ref InternetGateway
+ VpcId: !Ref VPC
+
+ PublicSubnet1:
+ Type: AWS::EC2::Subnet
+ Properties:
+ VpcId: !Ref VPC
+ AvailabilityZone: !Select [0, !GetAZs ]
+ CidrBlock: !Ref VpcPublicSubnet1Cidr
+ MapPublicIpOnLaunch: true
+ Tags:
+ - Key: Name
+ Value: !Sub ${Namespace} public subnet 1
+
+ PublicRouteTable:
+ Type: AWS::EC2::RouteTable
+ Properties:
+ VpcId: !Ref VPC
+ Tags:
+ - Key: Name
+ Value: !Sub ${Namespace} public routes
+
+ DefaultPublicRoute:
+ Type: AWS::EC2::Route
+ DependsOn: InternetGatewayAttachment
+ Properties:
+ RouteTableId: !Ref PublicRouteTable
+ DestinationCidrBlock: 0.0.0.0/0
+ GatewayId: !Ref InternetGateway
+
+ PublicSubnet1RouteTableAssociation:
+ Type: AWS::EC2::SubnetRouteTableAssociation
+ Properties:
+ RouteTableId: !Ref PublicRouteTable
+ SubnetId: !Ref PublicSubnet1
+
+ EncryptionKey:
+ Type: AWS::KMS::Key
+ Properties:
+ Description: 'This is the key used to secure resources in this account'
+ EnableKeyRotation: True
+ KeyPolicy:
+ Version: '2012-10-17'
+ Statement:
+ - Sid: Allow root access
+ Effect: 'Allow'
+ Principal:
+ AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
+ Action:
+ - 'kms:*'
+ Resource: '*'
+ - Sid: Allow use of the key by this account
+ Effect: 'Allow'
+ Principal:
+ AWS: '*'
+ Action:
+ - 'kms:DescribeKey'
+ - 'kms:Encrypt'
+ - 'kms:Decrypt'
+ - 'kms:ReEncrypt*'
+ - 'kms:GenerateDataKey'
+ - 'kms:GenerateDataKeyWithoutPlaintext'
+ - 'kms:CreateGrant'
+ - 'kms:RevokeGrant'
+ Resource: '*'
+ Condition:
+ StringEquals:
+ kms:CallerAccount: !Ref 'AWS::AccountId'
+
+ EncryptionKeyAlias:
+ Type: AWS::KMS::Alias
+ Properties:
+ AliasName: !Join ['', ['alias/', Ref: Namespace, '-encryption-key']]
+ TargetKeyId: !Ref EncryptionKey
+
+Outputs:
+ CrossAccountExecutionRoleArn:
+ Description: The arn of the cross account role.
+ Value: !GetAtt [CrossAccountExecutionRole, Arn]
+
+ VPC:
+ Description: VPC ID
+ Value: !Ref VPC
+
+ VpcPublicSubnet1:
+ Description: A reference to the public subnet in the 1st Availability Zone
+ Value: !Ref PublicSubnet1
+
+ EncryptionKeyArn:
+ Description: KMS Encryption Key Arn
+ Value: !GetAtt [EncryptionKey, Arn]
diff --git a/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/sagemaker-notebook-instance.cfn.yml b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/sagemaker-notebook-instance.cfn.yml
new file mode 100644
index 0000000000..135b38760a
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/sagemaker-notebook-instance.cfn.yml
@@ -0,0 +1,130 @@
+AWSTemplateFormatVersion: 2010-09-09
+
+Description: Galileo-Gateway SageMaker-Jupyter
+
+Parameters:
+ Namespace:
+ Type: String
+ Description: An environment name that will be prefixed to resource names
+ InstanceType:
+ Type: String
+ Description: EC2 instance type to launch
+ Default: ml.t3.xlarge
+ VPC:
+ Description: VPC for EMR nodes.
+ Type: AWS::EC2::VPC::Id
+ Subnet:
+ Description: Subnet for EMR nodes, from the VPC selected above
+ Type: AWS::EC2::Subnet::Id
+ S3Mounts:
+ Type: String
+ Description: A JSON array of objects with name, bucket and prefix properties used to mount data
+ IamPolicyDocument:
+ Type: String
+ Description: The IAM policy to be associated with the launched workstation
+ EnvironmentInstanceFiles:
+ Type: String
+ Description: >-
+ An S3 URI (starting with "s3://") that specifies the location of files to be copied to
+ the environment instance, including any bootstrap scripts
+ EncryptionKeyArn:
+ Type: String
+ Description: The ARN of the KMS encryption Key used to encrypt data in the notebook
+
+Conditions:
+ IamPolicyEmpty: !Equals [!Ref IamPolicyDocument, '{}']
+
+Resources:
+ SecurityGroup:
+ Type: AWS::EC2::SecurityGroup
+ Properties:
+ GroupDescription: SageMaker Notebook Instance
+ VpcId:
+ Ref: VPC
+
+ IAMRole:
+ Type: "AWS::IAM::Role"
+ Properties:
+ RoleName: !Join ["-", [Ref: Namespace, "sagemaker-notebook-role"]]
+ Path: "/"
+ AssumeRolePolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ -
+ Effect: "Allow"
+ Principal:
+ Service:
+ - "sagemaker.amazonaws.com"
+ Action:
+ - "sts:AssumeRole"
+ Policies:
+ - !If
+ - IamPolicyEmpty
+ - !Ref 'AWS::NoValue'
+ - PolicyName: !Join ['-', [Ref: Namespace, 's3-studydata-policy']]
+ PolicyDocument: !Ref IamPolicyDocument
+ - PolicyName: !Join ['-', [Ref: Namespace, 's3-bootstrap-script-policy']]
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Action: 's3:GetObject'
+ Resource: !Sub
+ - 'arn:aws:s3:::${S3Location}/*'
+ # Remove "s3://" prefix from EnvironmentInstanceFiles
+ - S3Location: !Select [ 1, !Split [ 's3://', !Ref EnvironmentInstanceFiles ] ]
+ - Effect: 'Allow'
+ Action: 's3:ListBucket'
+ Resource: !Sub
+ - 'arn:aws:s3:::${S3Bucket}'
+ - S3Bucket: !Select [ 2, !Split [ '/', !Ref EnvironmentInstanceFiles ]]
+ Condition:
+ StringLike:
+ s3:prefix: !Sub
+ - '${S3Prefix}/*'
+ - S3Prefix: !Select [ 3, !Split [ '/', !Ref EnvironmentInstanceFiles ]]
+
+ - PolicyName: cw-logs
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - logs:CreateLogStream
+ - logs:DescribeLogStreams
+ - logs:PutLogEvents
+ - logs:CreateLogGroup
+ Resource:
+ - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/sagemaker/*
+ # TODO: Consider also passing DefaultCodeRepository to allow persisting notebook data
+ BasicNotebookInstance:
+ Type: "AWS::SageMaker::NotebookInstance"
+ Properties:
+ InstanceType: !Ref InstanceType
+ RoleArn: !GetAtt IAMRole.Arn
+ SubnetId: !Ref Subnet
+ SecurityGroupIds:
+ - !Ref SecurityGroup
+ LifecycleConfigName: !GetAtt BasicNotebookInstanceLifecycleConfig.NotebookInstanceLifecycleConfigName
+ KmsKeyId: !Ref EncryptionKeyArn
+
+ BasicNotebookInstanceLifecycleConfig:
+ Type: "AWS::SageMaker::NotebookInstanceLifecycleConfig"
+ Properties:
+ OnCreate:
+ - Content:
+ Fn::Base64: !Sub |
+ #!/usr/bin/env bash
+ # Download and execute bootstrap script
+ aws s3 cp "${EnvironmentInstanceFiles}/get_bootstrap.sh" "/tmp"
+ chmod 500 "/tmp/get_bootstrap.sh"
+ /tmp/get_bootstrap.sh "${EnvironmentInstanceFiles}" '${S3Mounts}'
+
+Outputs:
+
+ NotebookInstanceName:
+ Description: The name of the SageMaker notebook instance.
+ Value: !GetAtt [ BasicNotebookInstance, NotebookInstanceName ]
+
+ WorkspaceInstanceRoleArn:
+ Description: IAM role assumed by the SageMaker workspace instance
+ Value: !GetAtt IAMRole.Arn
diff --git a/addons/addon-base-raas/packages/base-raas-post-deployment/.eslintrc.json b/addons/addon-base-raas/packages/base-raas-post-deployment/.eslintrc.json
new file mode 100644
index 0000000000..c700cf03a8
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-post-deployment/.eslintrc.json
@@ -0,0 +1,26 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "import/no-unresolved": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-raas/packages/base-raas-post-deployment/.gitignore b/addons/addon-base-raas/packages/base-raas-post-deployment/.gitignore
new file mode 100644
index 0000000000..659959de8f
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-post-deployment/.gitignore
@@ -0,0 +1,16 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/addons/addon-base-raas/packages/base-raas-post-deployment/.prettierrc.json b/addons/addon-base-raas/packages/base-raas-post-deployment/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-post-deployment/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-raas/packages/base-raas-post-deployment/jest.config.js b/addons/addon-base-raas/packages/base-raas-post-deployment/jest.config.js
new file mode 100644
index 0000000000..70544476d7
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-post-deployment/jest.config.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+};
diff --git a/addons/addon-base-raas/packages/base-raas-post-deployment/jsconfig.json b/addons/addon-base-raas/packages/base-raas-post-deployment/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-post-deployment/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-raas/packages/base-raas-post-deployment/lib/plugins/steps-plugin.js b/addons/addon-base-raas/packages/base-raas-post-deployment/lib/plugins/steps-plugin.js
new file mode 100644
index 0000000000..160ab4acb7
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-post-deployment/lib/plugins/steps-plugin.js
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+const CreateUserRoles = require('../steps/create-user-roles');
+const InjectServiceEndpoint = require('../steps/inject-service-endpoint');
+const CreateCloudFrontInterceptor = require('../steps/create-cloudfront-interceptor');
+/**
+ * Returns a map of post deployment steps
+ *
+ * @param existingStepsMap Map of existing post deployment steps
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>}
+ */
+// eslint-disable-next-line no-unused-vars
+async function getSteps(existingStepsMap, pluginRegistry) {
+ const stepsMap = new Map([
+ // The userService implementation of RaaS requires the roles to be created first before a user can be created
+ // One of the steps from the "existingStepsMap" tries to create root user.
+ // Make sure the CreateUserRoles gets executed first before executing other post deployment steps otherwise the root
+ // user creation step will fail
+ ['createUserRoles', new CreateUserRoles()],
+ ...existingStepsMap,
+ ['injectServiceEndpoint', new InjectServiceEndpoint()],
+ ['createCloudFrontInterceptor', new CreateCloudFrontInterceptor()],
+ ]);
+
+ return stepsMap;
+}
+
+const plugin = {
+ getSteps,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-raas/packages/base-raas-post-deployment/lib/steps/create-cloudfront-interceptor.js b/addons/addon-base-raas/packages/base-raas-post-deployment/lib/steps/create-cloudfront-interceptor.js
new file mode 100644
index 0000000000..24f0acaac1
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-post-deployment/lib/steps/create-cloudfront-interceptor.js
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+/**
+ * Post deployment step implementation that configures cloudFront interceptor (Lambda@Edge) to the website
+ * cloudFront distribution.
+ */
+class CreateCloudFrontInterceptor extends Service {
+ constructor() {
+ super();
+ this.dependency(['aws']);
+ }
+
+ async init() {
+ await super.init();
+ }
+
+ async execute() {
+ /*
+ * Pseudo Code:
+ * -- Get latest Lambda@Edge function ARN that needs to be configured from the settings
+ * -- Get existing Lambda@Edge function version ARN configured on CloudFront
+ * -- If no Lambda@Edge function is configured on CloudFront yet, then skip all checks and
+ * -- Publish new version of the Lambda@Edge function and
+ * -- Configure the new lambda version ARN on CloudFront and exit.
+ * -- If some Lambda@Edge function version is configured on CloudFront then get CodeSha256 for that version
+ * -- Get latest CodeSha256 value for the Lambda
+ * -- If the CodeSha256 value for the latest Lambda@Edge matches the one that's configured on CloudFront, then
+ * -- There is nothing to do. The latest Lambda is already configured on CloudFront. Just return.
+ * -- If the latest lambda@edge code Sha256 is different from what's configured then
+ * -- Publish new version of the Lambda@Edge function with latest Lambda code and
+ * -- Configure the new lambda version ARN on CloudFront
+ *
+ * Note: We need to publish Lambda Version using Lambda SDK here instead of simply using "AWS::Lambda::Version"
+ * CloudFormation resource in the "edge-lambda" stack to publish Lambda. That's because the "AWS::Lambda::Version"
+ * will end up publishing new version of the Lambda function every time. We want to publish only if the lambda@edge
+ * code has changed.
+ */
+ const aws = await this.service('aws');
+ const cloudFrontApi = new aws.sdk.CloudFront({ apiVersion: '2019-03-26' });
+ const cloudFrontId = this.settings.get('cloudFrontId');
+
+ // -- Get latest Lambda@Edge function ARN that needs to be configured from the settings
+ const latestEdgeLambdaArn = this.settings.get('edgeLambdaArn');
+
+ // -- Get existing Lambda@Edge function version ARN configured on CloudFront
+ const cloudFrontConfig = await cloudFrontApi.getDistributionConfig({ Id: cloudFrontId }).promise();
+ const existingLambdaArn = _.get(
+ cloudFrontConfig,
+ 'DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations.Items.0.LambdaFunctionARN',
+ );
+
+ // -- If no Lambda@Edge function is configured on CloudFront yet, then skip all checks and
+ // -- Publish new version of the Lambda@Edge function and
+ // -- Configure the new lambda version ARN on CloudFront and exit
+ if (_.isEmpty(existingLambdaArn)) {
+ this.log.info(`No edge lambda configured for cloudfront distribution "${cloudFrontId}"".`);
+ const publishedVersionArn = await this.publishNewLambdaVersion(latestEdgeLambdaArn);
+ await this.updateCloudFrontConfig(cloudFrontId, cloudFrontConfig, publishedVersionArn);
+ return;
+ }
+
+ // -- If some Lambda@Edge function version is configured on CloudFront then get CodeSha256 for that version
+ const existingSha256 = existingLambdaArn && (await this.getLambdaCodeSha256(existingLambdaArn));
+ // -- Get latest CodeSha256 value for the Lambda
+ const latestSha256 = await this.getLambdaCodeSha256(latestEdgeLambdaArn);
+
+ // -- If the CodeSha256 value for the latest Lambda@Edge matches the one that's configured on CloudFront, then
+ // -- There is nothing to do. The latest Lambda is already configured on CloudFront. Just return.
+ if (existingSha256 === latestSha256) {
+ this.log.info(
+ `Skip updating cloudfront distribution "${cloudFrontId}"". The Lambda@Edge version "${latestEdgeLambdaArn}" is already configured and has latest code.`,
+ );
+ return;
+ }
+
+ // -- If the latest lambda@edge code Sha256 is different from what's configured then
+ // -- Publish new version of the Lambda@Edge function with latest Lambda code and
+ // -- Configure the new lambda version ARN on CloudFront
+ const publishedVersionArn = await this.publishNewLambdaVersion(latestEdgeLambdaArn);
+ await this.updateCloudFrontConfig(cloudFrontId, cloudFrontConfig, publishedVersionArn);
+ }
+
+ async getLambdaCodeSha256(lambdaArn) {
+ const aws = await this.service('aws');
+ const lambdaApi = new aws.sdk.Lambda({ apiVersion: '2015-03-31', region: 'us-east-1' });
+ const lambdaInfo = await lambdaApi.getFunction({ FunctionName: lambdaArn }).promise();
+
+ return lambdaInfo.Configuration.CodeSha256;
+ }
+
+ async publishNewLambdaVersion(lambdaArn) {
+ const aws = await this.service('aws');
+ const lambdaApi = new aws.sdk.Lambda({ apiVersion: '2015-03-31', region: 'us-east-1' });
+ const lambdaInfo = await lambdaApi.publishVersion({ FunctionName: lambdaArn }).promise();
+
+ // Return ARN pointing to the new Lambda version we just published
+ return lambdaInfo.FunctionArn;
+ }
+
+ async updateCloudFrontConfig(cloudFrontId, cloudFrontConfig, lambdaVersionArn) {
+ const aws = await this.service('aws');
+ const cloudFrontApi = new aws.sdk.CloudFront({ apiVersion: '2019-03-26' });
+
+ this.log.info(`Updating cloudfront distribution "${cloudFrontId}"`);
+
+ // Prepare updateDistribution parameters
+ // 1. Set cloudFrontID
+ // 2. Set IfMatch to the value of ETag
+ // 3. Remove ETag
+ // See "https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFront.html#updateDistribution-property" for details
+ cloudFrontConfig.Id = cloudFrontId;
+ cloudFrontConfig.IfMatch = cloudFrontConfig.ETag;
+ delete cloudFrontConfig.ETag;
+
+ // 4. Add Lambda@Edge's ARN
+ _.set(cloudFrontConfig, 'DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations', {
+ Quantity: 1, // The number of Lambda function associations for this cache behavior. This needs to be same as Items.length
+ Items: [
+ {
+ LambdaFunctionARN: lambdaVersionArn,
+ EventType: 'origin-response',
+ },
+ ],
+ });
+
+ return cloudFrontApi.updateDistribution(cloudFrontConfig).promise();
+ }
+}
+
+module.exports = CreateCloudFrontInterceptor;
diff --git a/addons/addon-base-raas/packages/base-raas-post-deployment/lib/steps/create-user-roles.js b/addons/addon-base-raas/packages/base-raas-post-deployment/lib/steps/create-user-roles.js
new file mode 100644
index 0000000000..9dbc323daa
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-post-deployment/lib/steps/create-user-roles.js
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context');
+
+const userRoleItems = [
+ {
+ id: 'admin', // Don't delete this; the root is created with this role
+ description: 'Administrator',
+ userType: 'INTERNAL',
+ },
+ {
+ id: 'guest', // Don't delete this; external self-registered users start as this
+ description: 'External Guest',
+ userType: 'EXTERNAL',
+ },
+ {
+ id: 'internal-guest', // Don't delete this
+ description: 'Internal Guest',
+ userType: 'INTERNAL',
+ },
+ {
+ id: 'researcher',
+ description: 'Internal Researcher',
+ userType: 'INTERNAL',
+ },
+ {
+ id: 'external-researcher',
+ description: 'External Researcher',
+ userType: 'EXTERNAL',
+ },
+];
+
+const settingKeys = {
+ enableExternalResearchers: 'enableExternalResearchers',
+};
+
+class CreateUserRolesService extends Service {
+ constructor() {
+ super();
+ this.dependency(['userRolesService', 'aws']);
+ }
+
+ async createUserRoles() {
+ const [userRolesService] = await this.service(['userRolesService']);
+
+ const enableExternalResearchers = this.settings.optionalBoolean(settingKeys.enableExternalResearchers, false);
+ const disabledRoles = enableExternalResearchers ? [] : [{ id: 'external-researcher' }];
+ const rolesToCreate = _.differenceBy(userRoleItems, disabledRoles, 'id');
+ const requestContext = getSystemRequestContext();
+
+ const creationPromises = rolesToCreate.map(async role => {
+ try {
+ // check if the userRole already exists, do not create or update the item info
+ const userRole = await userRolesService.find(requestContext, { id: role.id });
+ if (!userRole) {
+ await userRolesService.create(requestContext, role);
+ this.log.info({ message: `Created user role ${role.id}`, userRole: role });
+ }
+ } catch (err) {
+ if (err.code === 'alreadyExists') {
+ // The user role already exists. Nothing to do.
+ this.log.info(`The userRole ${role.id} already exists. Did NOT overwrite that userRole's information.`);
+ } else {
+ // In case of any other error let it bubble up
+ throw err;
+ }
+ }
+ });
+ await Promise.all(creationPromises);
+
+ // Make sure there are no disabled roles in db.
+ // This can happen if the solution was deployed first with the roles enabled but then re-deployed after disabling
+ // certain roles
+ await this.deleteRoles(requestContext, disabledRoles);
+
+ this.log.info(`Finished creating user roles`);
+ }
+
+ async deleteRoles(requestContext, roles) {
+ const [userRolesService] = await this.service(['userRolesService']);
+ const deletionPromises = roles.map(async role => {
+ try {
+ await userRolesService.delete(requestContext, { id: role.id });
+ } catch (err) {
+ // The user role does not exist. Nothing to delete in that case
+ if (err.code !== 'notFound') {
+ // In case of any other error let it bubble up
+ throw err;
+ }
+ }
+ });
+ return Promise.all(deletionPromises);
+ }
+
+ async execute() {
+ return this.createUserRoles();
+ }
+}
+
+module.exports = CreateUserRolesService;
diff --git a/addons/addon-base-raas/packages/base-raas-post-deployment/lib/steps/inject-service-endpoint.js b/addons/addon-base-raas/packages/base-raas-post-deployment/lib/steps/inject-service-endpoint.js
new file mode 100644
index 0000000000..7dfcd00f00
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-post-deployment/lib/steps/inject-service-endpoint.js
@@ -0,0 +1,55 @@
+/*
+ * 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 */
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+class InjectServiceEndpoint extends Service {
+ constructor() {
+ // eslint-disable-line no-useless-constructor
+ super();
+ this.dependency(['aws']);
+ }
+
+ async init() {
+ await super.init();
+ }
+
+ async execute() {
+ const [aws] = await this.service(['aws']);
+
+ const lambdaName = this.settings.get('workflowLambdaName');
+ const backendStackName = this.settings.get('backendStackName');
+ const cfn = new aws.sdk.CloudFormation();
+ const backendStack = await cfn.describeStacks({ StackName: backendStackName }).promise();
+ const serviceEndpoint = _.find(backendStack.Stacks[0].Outputs, { OutputKey: 'ServiceEndpoint' }).OutputValue;
+
+ const lambda = new aws.sdk.Lambda();
+ const workflowLambda = await lambda.getFunction({ FunctionName: lambdaName }).promise();
+ const existingEnvironmentsVariables = workflowLambda.Configuration.Environment.Variables;
+ existingEnvironmentsVariables.APP_API_ENDPOINT = serviceEndpoint;
+ const updateParams = {
+ FunctionName: lambdaName,
+ Environment: {
+ Variables: existingEnvironmentsVariables,
+ },
+ };
+ await lambda.updateFunctionConfiguration(updateParams).promise();
+ this.log.info(`Created APP_API_ENDPOINT:${serviceEndpoint} as a parameter to ${lambdaName}`);
+ }
+}
+
+module.exports = InjectServiceEndpoint;
diff --git a/addons/addon-base-raas/packages/base-raas-post-deployment/package.json b/addons/addon-base-raas/packages/base-raas-post-deployment/package.json
new file mode 100644
index 0000000000..ef20d27433
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-post-deployment/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@aws-ee/base-raas-post-deployment",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A collection of base RaaS post deployment steps",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-services": "workspace:*",
+ "@aws-ee/base-services-container": "workspace:*",
+ "@aws-ee/base-raas-services": "workspace:*",
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.1.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; yarn run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/.eslintrc.json b/addons/addon-base-raas/packages/base-raas-rest-api/.eslintrc.json
new file mode 100644
index 0000000000..c700cf03a8
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/.eslintrc.json
@@ -0,0 +1,26 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "import/no-unresolved": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/.gitignore b/addons/addon-base-raas/packages/base-raas-rest-api/.gitignore
new file mode 100644
index 0000000000..659959de8f
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/.gitignore
@@ -0,0 +1,16 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/.prettierrc.json b/addons/addon-base-raas/packages/base-raas-rest-api/.prettierrc.json
new file mode 100644
index 0000000000..d3846d96f3
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/.prettierrc.json
@@ -0,0 +1,16 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all",
+ "overrides": [
+ {
+ "files": ["*.yml", "*.yaml"],
+ "options": {
+ "singleQuote": false
+ }
+ }
+ ]
+}
+
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/jest.config.js b/addons/addon-base-raas/packages/base-raas-rest-api/jest.config.js
new file mode 100644
index 0000000000..70544476d7
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/jest.config.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+};
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/jsconfig.json b/addons/addon-base-raas/packages/base-raas-rest-api/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/accounts-controller.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/accounts-controller.js
new file mode 100644
index 0000000000..0028d58062
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/accounts-controller.js
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+// const _ = require('lodash');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ const boom = context.boom;
+
+ const accountService = await context.service('accountService');
+
+ // ===============================================================
+ // GET / (mounted to /api/accounts)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+
+ const result = await accountService.list(requestContext);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id (mounted to /api/accounts)
+ // ===============================================================
+ router.get(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ const result = await accountService.mustFind(requestContext, { id });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST / (mounted to /api/accounts)
+ // ===============================================================
+ router.post(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const possibleBody = req.body;
+ const result = await accountService.provisionAccount(requestContext, possibleBody);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // PUT /:id (mounted to /api/accounts)
+ // ===============================================================
+ router.put(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+ const body = req.body || {};
+
+ if (body.id !== id)
+ throw boom.badRequest(
+ 'The accounts id provided in the url does not match the one in the submitted json object',
+ true,
+ );
+
+ const result = await accountService.update(requestContext, body);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // DELETE /:id (mounted to /api/accounts)
+ // ===============================================================
+ router.delete(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ await accountService.delete(requestContext, { id });
+ res.status(200).json({});
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/aws-accounts-controller.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/aws-accounts-controller.js
new file mode 100644
index 0000000000..a7a0081d02
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/aws-accounts-controller.js
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+// const _ = require('lodash');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ // const boom = context.boom;
+
+ const awsAccountsService = await context.service('awsAccountsService');
+ const accountService = await context.service('accountService');
+
+ // ===============================================================
+ // GET / (mounted to /api/aws-accounts)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+
+ const awsAccounts = await awsAccountsService.list(requestContext);
+ res.status(200).json(awsAccounts);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id (mounted to /api/aws-accounts)
+ // ===============================================================
+ router.get(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ const result = await awsAccountsService.mustFind(requestContext, { id });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST / (mounted to /api/aws-accounts)
+ // ===============================================================
+ router.post(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const possibleBody = req.body;
+ const result = await awsAccountsService.create(requestContext, possibleBody);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST / (mounted to /api/aws-accounts)
+ // ===============================================================
+ router.post(
+ '/provision',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const possibleBody = req.body;
+ await accountService.provisionAccount(requestContext, possibleBody);
+
+ res.status(200).json({ message: 'account creating' });
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/compute-controller.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/compute-controller.js
new file mode 100644
index 0000000000..0872b3adac
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/compute-controller.js
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+// const _ = require('lodash');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+
+ const computePlatformService = await context.service('computePlatformService');
+
+ // ===============================================================
+ // GET /platforms (mounted to /api/compute)
+ // ===============================================================
+ router.get(
+ '/platforms',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+
+ const platforms = await computePlatformService.listPlatforms(requestContext);
+ res.status(200).json(platforms);
+ }),
+ );
+
+ // ===============================================================
+ // GET /platforms/:id/configurations (mounted to /api/compute)
+ // ===============================================================
+ router.get(
+ '/platforms/:id/configurations',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const id = req.params.id;
+ const platforms = await computePlatformService.listConfigurations(requestContext, {
+ platformId: id,
+ includePrice: true,
+ });
+ res.status(200).json(platforms);
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/costs-controller.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/costs-controller.js
new file mode 100644
index 0000000000..978638088d
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/costs-controller.js
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+// const mime = require('mime');
+// var fs = require('fs');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+
+ const [costsService] = await context.service(['costsService']);
+
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const result = await costsService.getIndividualEnvironmentOrProjCost(requestContext, req.query);
+ res.status(200).json(result);
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/environment-controller.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/environment-controller.js
new file mode 100644
index 0000000000..99694cef11
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/environment-controller.js
@@ -0,0 +1,167 @@
+/*
+ * 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.
+ */
+
+// const mime = require('mime');
+// var fs = require('fs');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+
+ const [
+ environmentService,
+ environmentKeypairService,
+ environmentNotebookUrlService,
+ environmentSpotPriceHistoryService,
+ ] = await context.service([
+ 'environmentService',
+ 'environmentKeypairService',
+ 'environmentNotebookUrlService',
+ 'environmentSpotPriceHistoryService',
+ ]);
+
+ // ===============================================================
+ // GET / (mounted to /api/workspaces)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+
+ const result = await environmentService.list(requestContext);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id (mounted to /api/workspaces)
+ // ===============================================================
+ router.get(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ const result = await environmentService.mustFind(requestContext, { id });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST / (mounted to /api/workspaces)
+ // ===============================================================
+ router.post(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const possibleBody = req.body;
+ const result = requestContext.principal.isExternalUser
+ ? await environmentService.createExternal(requestContext, possibleBody)
+ : await environmentService.create(requestContext, possibleBody);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // PUT / (mounted to /api/workspaces)
+ // ===============================================================
+ router.put(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const possibleBody = req.body;
+ const result = await environmentService.update(requestContext, possibleBody);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // DELETE /:id (mounted to /api/workspaces)
+ // ===============================================================
+ router.delete(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ await environmentService.delete(requestContext, { id });
+ res.status(200).json({});
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/keypair (mounted to /api/workspaces)
+ // ===============================================================
+ router.get(
+ '/:id/keypair',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const id = req.params.id;
+
+ const result = await environmentKeypairService.mustFind(requestContext, id);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/password (mounted to /api/workspaces)
+ // ===============================================================
+ router.get(
+ '/:id/password',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const id = req.params.id;
+
+ const result = await environmentService.getWindowsPasswordData(requestContext, { id });
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/url (mounted to /api/workspaces)
+ // ===============================================================
+ router.get(
+ '/:id/url',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const id = req.params.id;
+ const result = await environmentNotebookUrlService.getNotebookPresignedUrl(requestContext, id);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /pricing/:type (mounted to /api/workspaces)
+ // ===============================================================
+ router.get(
+ '/pricing/:type',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const type = req.params.type;
+ const result = await environmentSpotPriceHistoryService.getPriceHistory(requestContext, type);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/indexes-controller.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/indexes-controller.js
new file mode 100644
index 0000000000..df8b29c075
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/indexes-controller.js
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+// const _ = require('lodash');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ const boom = context.boom;
+
+ const indexesService = await context.service('indexesService');
+
+ // ===============================================================
+ // GET / (mounted to /api/indexes)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+
+ const result = await indexesService.list(requestContext);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id (mounted to /api/indexes)
+ // ===============================================================
+ router.get(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ const result = await indexesService.mustFind(requestContext, { id });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST / (mounted to /api/indexes)
+ // ===============================================================
+ router.post(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const possibleBody = req.body;
+ const result = await indexesService.create(requestContext, possibleBody);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // PUT /:id (mounted to /api/indexes)
+ // ===============================================================
+ router.put(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+ const body = req.body || {};
+
+ if (body.id !== id)
+ throw boom.badRequest(
+ 'The indexes id provided in the url does not match the one in the submitted json object',
+ true,
+ );
+
+ const result = await indexesService.update(requestContext, body);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // DELETE /:id (mounted to /api/indexes)
+ // ===============================================================
+ router.delete(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ await indexesService.delete(requestContext, { id });
+ res.status(200).json({});
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/ip-controller.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/ip-controller.js
new file mode 100644
index 0000000000..2a1e065d3a
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/ip-controller.js
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+
+ // ===============================================================
+ // GET / (mounted to /api/ip)
+ // ===============================================================
+ router.get(
+ '',
+ wrap(async (req, res) => {
+ const ipString =
+ req.header('X-Forwarded-For') ||
+ _.get(req, 'requestContext.identity.sourceIp') ||
+ _.get(req, 'connection.remoteAddress');
+
+ // Note, by definition, the information in the 'X-Forwarded-For' should never
+ // be trusted as authoritative and should never be used for any kind of verification.
+ // Sometime 'X-Forwarded-For' can be an string with ', ' if there were multiple forwards.
+ // We take the first element.
+ res.status(200).json({
+ ipAddress: _.trim(_.first(_.split(ipString, ','))),
+ });
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/project-controller.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/project-controller.js
new file mode 100644
index 0000000000..23c36ecd40
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/project-controller.js
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+// const _ = require('lodash');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ const boom = context.boom;
+
+ const projectService = await context.service('projectService');
+
+ // ===============================================================
+ // GET / (mounted to /api/projects)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+
+ const result = await projectService.list(requestContext);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id (mounted to /api/projects)
+ // ===============================================================
+ router.get(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ const result = await projectService.mustFind(requestContext, { id });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST / (mounted to /api/projects)
+ // ===============================================================
+ router.post(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const possibleBody = req.body;
+ const result = await projectService.create(requestContext, possibleBody);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // PUT /:id (mounted to /api/projects)
+ // ===============================================================
+ router.put(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+ const body = req.body || {};
+
+ if (body.id !== id)
+ throw boom.badRequest(
+ 'The project id provided in the url does not match the one in the submitted json object',
+ true,
+ );
+
+ const result = await projectService.update(requestContext, body);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // DELETE /:id (mounted to /api/projects)
+ // ===============================================================
+ router.delete(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ await projectService.delete(requestContext, { id });
+ res.status(200).json({});
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/study-controller.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/study-controller.js
new file mode 100644
index 0000000000..5814910deb
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/study-controller.js
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+
+ const [studyService, studyPermissionService] = await context.service(['studyService', 'studyPermissionService']);
+
+ // ===============================================================
+ // GET / (mounted to /api/studies)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const { category } = req.query;
+
+ const result = await studyService.list(requestContext, category);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id (mounted to /api/studies)
+ // ===============================================================
+ router.get(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ await studyPermissionService.verifyRequestorAccess(requestContext, id, req.method);
+
+ const result = await studyService.mustFind(requestContext, id);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST / (mounted to /api/studies)
+ // ===============================================================
+ router.post(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const possibleBody = req.body;
+ const result = await studyService.create(requestContext, possibleBody);
+
+ // TODO we should move this call to the studyService itself, otherwise we need to do result.access = 'admin';
+ await studyPermissionService.create(requestContext, result.id);
+ result.access = 'admin'; // TODO see the todo comment above
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/files (mounted to /api/studies)
+ // ===============================================================
+ router.get(
+ '/:id/files',
+ wrap(async (req, res) => {
+ const studyId = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ await studyPermissionService.verifyRequestorAccess(requestContext, studyId, req.method);
+
+ const result = await studyService.listFiles(requestContext, studyId);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/upload-requests (mounted to /api/studies)
+ // ===============================================================
+ router.get(
+ '/:id/upload-requests',
+ wrap(async (req, res) => {
+ const studyId = req.params.id;
+ const requestContext = res.locals.requestContext;
+ const filenames = req.query.filenames.split(',');
+
+ // Check permissions against a PUT request since uploading files to the study
+ // is a mutating action
+ await studyPermissionService.verifyRequestorAccess(requestContext, studyId, 'PUT');
+
+ const result = await studyService.createPresignedPostRequests(requestContext, studyId, filenames);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/permissions (mounted to /api/studies)
+ // ===============================================================
+ router.get(
+ '/:id/permissions',
+ wrap(async (req, res) => {
+ const studyId = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ await studyPermissionService.verifyRequestorAccess(requestContext, studyId, req.method);
+
+ const result = await studyPermissionService.findByStudy(requestContext, studyId);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // PUT /:id/permissions (mounted to /api/studies)
+ // ===============================================================
+ router.put(
+ '/:id/permissions',
+ wrap(async (req, res) => {
+ const studyId = req.params.id;
+ const requestContext = res.locals.requestContext;
+ const updateRequest = req.body;
+
+ // Validate permissions and usage
+ await studyPermissionService.verifyRequestorAccess(requestContext, studyId, req.method);
+ const study = await studyService.mustFind(requestContext, studyId);
+ if (study.category === 'My Studies') {
+ throw context.boom.forbidden('Permissions cannot be set for studies in the "My Studies" category', true);
+ }
+
+ const result = await studyPermissionService.update(requestContext, studyId, updateRequest);
+ res.status(200).json(result);
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/template-controller.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/template-controller.js
new file mode 100644
index 0000000000..29adb194a5
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/template-controller.js
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+
+ const [externalCfnTemplateService] = await context.service(['externalCfnTemplateService']);
+
+ // ===============================================================
+ // GET /:id (mounted to /api/template)
+ // ===============================================================
+ router.get(
+ '/:id',
+ wrap(async (req, res) => {
+ const key = req.params.id;
+ const result = await externalCfnTemplateService.mustGetSignS3Url(key);
+ res.status(200).json(result);
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/user-roles-controller.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/user-roles-controller.js
new file mode 100644
index 0000000000..3fe60e5ad8
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/user-roles-controller.js
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+// const authProviderConstants = require('@aws-ee/base-services/lib/authentication-providers/constants')
+// .authenticationProviders;
+// const _ = require('lodash');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ // const boom = context.boom;
+ const [userRolesService] = await context.service(['userRolesService']);
+
+ // ===============================================================
+ // GET / (mounted to /api/user-roles)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const userRoles = await userRolesService.list();
+ res.status(200).json(userRoles);
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/users-controller.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/users-controller.js
new file mode 100644
index 0000000000..d4f15733b2
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/controllers/users-controller.js
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ // const boom = context.boom;
+ const [userService, dbPasswordService] = await context.service(['userService', 'dbPasswordService']);
+
+ // ===============================================================
+ // GET / (mounted to /api/users)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const users = await userService.listUsers(requestContext);
+ res.status(200).json(users);
+ }),
+ );
+
+ // ===============================================================
+ // POST / (mounted to /api/users)
+ // ===============================================================
+ router.post(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const createdUser = await userService.createUser(requestContext, req.body);
+ res.status(200).json(createdUser);
+ }),
+ );
+
+ // ===============================================================
+ // POST / (mounted to /api/users/bulk)
+ // ===============================================================
+ router.post(
+ '/bulk',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const users = req.body;
+ const defaultAuthNProviderId = req.query.authenticationProviderId;
+ const result = await userService.createUsers(requestContext, users, defaultAuthNProviderId);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // PUT /:username (mounted to /api/users)
+ // ===============================================================
+ router.put(
+ '/:username',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const username = req.params.username;
+ const userInBody = req.body || {};
+ const user = await userService.updateUser(requestContext, {
+ ...userInBody,
+ username,
+ });
+ res.status(200).json(user);
+ }),
+ );
+
+ // ===============================================================
+ // PUT /:username/password (mounted to /api/users)
+ // ===============================================================
+ router.put(
+ '/:username/password',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const username = req.params.username;
+ const { password } = req.body;
+
+ // Save password salted hash for the user in internal auth provider (i.e., in passwords table)
+ await dbPasswordService.savePassword(requestContext, { username, password });
+ res.status(200).json({ username, message: `Password successfully updated for user ${username}` });
+ }),
+ );
+
+ // ===============================================================
+ // DELETE /:id (mounted to /api/users)
+ // ===============================================================
+ router.delete(
+ '/:username',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const { username } = req.params;
+ const { authenticationProviderId, identityProviderName } = req.body;
+ await userService.deleteUser(requestContext, {
+ username,
+ authenticationProviderId,
+ identityProviderName,
+ });
+ res.status(200).json({ message: `user ${username} deleted` });
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/plugins/authentication-plugin.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/plugins/authentication-plugin.js
new file mode 100644
index 0000000000..efbc089e98
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/plugins/authentication-plugin.js
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context');
+
+/**
+ * The main authentication plugin function. This plugin implementation adds customization by
+ * checking for user's role. The authentication plugins are used by the "authentication-service.js".
+ *
+ * @param authenticationPluginPayload A plugin's payload. This has the shape { token, container, authResult }.
+ * @param authenticationPluginPayload.token The JWT token being used for authentication.
+ * @param authenticationPluginPayload.container Services container that provides service implementations for registered services
+ * @param authenticationPluginPayload.authResult The current authentication result containing authentication decision evaluated so far
+ * (by previous plugins or the original decision from the authenticationService).
+ *
+ * @returns {Promise<{container: *, authResult: {authenticated: boolean}, token: *}|*>}
+ */
+async function authenticate(authenticationPluginPayload) {
+ const { token, container, authResult } = authenticationPluginPayload;
+ const notAuthenticated = claims => ({ token, container, authResult: { ...claims, authenticated: false } });
+ const isAuthenticated = _.get(authResult, 'authenticated', false);
+
+ // if the current authentication decision is "not authenticated" then return right away
+ if (!isAuthenticated) return authenticationPluginPayload;
+
+ const logger = await container.find('log');
+ try {
+ const { username, authenticationProviderId, identityProviderName } = authResult;
+ const userService = await container.find('userService');
+ const user = await userService.mustFindUser({
+ username,
+ authenticationProviderId,
+ identityProviderName,
+ });
+ const userRoleId = _.get(user, 'userRole');
+ if (!userRoleId) {
+ // no user role, don't know what kind of user is this, return not authenticated
+ return notAuthenticated(...authResult);
+ }
+
+ const userRolesService = await container.find('userRolesService');
+ // Make sure the user's role exists
+ // It is possible that the user was created before with some role and then that role was disabled
+ // For example, if a user with an "external-researcher" role was created before but then the external-researcher role was
+ // disabled (by setting the enableExternalResearchers = false) in that case the "external-researcher" may no longer
+ // be a valid role but there may still be some existing users with that role. Those users should no longer be able
+ // to login.
+ await userRolesService.mustFind(getSystemRequestContext(), { id: userRoleId });
+ } catch (e) {
+ logger.error('Error authenticating the user');
+ logger.error(e);
+ return notAuthenticated(...authResult);
+ }
+ return authenticationPluginPayload;
+}
+
+const plugin = {
+ authenticate,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/plugins/authn-handler-services-plugin.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/plugins/authn-handler-services-plugin.js
new file mode 100644
index 0000000000..5bda73b7fb
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/plugins/authn-handler-services-plugin.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.
+ */
+
+const PluginRegistryService = require('@aws-ee/base-services/lib/plugin-registry/plugin-registry-service');
+const UserService = require('@aws-ee/base-raas-services/lib/user/user-service');
+const UserAuthzService = require('@aws-ee/base-raas-services/lib/user/user-authz-service');
+const UserRoleService = require('@aws-ee/base-raas-services/lib/user-roles/user-roles-service');
+const UserAttributesMapperService = require('@aws-ee/base-raas-services/lib/user/user-attributes-mapper-service');
+
+const settingKeys = {
+ tablePrefix: 'dbTablePrefix',
+};
+
+/**
+ * Registers the services needed by the workflow loop runner lambda function
+ * @param container An instance of ServicesContainer to register services to
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerServices(container, pluginRegistry) {
+ container.register('userService', new UserService());
+ // The base authn provider uses username by concatenating username with auth provider name and idp name
+ // In RaaS, the email address should be used as username so register custom UserAttributesMapperService that maps
+ // attribs from decoded token to user
+ container.register('userAttributesMapperService', new UserAttributesMapperService());
+ container.register('userRolesService', new UserRoleService());
+ container.register('pluginRegistryService', new PluginRegistryService(pluginRegistry), { lazy: false });
+ container.register('raasUserAuthzService', new UserAuthzService());
+}
+
+/**
+ * Registers static settings required by the workflow loop runner lambda function
+ * @param existingStaticSettings An existing static settings plain javascript object containing settings as key/value contributed by other plugins
+ * @param settings Default instance of settings service that resolves settings from environment variables
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point
+ *
+ * @returns {Promise<*>} A promise that resolves to static settings object
+ */
+// eslint-disable-next-line no-unused-vars
+function getStaticSettings(existingStaticSettings, settings, pluginRegistry) {
+ const staticSettings = {
+ ...existingStaticSettings,
+ };
+
+ // Register all dynamodb table names used by the base rest api addon
+ const tablePrefix = settings.get(settingKeys.tablePrefix);
+ const table = (key, suffix) => {
+ staticSettings[key] = `${tablePrefix}-${suffix}`;
+ };
+ table('dbTableUserRoles', 'DbUserRoles');
+
+ return staticSettings;
+}
+
+const plugin = {
+ getStaticSettings,
+ // getLoggingContext, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerSettingsService, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerLoggerService, // not implemented, the default behavior provided by addon-base is sufficient
+ registerServices,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/plugins/routes-plugin.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/plugins/routes-plugin.js
new file mode 100644
index 0000000000..39b94b152c
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/plugins/routes-plugin.js
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const setupAuthContext = require('@aws-ee/base-controllers/lib/middlewares/setup-auth-context');
+const prepareContext = require('@aws-ee/base-controllers/lib/middlewares/prepare-context');
+const ensureActive = require('@aws-ee/base-controllers/lib/middlewares/ensure-active');
+const ensureAdmin = require('@aws-ee/base-controllers/lib/middlewares/ensure-admin');
+const userController = require('@aws-ee/base-controllers/lib/user-controller');
+
+const usersController = require('../controllers/users-controller');
+const studyController = require('../controllers/study-controller');
+const environmentController = require('../controllers/environment-controller');
+const userRolesController = require('../controllers/user-roles-controller');
+const awsAccountsController = require('../controllers/aws-accounts-controller');
+const costsController = require('../controllers/costs-controller');
+const indexesController = require('../controllers/indexes-controller');
+const projectController = require('../controllers/project-controller');
+const accountsController = require('../controllers/accounts-controller');
+const templateController = require('../controllers/template-controller');
+const computeController = require('../controllers/compute-controller');
+const ipController = require('../controllers/ip-controller');
+
+/**
+ * Adds routes to the given routesMap.
+ *
+ * @param routesMap A Map containing routes. This object is a Map that has route paths as
+ * keys and an array of functions that configure the router as value. Each function in the
+ * array is expected have the following signature. The function accepts context and router
+ * arguments and returns a configured router.
+ *
+ * (context, router) => configured router
+ *
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} Returns a Map with the mapping of the routes vs their router configurer functions
+ */
+// eslint-disable-next-line no-unused-vars
+async function getRoutes(routesMap, pluginRegistry) {
+ // PROTECTED APIS for workflows accessible only to logged in ADMIN users. These routes are already registered by
+ // the workflow's api plugin (addons/addon-base-workflow-api/packages/base-worklfow-api/lib/plugins/routes-plugin.js)
+ // but are made available to all users.
+ // For RaaS, we want these APIs to be ONLY available to admin users. So append ensureAdmin middleware to existing
+ // routes middlewares
+ appendMiddleware(routesMap, '/api/step-templates', ensureAdmin);
+ appendMiddleware(routesMap, '/api/workflow-templates', ensureAdmin);
+ appendMiddleware(routesMap, '/api/workflows', ensureAdmin);
+
+ const routes = new Map([
+ ...routesMap,
+ // PROTECTED APIS accessible only to logged in users
+ ['/api/user', [setupAuthContext, prepareContext, userController]],
+
+ // PROTECTED APIS accessible only to logged in active users
+ ['/api/users', [setupAuthContext, prepareContext, ensureActive, usersController]],
+ ['/api/studies', [setupAuthContext, prepareContext, ensureActive, studyController]],
+ ['/api/workspaces', [setupAuthContext, prepareContext, ensureActive, environmentController]],
+ ['/api/user-roles', [setupAuthContext, prepareContext, ensureActive, userRolesController]],
+ ['/api/aws-accounts', [setupAuthContext, prepareContext, ensureActive, awsAccountsController]],
+ ['/api/costs', [setupAuthContext, prepareContext, ensureActive, costsController]],
+ ['/api/indexes', [setupAuthContext, prepareContext, ensureActive, indexesController]],
+ ['/api/projects', [setupAuthContext, prepareContext, ensureActive, projectController]],
+ ['/api/template', [setupAuthContext, prepareContext, ensureActive, templateController]],
+ ['/api/compute', [setupAuthContext, prepareContext, ensureActive, computeController]],
+ ['/api/ip', [setupAuthContext, prepareContext, ensureActive, ipController]],
+
+ // PROTECTED APIS accessible only to logged in active, admin users
+ ['/api/accounts', [setupAuthContext, prepareContext, ensureActive, ensureAdmin, accountsController]],
+ ]);
+ return routes;
+}
+
+/**
+ * A private utility function to append a middleware function for an existing route in the specified routesMap.
+ *
+ * For example, if the specified route has existing middlewares as [middleware1, middleware2, controller] and if
+ * middleware to append is "middleware3" then this function will modify the specified route as
+ * [middleware1, middleware2, middleware3, controller]
+ *
+ * @param routesMap
+ * @param route
+ * @param middleware
+ * @returns {*}
+ */
+function appendMiddleware(routesMap, route, middleware) {
+ // the existingMiddlewares is expected to be an array in [middleware1, middleware2, controler] form
+ const existingMiddlewares = routesMap.get(route);
+
+ if (!existingMiddlewares) {
+ throw new Error(`Cannot append a middleware no route found for path "${route}"`);
+ }
+ if (_.isEmpty(existingMiddlewares)) {
+ throw new Error(
+ `Cannot append a middleware. The route "${route}" needs to contain at least one controller function`,
+ );
+ }
+ const updatedMiddlewares = [
+ ...existingMiddlewares.slice(0, existingMiddlewares.length - 1),
+ middleware,
+ existingMiddlewares[existingMiddlewares.length - 1],
+ ];
+ routesMap.set(route, updatedMiddlewares);
+ return routesMap;
+}
+
+const plugin = {
+ getRoutes,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/lib/plugins/services-plugin.js b/addons/addon-base-raas/packages/base-raas-rest-api/lib/plugins/services-plugin.js
new file mode 100644
index 0000000000..99316cb261
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/lib/plugins/services-plugin.js
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+
+const PluginRegistryService = require('@aws-ee/base-services/lib/plugin-registry/plugin-registry-service');
+const UserAuthzService = require('@aws-ee/base-raas-services/lib/user/user-authz-service');
+const UserService = require('@aws-ee/base-raas-services/lib/user/user-service');
+const UserAttributesMapperService = require('@aws-ee/base-raas-services/lib/user/user-attributes-mapper-service');
+const StudyService = require('@aws-ee/base-raas-services/lib/study/study-service');
+const StudyPermissionService = require('@aws-ee/base-raas-services/lib/study/study-permission-service');
+const EnvironmentService = require('@aws-ee/base-raas-services/lib/environment/environment-service');
+const EnvironmentKeypairService = require('@aws-ee/base-raas-services/lib/environment/environment-keypair-service');
+const EnvironmentAmiService = require('@aws-ee/base-raas-services/lib/environment/environment-ami-service');
+const EnvironmentNotebookUrlService = require('@aws-ee/base-raas-services/lib/environment/environment-notebook-url-service');
+const EnvironmentSpotPriceHistoryService = require('@aws-ee/base-raas-services/lib/environment/environment-spot-price-history-service');
+const UserRolesService = require('@aws-ee/base-raas-services/lib/user-roles/user-roles-service');
+const AwsAccountsService = require('@aws-ee/base-raas-services/lib/aws-accounts/aws-accounts-service');
+const CostsService = require('@aws-ee/base-raas-services/lib/costs/costs-service');
+const CostApiCacheService = require('@aws-ee/base-raas-services/lib/cost-api-cache/cost-api-cache-service');
+const IndexesService = require('@aws-ee/base-raas-services/lib/indexes/indexes-service');
+const ProjectService = require('@aws-ee/base-raas-services/lib/project/project-service');
+const AccountService = require('@aws-ee/base-raas-services/lib/account/account-service');
+const CfnTemplateService = require('@aws-ee/base-raas-services/lib/cfn-templates/cfn-template-service');
+const ExternalCfnTemplateService = require('@aws-ee/base-raas-services/lib/external-cfn-template/external-cfn-template-service');
+const ComputePlatformService = require('@aws-ee/base-raas-services/lib/compute/compute-platform-service');
+const ComputePriceService = require('@aws-ee/base-raas-services/lib/compute/compute-price-service');
+const EnvironmentAuthzService = require('@aws-ee/base-raas-services/lib/environment/environment-authz-service');
+const EnvironmentMountService = require('@aws-ee/base-raas-services/lib/environment/environment-mount-service');
+
+const settingKeys = {
+ tablePrefix: 'dbTablePrefix',
+};
+
+/**
+ * Registers the services needed by the workflow loop runner lambda function
+ * @param container An instance of ServicesContainer to register services to
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerServices(container, pluginRegistry) {
+ container.register('userService', new UserService());
+ // The base authn provider uses username by concatenating username with auth provider name and idp name
+ // In RaaS, the email address should be used as username so register custom UserAttributesMapperService that maps
+ // attribs from decoded token to user
+ container.register('userAttributesMapperService', new UserAttributesMapperService());
+ container.register('userRolesService', new UserRolesService());
+ container.register('studyService', new StudyService());
+ container.register('studyPermissionService', new StudyPermissionService());
+ container.register('environmentService', new EnvironmentService());
+ container.register('environmentKeypairService', new EnvironmentKeypairService());
+ container.register('environmentAmiService', new EnvironmentAmiService());
+ container.register('environmentNotebookUrlService', new EnvironmentNotebookUrlService());
+ container.register('environmentSpotPriceHistoryService', new EnvironmentSpotPriceHistoryService());
+ container.register('environmentMountService', new EnvironmentMountService());
+ container.register('cfnTemplateService', new CfnTemplateService());
+ container.register('awsAccountsService', new AwsAccountsService());
+ container.register('costsService', new CostsService());
+ container.register('costApiCacheService', new CostApiCacheService());
+ container.register('indexesService', new IndexesService());
+ container.register('projectService', new ProjectService());
+ container.register('accountService', new AccountService());
+ container.register('externalCfnTemplateService', new ExternalCfnTemplateService());
+ container.register('computePlatformService', new ComputePlatformService());
+ container.register('computePriceService', new ComputePriceService());
+ container.register('pluginRegistryService', new PluginRegistryService(pluginRegistry), { lazy: false });
+
+ // Authorization Services from raas addon
+ container.register('raasUserAuthzService', new UserAuthzService());
+ container.register('environmentAuthzService', new EnvironmentAuthzService());
+}
+
+/**
+ * Registers static settings required by the workflow loop runner lambda function
+ * @param existingStaticSettings An existing static settings plain javascript object containing settings as key/value contributed by other plugins
+ * @param settings Default instance of settings service that resolves settings from environment variables
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point
+ *
+ * @returns {Promise<*>} A promise that resolves to static settings object
+ */
+// eslint-disable-next-line no-unused-vars
+function getStaticSettings(existingStaticSettings, settings, pluginRegistry) {
+ const staticSettings = {
+ ...existingStaticSettings,
+ };
+
+ // Register all dynamodb table names used by the base rest api addon
+ const tablePrefix = settings.get(settingKeys.tablePrefix);
+ const table = (key, suffix) => {
+ staticSettings[key] = `${tablePrefix}-${suffix}`;
+ };
+ table('dbTableStudies', 'DbStudies');
+ table('dbTableEnvironments', 'DbEnvironments');
+ table('dbTableUserRoles', 'DbUserRoles');
+ table('dbTableAwsAccounts', 'DbAwsAccounts');
+ table('dbTableIndexes', 'DbIndexes');
+ table('dbTableCostApiCaches', 'DbCostApiCaches');
+ table('dbTableAccounts', 'DbAccounts');
+ table('dbTableProjects', 'DbProjects');
+ table('dbTableStudyPermissions', 'DbStudyPermissions');
+
+ return staticSettings;
+}
+
+const plugin = {
+ getStaticSettings,
+ // getLoggingContext, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerSettingsService, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerLoggerService, // not implemented, the default behavior provided by addon-base is sufficient
+ registerServices,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-raas/packages/base-raas-rest-api/package.json b/addons/addon-base-raas/packages/base-raas-rest-api/package.json
new file mode 100644
index 0000000000..c48d6bcc06
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-rest-api/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@aws-ee/base-raas-rest-api",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A library containing a set of base RaaS related controllers",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-controllers": "workspace:*",
+ "@aws-ee/base-raas-services": "workspace:*",
+ "@aws-ee/base-services": "workspace:*",
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; yarn run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/.eslintrc.json b/addons/addon-base-raas/packages/base-raas-services/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/.gitignore b/addons/addon-base-raas/packages/base-raas-services/.gitignore
new file mode 100644
index 0000000000..05bd7c6845
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/.gitignore
@@ -0,0 +1,20 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+**/.webpack
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+/coverage/
+.build
diff --git a/addons/addon-base-raas/packages/base-raas-services/.prettierrc.json b/addons/addon-base-raas/packages/base-raas-services/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/jest.config.js b/addons/addon-base-raas/packages/base-raas-services/jest.config.js
new file mode 100644
index 0000000000..60c221fc1d
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/jest.config.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-raas/packages/base-raas-services/jsconfig.json b/addons/addon-base-raas/packages/base-raas-services/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/account/account-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/account/account-service.js
new file mode 100644
index 0000000000..97ece4fb39
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/account/account-service.js
@@ -0,0 +1,303 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+const { allowIfActive, allowIfAdmin } = require('@aws-ee/base-services/lib/authorization/authorization-utils');
+
+const { isExternalGuest, isExternalResearcher, isInternalGuest } = require('../helpers/is-role');
+const createSchema = require('../schema/create-account');
+const updateSchema = require('../schema/update-account');
+
+const settingKeys = {
+ tableName: 'dbTableAccounts',
+ apiHandlerArn: 'apiHandlerArn',
+ workflowRoleArn: 'workflowRoleArn',
+};
+
+class AccountService extends Service {
+ constructor() {
+ super();
+ this.dependency([
+ 'jsonSchemaValidationService',
+ 'dbService',
+ 'aws',
+ 'authorizationService',
+ 'workflowTriggerService',
+ 'auditWriterService',
+ ]);
+ }
+
+ async init() {
+ await super.init();
+ const [dbService] = await this.service(['dbService']);
+ const table = this.settings.get(settingKeys.tableName);
+
+ this._getter = () => dbService.helper.getter().table(table);
+ this._updater = () => dbService.helper.updater().table(table);
+ this._query = () => dbService.helper.query().table(table);
+ this._deleter = () => dbService.helper.deleter().table(table);
+ this._scanner = () => dbService.helper.scanner().table(table);
+ }
+
+ async find(requestContext, { id, fields = [] }) {
+ // ensure that the caller has permissions to read this account information
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(requestContext, { action: 'read', conditions: [allowIfActive, allowIfAdmin] }, { id });
+
+ const result = await this._getter()
+ .key({ id })
+ .projection(fields)
+ .get();
+
+ return this._fromDbToDataObject(result);
+ }
+
+ async mustFind(requestContext, { id, fields = [] }) {
+ const result = await this.find(requestContext, { id, fields });
+ if (!result) throw this.boom.notFound(`Accounts with id "${id}" does not exist`, true);
+ return result;
+ }
+
+ async provisionAccount(requestContext, rawData) {
+ // ensure that the caller has permissions to provision the account
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'provision', conditions: [allowIfActive, allowIfAdmin] },
+ rawData,
+ );
+
+ // TODO: prepare all params and pass them down to step and create ready account there
+ const [validationService, workflowTriggerService] = await this.service([
+ 'jsonSchemaValidationService',
+ 'workflowTriggerService',
+ ]);
+
+ // Validate input
+ await validationService.ensureValid(rawData, createSchema);
+
+ const { accountName, accountEmail, masterRoleArn, externalId, description } = rawData;
+
+ // Check launch pre-requisites
+ if (!(accountName && accountEmail && masterRoleArn && externalId && description)) {
+ const cause = this.getConfigError(accountName, accountEmail, masterRoleArn, externalId, description);
+ throw this.boom.badRequest(
+ `Creating AWS account process has not been correctly configured: missing ${cause}.`,
+ true,
+ );
+ }
+ const workflowRoleArn = this.settings.get(settingKeys.workflowRoleArn);
+ const apiHandlerArn = this.settings.get(settingKeys.apiHandlerArn);
+
+ // trigger the provision environment workflow
+ // TODO: remove CIDR default once its in the gui and backend
+ const input = {
+ requestContext,
+ accountName,
+ accountEmail,
+ masterRoleArn,
+ externalId,
+ description,
+ workflowRoleArn,
+ apiHandlerArn,
+ };
+ await workflowTriggerService.triggerWorkflow(requestContext, { workflowId: 'wf-provision-account' }, input);
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'provision-account', body: { accountName, accountEmail, description } });
+ }
+
+ async saveAccountToDb(requestContext, rawData, id, status = 'PENDING') {
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ // Prepare the db object
+ const date = new Date().toISOString();
+ const dbObject = this._fromRawToDbObject(rawData, {
+ status,
+ rev: 0,
+ createdBy: by,
+ updatedBy: by,
+ createdAt: date,
+ updatedAt: date,
+ });
+ // Time to save the the db object
+ let dbResult;
+ try {
+ dbResult = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_not_exists(id)') // yes we need this
+ .key({ id })
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`account with id "${id}" already exists`, true);
+ },
+ );
+ } catch (error) {
+ this.log.log(error);
+ }
+ return dbResult;
+ }
+
+ async update(requestContext, rawData) {
+ // ensure that the caller has permissions to update the account
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'update', conditions: [allowIfActive, allowIfAdmin] },
+ rawData,
+ );
+
+ // Validate the input
+ const jsonSchemaValidationService = await this.service('jsonSchemaValidationService');
+ await jsonSchemaValidationService.ensureValid(rawData, updateSchema);
+
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+
+ // Prepare the db object
+ const existingAccount = await this.mustFind(requestContext, { id: rawData.id });
+ const mergedCfnInfo = _.assign({}, existingAccount.cfnInfo, rawData.cfnInfo);
+ const updatedAccount = _.omit(_.assign({}, existingAccount, rawData, { cfnInfo: mergedCfnInfo }), ['id']);
+ const dbObject = this._fromRawToDbObject(updatedAccount, {
+ updatedBy: by,
+ updatedAt: new Date().toISOString(),
+ });
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id: rawData.id })
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ throw this.boom.notFound(`environment with id "${rawData.id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'update-account', body: rawData });
+
+ return result;
+ }
+
+ async delete(requestContext, { id }) {
+ // ensure that the caller has permissions to delete the account
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'delete', conditions: [allowIfActive, allowIfAdmin] },
+ { id },
+ );
+
+ // Lets now remove the item from the database
+ const result = await runAndCatch(
+ async () => {
+ return this._deleter()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .delete();
+ },
+ async () => {
+ throw this.boom.notFound(`awsAccounts with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'delete-account', body: { id } });
+
+ return result;
+ }
+
+ getConfigError(cfnExecutionRole, roleExternalId, accountId, vpcId, subnetId) {
+ const causes = [];
+
+ if (!cfnExecutionRole) causes.push('IAM role');
+ if (!roleExternalId) causes.push('External ID');
+ if (!accountId) causes.push('AWS account ID');
+ if (!vpcId) causes.push('VPC ID');
+ if (!subnetId) causes.push('VPC Subnet ID');
+
+ if (causes.length > 1) {
+ const last = causes.pop();
+ return `${causes.join(', ')} and ${last}`;
+ }
+ if (causes.length > 0) {
+ return causes[0];
+ }
+ return undefined;
+ }
+
+ async list(requestContext, { fields = [] } = {}) {
+ const restrict =
+ isExternalGuest(requestContext) || isExternalResearcher(requestContext) || isInternalGuest(requestContext);
+
+ if (restrict) return [];
+
+ // Future task: add further checks
+
+ // Remember doing a scan is not a good idea if you billions of rows
+ return this._scanner()
+ .limit(1000)
+ .projection(fields)
+ .scan();
+ }
+
+ // Do some properties renaming to prepare the object to be saved in the database
+ _fromRawToDbObject(rawObject, overridingProps = {}) {
+ const dbObject = { ...rawObject, ...overridingProps };
+ return dbObject;
+ }
+
+ // Do some properties renaming to restore the object that was saved in the database
+ _fromDbToDataObject(rawDb, overridingProps = {}) {
+ if (_.isNil(rawDb)) return rawDb; // important, leave this if statement here, otherwise, your update methods won't work correctly
+ if (!_.isObject(rawDb)) return rawDb;
+
+ const dataObject = { ...rawDb, ...overridingProps };
+ return dataObject;
+ }
+
+ async assertAuthorized(requestContext, { action, conditions }, ...args) {
+ const authorizationService = await this.service('authorizationService');
+
+ // The "authorizationService.assertAuthorized" below will evaluate permissions by calling the "conditions" functions first
+ // It will then give a chance to all registered plugins (if any) to perform their authorization.
+ // The plugins can even override the authorization decision returned by the conditions
+ // See "authorizationService.authorize" method for more details
+ await authorizationService.assertAuthorized(
+ requestContext,
+ { extensionPoint: 'account-authz', action, conditions },
+ ...args,
+ );
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+module.exports = AccountService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/aws-accounts/aws-accounts-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/aws-accounts/aws-accounts-service.js
new file mode 100644
index 0000000000..c42e164126
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/aws-accounts/aws-accounts-service.js
@@ -0,0 +1,367 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const uuid = require('uuid/v1');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+const { allowIfActive, allowIfAdmin } = require('@aws-ee/base-services/lib/authorization/authorization-utils');
+
+const { isExternalGuest, isExternalResearcher, isInternalGuest } = require('../helpers/is-role');
+const createSchema = require('../schema/create-aws-accounts');
+const ensureExternalSchema = require('../schema/ensure-external-aws-accounts');
+const updateSchema = require('../schema/update-aws-accounts');
+
+const settingKeys = {
+ tableName: 'dbTableAwsAccounts',
+ environmentInstanceFiles: 'environmentInstanceFiles',
+};
+
+class AwsAccountsService extends Service {
+ constructor() {
+ super();
+ this.dependency([
+ 'jsonSchemaValidationService',
+ 'authorizationService',
+ 'dbService',
+ 'lockService',
+ 's3Service',
+ 'auditWriterService',
+ ]);
+ }
+
+ async init() {
+ await super.init();
+ const [dbService] = await this.service(['dbService']);
+ const table = this.settings.get(settingKeys.tableName);
+
+ this._getter = () => dbService.helper.getter().table(table);
+ this._updater = () => dbService.helper.updater().table(table);
+ this._query = () => dbService.helper.query().table(table);
+ this._deleter = () => dbService.helper.deleter().table(table);
+ this._scanner = () => dbService.helper.scanner().table(table);
+ }
+
+ async find(requestContext, { id, fields = [] }) {
+ const restrict =
+ isExternalGuest(requestContext) || isExternalResearcher(requestContext) || isInternalGuest(requestContext);
+
+ if (restrict) return undefined;
+
+ // Future task: add further checks
+
+ const result = await this._getter()
+ .key({ id })
+ .projection(fields)
+ .get();
+
+ return this._fromDbToDataObject(result);
+ }
+
+ async mustFind(requestContext, { id, fields = [] }) {
+ const result = await this.find(requestContext, { id, fields });
+ if (!result) throw this.boom.notFound(`awsAccounts with id "${id}" does not exist`, true);
+ return result;
+ }
+
+ async updateEnvironmentInstanceFilesBucketPolicy() {
+ const [s3Service, lockService] = await this.service(['s3Service', 'lockService']);
+ const environmentInstanceUri = this.settings.get(settingKeys.environmentInstanceFiles);
+ const { s3BucketName, s3Key: s3Prefix } = s3Service.parseS3Details(environmentInstanceUri);
+
+ const accountList = await this.list({ fields: ['accountId'] });
+
+ const accountArns = accountList.map(({ accountId }) => `arn:aws:iam::${accountId}:root`);
+
+ // Update S3 bucket policy
+ const s3Client = s3Service.api;
+ const s3LockKey = `s3|bucket-policy|${s3BucketName}`;
+ await lockService.tryWriteLockAndRun({ id: s3LockKey }, async () => {
+ const listSid = `List:${s3Prefix}`;
+ const getSid = `Get:${s3Prefix}`;
+
+ const securityStatements = [
+ {
+ Sid: 'Deny requests that do not use TLS',
+ Effect: 'Deny',
+ Principal: '*',
+ Action: 's3:*',
+ Resource: `arn:aws:s3:::${s3BucketName}/*`,
+ Condition: { Bool: { 'aws:SecureTransport': false } },
+ },
+ {
+ Sid: 'Deny requests that do not use SigV4',
+ Effect: 'Deny',
+ Principal: '*',
+ Action: 's3:*',
+ Resource: `arn:aws:s3:::${s3BucketName}/*`,
+ Condition: {
+ StringNotEquals: {
+ 's3:signatureversion': 'AWS4-HMAC-SHA256',
+ },
+ },
+ },
+ ];
+ const listStatement = {
+ Sid: listSid,
+ Effect: 'Allow',
+ Principal: { AWS: accountArns },
+ Action: 's3:ListBucket',
+ Resource: `arn:aws:s3:::${s3BucketName}`,
+ Condition: {
+ StringLike: {
+ 's3:prefix': [`${s3Prefix}*`],
+ },
+ },
+ };
+ const getStatement = {
+ Sid: getSid,
+ Effect: 'Allow',
+ Principal: { AWS: accountArns },
+ Action: ['s3:GetObject'],
+ Resource: [`arn:aws:s3:::${s3BucketName}/${s3Prefix}*`],
+ };
+
+ const Policy = JSON.stringify({
+ Version: '2012-10-17',
+ Statement: [...securityStatements, listStatement, getStatement],
+ });
+
+ return s3Client.putBucketPolicy({ Bucket: s3BucketName, Policy }).promise();
+ });
+ }
+
+ async create(requestContext, rawData) {
+ // ensure that the caller has permissions to create the account
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'create', conditions: [allowIfActive, allowIfAdmin] },
+ rawData,
+ );
+
+ // Validate input
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+ await validationService.ensureValid(rawData, createSchema);
+
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const id = uuid();
+
+ // Prepare the db object
+ const dbObject = this._fromRawToDbObject(rawData, { rev: 0, createdBy: by, updatedBy: by });
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_not_exists(id)') // yes we need this
+ .key({ id })
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`awsAccounts with id "${id}" already exists`, true);
+ },
+ );
+
+ await this.updateEnvironmentInstanceFilesBucketPolicy();
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-aws-account', body: result });
+
+ return result;
+ }
+
+ async ensureExternalAccount(requestContext, rawData) {
+ // TODO: ensure that the caller is an external user
+ // await this.assertAuthorized(requestContext, 'create', rawData);
+
+ // Validate input
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+ await validationService.ensureValid(rawData, ensureExternalSchema);
+
+ // TODO: setup a GSI and query that for the accountId
+ const accounts = await this.list({ fields: ['accountId'] });
+ const account = accounts.find(({ accountId }) => accountId === rawData.accountId);
+ // If the account has already been added don't add again
+ if (account) {
+ return account;
+ }
+
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const id = uuid();
+
+ rawData.description = `External account for user ${by.username}`;
+
+ // Prepare the db object
+ const dbObject = this._fromRawToDbObject(rawData, { rev: 0, createdBy: by, updatedBy: by });
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_not_exists(id)') // yes we need this
+ .key({ id })
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`awsAccounts with id "${id}" already exists`, true);
+ },
+ );
+
+ await this.updateEnvironmentInstanceFilesBucketPolicy();
+ return result;
+ }
+
+ async update(requestContext, rawData) {
+ // ensure that the caller has permissions to update the account
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'update', conditions: [allowIfActive, allowIfAdmin] },
+ rawData,
+ );
+
+ // Validate input
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+ await validationService.ensureValid(rawData, updateSchema);
+
+ // For now, we assume that 'updatedBy' is always a user and not a group
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const { id, rev } = rawData;
+
+ // Prepare the db object
+ const dbObject = _.omit(this._fromRawToDbObject(rawData, { updatedBy: by }), ['rev']);
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .rev(rev)
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ // There are two scenarios here:
+ // 1 - The awsaccounts does not exist
+ // 2 - The "rev" does not match
+ // We will display the appropriate error message accordingly
+ const existing = await this.find(requestContext, { id, fields: ['id', 'updatedBy'] });
+ if (existing) {
+ throw this.boom.badRequest(
+ `awsAccounts information changed by "${
+ (existing.updatedBy || {}).username
+ }" just before your request is processed, please try again`,
+ true,
+ );
+ }
+ throw this.boom.notFound(`awsAccounts with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'update-aws-account', body: result });
+
+ return result;
+ }
+
+ async delete(requestContext, { id }) {
+ // ensure that the caller has permissions to delete the account
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'delete', conditions: [allowIfActive, allowIfAdmin] },
+ { id },
+ );
+
+ // Lets now remove the item from the database
+ const result = await runAndCatch(
+ async () => {
+ return this._deleter()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .delete();
+ },
+ async () => {
+ throw this.boom.notFound(`awsAccounts with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'delete-aws-account', body: { id } });
+
+ return result;
+ }
+
+ async list(requestContext, { fields = [] } = {}) {
+ const restrict =
+ isExternalGuest(requestContext) || isExternalResearcher(requestContext) || isInternalGuest(requestContext);
+
+ if (restrict) return [];
+
+ // Future task: add further checks
+
+ // Remember doing a scan is not a good idea if you billions of rows
+ return this._scanner()
+ .limit(1000)
+ .projection(fields)
+ .scan();
+ }
+
+ // Do some properties renaming to prepare the object to be saved in the database
+ _fromRawToDbObject(rawObject, overridingProps = {}) {
+ const dbObject = { ...rawObject, ...overridingProps };
+ return dbObject;
+ }
+
+ // Do some properties renaming to restore the object that was saved in the database
+ _fromDbToDataObject(rawDb, overridingProps = {}) {
+ if (_.isNil(rawDb)) return rawDb; // important, leave this if statement here, otherwise, your update methods won't work correctly
+ if (!_.isObject(rawDb)) return rawDb;
+
+ const dataObject = { ...rawDb, ...overridingProps };
+ return dataObject;
+ }
+
+ async assertAuthorized(requestContext, { action, conditions }, ...args) {
+ const authorizationService = await this.service('authorizationService');
+
+ // The "authorizationService.assertAuthorized" below will evaluate permissions by calling the "conditions" functions first
+ // It will then give a chance to all registered plugins (if any) to perform their authorization.
+ // The plugins can even override the authorization decision returned by the conditions
+ // See "authorizationService.authorize" method for more details
+ await authorizationService.assertAuthorized(
+ requestContext,
+ { extensionPoint: 'aws-account-authz', action, conditions },
+ ...args,
+ );
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+module.exports = AwsAccountsService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/cfn-templates/cfn-template-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/cfn-templates/cfn-template-service.js
new file mode 100644
index 0000000000..7c050a45a4
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/cfn-templates/cfn-template-service.js
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+class CfnTemplateService extends Service {
+ constructor() {
+ super();
+ this.dependency(['pluginRegistryService']);
+ }
+
+ async init() {
+ await super.init();
+ this.store = []; // an array of objects of this shape: { key: , value: { yaml } }
+ const registry = await this.service('pluginRegistryService');
+ // We loop through each plugin and ask it to register its cfn templates
+ const plugins = await registry.getPlugins('cfn-templates');
+ // eslint-disable-next-line no-restricted-syntax
+ for (const plugin of plugins) {
+ // eslint-disable-next-line no-await-in-loop
+ await plugin.registerCfnTemplates(this);
+ }
+ }
+
+ async add({ name, yaml }) {
+ const existing = await this.getTemplate(name);
+ if (existing)
+ throw this.boom.badRequest(
+ `You tried to register a cfn template, but a cfn template with the same name "${name}" already exists`,
+ true,
+ );
+
+ this.store.push({ key: name, value: yaml });
+ }
+
+ async getTemplate(name) {
+ const entry = _.find(this.store, ['key', name]);
+ return entry ? entry.value : undefined;
+ }
+}
+
+module.exports = CfnTemplateService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/compute/compute-platform-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/compute/compute-platform-service.js
new file mode 100644
index 0000000000..bf78018c94
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/compute/compute-platform-service.js
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { processInBatches } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const { getPlatforms } = require('./data/compute-platforms');
+const { getConfigurations } = require('./data/compute-configurations');
+
+/**
+ * The purpose of this service is to answer questions like:
+ * - What are the available compute platforms given a user?
+ * - What are the available compute configurations for a given compute platform given a user?
+ * - For certain compute configurations, pricing is also computed on the fly rather than hard coded.
+ */
+class ComputePlatformService extends Service {
+ constructor() {
+ super();
+ this.dependency(['computePriceService']);
+ }
+
+ // This method expects requestContext.principal object to be fully populated
+ // eslint-disable-next-line no-unused-vars
+ async listPlatforms(requestContext) {
+ return getPlatforms(requestContext.principal);
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ async listConfigurations(requestContext, { platformId = '', includePrice = false } = {}) {
+ const [priceService] = await this.service(['computePriceService']);
+ const user = requestContext.principal;
+
+ // Check if the user can view the platformId before returning the configurations
+ const allowedPlatforms = getPlatforms(user) || [];
+ if (!_.some(allowedPlatforms, ['id', platformId])) return [];
+
+ const configurations = getConfigurations(platformId, user);
+ const doWork = async configuration => {
+ const priceInfo = await priceService.calculatePriceInfo(configuration);
+ configuration.priceInfo = priceInfo;
+ };
+
+ if (includePrice) {
+ await processInBatches(configurations, 10, doWork);
+ }
+
+ return configurations;
+ }
+}
+
+module.exports = ComputePlatformService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/compute/compute-price-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/compute/compute-price-service.js
new file mode 100644
index 0000000000..90d6149b49
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/compute/compute-price-service.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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const settingKeys = {
+ awsRegion: 'awsRegion',
+};
+
+// WARNING WARNING WARNING WARNING
+// This service has very limited functionality at the moment.
+// Most of the prices are hard coded in the configuration and not calculated by
+// this service. This service needs to be modified to allow for extension points, etc.
+// At this time (05/12/20), this service only calculates the average of the spot prices
+// for ec2 instances used in EMR.
+class ComputePriceService extends Service {
+ constructor() {
+ super();
+ this.dependency(['aws']);
+ }
+
+ // Looks at the existing computeConfiguration.princeInfo object and returns a new one
+ // with calculated prices (when applicable). IMPORTANT: please read the class top comments.
+ async calculatePriceInfo(computeConfiguration) {
+ // We don't check for any permissions here. This service is considered lower level service,
+ // and most of the time it is not directly interacting with controllers.
+ //
+ // Future enhancement should be to add extension points
+
+ const region = this.settings.get(settingKeys.awsRegion);
+ const configType = computeConfiguration.type;
+ const originalPrinceInfo = computeConfiguration.priceInfo || {};
+ let priceInfo;
+
+ // This switch logic should be eventually turned into an extension point
+ switch (configType) {
+ case 'emr':
+ priceInfo = await this.computeEmrPrice(computeConfiguration);
+ break;
+ default:
+ priceInfo = { ...originalPrinceInfo, region };
+ }
+
+ return priceInfo;
+ }
+
+ // LIMITATION: this method calls ec2.describeSpotPriceHistory API which is throttle (100 requests per second)
+ // https://docs.aws.amazon.com/AWSEC2/latest/APIReference/throttling.html
+ async computeEmrPrice(configuration) {
+ // example of priceInfo => { value: 0.504, unit: 'USD', timeUnit: 'hour', type: 'onDemand', region: 'us-east-1', spotBidPrice: (if type is 'spot') },
+ const originalPrinceInfo = configuration.priceInfo || {};
+ if (originalPrinceInfo.timeUnit !== 'hour')
+ return this.boom.badRequest('Pricing with a time unit other than "hour" is not supported', true);
+
+ const region = this.settings.get(settingKeys.awsRegion);
+ const priceType = originalPrinceInfo.type; // can be onDemand or spot
+ const getParam = name => {
+ // First we see if the paramter is considered immutable, if so, we return its immutable value
+ // otherwise we return the mutable value if it exists
+ const immutable = _.get(configuration, ['params', 'immutable', name]);
+ if (!_.isUndefined(immutable)) return immutable;
+ return _.get(configuration, ['params', 'mutable', name]);
+ };
+ const emr = getParam('emr') || {};
+ const ec2Type = emr.workerInstanceSize;
+ const ec2Count = emr.workerInstanceCount;
+ const ec2OnDemandPrice = emr.workerInstanceOnDemandPrice;
+ const ec2MasterOnDemandPrice = emr.masterInstanceOnDemandPrice;
+ const spotBidMultiplier = getParam('spotBidMultiplier');
+
+ if (priceType === 'onDemand') {
+ return { ...originalPrinceInfo, region, value: ec2MasterOnDemandPrice + ec2Count * ec2OnDemandPrice };
+ }
+
+ if (priceType !== 'spot') return originalPrinceInfo;
+
+ const priceHistory = await this.getSpotPriceHistory(ec2Type, region);
+ let bidPrice = ec2OnDemandPrice;
+ if (!_.isEmpty(priceHistory)) {
+ const average = _.sum(_.map(priceHistory, item => item.spotPrice)) / priceHistory.length;
+ bidPrice = average * spotBidMultiplier;
+ }
+
+ return {
+ ...originalPrinceInfo,
+ region,
+ value: ec2MasterOnDemandPrice + ec2Count * bidPrice,
+ spotBidPrice: bidPrice,
+ spotBidMultiplier,
+ };
+ }
+
+ async getSpotPriceHistory(ec2Type, region = 'us-east-1') {
+ const aws = await this.service('aws');
+
+ const ec2 = new aws.sdk.EC2({
+ region,
+ });
+
+ const { SpotPriceHistory } = await ec2
+ .describeSpotPriceHistory({
+ InstanceTypes: [ec2Type],
+ ProductDescriptions: ['Linux/UNIX'],
+ StartTime: new Date(),
+ })
+ .promise();
+
+ return SpotPriceHistory.map(({ AvailabilityZone, SpotPrice }) => ({
+ availabilityZone: AvailabilityZone,
+ spotPrice: parseFloat(SpotPrice),
+ }));
+ }
+}
+
+module.exports = ComputePriceService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/compute-configurations.js b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/compute-configurations.js
new file mode 100644
index 0000000000..637de63868
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/compute-configurations.js
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+// A temporarily place to keep the information about the compute platforms and their configurations
+const sagemaker = require('./sagemaker/configurations');
+const emr = require('./emr/configurations');
+const ec2 = require('./ec2/configurations');
+
+const getConfigurations = (platformId, user = {}) => {
+ return [
+ ...sagemaker.getConfigurations(platformId, user),
+ ...emr.getConfigurations(platformId, user),
+ ...ec2.getConfigurations(platformId, user),
+ ];
+};
+
+module.exports = {
+ getConfigurations,
+};
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/compute-platforms.js b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/compute-platforms.js
new file mode 100644
index 0000000000..6a28edc59d
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/compute-platforms.js
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+// A temporarily place to keep the information about the compute platforms and their configurations
+const sagemaker = require('./sagemaker/platforms');
+const emr = require('./emr/platforms');
+const ec2 = require('./ec2/platforms');
+
+const getPlatforms = (user = {}) => {
+ return [...sagemaker.getPlatforms(user), ...emr.getPlatforms(user), ...ec2.getPlatforms(user)];
+};
+
+module.exports = {
+ getPlatforms,
+};
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/ec2/configurations.js b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/ec2/configurations.js
new file mode 100644
index 0000000000..ec23b83814
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/ec2/configurations.js
@@ -0,0 +1,192 @@
+/*
+ * 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.
+ */
+
+// A temporarily place to keep the information about the compute configurations
+const _ = require('lodash');
+
+const configurations = [
+ {
+ id: 'ec2-linux_small',
+ type: 'ec2-linux',
+ title: 'Small',
+ displayOrder: 1,
+ priceInfo: { value: 0.504, unit: 'USD', timeUnit: 'hour', type: 'onDemand' },
+ desc:
+ 'A small environment is meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '8',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '64',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'r5.2xlarge',
+ },
+ mutable: {
+ cidr: '',
+ },
+ },
+ },
+ {
+ id: 'ec2-linux_medium',
+ type: 'ec2-linux',
+ title: 'Medium',
+ displayOrder: 2,
+ priceInfo: { value: 2.016, unit: 'USD', timeUnit: 'hour', type: 'onDemand' },
+ desc: 'A medium environment is meant for average sized problems.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '32',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '256',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'r5.8xlarge',
+ },
+ mutable: {
+ cidr: '',
+ },
+ },
+ },
+ {
+ id: 'ec2-linux_large',
+ type: 'ec2-linux',
+ title: 'Large',
+ displayOrder: 3,
+ priceInfo: { value: 4.032, unit: 'USD', timeUnit: 'hour', type: 'onDemand' },
+ desc: 'A large environment is meant for the largest of problems. It costs the most amount per hour.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '64',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '512',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'r5.16xlarge',
+ },
+ mutable: {
+ cidr: '',
+ },
+ },
+ },
+ {
+ id: 'ec2-windows_small',
+ type: 'ec2-windows',
+ title: 'Small',
+ displayOrder: 4,
+ priceInfo: { value: 0.872, unit: 'USD', timeUnit: 'hour', type: 'onDemand' },
+ desc:
+ 'A small environment is meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '8',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '64',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'r5.2xlarge',
+ },
+ mutable: {
+ cidr: '',
+ },
+ },
+ },
+ {
+ id: 'ec2-windows_medium',
+ type: 'ec2-windows',
+ title: 'Medium',
+ displayOrder: 5,
+ priceInfo: { value: 3.488, unit: 'USD', timeUnit: 'hour', type: 'onDemand' },
+ desc: 'A medium environment is meant for average sized problems.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '32',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '256',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'r5.8xlarge',
+ },
+ mutable: {
+ cidr: '',
+ },
+ },
+ },
+ {
+ id: 'ec2-windows_large',
+ type: 'ec2-windows',
+ title: 'Large',
+ displayOrder: 1,
+ priceInfo: { value: 6.976, unit: 'USD', timeUnit: 'hour', type: 'onDemand' },
+ desc: 'A large environment is meant for the largest of problems. It costs the most amount per hour.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '64',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '512',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'r5.16xlarge',
+ },
+ mutable: {
+ cidr: '',
+ },
+ },
+ },
+];
+
+const filterByType = platformId => {
+ const map = { 'ec2-linux-1': 'ec2-linux', 'ec2-windows-1': 'ec2-windows' };
+ const type = map[platformId];
+ return _.filter(configurations, ['type', type]);
+};
+
+// Which user can view which configuration
+const getConfigurations = (platformId, user) =>
+ _.get(user, 'userRole') !== 'external-researcher' ? filterByType(platformId) : []; // external researchers can't view ec2 configurations for now
+
+module.exports = {
+ getConfigurations,
+};
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/ec2/platforms.js b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/ec2/platforms.js
new file mode 100644
index 0000000000..6a439672d4
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/ec2/platforms.js
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+// A temporarily place to keep the information about the compute platforms
+const _ = require('lodash');
+
+const platforms = [
+ {
+ id: 'ec2-linux-1',
+ type: 'ec2-linux',
+ title: 'EC2 - Linux',
+ displayOrder: 3,
+ desc: `Secure, resizable compute in the cloud`,
+ },
+ {
+ id: 'ec2-windows-1',
+ type: 'ec2-windows',
+ title: 'EC2 - Windows',
+ displayOrder: 4,
+ desc: `Secure, resizable compute in the cloud`,
+ },
+];
+
+// Which user can view which type
+const getPlatforms = user => (_.get(user, 'userRole') !== 'external-researcher' ? _.slice(platforms) : []); // external researchers can't view ec2 platforms for now
+
+module.exports = {
+ getPlatforms,
+};
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/emr/configurations.js b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/emr/configurations.js
new file mode 100644
index 0000000000..6fa96b372b
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/emr/configurations.js
@@ -0,0 +1,268 @@
+/*
+ * 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.
+ */
+
+// A temporarily place to keep the information about the compute configurations
+const _ = require('lodash');
+
+// IMPORTANT - IMPORTANT - IMPORTANT
+// All spot priceInfo will be calculated by the pricing service
+const configurations = [
+ {
+ id: 'emr_small',
+ type: 'emr',
+ title: 'Small - On Demand',
+ displayOrder: 1,
+ priceInfo: { value: undefined, unit: 'USD', timeUnit: 'hour', type: 'onDemand' },
+ desc:
+ 'A small research workspace meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '4',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '16',
+ },
+ {
+ key: 'Worker nodes',
+ value: '1',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'm5.xlarge',
+ emr: {
+ masterInstanceOnDemandPrice: 0.192,
+ workerInstanceSize: 'm5.xlarge',
+ workerInstanceCount: 1,
+ workerInstanceOnDemandPrice: 0.192,
+ diskSizeGb: 10,
+ },
+ },
+ mutable: {
+ cidr: '',
+ },
+ },
+ },
+
+ {
+ id: 'emr_small_spot',
+ type: 'emr',
+ title: 'Small - Spot',
+ displayOrder: 2,
+ priceInfo: { value: undefined, unit: 'USD', timeUnit: 'hour', type: 'spot' },
+ desc:
+ 'A small research workspace meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour. This research workspace uses spot pricing for worker nodes with a maximum price of 1.3x the spot history price.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '4',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '16',
+ },
+ {
+ key: 'Worker nodes',
+ value: '1',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'm5.xlarge',
+ spotBidMultiplier: 1.3,
+ emr: {
+ masterInstanceOnDemandPrice: 0.192,
+ workerInstanceSize: 'm5.xlarge',
+ workerInstanceCount: 1,
+ workerInstanceOnDemandPrice: 0.192,
+ diskSizeGb: 10,
+ },
+ },
+ mutable: {
+ cidr: '',
+ },
+ },
+ },
+
+ {
+ id: 'emr_medium',
+ type: 'emr',
+ title: 'Medium - On Demand',
+ displayOrder: 3,
+ priceInfo: { value: undefined, unit: 'USD', timeUnit: 'hour', type: 'onDemand' },
+ desc:
+ 'A medium research workspace meant for average sized problems. This research workspace uses spot pricing for worker nodes with a maximum price of the on demand price.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '4',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '16',
+ },
+ {
+ key: 'Worker nodes',
+ value: '8',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'm5.xlarge',
+ emr: {
+ masterInstanceOnDemandPrice: 0.192,
+ workerInstanceSize: 'm5.xlarge',
+ workerInstanceCount: 8,
+ workerInstanceOnDemandPrice: 0.192,
+ diskSizeGb: 10,
+ },
+ },
+ mutable: {
+ cidr: '',
+ },
+ },
+ },
+
+ {
+ id: 'emr_medium_spot',
+ type: 'emr',
+ title: 'Medium - Spot',
+ displayOrder: 4,
+ priceInfo: { value: undefined, unit: 'USD', timeUnit: 'hour', type: 'spot' },
+ desc:
+ 'A medium research workspace meant for average sized problems. This research workspace uses spot pricing for worker nodes with a maximum price of 1.3x the spot history price.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '4',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '16',
+ },
+ {
+ key: 'Worker nodes',
+ value: '8',
+ },
+ ],
+ params: {
+ immutable: {
+ spotBidMultiplier: 1.3,
+ size: 'm5.xlarge',
+ emr: {
+ masterInstanceOnDemandPrice: 0.192,
+ workerInstanceSize: 'm5.xlarge',
+ workerInstanceCount: 8,
+ workerInstanceOnDemandPrice: 0.192,
+ diskSizeGb: 10,
+ },
+ },
+ mutable: {
+ cidr: '',
+ },
+ },
+ },
+
+ {
+ id: 'emr_large',
+ type: 'emr',
+ title: 'Large - On Demand',
+ displayOrder: 5,
+ priceInfo: { value: undefined, unit: 'USD', timeUnit: 'hour', type: 'onDemand' },
+ desc:
+ 'A large research workspace meant for the largest of problems. It costs the most amount per hour. This research workspace uses spot pricing for worker nodes with a maximum price of the on demand price.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '96',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '384',
+ },
+ {
+ key: 'Worker nodes',
+ value: '8',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'm5.24xlarge',
+ emr: {
+ masterInstanceOnDemandPrice: 0.192,
+ workerInstanceSize: 'm5.24xlarge',
+ workerInstanceCount: 8,
+ workerInstanceOnDemandPrice: 4.608,
+ diskSizeGb: 10,
+ },
+ },
+ mutable: {
+ cidr: '',
+ },
+ },
+ },
+
+ {
+ id: 'emr_large_spot',
+ type: 'emr',
+ title: 'Large - Spot',
+ displayOrder: 6,
+ priceInfo: { value: undefined, unit: 'USD', timeUnit: 'hour', type: 'spot' },
+ desc:
+ 'A large research workspace meant for the largest of problems. It costs the most amount per hour. This research workspace uses spot pricing for worker nodes with a maximum price of 1.3x the spot history price.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '96',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '384',
+ },
+ {
+ key: 'Worker nodes',
+ value: '8',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'm5.24xlarge',
+ spotBidMultiplier: 1.3,
+ emr: {
+ masterInstanceOnDemandPrice: 0.192,
+ workerInstanceSize: 'm5.24xlarge',
+ workerInstanceCount: 8,
+ workerInstanceOnDemandPrice: 4.608,
+ diskSizeGb: 10,
+ },
+ },
+ mutable: {
+ cidr: '',
+ },
+ },
+ },
+];
+
+// These configurations belong to which compute platform ids
+const filterByType = platformId => (['emr-1'].includes(platformId) ? _.slice(configurations) : []);
+
+// Which user can view which configuration
+const getConfigurations = (platformId, _user) => filterByType(platformId); // All users can see all configurations
+
+module.exports = {
+ getConfigurations,
+};
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/emr/platforms.js b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/emr/platforms.js
new file mode 100644
index 0000000000..d255849d40
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/emr/platforms.js
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+// A temporarily place to keep the information about the compute platforms
+const _ = require('lodash');
+
+const platforms = [
+ {
+ id: 'emr-1',
+ type: 'emr',
+ title: 'EMR',
+ displayOrder: 2,
+ desc: `An Amazon EMR research workspace that comes with:
+ * Hail 0.2
+ * Jupyter Lab
+ * Spark 2.4.4
+ * Hadoop 2.8.5
+`,
+ },
+];
+
+// Which user can view which type
+const getPlatforms = () => _.slice(platforms); // all users can see all emr platforms
+
+module.exports = {
+ getPlatforms,
+};
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/sagemaker/configurations.js b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/sagemaker/configurations.js
new file mode 100644
index 0000000000..29e047da85
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/sagemaker/configurations.js
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+
+// A temporarily place to keep the information about the compute configurations
+const _ = require('lodash');
+
+const configurations = [
+ {
+ id: 'sagemaker__small',
+ type: 'sagemaker',
+ title: 'Small',
+ displayOrder: 1,
+ priceInfo: { value: 0.0464, unit: 'USD', timeUnit: 'hour', type: 'onDemand' },
+ desc:
+ 'A small research workspace meant for prototyping and proving out scripts before scaling up to a larger. It costs the least amount per hour.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '4',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '16',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'ml.t2.medium',
+ cidr: '0.0.0.0/0',
+ },
+ mutable: {},
+ },
+ },
+ {
+ id: 'sagemaker__medium',
+ type: 'sagemaker',
+ title: 'Medium',
+ displayOrder: 2,
+ price: 1.075,
+ priceInfo: { value: 1.075, unit: 'USD', timeUnit: 'hour', type: 'onDemand' },
+ desc: 'A medium research workspace meant for average sized problems.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '32',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '128',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'ml.m5.4xlarge',
+ cidr: '0.0.0.0/0',
+ },
+ mutable: {},
+ },
+ },
+ {
+ id: 'sagemaker__large',
+ type: 'sagemaker',
+ title: 'Large',
+ displayOrder: 3,
+ priceInfo: { value: 6.451, unit: 'USD', timeUnit: 'hour', type: 'onDemand' },
+ desc: 'A large research workspace meant for the largest of problems. It costs the most amount per hour.',
+ displayProps: [
+ {
+ key: 'vCPU',
+ value: '96',
+ },
+ {
+ key: 'Memory (GiB)',
+ value: '384',
+ },
+ ],
+ params: {
+ immutable: {
+ size: 'ml.m5.24xlarge',
+ cidr: '0.0.0.0/0',
+ },
+ mutable: {},
+ },
+ },
+];
+
+// These configurations belong to which compute platform ids
+const filterByType = platformId => (['sagemaker-1'].includes(platformId) ? _.slice(configurations) : []);
+
+// Which user can view which configuration
+const getConfigurations = platformId => filterByType(platformId); // All users can see all configurations
+
+module.exports = {
+ getConfigurations,
+};
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/sagemaker/platforms.js b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/sagemaker/platforms.js
new file mode 100644
index 0000000000..7bccbf7e57
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/compute/data/sagemaker/platforms.js
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+// A temporarily place to keep the information about the compute platforms
+const _ = require('lodash');
+
+const platforms = [
+ {
+ id: 'sagemaker-1',
+ type: 'sagemaker',
+ title: 'SageMaker',
+ displayOrder: 1,
+ desc: `An Amazon SageMaker Jupyter Notebook that comes with:
+ * TensorFlow
+ * Apache MXNet
+ * Scikit-learn
+`,
+ },
+];
+
+// Which user can view which type
+const getPlatforms = () => _.slice(platforms); // All users can see all platforms
+
+module.exports = {
+ getPlatforms,
+};
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/cost-api-cache/cost-api-cache-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/cost-api-cache/cost-api-cache-service.js
new file mode 100644
index 0000000000..c29addfc1a
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/cost-api-cache/cost-api-cache-service.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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const createSchema = require('../schema/create-cost-api-cache');
+const updateSchema = require('../schema/update-cost-api-cache');
+
+const settingKeys = {
+ tableName: 'dbTableCostApiCaches',
+};
+
+class CostApiCacheService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'dbService']);
+ }
+
+ async init() {
+ await super.init();
+ const [dbService] = await this.service(['dbService']);
+ const table = this.settings.get(settingKeys.tableName);
+
+ this._getter = () => dbService.helper.getter().table(table);
+ this._updater = () => dbService.helper.updater().table(table);
+ this._query = () => dbService.helper.query().table(table);
+ this._deleter = () => dbService.helper.deleter().table(table);
+ this._scanner = () => dbService.helper.scanner().table(table);
+ }
+
+ async find(requestContext, { indexId, query, fields = [] }) {
+ const result = await this._getter()
+ .key({ indexId, query })
+ .projection(fields)
+ .get();
+
+ return this._fromDbToDataObject(result);
+ }
+
+ async mustFind(requestContext, { indexId, query, fields = [] }) {
+ const result = await this.find(requestContext, { indexId, query, fields });
+ if (!result) throw this.boom.notFound(`costApiCache with id "${indexId}" does not exist`, true);
+ return result;
+ }
+
+ async create(requestContext, rawData) {
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+
+ // Validate input
+ await validationService.ensureValid(rawData, createSchema);
+
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const { indexId, query } = rawData;
+
+ // Prepare the db object
+ const dbObject = this._fromRawToDbObject(rawData, { rev: 0, createdBy: by, updatedBy: by });
+
+ // Time to save the the db object
+ const result = await runAndCatch(async () => {
+ return this._updater()
+ .key({ indexId, query })
+ .item(dbObject)
+ .update();
+ });
+
+ return result;
+ }
+
+ async update(requestContext, rawData) {
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+
+ // Validate input
+ await validationService.ensureValid(rawData, updateSchema);
+
+ // For now, we assume that 'updatedBy' is always a user and not a group
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const { indexId, rev } = rawData;
+
+ // Prepare the db object
+ const dbObject = _.omit(this._fromRawToDbObject(rawData, { updatedBy: by }), ['rev']);
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .key({ indexId })
+ .rev(rev)
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ // There are two scenarios here:
+ // 1 - The costapicache does not exist
+ // 2 - The "rev" does not match
+ // We will display the appropriate error message accordingly
+ const existing = await this.find(requestContext, { indexId, fields: ['indexId', 'updatedBy'] });
+ if (existing) {
+ throw this.boom.badRequest(
+ `costApiCache information changed by "${
+ (existing.updatedBy || {}).username
+ }" just before your request is processed, please try again`,
+ true,
+ );
+ }
+ throw this.boom.notFound(`costApiCache with indexId "${indexId}" does not exist`, true);
+ },
+ );
+
+ return result;
+ }
+
+ async list({ fields = [] } = {}) {
+ // Remember doing a scanning is not a good idea if you billions of rows
+ return this._scanner()
+ .limit(1000)
+ .projection(fields)
+ .scan();
+ }
+
+ // Do some properties renaming to prepare the object to be saved in the database
+ _fromRawToDbObject(rawObject, overridingProps = {}) {
+ const dbObject = { ...rawObject, ...overridingProps };
+ return dbObject;
+ }
+
+ // Do some properties renaming to restore the object that was saved in the database
+ _fromDbToDataObject(rawDb, overridingProps = {}) {
+ if (_.isNil(rawDb)) return rawDb; // important, leave this if statement here, otherwise, your update methods won't work correctly
+ if (!_.isObject(rawDb)) return rawDb;
+
+ const dataObject = { ...rawDb, ...overridingProps };
+ return dataObject;
+ }
+}
+
+module.exports = CostApiCacheService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/costs/costs-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/costs/costs-service.js
new file mode 100644
index 0000000000..0e0c9ea3bc
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/costs/costs-service.js
@@ -0,0 +1,219 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+const { allowIfActive } = require('@aws-ee/base-services/lib/authorization/authorization-utils');
+const { allowIfHasRole } = require('../user/helpers/user-authz-utils');
+
+class CostsService extends Service {
+ constructor() {
+ super();
+ this.dependency([
+ 'aws',
+ 'awsAccountsService',
+ 'environmentService',
+ 'indexesService',
+ 'costApiCacheService',
+ 'authorizationService',
+ ]);
+ }
+
+ async init() {
+ await super.init();
+ }
+
+ async getIndividualEnvironmentOrProjCost(requestContext, query) {
+ // ensure that the caller has permissions to read the cost
+ // Perform default condition checks to make sure the user is active and has allowed roles
+ const allowIfHasCorrectRoles = (reqContext, { action }) =>
+ allowIfHasRole(reqContext, { action, resource: 'environment-or-project-cost' }, ['admin', 'researcher']);
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'read', conditions: [allowIfActive, allowIfHasCorrectRoles] },
+ query,
+ );
+
+ const { env, proj, groupByUser, groupByEnv, groupByService, numberOfDaysInPast } = query;
+ const [environmentService, costApiCacheService] = await this.service(['environmentService', 'costApiCacheService']);
+
+ if (groupByUser === 'true' && groupByEnv === 'true' && groupByService === 'true') {
+ return 'Can not groupByUser, groupByEnv, and groupByService. Please pick at most 2 out of the 3.';
+ }
+ let indexId = '';
+
+ if (proj) {
+ indexId = proj;
+ } else {
+ // The following will only succeed if the user has permissions to access the specified environment
+ const result = await environmentService.mustFind(requestContext, { id: env });
+ indexId = result.indexId;
+ }
+
+ const cacheResponse = await costApiCacheService.find(requestContext, { indexId, query: JSON.stringify(query) });
+ if (cacheResponse) {
+ const updatedAt = new Date(cacheResponse.updatedAt);
+ const now = new Date();
+ const elapsedHours = (now - updatedAt) / 1000 / 60 / 60;
+ if (elapsedHours < 12) {
+ return JSON.parse(cacheResponse.result);
+ }
+ }
+
+ let filter = {};
+ if (proj) {
+ filter = {
+ Tags: {
+ Key: 'Proj',
+ Values: [proj],
+ },
+ };
+ } else {
+ filter = {
+ Tags: {
+ Key: 'Env',
+ Values: [env],
+ },
+ };
+ }
+
+ const groupBy = [];
+ if (groupByService === 'true') {
+ groupBy.push({
+ Type: 'DIMENSION',
+ Key: 'SERVICE',
+ });
+ }
+ if (groupByUser === 'true') {
+ groupBy.push({
+ Type: 'TAG',
+ Key: 'CreatedBy',
+ });
+ }
+ if (groupByEnv === 'true') {
+ groupBy.push({
+ Type: 'TAG',
+ Key: 'Env',
+ });
+ }
+
+ const response = await this.callAwsCostExplorerApi(requestContext, indexId, numberOfDaysInPast, filter, groupBy);
+
+ const rawCacheData = {
+ indexId,
+ query: JSON.stringify(query),
+ result: JSON.stringify(response),
+ };
+ costApiCacheService.create(requestContext, rawCacheData);
+
+ return response;
+ }
+
+ async callAwsCostExplorerApi(requestContext, indexId, numberOfDaysInPast, filter, groupBy) {
+ const [aws] = await this.service(['aws']);
+ const { accessKeyId, secretAccessKey, sessionToken } = await this.getCredentials(requestContext, indexId);
+
+ const costExplorer = new aws.sdk.CostExplorer({
+ apiVersion: '2017-10-25',
+ region: 'us-east-1',
+ accessKeyId,
+ secretAccessKey,
+ sessionToken,
+ });
+ const now = new Date();
+ const startDate = new Date();
+ startDate.setDate(now.getDate() - numberOfDaysInPast);
+
+ const result = await costExplorer
+ .getCostAndUsage({
+ TimePeriod: {
+ Start: startDate.toISOString().split('T')[0],
+ End: now.toISOString().split('T')[0],
+ },
+ Granularity: 'DAILY',
+ Metrics: ['BlendedCost'],
+ Filter: filter,
+ GroupBy: groupBy,
+ })
+ .promise();
+
+ const response = result.ResultsByTime.map(item => {
+ const costItems = {};
+ item.Groups.forEach(group => {
+ if (group.Metrics.BlendedCost.Amount > 0) {
+ costItems[group.Keys] = {
+ amount: Math.round(group.Metrics.BlendedCost.Amount * 100) / 100,
+ unit: group.Metrics.BlendedCost.Unit,
+ };
+ }
+ });
+ return {
+ startDate: item.TimePeriod.Start,
+ cost: costItems,
+ };
+ });
+
+ return response;
+ }
+
+ async getCredentials(requestContext, indexId) {
+ const [aws, awsAccountsService, indexesService] = await this.service([
+ 'aws',
+ 'awsAccountsService',
+ 'indexesService',
+ ]);
+ const { roleArn: RoleArn, externalId: ExternalId } = await runAndCatch(
+ async () => {
+ const { awsAccountId } = await indexesService.mustFind(requestContext, { id: indexId });
+
+ return awsAccountsService.mustFind(requestContext, { id: awsAccountId });
+ },
+ async () => {
+ throw this.boom.badRequest(`account with id "${indexId} is not available`);
+ },
+ );
+
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const sts = new aws.sdk.STS({ region: 'us-east-1' });
+ const {
+ Credentials: { AccessKeyId: accessKeyId, SecretAccessKey: secretAccessKey, SessionToken: sessionToken },
+ } = await sts
+ .assumeRole({
+ RoleArn,
+ RoleSessionName: `RaaS-${by.username}`,
+ ExternalId,
+ })
+ .promise();
+
+ return { accessKeyId, secretAccessKey, sessionToken };
+ }
+
+ async assertAuthorized(requestContext, { action, conditions }, ...args) {
+ const authorizationService = await this.service('authorizationService');
+
+ // The "authorizationService.assertAuthorized" below will evaluate permissions by calling the "conditions" functions first
+ // It will then give a chance to all registered plugins (if any) to perform their authorization.
+ // The plugins can even override the authorization decision returned by the conditions
+ // See "authorizationService.authorize" method for more details
+ await authorizationService.assertAuthorized(
+ requestContext,
+ { extensionPoint: 'cost-authz', action, conditions },
+ ...args,
+ );
+ }
+}
+
+module.exports = CostsService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-ami-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-ami-service.js
new file mode 100644
index 0000000000..520de60b8a
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-ami-service.js
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const NodeCache = require('node-cache');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+class EnvironmentAmiService extends Service {
+ constructor() {
+ super();
+ this.dependency(['aws']);
+ this.cacheService = new NodeCache();
+ }
+
+ async init() {
+ await super.init();
+ const aws = await this.service('aws');
+ this.ec2 = new aws.sdk.EC2();
+ }
+
+ async getLatest(amiPrefix) {
+ const cacheKey = `${amiPrefix}-latestAMI`;
+ const latest = this.cacheService.get(cacheKey);
+ if (_.isEmpty(latest)) {
+ const results = await this.list(amiPrefix);
+ if (_.isEmpty(results)) {
+ throw this.boom.notFound(
+ `Unable to find the latest AMI with prefix ${amiPrefix} to create the environment`,
+ true,
+ );
+ }
+ const result = results[0];
+ this.cacheService.set(cacheKey, result, 60 * 5);
+ return result;
+ }
+ return latest;
+ }
+
+ async list(amiPrefix) {
+ const params = {
+ Filters: [
+ {
+ Name: 'name',
+ Values: [`${amiPrefix}*`],
+ },
+ ],
+ };
+ const images = await this.ec2.describeImages(params).promise();
+ const results = images.Images.map(image => {
+ return {
+ imageId: image.ImageId,
+ createdAt: new Date(image.CreationDate),
+ name: image.Name,
+ };
+ });
+
+ return _.reverse(_.sortBy(results, ['createdAt']));
+ }
+
+ async ensurePermissions({ imageId, accountId }) {
+ const params = {
+ ImageId: imageId,
+ LaunchPermission: {
+ Add: [
+ {
+ UserId: accountId,
+ },
+ ],
+ },
+ };
+ const result = await (async () => {
+ try {
+ const attributes = await this.ec2.modifyImageAttribute(params).promise();
+ return attributes;
+ } catch (_e) {
+ throw this.boom.badRequest(`Unable to modify permissions on the software image for the selected index.`, true);
+ }
+ })();
+ return result;
+ }
+}
+
+module.exports = EnvironmentAmiService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-authz-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-authz-service.js
new file mode 100644
index 0000000000..a7175d777e
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-authz-service.js
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const {
+ allow,
+ deny,
+ isDeny,
+ allowIfActive,
+ allowIfCurrentUserOrAdmin,
+} = require('@aws-ee/base-services/lib/authorization/authorization-utils');
+
+const { allowIfHasRole } = require('../user/helpers/user-authz-utils');
+
+class EnvironmentAuthzService extends Service {
+ async authorize(requestContext, { resource, action, effect, reason }, ...args) {
+ let permissionSoFar = { effect };
+ // if effect is "deny" already (due to any of the previous plugins returning "deny") then return "deny" right away
+ if (isDeny(permissionSoFar)) return { resource, action, effect, reason };
+
+ // Make sure the caller is active. This basic check is required irrespective of "action" so checking it here
+ permissionSoFar = await allowIfActive(requestContext, { action });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ switch (action) {
+ case 'get':
+ case 'update':
+ case 'delete':
+ return this.allowIfOwnerOrAdmin(requestContext, { action }, ...args);
+ case 'list':
+ return this.authorizeList(requestContext, { action }, ...args);
+ case 'create':
+ return this.authorizeCreate(requestContext, { action }, ...args);
+ case 'create-external':
+ return this.authorizeCreateExternal(requestContext, { action }, ...args);
+ default:
+ // This authorizer does not know how to perform authorization for the specified action.
+ // Return with the current authorization decision collected so far (from other plugins, if any)
+ return { effect };
+ }
+ }
+
+ async allowIfOwnerOrAdmin(requestContext, { action }, environment) {
+ const envCreator = _.get(environment, 'createdBy');
+ if (_.isEmpty(envCreator)) {
+ return deny(`Cannot ${action} the workspace. Workspace creator information is not available`);
+ }
+
+ // Allow if the caller is the environment creator (owner) or admin
+ let permissionSoFar = await allowIfCurrentUserOrAdmin(requestContext, { action }, envCreator);
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ // Even if the user is owner (creator) of the env his/her role may have changed (e.g., to guest or internal-guest)
+ // that may not allow it to perform the specified action on the environment (after the environment was created initially)
+ permissionSoFar = allowIfHasRole(requestContext, { action, resource: 'environment' }, [
+ 'admin',
+ 'researcher',
+ 'external-researcher',
+ ]);
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ return permissionSoFar;
+ }
+
+ async authorizeList(requestContext, { action }) {
+ // Make sure the current user role allows listing an environment
+ const permissionSoFar = allowIfHasRole(requestContext, { action, resource: 'environment' }, [
+ 'admin',
+ 'researcher',
+ 'external-researcher',
+ ]);
+ return permissionSoFar;
+ }
+
+ async authorizeCreateExternal(requestContext, { action }, environment) {
+ // Make sure the current user role allows creating an external environment
+ const permissionSoFar = allowIfHasRole(requestContext, { action, resource: 'environment' }, [
+ 'external-researcher',
+ ]);
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ // Make sure the projectId is not specified, as external environments do not belong to any projects
+ const projectId = _.get(environment, 'projectId');
+ if (projectId) {
+ return deny(`Cannot ${action} external workspace. External workspace cannot be associated to a project`, false);
+ }
+ return allow();
+ }
+
+ async authorizeCreate(requestContext, { action }, environment) {
+ // Make sure the current user role allows creating an environment
+ const permissionSoFar = allowIfHasRole(requestContext, { action, resource: 'environment' }, [
+ 'admin',
+ 'researcher',
+ ]);
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ // Make sure the user has access to the project
+ const projectId = _.get(environment, 'projectId');
+ const projectIds = _.get(requestContext, 'principal.projectId'); // The 'projectId' field on principal is actually an array of project ids
+ if (!projectId) {
+ return deny(`Cannot ${action} workspace. No project is specified`, true);
+ }
+ if (!_.includes(projectIds, projectId)) {
+ return deny(
+ `Cannot ${action} workspace. You do not have access to project "${projectId}". Please contact your administrator.`,
+ true,
+ );
+ }
+ return allow();
+ }
+}
+module.exports = EnvironmentAuthzService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-keypair-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-keypair-service.js
new file mode 100644
index 0000000000..e8c7c217e8
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-keypair-service.js
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const settingKeys = {
+ paramStoreRoot: 'paramStoreRoot',
+};
+
+class EnvironmentKeypairService extends Service {
+ constructor() {
+ super();
+ this.dependency(['aws', 'environmentService', 'auditWriterService']);
+ }
+
+ async create(requestContext, id, credentials) {
+ const [aws, environmentService] = await this.service(['aws', 'environmentService']);
+
+ // The call below will only succeed if the user has access to the specified environment and if the environment exists.
+ // This is to prevent users from creating keypairs for non-existing environments or environments they do not have
+ // access to
+ const environment = await environmentService.mustFind(requestContext, { id });
+
+ const ec2 = new aws.sdk.EC2(credentials);
+
+ // The "id" is environment id as well as the ec2 keypair name
+ const keyPair = await ec2.createKeyPair({ KeyName: id }).promise();
+
+ const ssm = new aws.sdk.SSM(credentials);
+ const parameterName = `/${this.settings.get(settingKeys.paramStoreRoot)}/environments/${id}`;
+ await ssm
+ .putParameter({
+ Name: parameterName,
+ Type: 'SecureString',
+ Value: keyPair.KeyMaterial,
+ Description: `ssh key for environment ${environment.id}`,
+ Overwrite: true,
+ })
+ .promise();
+
+ const keyName = keyPair.KeyName;
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-environment-keypair', body: { keyName } });
+
+ return keyName;
+ }
+
+ async mustFind(requestContext, id) {
+ const [aws, environmentService] = await this.service(['aws', 'environmentService']);
+
+ // The "environmentService.credsForAccountWithEnvironment" call below will only succeed, if the user has permissions
+ // to access the specified environment.
+ // The "id" is environment id as well as the ec2 keypair name
+ const ssm = new aws.sdk.SSM(await environmentService.credsForAccountWithEnvironment(requestContext, { id }));
+ const parameterName = `/${this.settings.get(settingKeys.paramStoreRoot)}/environments/${id}`;
+ const privateKey = await ssm
+ .getParameter({
+ Name: parameterName,
+ WithDecryption: true,
+ })
+ .promise();
+
+ return { privateKey: privateKey.Parameter.Value };
+ }
+
+ async delete(requestContext, id, credentials) {
+ const [aws, environmentService] = await this.service(['aws', 'environmentService']);
+
+ // The call below will only succeed if the user has access to the specified environment and if the environment exists.
+ // This is to prevent users from deleting any random key-pairs.
+ // They are allowed to delete only key-pairs for environments they have access to
+ // The "id" is environment id as well as the ec2 key-pair name
+ const environment = await environmentService.mustFind(requestContext, { id });
+
+ const ec2 = new aws.sdk.EC2(credentials);
+ const ssm = new aws.sdk.SSM(credentials);
+ const parameterName = `/${this.settings.get(settingKeys.paramStoreRoot)}/environments/${id}`;
+
+ // The "id" is environment id as well as the ec2 keypair name
+ await ec2.deleteKeyPair({ KeyName: environment.id }).promise();
+
+ await ssm
+ .deleteParameter({
+ Name: parameterName,
+ })
+ .promise();
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'delete-environment-keypair', body: { keyName: environment.id } });
+
+ return true;
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+module.exports = EnvironmentKeypairService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-mount-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-mount-service.js
new file mode 100644
index 0000000000..75aec2519d
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-mount-service.js
@@ -0,0 +1,378 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const settingKeys = {
+ environmentInstanceFiles: 'environmentInstanceFiles',
+ studyDataBucketName: 'studyDataBucketName',
+ studyDataKmsKeyAlias: 'studyDataKmsKeyAlias',
+ studyDataKmsKeyArn: 'studyDataKmsKeyArn',
+ studyDataKmsPolicyWorkspaceSid: 'studyDataKmsPolicyWorkspaceSid',
+};
+
+const parseS3Arn = arn => {
+ const path = arn.slice('arn:aws:s3:::'.length);
+ const slashIndex = path.indexOf('/');
+ return slashIndex !== -1
+ ? {
+ bucket: path.slice(0, slashIndex),
+ prefix: arn.slice(arn.indexOf('/') + 1),
+ }
+ : {
+ bucket: path,
+ prefix: '/',
+ };
+};
+
+class EnvironmentMountService extends Service {
+ constructor() {
+ super();
+ this.dependency(['aws', 'lockService', 'studyService', 'studyPermissionService']);
+ }
+
+ async getCfnMountParameters(requestContext, rawDataV1) {
+ const studyInfo = await this._getStudyInfo(requestContext, rawDataV1);
+ await this._validateStudyPermissions(requestContext, studyInfo);
+ const s3Mounts = this._prepareS3Mounts(studyInfo);
+ const iamPolicyDocument = await this._generateIamPolicyDoc(studyInfo);
+
+ return {
+ s3Mounts: JSON.stringify(s3Mounts.map(({ id, bucket, prefix }) => ({ id, bucket, prefix }))),
+ iamPolicyDocument: JSON.stringify(iamPolicyDocument),
+ environmentInstanceFiles: this.settings.get(settingKeys.environmentInstanceFiles),
+ s3Prefixes: s3Mounts.filter(({ category }) => category !== 'Open Data').map(mount => mount.prefix),
+ };
+ }
+
+ async addRoleArnToLocalResourcePolicies(workspaceRoleArn, s3Prefixes) {
+ // Define function to handle updating resource policy principals where the current principals
+ // may be an array or a string
+ const updateAwsPrincipals = (awsPrincipals, newPrincipal) => {
+ if (Array.isArray(awsPrincipals)) {
+ awsPrincipals.push(newPrincipal);
+ } else {
+ awsPrincipals = [awsPrincipals, newPrincipal];
+ }
+ return awsPrincipals;
+ };
+
+ return this._updateResourcePolicies({ updateAwsPrincipals, workspaceRoleArn, s3Prefixes });
+ }
+
+ async removeRoleArnFromLocalResourcePolicies(workspaceRoleArn, s3Prefixes) {
+ // Define function to handle updating resource policy principals where the current principals
+ // may be an array or a string
+ const updateAwsPrincipals = (awsPrincipals, removedPrincipal) => {
+ if (Array.isArray(awsPrincipals)) {
+ awsPrincipals = awsPrincipals.filter(principal => principal !== removedPrincipal);
+ } else {
+ awsPrincipals = [];
+ }
+ return awsPrincipals;
+ };
+
+ return this._updateResourcePolicies({ updateAwsPrincipals, workspaceRoleArn, s3Prefixes });
+ }
+
+ async _updateResourcePolicies({ updateAwsPrincipals, workspaceRoleArn, s3Prefixes }) {
+ if (s3Prefixes.length === 0) {
+ return;
+ }
+
+ // Get S3 and KMS resource names
+ const s3BucketName = this.settings.get(settingKeys.studyDataBucketName);
+ let kmsKeyAlias = this.settings.get(settingKeys.studyDataKmsKeyAlias);
+ if (!kmsKeyAlias.startsWith('alias/')) {
+ kmsKeyAlias = `alias/${kmsKeyAlias}`;
+ }
+
+ // Setup services and SDK clients
+ const [aws, lockService] = await this.service(['aws', 'lockService']);
+ const s3Client = new aws.sdk.S3();
+ const kmsClient = new aws.sdk.KMS();
+
+ // Perform locked updates to prevent inconsistencies from race conditions
+ const s3LockKey = `s3|bucket-policy|${s3BucketName}`;
+ const kmsLockKey = `kms|key-policy|${kmsKeyAlias}`;
+ await Promise.all([
+ // Update S3 bucket policy
+ lockService.tryWriteLockAndRun({ id: s3LockKey }, async () => {
+ // Get existing policy
+ const s3Policy = JSON.parse((await s3Client.getBucketPolicy({ Bucket: s3BucketName }).promise()).Policy);
+
+ // Get statements for listing and reading study data, respectively
+ const statements = s3Policy.Statement;
+ s3Prefixes.forEach(prefix => {
+ const listSid = `List:${prefix}`;
+ const getSid = `Get:${prefix}`;
+
+ // Define default statements to be used if we can't find existing ones
+ let listStatement = {
+ Sid: listSid,
+ Effect: 'Allow',
+ Principal: { AWS: [] },
+ Action: 's3:ListBucket',
+ Resource: `arn:aws:s3:::${s3BucketName}`,
+ Condition: {
+ StringLike: {
+ 's3:prefix': [`${prefix}*`],
+ },
+ },
+ };
+ let getStatement = {
+ Sid: getSid,
+ Effect: 'Allow',
+ Principal: { AWS: [] },
+ Action: ['s3:GetObject'],
+ Resource: [`arn:aws:s3:::${s3BucketName}/${prefix}*`],
+ };
+
+ // Pull out existing statements if available
+ statements.forEach(statement => {
+ if (statement.Sid === listSid) {
+ listStatement = statement;
+ } else if (statement.Sid === getSid) {
+ getStatement = statement;
+ }
+ });
+
+ // Update statement and policy
+ // NOTE: The S3 API *should* remove duplicate principals, if any
+ listStatement.Principal.AWS = updateAwsPrincipals(listStatement.Principal.AWS, workspaceRoleArn);
+ getStatement.Principal.AWS = updateAwsPrincipals(getStatement.Principal.AWS, workspaceRoleArn);
+
+ s3Policy.Statement = s3Policy.Statement.filter(statement => ![listSid, getSid].includes(statement.Sid));
+ [listStatement, getStatement].forEach(statement => {
+ // Only add updated statement if it contains principals (otherwise leave it out)
+ if (statement.Principal.AWS.length > 0) {
+ s3Policy.Statement.push(statement);
+ }
+ });
+ });
+
+ // Update policy
+ await s3Client.putBucketPolicy({ Bucket: s3BucketName, Policy: JSON.stringify(s3Policy) }).promise();
+ }),
+
+ // Update KMS key policy
+ lockService.tryWriteLockAndRun({ id: kmsLockKey }, async () => {
+ // Get existing policy
+ const keyId = (await kmsClient.describeKey({ KeyId: kmsKeyAlias }).promise()).KeyMetadata.KeyId;
+ const kmsPolicy = JSON.parse(
+ (await kmsClient.getKeyPolicy({ KeyId: keyId, PolicyName: 'default' }).promise()).Policy,
+ );
+
+ // Get statement
+ const sid = this.settings.get(settingKeys.studyDataKmsPolicyWorkspaceSid);
+ let environmentStatement = kmsPolicy.Statement.find(statement => statement.Sid === sid);
+ if (!environmentStatement) {
+ // Create new statement if it doesn't already exist
+ environmentStatement = {
+ Sid: sid,
+ Effect: 'Allow',
+ Principal: { AWS: [] },
+ Action: ['kms:Encrypt', 'kms:Decrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:DescribeKey'],
+ Resource: '*', // Only refers to this key since it's a resource policy
+ };
+ }
+
+ // Update policy
+ // NOTE: The S3 API *should* remove duplicate principals, if any
+ environmentStatement.Principal.AWS = updateAwsPrincipals(environmentStatement.Principal.AWS, workspaceRoleArn);
+
+ kmsPolicy.Statement = kmsPolicy.Statement.filter(statement => statement.Sid !== sid);
+ if (environmentStatement.Principal.AWS.length > 0) {
+ // Only add updated statement if it contains principals (otherwise leave it out)
+ kmsPolicy.Statement.push(environmentStatement);
+ }
+
+ await kmsClient
+ .putKeyPolicy({ KeyId: keyId, PolicyName: 'default', Policy: JSON.stringify(kmsPolicy) })
+ .promise();
+ }),
+ ]);
+ }
+
+ async _getStudyInfo(requestContext, environment) {
+ let studyInfo = [];
+ const studyIds = environment.instanceInfo.files;
+ if (studyIds && studyIds.length) {
+ const studyService = await this.service('studyService');
+ studyInfo = await Promise.all(
+ studyIds.map(async studyId => {
+ try {
+ const { id, name, category, resources } = await studyService.mustFind(requestContext, studyId);
+ return { id, name, category, resources };
+ } catch (error) {
+ // Because the studies update periodically we cannot guarantee consistency
+ // so filter anything invalid here
+ console.error(error);
+ return { name: '', resources: [] };
+ }
+ }),
+ );
+ }
+
+ return studyInfo;
+ }
+
+ async _validateStudyPermissions(requestContext, studyInfo) {
+ let permissions = {};
+ if (studyInfo.length) {
+ // Get requested study IDs
+ const requestedStudyIds = studyInfo.map(study => study.id);
+
+ // Retrieve and verify user's study permissions
+ const studyPermissionService = await this.service('studyPermissionService');
+ const storedPermissions = await studyPermissionService.getRequestorPermissions(requestContext);
+
+ // If there are no stored permissions, use a empty permissions object
+ permissions = storedPermissions || studyPermissionService.getEmptyUserPermissions();
+
+ // Add Open Data read access for everyone
+ permissions.readonlyAccess = permissions.readonlyAccess.concat(
+ studyInfo.filter(study => study.category === 'Open Data').map(study => study.id),
+ );
+
+ // Determine whether any forbidden studies were requested
+ const allowedStudies = permissions.adminAccess.concat(permissions.readonlyAccess);
+ const forbiddenStudies = _.difference(requestedStudyIds, allowedStudies);
+
+ if (forbiddenStudies.length) {
+ throw new Error(`Studies not found: ${forbiddenStudies.join(',')}`);
+ }
+ }
+ return permissions;
+ }
+
+ _prepareS3Mounts(studyInfo) {
+ let mounts = [];
+ if (studyInfo.length) {
+ // There might be multiple resources. In the future we may flatMap, for now...
+ mounts = studyInfo.reduce(
+ (result, { id, resources, category }) =>
+ result.concat(
+ resources.map(resource => {
+ const { bucket, prefix } = parseS3Arn(resource.arn);
+ return { id, bucket, prefix, category };
+ }),
+ ),
+ [],
+ );
+ }
+
+ return mounts;
+ }
+
+ async _generateIamPolicyDoc(studyInfo) {
+ let policyDoc = {};
+ if (studyInfo.length) {
+ const objectLevelActions = ['s3:GetObject'];
+
+ // Collect study resources
+ const objectPathArns = _.flatten(
+ studyInfo.map(info =>
+ info.resources
+ // Pull out resource ARNs
+ .map(resource => resource.arn)
+ // Only grab S3 ARNs
+ .filter(arn => arn.startsWith('arn:aws:s3:'))
+ // Normalize the ARNs by ensuring they end with "/*"
+ .map(arn => {
+ switch (arn.slice(-1)) {
+ case '*':
+ break;
+ case '/':
+ arn += '*';
+ break;
+ default:
+ arn += '/*';
+ }
+
+ return arn;
+ }),
+ ),
+ );
+
+ // Build policy statements for object-level permissions
+ const statements = [];
+ statements.push({
+ Sid: 'S3StudyReadAccess',
+ Effect: 'Allow',
+ Action: objectLevelActions,
+ Resource: objectPathArns,
+ });
+
+ // Create map of buckets whose paths need list access
+ const bucketPaths = {};
+ objectPathArns.forEach(arn => {
+ const { bucket, prefix } = parseS3Arn(arn);
+ if (!(bucket in bucketPaths)) {
+ bucketPaths[bucket] = [];
+ }
+ bucketPaths[bucket].push(prefix);
+ });
+
+ // Add bucket list permissions to statements
+ let bucketCtr = 1;
+ Object.keys(bucketPaths).forEach(bucketName => {
+ statements.push({
+ Sid: `studyListS3Access${bucketCtr}`,
+ Effect: 'Allow',
+ Action: 's3:ListBucket',
+ Resource: `arn:aws:s3:::${bucketName}`,
+ Condition: {
+ StringLike: {
+ 's3:prefix': bucketPaths[bucketName],
+ },
+ },
+ });
+ bucketCtr += 1;
+ });
+
+ // // Add KMS Permissions
+ const studyDataKmsAliasArn = this.settings.get(settingKeys.studyDataKmsKeyArn);
+
+ // Get KMS Key ARN from KMS Alias ARN
+ // The "Decrypt","DescribeKey","GenerateDataKey" etc require KMS KEY ARN and not ALIAS ARN
+ const [aws] = await this.service(['aws']);
+ const kmsClient = new aws.sdk.KMS();
+ const data = await kmsClient
+ .describeKey({
+ KeyId: studyDataKmsAliasArn,
+ })
+ .promise();
+ const studyDataKmsKeyArn = data.KeyMetadata.Arn;
+ statements.push({
+ Sid: 'studyKMSAccess',
+ Action: ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:GenerateDataKey', 'kms:ReEncrypt*'],
+ Effect: 'Allow',
+ Resource: studyDataKmsKeyArn,
+ });
+
+ // Build final policyDoc
+ policyDoc = {
+ Version: '2012-10-17',
+ Statement: statements,
+ };
+ }
+
+ return policyDoc;
+ }
+}
+
+module.exports = EnvironmentMountService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-notebook-url-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-notebook-url-service.js
new file mode 100644
index 0000000000..1ac9905ecf
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-notebook-url-service.js
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+class EnvironmentNotebookUrlService extends Service {
+ constructor() {
+ super();
+ this.dependency(['aws', 'environmentService', 'auditWriterService']);
+ }
+
+ async init() {
+ await super.init();
+ }
+
+ async getNotebookPresignedUrl(requestContext, id) {
+ const [aws, environmentService] = await this.service(['aws', 'environmentService']);
+
+ // The following will succeed only if the user has permissions to access the specified environment
+ const { instanceInfo } = await environmentService.mustFind(requestContext, { id });
+
+ const params = {
+ NotebookInstanceName: instanceInfo.NotebookInstanceName,
+ };
+ const sagemaker = new aws.sdk.SageMaker(
+ await environmentService.credsForAccountWithEnvironment(requestContext, { id }),
+ );
+ const url = await sagemaker.createPresignedNotebookInstanceUrl(params).promise();
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'notebook-presigned-url-requested', body: { id } });
+
+ return url;
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+module.exports = EnvironmentNotebookUrlService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-service.js
new file mode 100644
index 0000000000..ef712ffda6
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-service.js
@@ -0,0 +1,704 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const uuid = require('uuid/v1');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const createSchema = require('../schema/create-environment');
+const updateSchema = require('../schema/update-environment');
+
+const settingKeys = {
+ tableName: 'dbTableEnvironments',
+ awsAccountsTableName: 'dbTableAwsAccounts',
+ ec2LinuxAmiPrefix: 'ec2LinuxAmiPrefix',
+ ec2WindowsAmiPrefix: 'ec2WindowsAmiPrefix',
+ emrAmiPrefix: 'emrAmiPrefix',
+};
+const workflowIds = {
+ create: 'wf-create-environment',
+ delete: 'wf-delete-environment',
+};
+
+class EnvironmentService extends Service {
+ constructor() {
+ super();
+ this.dependency([
+ 'jsonSchemaValidationService',
+ 'dbService',
+ 'workflowTriggerService',
+ 'environmentAmiService',
+ 'environmentMountService',
+ 'aws',
+ 'awsAccountsService',
+ 'indexesService',
+ 'projectService',
+ 'computePlatformService',
+ 'authorizationService',
+ 'environmentAuthzService',
+ 'auditWriterService',
+ 'userService',
+ ]);
+ }
+
+ async init() {
+ await super.init();
+ const [dbService, environmentAuthzService] = await this.service(['dbService', 'environmentAuthzService']);
+ const table = this.settings.get(settingKeys.tableName);
+
+ this._getter = () => dbService.helper.getter().table(table);
+ this._updater = () => dbService.helper.updater().table(table);
+ this._deleter = () => dbService.helper.deleter().table(table);
+ this._scanner = () => dbService.helper.scanner().table(table);
+
+ // A private authorization condition function that just delegates to the environmentAuthzService
+ this._allowAuthorized = (requestContext, { resource, action, effect, reason }, ...args) =>
+ environmentAuthzService.authorize(requestContext, { resource, action, effect, reason }, ...args);
+ }
+
+ async list(requestContext) {
+ // Make sure the user has permissions to "list" environments
+ // The following will result in checking permissions by calling the condition function "this._allowAuthorized" first
+ await this.assertAuthorized(requestContext, { action: 'list', conditions: [this._allowAuthorized] });
+
+ // TODO: Handle pagination
+
+ const environments = await this._scanner()
+ .limit(1000)
+ .scan();
+
+ if (requestContext.principal.isAdmin) {
+ return environments;
+ }
+
+ // map environment ownership
+ const { username: reqUsername, ns: reqNs } = requestContext.principalIdentifier;
+ const envMap = environments.map((env, index) => {
+ const { username: ownerUsername, ns: ownerNs } = env.createdBy;
+ const isOwner = reqUsername === ownerUsername && reqNs === ownerNs;
+
+ const { sharedWithUsers = [] } = env;
+ const isShared = Array.isArray(sharedWithUsers)
+ ? sharedWithUsers.some(u => u.username === reqUsername && u.ns === reqNs)
+ : false;
+
+ return {
+ isOwner,
+ // should not be owner and shared at the same time
+ isShared: !isOwner && isShared,
+ environmentsIndex: index,
+ };
+ });
+
+ // environments owned by user
+ const envOwner = envMap.filter(item => item.isOwner);
+
+ // environments shared with user
+ const envShared = envMap.filter(item => item.isShared);
+
+ // enviroments not owned by user
+ const envNonOwnerOrShared = envMap.filter(item => !item.isOwner && !item.isShared);
+
+ // gather environments where the current user is a project admin
+ const envProjectAdminPromises = envNonOwnerOrShared.map(env =>
+ this.isEnvironmentProjectAdmin(requestContext, environments[env.environmentsIndex]).catch(error => error),
+ );
+ // NOTE refactor to use Promise.allSettled() once on Node JS >= 12
+ const envProjectAdminPromiseResolutions = await Promise.all(envProjectAdminPromises);
+ const envProjectAdmin = envProjectAdminPromiseResolutions
+ .map((isProjectAdmin, index) => {
+ if (isProjectAdmin instanceof Error) {
+ // eslint-disable-next-line no-console
+ console.error('error listing environment checking isProjectAdmin: ', isProjectAdmin);
+ return { isProjectAdmin: false };
+ }
+ return {
+ isProjectAdmin,
+ environmentsIndex: envNonOwnerOrShared[index].environmentsIndex,
+ };
+ })
+ .filter(item => item.isProjectAdmin);
+
+ return [...envOwner, ...envShared, ...envProjectAdmin].map(item => environments[item.environmentsIndex]);
+ }
+
+ async find(requestContext, { id, fields = [] }) {
+ // Make sure 'createdBy' is always returned as that's required for authorizing the 'get' action
+ // If empty "fields" is specified then it means the caller is asking for all fields. No need to append 'createdBy'
+ // in that case.
+ const fieldsToGet = _.isEmpty(fields) ? fields : _.uniq([...fields, 'createdBy']);
+ const result = await this._getter()
+ .key({ id })
+ .projection(fieldsToGet)
+ .get();
+
+ if (result) {
+ // ensure that the caller has permissions to retrieve the specified environment
+ // The following will result in checking permissions by calling the condition function "this._allowAuthorized" first
+ await this.assertAuthorized(requestContext, { action: 'get', conditions: [this._allowAuthorized] }, result);
+ }
+
+ return this._fromDbToDataObject(result);
+ }
+
+ async mustFind(requestContext, { id, fields = [] }) {
+ const result = await this.find(requestContext, { id, fields });
+ if (!result) throw this.boom.notFound(`environment with id "${id}" does not exist`, true);
+ return result;
+ }
+
+ async saveEnvironmentToDb(requestContext, rawData, id, status = 'PENDING') {
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ // Prepare the db object
+ const date = new Date().toISOString();
+ const dbObject = this._fromRawToDbObject(rawData, {
+ status,
+ rev: 0,
+ createdBy: by,
+ updatedBy: by,
+ createdAt: date,
+ updatedAt: date,
+ });
+
+ // Time to save the the db object
+ const dbResult = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_not_exists(id)') // yes we need this
+ .key({ id })
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`environment with id "${id}" already exists`, true);
+ },
+ );
+ return dbResult;
+ }
+
+ async ensureAmiAccess(_requestContext, accountId, type) {
+ // TODO - ami prefix should be coming from compute configuration object
+ const amiPrefixKey = {
+ 'ec2-linux': settingKeys.ec2LinuxAmiPrefix,
+ 'ec2-windows': settingKeys.ec2WindowsAmiPrefix,
+ 'emr': settingKeys.emrAmiPrefix,
+ }[type];
+
+ const amiPrefix = this.settings.get(amiPrefixKey);
+
+ const environmentAmiService = await this.service('environmentAmiService');
+
+ const { imageId } = await environmentAmiService.getLatest(amiPrefix);
+ await environmentAmiService.ensurePermissions({ imageId, accountId });
+
+ return imageId;
+ }
+
+ // This method is used to transform a newer environment request object (rawDataV2) to
+ // the existing environment raw request (rawDataV1). This is needed because a complete refactoring has
+ // not been done yet. The newer environment request object has a better modeling approach.
+ //
+ // This method returns rawDataV1 object that can be used inside the create and createExternal methods
+ // The shape of the rawDataV1 is { name, description, indexId, projectId, isExternal, instanceInfo }
+ // where instanceInfo has the shape { type, cidr, size, files, config }
+ async transformToRawDataV1(requestContext, rawDataV2, indexId) {
+ const [computePlatformService] = await this.service(['computePlatformService']);
+
+ // { platformId, configurationId, name, description, projectId, studyIds, params }
+ const { projectId, platformId, configurationId, studyIds } = rawDataV2;
+ const configurations = await computePlatformService.listConfigurations(requestContext, {
+ platformId,
+ includePrice: true,
+ });
+ const configuration = _.find(configurations, ['id', configurationId]);
+
+ const isMutableParam = name => _.has(configuration, ['params', 'mutable', name]);
+ const getParam = name => {
+ // First we see if the paramter is considered immutable, if so, we return its immutable value
+ // otherwise we return the one from the rawDataV2.params if the parameter name is declared
+ // in the configuration as mutable.
+ const immutable = _.get(configuration, ['params', 'immutable', name]);
+ if (!_.isUndefined(immutable)) return immutable;
+ if (!isMutableParam(name)) return undefined;
+ return _.get(rawDataV2, ['params', name]) || _.get(configuration, ['params', 'mutable', name]);
+ };
+
+ if (_.isUndefined(configuration)) {
+ throw this.boom.badRequest('You do not have permissions to create this configuration', true);
+ }
+
+ const instanceInfo = {};
+ const priceInfo = configuration.priceInfo;
+ const addIfDefined = (key, value) => {
+ if (_.isUndefined(value)) return;
+ instanceInfo[key] = value;
+ };
+
+ addIfDefined('type', configuration.type);
+ addIfDefined('cidr', getParam('cidr'));
+ addIfDefined('size', getParam('size'));
+ addIfDefined('files', studyIds); // Yes, rawDataV1 thinks that studyIds are files
+ addIfDefined('config', getParam('emr') || {});
+
+ if (priceInfo.type === 'spot') {
+ _.set(instanceInfo, 'config.spotBidPrice', priceInfo.spotBidPrice);
+ }
+
+ const rawDataV1 = {
+ platformId, // Even though v1 does not have this prop, we are adding it here
+ configurationId, // Even though v1 does not have this prop, we are adding it here
+ priceInfo, // Even though v1 does not have this props, we are adding it here
+ name: rawDataV2.name,
+ description: rawDataV2.description,
+ instanceInfo,
+ isExternal: _.get(requestContext, 'principal.isExternalUser', false),
+ };
+
+ if (projectId) rawDataV1.projectId = projectId;
+ if (indexId) rawDataV1.indexId = indexId;
+
+ return rawDataV1;
+ }
+
+ async createExternal(requestContext, rawDataV2) {
+ // Make sure the user has permissions to create the external environment
+ // The following will result in checking permissions by calling the condition function "this._allowAuthorized" first
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'create-external', conditions: [this._allowAuthorized] },
+ rawDataV2,
+ );
+
+ const [validationService, awsAccountsService, environmentMountService] = await this.service([
+ 'jsonSchemaValidationService',
+ 'awsAccountsService',
+ 'environmentMountService',
+ ]);
+ // Validate input
+ await validationService.ensureValid(rawDataV2, createSchema);
+
+ const { accountId, ...rawData } = rawDataV2;
+ const rawDataV1 = await this.transformToRawDataV1(requestContext, rawData);
+
+ const mountInformation = await environmentMountService.getCfnMountParameters(requestContext, rawDataV1);
+
+ Object.assign(rawDataV1.instanceInfo, mountInformation);
+
+ const savedEnvironment = await this.saveEnvironmentToDb(requestContext, rawDataV1, uuid(), 'PENDING');
+
+ await awsAccountsService.ensureExternalAccount(requestContext, { accountId });
+
+ const {
+ instanceInfo: { type },
+ } = savedEnvironment;
+ // Get AMI configuration where applicable
+ if (['ec2-linux', 'ec2-windows', 'emr'].includes(type)) {
+ const imageId = await this.ensureAmiAccess(requestContext, accountId, type);
+ savedEnvironment.amiImage = imageId;
+ }
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-external-environment', body: rawDataV2 });
+
+ return savedEnvironment;
+ }
+
+ async create(requestContext, rawDataV2) {
+ const [
+ validationService,
+ workflowTriggerService,
+ awsAccountsService,
+ indexesService,
+ projectService,
+ ] = await this.service([
+ 'jsonSchemaValidationService',
+ 'workflowTriggerService',
+ 'awsAccountsService',
+ 'indexesService',
+ 'projectService',
+ ]);
+
+ // Make sure the user has permissions to create the environment
+ // The following will result in checking permissions by calling the condition function "this._allowAuthorized" first
+ await this.assertAuthorized(requestContext, { action: 'create', conditions: [this._allowAuthorized] }, rawDataV2);
+
+ // Validate input
+ await validationService.ensureValid(rawDataV2, createSchema);
+
+ // { platformId, configurationId, name, description, projectId, studyIds, params } => rawDataV2
+ const { projectId } = rawDataV2;
+
+ // Lets find the index id, by looking at the project and then load get the check the index id
+ const { indexId } = await projectService.mustFind(requestContext, { id: projectId });
+
+ // Time to convert the new rawDataV2 to the not so appealing rawDataV1
+ const rawDataV1 = await this.transformToRawDataV1(requestContext, rawDataV2, indexId);
+
+ // Get the aws account information
+ const { awsAccountId } = await indexesService.mustFind(requestContext, { id: indexId });
+ const {
+ roleArn: cfnExecutionRole,
+ externalId: roleExternalId,
+ accountId,
+ vpcId,
+ subnetId,
+ encryptionKeyArn,
+ } = await awsAccountsService.mustFind(requestContext, { id: awsAccountId });
+
+ // Check launch pre-requisites
+ if (!(cfnExecutionRole && roleExternalId && accountId && vpcId && subnetId && encryptionKeyArn)) {
+ const cause = this.getConfigError(cfnExecutionRole, roleExternalId, accountId, vpcId, subnetId, encryptionKeyArn);
+ throw this.boom.badRequest(`Index "${indexId}" has not been correctly configured: missing ${cause}.`, true);
+ }
+
+ // Generate environment ID
+ const id = uuid();
+
+ const { instanceInfo } = rawDataV1;
+ const { type, cidr } = instanceInfo;
+
+ // trigger the provision environment workflow
+ // TODO: remove CIDR default once its in the gui and backend
+ const input = {
+ environmentId: id,
+ requestContext,
+ cfnExecutionRole,
+ roleExternalId,
+ vpcId,
+ subnetId,
+ encryptionKeyArn,
+ type,
+ cidr,
+ };
+
+ // Get AMI configuration where applicable
+ if (['ec2-linux', 'ec2-windows', 'emr'].includes(type)) {
+ const imageId = await this.ensureAmiAccess(requestContext, accountId, type);
+ Object.assign(input, { amiImage: imageId });
+ }
+
+ // Time to save the the db object and trigger the workflow
+ const meta = { workflowId: workflowIds.create };
+
+ const dbResult = await this.saveEnvironmentToDb(requestContext, rawDataV1, id);
+ await workflowTriggerService.triggerWorkflow(requestContext, meta, input);
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-environment', body: rawDataV2 });
+
+ return dbResult;
+ }
+
+ getConfigError(cfnExecutionRole, roleExternalId, accountId, vpcId, subnetId, encryptionKeyArn) {
+ const causes = [];
+
+ if (!cfnExecutionRole) causes.push('IAM role');
+ if (!roleExternalId) causes.push('External ID');
+ if (!accountId) causes.push('AWS account ID');
+ if (!vpcId) causes.push('VPC ID');
+ if (!subnetId) causes.push('VPC Subnet ID');
+ if (!encryptionKeyArn) causes.push('Encryption Key ARN');
+
+ if (causes.length > 1) {
+ const last = causes.pop();
+ return `${causes.join(', ')} and ${last}`;
+ }
+ if (causes.length > 0) {
+ return causes[0];
+ }
+
+ return undefined;
+ }
+
+ async update(requestContext, environment) {
+ const jsonSchemaValidationService = await this.service('jsonSchemaValidationService');
+ await jsonSchemaValidationService.ensureValid(environment, updateSchema);
+
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+
+ // Prepare the db object
+ // const dbObject = _.omit(this._fromRawToDbObject(dataObject, { updatedBy: by }), ['rev']);
+ const existingEnvironment = await this.mustFind(requestContext, { id: environment.id });
+
+ // Make sure the user has permissions to update the environment
+ // The following will result in checking permissions by calling the condition function "this._allowAuthorized" first
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'update', conditions: [this._allowAuthorized] },
+ existingEnvironment,
+ );
+
+ if (existingEnvironment.isExternal) {
+ // If the environment has finished and there are s3Prefixes to mount update the policies
+ if (
+ environment.status === 'COMPLETED' &&
+ existingEnvironment.status === 'PENDING' &&
+ existingEnvironment.instanceInfo.s3Prefixes.length > 0
+ ) {
+ const environmentMountService = await this.service('environmentMountService');
+
+ await environmentMountService.addRoleArnToLocalResourcePolicies(
+ environment.instanceInfo.WorkspaceInstanceRoleArn,
+ existingEnvironment.instanceInfo.s3Prefixes,
+ );
+ }
+ }
+
+ const mergedInstanceInfo = _.assign({}, existingEnvironment.instanceInfo, environment.instanceInfo);
+ const updatedEnvironment = _.omit(
+ _.assign({}, existingEnvironment, environment, { instanceInfo: mergedInstanceInfo }),
+ ['id'],
+ );
+ const dbObject = this._fromRawToDbObject(updatedEnvironment, {
+ updatedBy: by,
+ updatedAt: new Date().toISOString(),
+ });
+
+ // validate sharedWithUsers
+ const { sharedWithUsers } = environment;
+ try {
+ if (Array.isArray(sharedWithUsers)) {
+ const userService = await this.service('userService');
+ await userService.validateUsers(sharedWithUsers);
+ }
+ } catch (error) {
+ if (error.safe) {
+ throw error;
+ }
+ const errorMessage = 'error updating environment - shared with users';
+ // eslint-disable-next-line no-console
+ console.error(errorMessage, error);
+ throw this.boom.internalError(errorMessage, true);
+ }
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id: environment.id })
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ throw this.boom.notFound(`environment with id "${environment.id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'update-environment', body: environment });
+
+ return result;
+ }
+
+ // use this authorization check function for safe changes that can be done by
+ // project admins and shared environment users
+ // this should not be used for destructive actions such as terminate/delete
+ // or other dangerous updates
+ async isSafeMutationAuthorized(requestContext, environment) {
+ // check if user is admin
+ const isAdmin = requestContext.principal.isAdmin;
+ if (isAdmin) {
+ return true;
+ }
+
+ // check if user is the environment owner
+ const { username: ownerUsername, ns: ownerNs } = environment.createdBy;
+ const { username: reqUsername, ns: reqNs } = requestContext.principalIdentifier;
+ const isOwner = reqUsername === ownerUsername && reqNs === ownerNs;
+ if (isOwner) {
+ return true;
+ }
+
+ // check if environment has been shared with user
+ const { sharedWithUsers = [] } = environment;
+ const isShared = Array.isArray(sharedWithUsers)
+ ? sharedWithUsers.some(u => u.username === reqUsername && u.ns === reqNs)
+ : false;
+ if (isShared) {
+ return true;
+ }
+
+ // check if the project associated with the environment includes the user as a project admin
+ let isProjectAdmin = false;
+ try {
+ isProjectAdmin = await this.isEnvironmentProjectAdmin(requestContext, environment);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error('error checking isProjectAdmin in mutation authorization: ', error);
+ }
+ if (isProjectAdmin) {
+ return true;
+ }
+
+ return false;
+ }
+
+ async delete(requestContext, { id }) {
+ const existingEnvironment = await this.mustFind(requestContext, { id });
+
+ // Make sure the user has permissions to delete the environment
+ // The following will result in checking permissions by calling the condition function "this._allowAuthorized" first
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'delete', conditions: [this._allowAuthorized] },
+ existingEnvironment,
+ );
+
+ if (existingEnvironment.status === 'TERMINATING' || existingEnvironment.status === 'TERMINATED') {
+ this.log.info(`environment with id "${existingEnvironment.id}" is already terminating`);
+ return;
+ }
+
+ if (existingEnvironment.isExternal) {
+ // If studies were mounted, update the resoure policies to remove access
+ if (
+ existingEnvironment.instanceInfo.WorkspaceInstanceRoleArn &&
+ existingEnvironment.instanceInfo.s3Prefixes.length > 0
+ ) {
+ const environmentMountService = await this.service('environmentMountService');
+
+ await environmentMountService.removeRoleArnFromLocalResourcePolicies(
+ existingEnvironment.instanceInfo.WorkspaceInstanceRoleArn,
+ existingEnvironment.instanceInfo.s3Prefixes,
+ );
+ }
+
+ existingEnvironment.status = 'TERMINATED';
+ await this.update(requestContext, _.pick(existingEnvironment, ['id', 'status']));
+ return;
+ }
+
+ const [awsAccountsService, indexesService] = await this.service(['awsAccountsService', 'indexesService']);
+ const { indexId } = existingEnvironment;
+ const { awsAccountId } = await indexesService.mustFind(requestContext, { id: indexId });
+ const { roleArn: cfnExecutionRole, externalId: roleExternalId } = await awsAccountsService.mustFind(
+ requestContext,
+ { id: awsAccountId },
+ );
+
+ // trigger the delete environment workflow
+ const input = {
+ environmentId: existingEnvironment.id,
+ requestContext,
+ cfnExecutionRole,
+ roleExternalId,
+ };
+
+ const meta = { workflowId: workflowIds.delete };
+ const workflowTriggerService = await this.service('workflowTriggerService');
+ await workflowTriggerService.triggerWorkflow(requestContext, meta, input);
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'delete-environment', body: { id } });
+ }
+
+ async credsForAccountWithEnvironment(requestContext, { id }) {
+ const [aws, awsAccountsService, indexesService] = await this.service([
+ 'aws',
+ 'awsAccountsService',
+ 'indexesService',
+ ]);
+
+ // The following will succeed only if the user has permissions to access
+ // the specified environment
+ const { status, indexId } = await this.mustFind(requestContext, {
+ id,
+ fields: ['status', 'indexId', 'createdBy'],
+ });
+
+ if (status !== 'COMPLETED') {
+ this.boom.badRequest(`environment with id "${id}" is not ready`, true);
+ }
+
+ const { awsAccountId } = await indexesService.mustFind(requestContext, { id: indexId });
+ const { roleArn: RoleArn, externalId: ExternalId } = await awsAccountsService.mustFind(requestContext, {
+ id: awsAccountId,
+ });
+
+ const sts = new aws.sdk.STS();
+ const {
+ Credentials: { AccessKeyId: accessKeyId, SecretAccessKey: secretAccessKey, SessionToken: sessionToken },
+ } = await sts
+ .assumeRole({
+ RoleArn,
+ RoleSessionName: `RaaS-${requestContext.principalIdentifier.username}`,
+ ExternalId,
+ })
+ .promise();
+
+ return { accessKeyId, secretAccessKey, sessionToken };
+ }
+
+ async getWindowsPasswordData(requestContext, { id }) {
+ // Get environment and validate usage
+ // The following will succeed only if the user has permissions to access the specified environment
+ const environment = await this.mustFind(requestContext, { id });
+ if (environment.instanceInfo.type !== 'ec2-windows') {
+ throw this.boom.badRequest('Passwords can only be retrieved for EC2 Windows environments', true);
+ }
+
+ // Retrieve password data
+ const aws = await this.service('aws');
+ const ec2Client = new aws.sdk.EC2(await this.credsForAccountWithEnvironment(requestContext, { id }));
+ const { PasswordData } = await ec2Client
+ .getPasswordData({ InstanceId: environment.instanceInfo.Ec2WorkspaceInstanceId })
+ .promise();
+
+ return { passwordData: PasswordData };
+ }
+
+ // Do some properties renaming to prepare the object to be saved in the database
+ _fromRawToDbObject(rawObject, overridingProps = {}) {
+ const dbObject = { ...rawObject, ...overridingProps };
+ return dbObject;
+ }
+
+ // Do some properties renaming to restore the object that was saved in the database
+ _fromDbToDataObject(rawDb, overridingProps = {}) {
+ if (_.isNil(rawDb)) return rawDb; // important, leave this if statement here, otherwise, your update methods won't work correctly
+ if (!_.isObject(rawDb)) return rawDb;
+
+ const dataObject = { ...rawDb, ...overridingProps };
+ return dataObject;
+ }
+
+ async assertAuthorized(requestContext, { action, conditions }, ...args) {
+ const authorizationService = await this.service('authorizationService');
+
+ // The "authorizationService.assertAuthorized" below will evaluate permissions by calling the "conditions" functions first
+ // It will then give a chance to all registered plugins (if any) to perform their authorization.
+ // The plugins can even override the authorization decision returned by the conditions
+ // See "authorizationService.authorize" method for more details
+ await authorizationService.assertAuthorized(
+ requestContext,
+ { extensionPoint: 'environment-authz', action, conditions },
+ ...args,
+ );
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+module.exports = EnvironmentService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-spot-price-history-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-spot-price-history-service.js
new file mode 100644
index 0000000000..464af8fbaf
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/environment/environment-spot-price-history-service.js
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+const { allowIfActive } = require('@aws-ee/base-services/lib/authorization/authorization-utils');
+const { allowIfHasRole } = require('../user/helpers/user-authz-utils');
+
+class EnvironmentSpotPriceHistoryService extends Service {
+ constructor() {
+ super();
+ this.dependency(['aws', 'authorizationService']);
+ }
+
+ async getPriceHistory(requestContext, type) {
+ // ensure that the caller has permissions to get price history
+ // Perform default condition checks to make sure the user is active and has allowed roles
+ const allowIfHasCorrectRoles = (reqContext, { action }) =>
+ allowIfHasRole(reqContext, { action, resource: 'environment-spot-price-history' }, [
+ 'admin',
+ 'researcher',
+ 'external-researcher',
+ ]);
+
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'read', conditions: [allowIfActive, allowIfHasCorrectRoles] },
+ type,
+ );
+
+ const aws = await this.service('aws');
+
+ const ec2 = new aws.sdk.EC2();
+
+ const { SpotPriceHistory } = await runAndCatch(
+ async () =>
+ ec2
+ .describeSpotPriceHistory({
+ InstanceTypes: [type],
+ ProductDescriptions: ['Linux/UNIX'],
+ StartTime: new Date(),
+ })
+ .promise(),
+ async () => {
+ throw this.boom.badRequest(`Price history not available for "${type}" instance type.`);
+ },
+ );
+
+ return SpotPriceHistory.map(({ AvailabilityZone, SpotPrice }) => ({
+ availabilityZone: AvailabilityZone,
+ spotPrice: parseFloat(SpotPrice),
+ }));
+ }
+
+ async assertAuthorized(requestContext, { action, conditions }, ...args) {
+ const authorizationService = await this.service('authorizationService');
+
+ // The "authorizationService.assertAuthorized" below will evaluate permissions by calling the "conditions" functions first
+ // It will then give a chance to all registered plugins (if any) to perform their authorization.
+ // The plugins can even override the authorization decision returned by the conditions
+ // See "authorizationService.authorize" method for more details
+ await authorizationService.assertAuthorized(
+ requestContext,
+ { extensionPoint: 'environment-spot-price-history-authz', action, conditions },
+ ...args,
+ );
+ }
+}
+
+module.exports = EnvironmentSpotPriceHistoryService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/external-cfn-template/external-cfn-template-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/external-cfn-template/external-cfn-template-service.js
new file mode 100644
index 0000000000..b76a1542b9
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/external-cfn-template/external-cfn-template-service.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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+class ExternalCfnTemplateService extends Service {
+ constructor() {
+ super();
+ this.dependency(['s3Service']);
+ }
+
+ async init() {
+ await super.init();
+ }
+
+ async getSignS3Url(key) {
+ const [s3Service] = await this.service(['s3Service']);
+ const bucket = this.settings.get('externalCfnTemplatesBucketName');
+ const request = { files: [{ key, bucket }] };
+ const urls = await s3Service.sign(request);
+ return urls[0].signedUrl;
+ }
+
+ async mustGetSignS3Url(key) {
+ const result = await this.getSignS3Url(key);
+ if (!result) throw this.boom.notFound(`template with key "${key}" does not exist`, true);
+ return result;
+ }
+}
+
+module.exports = ExternalCfnTemplateService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/helpers/aws-tags.js b/addons/addon-base-raas/packages/base-raas-services/lib/helpers/aws-tags.js
new file mode 100644
index 0000000000..2a4de92ea3
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/helpers/aws-tags.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.
+ */
+
+const xml = require('xml');
+
+/**
+ * Generates an XML document containing AWS tags.
+ *
+ * @example
+ *
+ * ```javascript
+ * buildTaggingXml(
+ * {
+ * UploadedBy: "me,theuser",
+ * Comment: "]>&xxe; ",
+ * },
+ * true, // pretty print
+ * );
+ * ```
+ *
+ * @param {Object=} tags
+ * @param {boolean=} pretty
+ * @returns {string} an xml tagging configuration document
+ */
+const buildTaggingXml = (tags = {}, pretty = false) =>
+ xml(
+ {
+ Tagging: [
+ {
+ TagSet: Object.entries(tags).map(([Key, Value]) => ({ Tag: [{ Key }, { Value }] })),
+ },
+ ],
+ },
+ { indent: pretty ? ' ' : undefined },
+ );
+
+module.exports = {
+ buildTaggingXml,
+};
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/helpers/is-role.js b/addons/addon-base-raas/packages/base-raas-services/lib/helpers/is-role.js
new file mode 100644
index 0000000000..9aebfb10e5
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/helpers/is-role.js
@@ -0,0 +1,28 @@
+const _ = require('lodash');
+
+function isRole(requestContext, roleName) {
+ return _.get(requestContext, 'principal.userRole') === roleName;
+}
+
+function isExternalGuest(requestContext) {
+ return isRole(requestContext, 'guest');
+}
+
+function isInternalGuest(requestContext) {
+ return isRole(requestContext, 'internal-guest');
+}
+
+function isExternalResearcher(requestContext) {
+ return isRole(requestContext, 'external-researcher');
+}
+
+function isInternalResearcher(requestContext) {
+ return isRole(requestContext, 'researcher');
+}
+
+module.exports = {
+ isInternalResearcher,
+ isExternalResearcher,
+ isInternalGuest,
+ isExternalGuest,
+};
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/indexes/indexes-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/indexes/indexes-service.js
new file mode 100644
index 0000000000..8da65e56f5
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/indexes/indexes-service.js
@@ -0,0 +1,245 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+const { allowIfActive, allowIfAdmin } = require('@aws-ee/base-services/lib/authorization/authorization-utils');
+
+const { isExternalGuest, isExternalResearcher, isInternalGuest } = require('../helpers/is-role');
+const createSchema = require('../schema/create-indexes');
+const updateSchema = require('../schema/update-indexes');
+
+const settingKeys = {
+ tableName: 'dbTableIndexes',
+};
+
+class IndexesService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'authorizationService', 'dbService', 'auditWriterService']);
+ }
+
+ async init() {
+ await super.init();
+ const [dbService] = await this.service(['dbService']);
+ const table = this.settings.get(settingKeys.tableName);
+
+ this._getter = () => dbService.helper.getter().table(table);
+ this._updater = () => dbService.helper.updater().table(table);
+ this._query = () => dbService.helper.query().table(table);
+ this._deleter = () => dbService.helper.deleter().table(table);
+ this._scanner = () => dbService.helper.scanner().table(table);
+ }
+
+ async find(requestContext, { id, fields = [] }) {
+ const restrict =
+ isExternalGuest(requestContext) || isExternalResearcher(requestContext) || isInternalGuest(requestContext);
+
+ if (restrict) return undefined;
+
+ // Future task: add further checks
+
+ const result = await this._getter()
+ .key({ id })
+ .projection(fields)
+ .get();
+
+ return this._fromDbToDataObject(result);
+ }
+
+ async mustFind(requestContext, { id, fields = [] }) {
+ const result = await this.find(requestContext, { id, fields });
+ if (!result) throw this.boom.notFound(`indexes with id "${id}" does not exist`, true);
+ return result;
+ }
+
+ async create(requestContext, rawData) {
+ // ensure that the caller has permissions to create the index
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'create', conditions: [allowIfActive, allowIfAdmin] },
+ rawData,
+ );
+
+ // Validate input
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+ await validationService.ensureValid(rawData, createSchema);
+
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const id = rawData.id;
+
+ // Prepare the db object
+ const dbObject = this._fromRawToDbObject(rawData, { rev: 0, createdBy: by, updatedBy: by });
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_not_exists(id)') // yes we need this
+ .key({ id })
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`indexes with id "${id}" already exists`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-index', body: result });
+
+ return result;
+ }
+
+ async update(requestContext, rawData) {
+ // ensure that the caller has permissions to update the index
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'update', conditions: [allowIfActive, allowIfAdmin] },
+ rawData,
+ );
+
+ // Validate input
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+ await validationService.ensureValid(rawData, updateSchema);
+
+ // For now, we assume that 'updatedBy' is always a user and not a group
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const { id, rev } = rawData;
+
+ // Prepare the db object
+ const dbObject = _.omit(this._fromRawToDbObject(rawData, { updatedBy: by }), ['rev']);
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .rev(rev)
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ // There are two scenarios here:
+ // 1 - The indexes does not exist
+ // 2 - The "rev" does not match
+ // We will display the appropriate error message accordingly
+ const existing = await this.find(requestContext, { id, fields: ['id', 'updatedBy'] });
+ if (existing) {
+ throw this.boom.badRequest(
+ `indexes information changed by "${
+ (existing.updatedBy || {}).username
+ }" just before your request is processed, please try again`,
+ true,
+ );
+ }
+ throw this.boom.notFound(`indexes with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'update-index', body: result });
+
+ return result;
+ }
+
+ async delete(requestContext, { id }) {
+ // ensure that the caller has permissions to update the index
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'delete', conditions: [allowIfActive, allowIfAdmin] },
+ { id },
+ );
+
+ // Lets now remove the item from the database
+ const result = await runAndCatch(
+ async () => {
+ return this._deleter()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .delete();
+ },
+ async () => {
+ throw this.boom.notFound(`indexes with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'delete-index', body: { id } });
+
+ return result;
+ }
+
+ async list(requestContext, { fields = [] } = {}) {
+ const restrict =
+ isExternalGuest(requestContext) || isExternalResearcher(requestContext) || isInternalGuest(requestContext);
+
+ if (restrict) return [];
+
+ // Future task: add further checks
+
+ // Remember doing a scan is not a good idea if you billions of rows
+ return this._scanner()
+ .limit(1000)
+ .projection(fields)
+ .scan();
+ }
+
+ // Do some properties renaming to prepare the object to be saved in the database
+ _fromRawToDbObject(rawObject, overridingProps = {}) {
+ const dbObject = { ...rawObject, ...overridingProps };
+ return dbObject;
+ }
+
+ // Do some properties renaming to restore the object that was saved in the database
+ _fromDbToDataObject(rawDb, overridingProps = {}) {
+ if (_.isNil(rawDb)) return rawDb; // important, leave this if statement here, otherwise, your update methods won't work correctly
+ if (!_.isObject(rawDb)) return rawDb;
+
+ const dataObject = { ...rawDb, ...overridingProps };
+ return dataObject;
+ }
+
+ async assertAuthorized(requestContext, { action, conditions }, ...args) {
+ const authorizationService = await this.service('authorizationService');
+
+ // The "authorizationService.assertAuthorized" below will evaluate permissions by calling the "conditions" functions first
+ // It will then give a chance to all registered plugins (if any) to perform their authorization.
+ // The plugins can even override the authorization decision returned by the conditions
+ // See "authorizationService.authorize" method for more details
+ await authorizationService.assertAuthorized(
+ requestContext,
+ { extensionPoint: 'index-authz', action, conditions },
+ ...args,
+ );
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+module.exports = IndexesService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/plugins/authorization-plugin.js b/addons/addon-base-raas/packages/base-raas-services/lib/plugins/authorization-plugin.js
new file mode 100644
index 0000000000..d0b4032c56
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/plugins/authorization-plugin.js
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+const authorizationPluginFactory = require('@aws-ee/base-services/lib/authorization/authorization-plugin-factory');
+
+module.exports = authorizationPluginFactory('addon/raas/authorizers');
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/project/project-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/project/project-service.js
new file mode 100644
index 0000000000..8e3535574b
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/project/project-service.js
@@ -0,0 +1,245 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+const { allowIfActive, allowIfAdmin } = require('@aws-ee/base-services/lib/authorization/authorization-utils');
+
+const { isExternalGuest, isExternalResearcher, isInternalGuest } = require('../helpers/is-role');
+const createSchema = require('../schema/create-project');
+const updateSchema = require('../schema/update-project');
+
+const settingKeys = {
+ tableName: 'dbTableProjects',
+};
+
+class ProjectService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'authorizationService', 'dbService', 'auditWriterService']);
+ }
+
+ async init() {
+ await super.init();
+ const [dbService] = await this.service(['dbService']);
+ const table = this.settings.get(settingKeys.tableName);
+
+ this._getter = () => dbService.helper.getter().table(table);
+ this._updater = () => dbService.helper.updater().table(table);
+ this._query = () => dbService.helper.query().table(table);
+ this._deleter = () => dbService.helper.deleter().table(table);
+ this._scanner = () => dbService.helper.scanner().table(table);
+ }
+
+ async find(requestContext, { id, fields = [] }) {
+ const restrict =
+ isExternalGuest(requestContext) || isExternalResearcher(requestContext) || isInternalGuest(requestContext);
+
+ if (restrict) return undefined;
+
+ // Future task: return undefined if the user is not associated with this project, unless they are admin
+
+ const result = await this._getter()
+ .key({ id })
+ .projection(fields)
+ .get();
+
+ return this._fromDbToDataObject(result);
+ }
+
+ async mustFind(requestContext, { id, fields = [] }) {
+ const result = await this.find(requestContext, { id, fields });
+ if (!result) throw this.boom.notFound(`project with id "${id}" does not exist`, true);
+ return result;
+ }
+
+ async create(requestContext, rawData) {
+ // ensure that the caller has permissions to create the index
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'create', conditions: [allowIfActive, allowIfAdmin] },
+ rawData,
+ );
+
+ // Validate input
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+ await validationService.ensureValid(rawData, createSchema);
+
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const id = rawData.id;
+
+ // Prepare the db object
+ const dbObject = this._fromRawToDbObject(rawData, { rev: 0, createdBy: by, updatedBy: by });
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_not_exists(id)') // yes we need this
+ .key({ id })
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`project with id "${id}" already exists`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-project', body: result });
+
+ return result;
+ }
+
+ async update(requestContext, rawData) {
+ // ensure that the caller has permissions to update the index
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'update', conditions: [allowIfActive, allowIfAdmin] },
+ rawData,
+ );
+
+ // Validate input
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+ await validationService.ensureValid(rawData, updateSchema);
+
+ // For now, we assume that 'updatedBy' is always a user and not a group
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const { id, rev } = rawData;
+
+ // Prepare the db object
+ const dbObject = _.omit(this._fromRawToDbObject(rawData, { updatedBy: by }), ['rev']);
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .rev(rev)
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ // There are two scenarios here:
+ // 1 - The project does not exist
+ // 2 - The "rev" does not match
+ // We will display the appropriate error message accordingly
+ const existing = await this.find(requestContext, { id, fields: ['id', 'updatedBy'] });
+ if (existing) {
+ throw this.boom.badRequest(
+ `project information changed by "${
+ (existing.updatedBy || {}).username
+ }" just before your request is processed, please try again`,
+ true,
+ );
+ }
+ throw this.boom.notFound(`project with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'update-project', body: result });
+
+ return result;
+ }
+
+ async delete(requestContext, { id }) {
+ // ensure that the caller has permissions to delete the index
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'delete', conditions: [allowIfActive, allowIfAdmin] },
+ { id },
+ );
+
+ // Lets now remove the item from the database
+ const result = await runAndCatch(
+ async () => {
+ return this._deleter()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .delete();
+ },
+ async () => {
+ throw this.boom.notFound(`project with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'delete-project', body: { id } });
+
+ return result;
+ }
+
+ async list(requestContext, { fields = [] } = {}) {
+ const restrict =
+ isExternalGuest(requestContext) || isExternalResearcher(requestContext) || isInternalGuest(requestContext);
+
+ if (restrict) return [];
+
+ // Future task: only return projects that the user has been associated with unless the user is an admin
+
+ // Remember doing a scan is not a good idea if you billions of rows
+ return this._scanner()
+ .limit(1000)
+ .projection(fields)
+ .scan();
+ }
+
+ // Do some properties renaming to prepare the object to be saved in the database
+ _fromRawToDbObject(rawObject, overridingProps = {}) {
+ const dbObject = { ...rawObject, ...overridingProps };
+ return dbObject;
+ }
+
+ // Do some properties renaming to restore the object that was saved in the database
+ _fromDbToDataObject(rawDb, overridingProps = {}) {
+ if (_.isNil(rawDb)) return rawDb; // important, leave this if statement here, otherwise, your update methods won't work correctly
+ if (!_.isObject(rawDb)) return rawDb;
+
+ const dataObject = { ...rawDb, ...overridingProps };
+ return dataObject;
+ }
+
+ async assertAuthorized(requestContext, { action, conditions }, ...args) {
+ const authorizationService = await this.service('authorizationService');
+
+ // The "authorizationService.assertAuthorized" below will evaluate permissions by calling the "conditions" functions first
+ // It will then give a chance to all registered plugins (if any) to perform their authorization.
+ // The plugins can even override the authorization decision returned by the conditions
+ // See "authorizationService.authorize" method for more details
+ await authorizationService.assertAuthorized(
+ requestContext,
+ { extensionPoint: 'project-authz', action, conditions },
+ ...args,
+ );
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+module.exports = ProjectService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-account.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-account.json
new file mode 100644
index 0000000000..4b35307695
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-account.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "accountName": {
+ "type": "string"
+ },
+ "accountEmail": {
+ "type": "string"
+ },
+ "masterRoleArn": {
+ "type": "string"
+ },
+ "externalId": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "required": ["accountName", "accountEmail", "masterRoleArn", "externalId"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-aws-accounts.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-aws-accounts.json
new file mode 100644
index 0000000000..ba3917b31d
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-aws-accounts.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 2048
+ },
+ "accountId": {
+ "type": "string",
+ "pattern": "^[0-9]{12}$"
+ },
+ "roleArn": {
+ "type": "string",
+ "minLength": 10
+ },
+ "externalId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "vpcId": {
+ "type": "string",
+ "pattern": "^vpc-[a-f0-9]{8,17}$"
+ },
+ "subnetId": {
+ "type": "string",
+ "pattern": "^subnet-[a-f0-9]{8,17}$"
+ },
+ "encryptionKeyArn": {
+ "type": "string",
+ "pattern": "^arn:aws:kms:.*$"
+ }
+ },
+ "required": ["name", "roleArn", "externalId", "accountId", "vpcId", "subnetId", "encryptionKeyArn"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-cost-api-cache.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-cost-api-cache.json
new file mode 100644
index 0000000000..75fa63ca55
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-cost-api-cache.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "indexId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "query": {
+ "type": "string"
+ },
+ "result": {
+ "type": "string"
+ }
+ },
+ "required": ["indexId", "query", "result"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-environment-keypair.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-environment-keypair.json
new file mode 100644
index 0000000000..1341463c52
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-environment-keypair.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string",
+ "maxLength": 200
+ },
+ "environmentId": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "environmentId"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-environment.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-environment.json
new file mode 100644
index 0000000000..ecfb64d9d9
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-environment.json
@@ -0,0 +1,68 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string",
+ "maxLength": 128,
+ "minLength": 3,
+ "pattern": "^[A-Za-z][A-Za-z0-9-]+$"
+ },
+ "platformId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "configurationId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 2048
+ },
+ "accountId": {
+ "type": "string",
+ "minLength": 12
+ },
+ "projectId": {
+ "type": "string"
+ },
+ "params": {
+ "type": "object",
+ "additionalProperties": true
+ },
+ "studyIds": {
+ "type": "array",
+ "items": [
+ {
+ "type": "string",
+ "minLength": 1
+ }
+ ]
+ },
+ "sharedWithUsers": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 3
+ },
+ "ns": {
+ "type": "string",
+ "minLength": 3
+ }
+ }
+ }
+ ],
+ "default": []
+ }
+ },
+ "required": ["name", "platformId", "configurationId"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-indexes.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-indexes.json
new file mode 100644
index 0000000000..cdc6348648
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-indexes.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "pattern": "^[A-Za-z0-9-_]+$"
+ },
+ "awsAccountId": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "awsAccountId"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-project.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-project.json
new file mode 100644
index 0000000000..0f3207af1e
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-project.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "pattern": "^[A-Za-z0-9-_]+$"
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 3000
+ },
+ "indexId": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "indexId"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-study.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-study.json
new file mode 100644
index 0000000000..20f1880ffe
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-study.json
@@ -0,0 +1,53 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "pattern": "^[A-Za-z0-9-_ ]+$"
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 2048
+ },
+ "category": {
+ "type": "string",
+ "enum": ["My Studies", "Organization", "Open Data"]
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 8192
+ },
+ "projectId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "pattern": "^[A-Za-z0-9-_]+$"
+ },
+ "uploadLocationEnabled": {
+ "type": "boolean"
+ },
+ "sha": {
+ "type": "string",
+ "maxLength": 64
+ },
+ "resources": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "arn": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "required": ["id", "category"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-user-roles.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-user-roles.json
new file mode 100644
index 0000000000..e394f9ee89
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-user-roles.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "pattern": "^[A-Za-z0-9-_ ]+$"
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 2048
+ },
+ "userType": {
+ "type": "string",
+ "enum": [
+ "INTERNAL", "EXTERNAL"
+ ]
+ }
+ },
+ "required": [
+ "id",
+ "userType"
+ ]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-user.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-user.json
new file mode 100644
index 0000000000..baaa7aa011
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/create-user.json
@@ -0,0 +1,77 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "pattern": "^[A-Za-z0-9-_.]+$|^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"
+ },
+ "usernameInIdp": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "pattern": "^[A-Za-z0-9-_.]+$|^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"
+ },
+ "password": {
+ "type": "string"
+ },
+ "authenticationProviderId": {
+ "type": "string"
+ },
+ "identityProviderName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"
+ },
+ "firstName": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500,
+ "pattern": "^[A-Za-z0-9 .-]+$"
+ },
+ "lastName": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500,
+ "pattern": "^[A-Za-z0-9 .-]+$"
+ },
+ "userType": {
+ "type": "string",
+ "enum": ["root"]
+ },
+ "isSamlAuthenticatedUser": {
+ "type": "boolean"
+ },
+ "isAdmin": {
+ "type": "boolean"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["active", "inactive", "pending"]
+ },
+ "rev": {
+ "type": "number"
+ },
+ "userRole": {
+ "type": "string"
+ },
+ "projectId": {
+ "type": "array"
+ },
+ "isExternalUser": {
+ "type": "boolean"
+ },
+ "encryptedCreds": {
+ "type": "string"
+ },
+ "applyReason": {
+ "type": "string"
+ }
+ },
+ "required": ["username"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/ensure-external-aws-accounts.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/ensure-external-aws-accounts.json
new file mode 100644
index 0000000000..ccc012dae3
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/ensure-external-aws-accounts.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "accountId": {
+ "type": "string",
+ "pattern": "^[0-9]{12}$"
+ }
+ },
+ "required": ["accountId"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-account.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-account.json
new file mode 100644
index 0000000000..febfb31c9f
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-account.json
@@ -0,0 +1,52 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "pattern": "^[A-Za-z0-9-_]+$"
+ },
+ "stackId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ },
+ "cfnInfo": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "vpcId": {
+ "type": "string",
+ "pattern": "^vpc-[a-f0-9]{8,17}$"
+ },
+ "subnetId": {
+ "type": "string",
+ "pattern": "^subnet-[a-f0-9]{8,17}$"
+ },
+ "crossAccountExecutionRoleArn": {
+ "type": "string",
+ "pattern": "^arn:aws:iam::.*$"
+ },
+ "stackId": {
+ "type": "string"
+ },
+ "encryptionKeyArn": {
+ "type": "string",
+ "pattern": "^arn:aws:kms:.*$"
+ }
+ }
+ },
+ "status": {
+ "type": "string",
+ "maxLength": 2048
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 2048
+ }
+ },
+ "required": ["id"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-aws-accounts.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-aws-accounts.json
new file mode 100644
index 0000000000..9f8266dad1
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-aws-accounts.json
@@ -0,0 +1,46 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "pattern": "^[A-Za-z0-9-_ ]+$"
+ },
+ "rev": {
+ "type": "number"
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 2048
+ },
+ "accountId": {
+ "type": "string",
+ "pattern": "^[0-9]{12}$"
+ },
+ "roleArn": {
+ "type": "string",
+ "minLength": 10
+ },
+ "externalId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "vpcId": {
+ "type": "string",
+ "pattern": "^vpc-[a-f0-9]{8,17}$"
+ },
+ "subnetId": {
+ "type": "string",
+ "pattern": "^subnet-[a-f0-9]{8,17}$"
+ }
+ },
+ "required": ["id", "rev", "identityPoolId", "userRoleAccess", "roleArn", "externalId"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-cost-api-cache.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-cost-api-cache.json
new file mode 100644
index 0000000000..9c68a907a1
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-cost-api-cache.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "indexId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "query": {
+ "type": "string"
+ },
+ "result": {
+ "type": "string"
+ }
+ },
+ "required": ["indexId", "query", "result"]
+}
\ No newline at end of file
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-environment.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-environment.json
new file mode 100644
index 0000000000..abc30d30c2
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-environment.json
@@ -0,0 +1,50 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "pattern": "^[A-Za-z0-9-_ ]+$"
+ },
+ "status": {
+ "type": "string",
+ "maxLength": 2048
+ },
+ "stackId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ },
+ "instanceInfo": {
+ "type": "object"
+ },
+ "sharedWithUsers": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 3
+ },
+ "ns": {
+ "type": "string",
+ "minLength": 3
+ }
+ }
+ }
+ ],
+ "default": []
+ },
+ "error": {
+ "type": "string",
+ "maxLength": 2048
+ }
+ },
+ "required": ["id"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-indexes.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-indexes.json
new file mode 100644
index 0000000000..3e02c74c38
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-indexes.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "pattern": "^[A-Za-z0-9-_ ]+$"
+ },
+ "rev": {
+ "type": "number"
+ },
+ "awsAccountId": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "rev"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-project.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-project.json
new file mode 100644
index 0000000000..724053cef3
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-project.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "pattern": "^[A-Za-z0-9-_ ]+$"
+ },
+ "rev": {
+ "type": "number"
+ },
+ "indexId": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 3000
+ }
+ },
+ "required": ["id", "rev", "indexId"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-study-permissions.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-study-permissions.json
new file mode 100644
index 0000000000..4c844e1aff
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-study-permissions.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "definitions": {
+ "principalIdentifier": {
+ "type": "object",
+ "properties": {
+ "ns": { "type": "string" },
+ "username": { "type": "string" }
+ },
+ "required": ["ns", "username"]
+ },
+ "permissionLevel": {
+ "type": "string",
+ "enum": ["admin", "readonly"]
+ },
+ "userEntry": {
+ "type": "object",
+ "properties": {
+ "principalIdentifier": { "$ref": "#/definitions/principalIdentifier" },
+ "permissionLevel": { "$ref": "#/definitions/permissionLevel" }
+ },
+ "required": ["principalIdentifier", "permissionLevel"]
+ }
+ },
+ "properties": {
+ "usersToAdd": {
+ "type": "array",
+ "items": { "$ref": "#/definitions/userEntry" }
+ },
+ "usersToRemove": {
+ "type": "array",
+ "items": { "$ref": "#/definitions/userEntry" }
+ }
+ }
+}
\ No newline at end of file
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-study.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-study.json
new file mode 100644
index 0000000000..a8f6c29c95
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-study.json
@@ -0,0 +1,43 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "pattern": "^[A-Za-z0-9-_ ]+$"
+ },
+ "rev": {
+ "type": "number"
+ },
+ "name": {
+ "type": "string",
+ "maxLength": 2048
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 8192
+ },
+ "sha": {
+ "type": "string",
+ "maxLength": 64
+ },
+ "resources": {
+ "type": "array",
+ "items": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "arn": {
+ "type": "string"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "required": ["id", "rev"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-user-roles.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-user-roles.json
new file mode 100644
index 0000000000..fd0e8de9c4
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-user-roles.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "pattern": "^[A-Za-z0-9-_ ]+$"
+ },
+ "rev": {
+ "type": "number"
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 2048
+ },
+ "userType": {
+ "type": "string",
+ "enum": [
+ "INTERNAL", "EXTERNAL"
+ ]
+ }
+ },
+ "required": [
+ "id",
+ "rev",
+ "userType"
+ ]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-user.json b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-user.json
new file mode 100644
index 0000000000..9e23450eeb
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/schema/update-user.json
@@ -0,0 +1,70 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "pattern": "^[A-Za-z0-9-_.]+$|^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"
+ },
+ "usernameInIdp": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "pattern": "^[A-Za-z0-9-_.]+$|^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"
+ },
+ "authenticationProviderId": {
+ "type": "string"
+ },
+ "identityProviderName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"
+ },
+ "firstName": {
+ "type": "string",
+ "maxLength": 500
+ },
+ "lastName": {
+ "type": "string",
+ "maxLength": 500
+ },
+ "userType": {
+ "type": "string",
+ "enum": ["root"]
+ },
+ "isSamlAuthenticatedUser": {
+ "type": "boolean"
+ },
+ "isAdmin": {
+ "type": "boolean"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["active", "inactive", "pending"]
+ },
+ "rev": {
+ "type": "number"
+ },
+ "userRole": {
+ "type": "string"
+ },
+ "projectId": {
+ "type": "array"
+ },
+ "isExternalUser": {
+ "type": "boolean"
+ },
+ "encryptedCreds": {
+ "type": "string"
+ },
+ "applyReason": {
+ "type": "string"
+ }
+ },
+ "required": ["username", "rev"]
+}
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/study/study-permission-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/study/study-permission-service.js
new file mode 100644
index 0000000000..5d4aad2a37
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/study/study-permission-service.js
@@ -0,0 +1,340 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const updateSchema = require('../schema/update-study-permissions');
+
+const settingKeys = {
+ tableName: 'dbTableStudyPermissions',
+};
+const keyPrefixes = {
+ study: 'Study:',
+ user: 'User:',
+};
+
+class StudyPermissionService extends Service {
+ constructor() {
+ super();
+ this.dependency(['dbService', 'jsonSchemaValidationService', 'lockService', 'userService']);
+ }
+
+ async init() {
+ // Get services
+ this.jsonSchemaValidationService = await this.service('jsonSchemaValidationService');
+ this.lockService = await this.service('lockService');
+ this.userService = await this.service('jsonSchemaValidationService');
+
+ // Setup DB helpers
+ const dbService = await this.service('dbService');
+ this.tableName = this.settings.get(settingKeys.tableName);
+ this._getter = () => dbService.helper.getter().table(this.tableName);
+ this._updater = () => dbService.helper.updater().table(this.tableName);
+ this._query = () => dbService.helper.query().table(this.tableName);
+ this._deleter = () => dbService.helper.deleter().table(this.tableName);
+ this._scanner = () => dbService.helper.scanner().table(this.tableName);
+ }
+
+ /**
+ * Public Methods
+ */
+ async findByStudy(requestContext, studyId, fields = []) {
+ const id = StudyPermissionService.getQualifiedKey(studyId, 'study');
+ const record = await this._getter()
+ .key({ id })
+ .projection(fields)
+ .get();
+
+ return StudyPermissionService.sanitizeStudyRecord(record);
+ }
+
+ async findByUser(requestContext, username, fields = []) {
+ const id = StudyPermissionService.getQualifiedKey(username, 'user');
+ return this._getter()
+ .key({ id })
+ .projection(fields)
+ .get();
+ }
+
+ async create(requestContext, studyId) {
+ let result;
+
+ // Build study record
+ const creator = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const studyRecord = {
+ id: StudyPermissionService.getQualifiedKey(studyId, 'study'),
+ recordType: 'study',
+ adminUsers: [creator],
+ readonlyUsers: [],
+ createdBy: creator,
+ };
+
+ // Create DB records
+ await Promise.all([
+ // Create/Update user record
+ this.upsertUserRecord(requestContext, {
+ studyId,
+ principalIdentifier: creator,
+ addOrRemove: 'add',
+ permissionLevel: 'admin',
+ }),
+
+ // Create study record
+ runAndCatch(
+ async () => {
+ result = await this._updater()
+ .condition('attribute_not_exists(id)') // Error if already exists
+ .key({ id: studyRecord.id })
+ .item(studyRecord)
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`Permission record for study with id "${studyRecord.id}" already exists`, true);
+ },
+ ),
+ ]);
+
+ // Return study record
+ return StudyPermissionService.sanitizeStudyRecord(result);
+ }
+
+ async update(requestContext, studyId, updateRequest) {
+ let result;
+
+ // Validate input
+ await this.jsonSchemaValidationService.ensureValid(updateRequest, updateSchema);
+
+ // Create DB lock before pulling existing record
+ const lockKey = this.getLockKey(studyId, 'study');
+
+ await this.lockService.tryWriteLockAndRun({ id: lockKey }, async () => {
+ // Get existing record
+ const studyRecord = await this.findByStudy(requestContext, studyId);
+
+ // Build updated study record
+ const updater = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ studyRecord.updatedBy = updater;
+
+ const filterByLevel = filterLevel => userEntry => userEntry.permissionLevel === filterLevel;
+ ['admin', 'readonly'].forEach(permissionLevel => {
+ studyRecord[`${permissionLevel}Users`] = _.pullAllWith(
+ // Existing/Added users
+ _.unionWith(
+ studyRecord[`${permissionLevel}Users`],
+ updateRequest.usersToAdd
+ .filter(filterByLevel(permissionLevel))
+ .map(userEntry => userEntry.principalIdentifier),
+ _.isEqual,
+ ),
+
+ // Removed users
+ updateRequest.usersToRemove
+ .filter(filterByLevel(permissionLevel))
+ .map(userEntry => userEntry.principalIdentifier),
+ _.isEqual,
+ );
+ });
+
+ // Halt the update if all admins have been removed
+ if (studyRecord.adminUsers.length < 1) {
+ throw this.boom.badRequest('At least one Admin must be assigned to the study', true);
+ }
+
+ // Update DB records
+ result = await Promise.all([
+ // Update study record
+ this._updater()
+ .key({ id: StudyPermissionService.getQualifiedKey(studyRecord.id, 'study') })
+ .item(studyRecord)
+ .update(),
+
+ // Update user records
+ ...updateRequest.usersToAdd.map(userEntry =>
+ this.upsertUserRecord(requestContext, {
+ studyId,
+ principalIdentifier: userEntry.principalIdentifier,
+ addOrRemove: 'add',
+ permissionLevel: userEntry.permissionLevel,
+ }),
+ ),
+ ...updateRequest.usersToRemove.map(userEntry =>
+ this.upsertUserRecord(requestContext, {
+ studyId,
+ principalIdentifier: userEntry.principalIdentifier,
+ addOrRemove: 'remove',
+ permissionLevel: userEntry.permissionLevel,
+ }),
+ ),
+ ]);
+ });
+
+ // Return study record
+ return StudyPermissionService.sanitizeStudyRecord(result[0]);
+ }
+
+ async delete(requestContext, studyId) {
+ let result;
+
+ // Create DB lock before pulling existing record
+ const lockKey = this.getLockKey(studyId, 'study');
+ await this.lockService.tryWriteLockAndRun({ id: lockKey }, async () => {
+ // Get record
+ const studyRecord = await this.findByStudy(requestContext, studyId);
+
+ // Delete
+ result = await Promise.all([
+ // Delete study record
+ this._deleter()
+ .key({ id: StudyPermissionService.getQualifiedKey(studyId, 'study') })
+ .delete(),
+
+ // Remove study from user records
+ studyRecord.adminUsers.map(async principalIdentifier =>
+ this.upsertUserRecord(requestContext, {
+ studyId,
+ principalIdentifier,
+ addOrRemove: 'remove',
+ permissionLevel: 'admin',
+ }),
+ ),
+
+ studyRecord.readonlyUsers.map(async principalIdentifier =>
+ this.upsertUserRecord(requestContext, {
+ studyId,
+ principalIdentifier,
+ addOrRemove: 'remove',
+ permissionLevel: 'readonly',
+ }),
+ ),
+ ]);
+ });
+
+ // Return study record
+ return StudyPermissionService.sanitizeStudyRecord(result[0]);
+ }
+
+ async getRequestorPermissions(requestContext) {
+ return this.findByUser(requestContext, requestContext.principalIdentifier.username, [
+ 'adminAccess',
+ 'readonlyAccess',
+ ]);
+ }
+
+ async verifyRequestorAccess(requestContext, studyId, action) {
+ const mutatingActions = ['POST', 'PUT', 'PATCH', 'DELETE'];
+ const nonMutatingActions = ['GET'];
+ const notFoundError = this.boom.notFound(`Study with id "${studyId}" does not exist`, true);
+ const forbiddenError = this.boom.forbidden();
+
+ // Ensure a valid action was passed
+ if (!mutatingActions.concat(nonMutatingActions).includes(action)) {
+ throw this.boom.internalError(`Invalid action passed to verifyRequestorAccess(): ${action}`);
+ }
+
+ // Get user permissions
+ const permissions = await this.getRequestorPermissions(requestContext);
+ if (!permissions) {
+ throw notFoundError;
+ }
+
+ // Check whether user has any access
+ const hasAdminAccess = permissions.adminAccess.some(accessibleId => accessibleId === studyId);
+ const hasReadonlyAccess = permissions.readonlyAccess.some(accessibleId => accessibleId === studyId);
+ if (!(hasAdminAccess || hasReadonlyAccess)) {
+ throw notFoundError;
+ }
+
+ // Deny mutating actions to non-admin users
+ if (!hasAdminAccess && mutatingActions.includes(action)) {
+ throw forbiddenError;
+ }
+ }
+
+ getEmptyUserPermissions() {
+ return { adminAccess: [], readonlyAccess: [] };
+ }
+
+ /**
+ * Private Methods
+ */
+ async upsertUserRecord(requestContext, { studyId, principalIdentifier, addOrRemove, permissionLevel }) {
+ // Create DB lock before pulling existing record
+ let result;
+ const lockKey = this.getLockKey(principalIdentifier.username, 'user');
+ await this.lockService.tryWriteLockAndRun({ id: lockKey }, async () => {
+ // Check for existing record; build new record if necessary
+ let record = await this.findByUser(requestContext, principalIdentifier.username);
+ if (!record) {
+ record = {
+ id: StudyPermissionService.getQualifiedKey(principalIdentifier.username, 'user'),
+ recordType: 'user',
+ principalIdentifier,
+ ...this.getEmptyUserPermissions(),
+ };
+ }
+
+ // Deterrmine permission level
+ if (!['admin', 'readonly'].includes(permissionLevel)) {
+ throw this.boom.internalError('Bad permission level passed to _upsertUserRecord:', permissionLevel);
+ }
+ const permissionLevelKey = `${permissionLevel}Access`;
+
+ // Add or remove permissions from user record
+ switch (addOrRemove) {
+ case 'add':
+ record[permissionLevelKey] = _.union(record[permissionLevelKey], [studyId]);
+ break;
+ case 'remove':
+ _.pull(record[permissionLevelKey], studyId);
+ break;
+ default:
+ throw this.boom.internalError('Badd addOrRemove value passed to _upsertUserRecord:', addOrRemove);
+ }
+
+ // Update database
+ result = await this._updater()
+ .key({ id: record.id })
+ .item(record)
+ .update();
+ });
+
+ return result;
+ }
+
+ static getQualifiedKey(studyOrUserId, recordType) {
+ // recordType must be 'study' or 'user'
+ if (!(recordType in keyPrefixes)) {
+ throw this.boom.internalError('Bad record type passed to getQualifiedKey:', recordType);
+ }
+
+ return `${keyPrefixes[recordType]}${studyOrUserId}`;
+ }
+
+ static sanitizeStudyRecord(record) {
+ // Delete recordType and remove key prefix since they're just used for
+ // internal indexing
+ delete record.recordType;
+ record.id = record.id.slice(keyPrefixes.study.length);
+ return record;
+ }
+
+ getLockKey(studyOrUserId, recordType) {
+ return `${this.tableName}|${StudyPermissionService.getQualifiedKey(studyOrUserId, recordType)}`;
+ }
+}
+
+module.exports = StudyPermissionService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/study/study-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/study/study-service.js
new file mode 100644
index 0000000000..bb596f5d09
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/study/study-service.js
@@ -0,0 +1,383 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const { buildTaggingXml } = require('../helpers/aws-tags');
+const createSchema = require('../schema/create-study');
+const updateSchema = require('../schema/update-study');
+
+const settingKeys = {
+ tableName: 'dbTableStudies',
+ categoryIndexName: 'dbTableStudiesCategoryIndex',
+ studyDataBucketName: 'studyDataBucketName',
+};
+
+class StudyService extends Service {
+ constructor() {
+ super();
+ this.dependency([
+ 'aws',
+ 'jsonSchemaValidationService',
+ 'dbService',
+ 'studyPermissionService',
+ 'projectService',
+ 'auditWriterService',
+ ]);
+ }
+
+ async init() {
+ await super.init();
+ const [aws, dbService, studyPermissionService] = await this.service(['aws', 'dbService', 'studyPermissionService']);
+ this.s3Client = new aws.sdk.S3();
+ this.studyPermissionService = studyPermissionService;
+
+ const table = this.settings.get(settingKeys.tableName);
+ this._getter = () => dbService.helper.getter().table(table);
+ this._updater = () => dbService.helper.updater().table(table);
+ this._query = () => dbService.helper.query().table(table);
+ this._deleter = () => dbService.helper.deleter().table(table);
+ this._scanner = () => dbService.helper.scanner().table(table);
+
+ this.categoryIndex = this.settings.get(settingKeys.categoryIndexName);
+ this.studyDataBucket = this.settings.get(settingKeys.studyDataBucketName);
+ }
+
+ /**
+ * Public Methods
+ */
+ async find(requestContext, id, fields = []) {
+ const result = await this._getter()
+ .key({ id })
+ .projection(fields)
+ .get();
+
+ return this.fromDbToDataObject(result);
+ }
+
+ async mustFind(requestContext, id, fields = []) {
+ const result = await this.find(requestContext, id, fields);
+ if (!result) throw this.notFoundError(id);
+ return result;
+ }
+
+ async create(requestContext, rawData) {
+ const [validationService, projectService] = await this.service(['jsonSchemaValidationService', 'projectService']);
+
+ // Validate input
+ await validationService.ensureValid(rawData, createSchema);
+
+ // The open data studies do not need to be associated to any project
+ // for everything else make sure projectId is specified
+ if (rawData.category !== 'Open Data') {
+ if (!rawData.projectId) {
+ throw this.boom.badRequest('Missing required projectId');
+ }
+ await projectService.mustFind(requestContext, { id: rawData.projectId });
+ }
+
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const id = rawData.id;
+
+ // Prepare the db object
+ const dbObject = this.fromRawToDbObject(rawData, { rev: 0, createdBy: by, updatedBy: by });
+
+ // Create file upload location if necessary
+ let studyFileLocation;
+ if (rawData.uploadLocationEnabled) {
+ if (!dbObject.resources) {
+ dbObject.resources = [];
+ }
+ studyFileLocation = this.getFilesPrefix(requestContext, id, rawData.category);
+ dbObject.resources.push({ arn: `arn:aws:s3:::${this.studyDataBucket}/${studyFileLocation}` });
+ }
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_not_exists(id)') // Error if already exists
+ .key({ id })
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`study with id "${id}" already exists`, true);
+ },
+ );
+
+ // Create a zero-byte object for the study in the study bucket if requested
+ if (rawData.uploadLocationEnabled) {
+ await this.s3Client
+ .putObject({
+ Bucket: this.studyDataBucket,
+ Key: studyFileLocation,
+ // ServerSideEncryption: 'aws:kms', // Not required as S3 bucket has default encryption specified
+ Tagging: `projectId=${rawData.projectId}`,
+ })
+ .promise();
+ }
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-study', body: result });
+
+ return result;
+ }
+
+ async update(requestContext, rawData) {
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+
+ // Validate input
+ await validationService.ensureValid(rawData, updateSchema);
+
+ // For now, we assume that 'updatedBy' is always a user and not a group
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const { id, rev } = rawData;
+
+ // Prepare the db object
+ const dbObject = _.omit(this.fromRawToDbObject(rawData, { updatedBy: by }), ['rev']);
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .rev(rev)
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ // There are two scenarios here:
+ // 1 - The study does not exist
+ // 2 - The "rev" does not match
+ // We will display the appropriate error message accordingly
+ const existing = await this.find(requestContext, id, ['id', 'updatedBy']);
+ if (existing) {
+ throw this.boom.badRequest(
+ `study information changed by "${
+ (existing.updatedBy || {}).username
+ }" just before your request is processed, please try again`,
+ true,
+ );
+ }
+ throw this.boom.notFound(`study with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'update-study', body: result });
+
+ return result;
+ }
+
+ async delete(requestContext, id) {
+ // Lets now remove the item from the database
+ const result = await runAndCatch(
+ async () => {
+ return this._deleter()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .delete();
+ },
+ async () => {
+ throw this.boom.notFound(`study with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'delete-study', body: { id } });
+
+ return result;
+ }
+
+ async list(requestContext, category, fields = []) {
+ // Get studies allowed for user
+ let result = [];
+ switch (category) {
+ case 'Open Data':
+ // Readable by all
+ result = this._query()
+ .index(this.categoryIndex)
+ .key('category', category)
+ .limit(1000)
+ .projection(fields)
+ .query();
+ break;
+
+ default: {
+ // Generate results based on access
+ const permissions = await this.studyPermissionService.getRequestorPermissions(requestContext);
+ if (permissions) {
+ // We can't give duplicate keys to the batch get, so ensure that allowedStudies is unique
+ const allowedStudies = _.uniq(permissions.adminAccess.concat(permissions.readonlyAccess));
+ if (allowedStudies.length) {
+ const rawResult = await this._getter()
+ .keys(allowedStudies.map(studyId => ({ id: studyId })))
+ .projection(fields)
+ .get();
+
+ // Filter by category and inject requestor's access level
+ const studyAccessMap = {};
+ ['admin', 'readonly'].forEach(level =>
+ permissions[`${level}Access`].forEach(studyId => {
+ studyAccessMap[studyId] = level;
+ }),
+ );
+ result = rawResult
+ .filter(study => study.category === category)
+ .map(study => ({
+ ...study,
+ access: studyAccessMap[study.id],
+ }));
+ }
+ }
+ }
+ }
+
+ // Return result
+ return result;
+ }
+
+ /**
+ * Creates a presigned post URL and form fields for use in uploading objects to S3 from the browser.
+ * Note: In order for browser uplaod to work, the destination bucket will need to have an appropriate CORS policy configured.
+ * @see https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html
+ *
+ * @param {Object} requestContext the request context provided by @aws-ee/base-services-container/lib/request-context
+ * @param {string} studyId the ID of the study in which the uploaded files should be shored.
+ * @param {string} filenames the filenames that will be used for the upload.
+ * @param {CreatePresignedPostOptions} options additional options.
+ *
+ * @typedef {Object} CreatePresignedPostOptions
+ * @property {boolean} [multiPart] set to true to allow multipart uploading.
+ *
+ * @returns {Promise} the url and fields to use when performing the upload
+ */
+ async createPresignedPostRequests(requestContext, studyId, filenames, encrypt = true, multiPart = true) {
+ // Get study details and check permissinos
+ const study = await this.mustFind(requestContext, studyId);
+
+ // Loop through requested files and generate presigned POST requests
+ const prefix = this.getFilesPrefix(requestContext, study.id, study.category);
+ return Promise.all(
+ filenames.map(filename => {
+ // Prep request
+ /** @type {AWS.S3.PresignedPost.Params} */
+ const params = { Bucket: this.studyDataBucket, Fields: { key: `${prefix}${filename}` } };
+ if (multiPart) {
+ params.Fields.enctype = 'multipart/form-data';
+ }
+ if (encrypt) {
+ // Nothing to do here because the S3 bucket has default encryption specified and has policy that denies any
+ // uploads not using default encryption
+ // If S3 bucket did not enforce default encryption using aws:kms then we must specify
+ // 'x-amz-server-side-encryption' and 'x-amz-server-side-encryption-aws-kms-key-id' here as follows
+ //
+ // params.Fields['x-amz-server-side-encryption'] = 'aws:kms';
+ // Specify the KMS Key ID here for encryption
+ // https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
+ // params.Fields['x-amz-server-side-encryption-aws-kms-key-id'] = arn of the StudyDataEncryptionKey
+ }
+ params.Fields.tagging = buildTaggingXml({
+ uploadedBy: requestContext.principal.username,
+ projectId: study.projectId,
+ });
+
+ // s3.createPresignedPost does not expose a `.promise()` method like other AWS SDK APIs.
+ // Since we want to expose the operation as an asynchronous operation, we have to manually wrap it in a Promise.
+ return new Promise((resolve, reject) =>
+ this.s3Client.createPresignedPost(params, (err, data) => {
+ if (err) {
+ return reject(err);
+ }
+ return resolve(data);
+ }),
+ );
+ }),
+ ).then(requests =>
+ requests.reduce(
+ (allRequests, currRequest, currIdx) => ({
+ // Convert presigned request data to an object key -> data map
+ ...allRequests,
+ [filenames[currIdx]]: currRequest,
+ }),
+ {},
+ ),
+ );
+ }
+
+ async listFiles(requestContext, studyId) {
+ // TODO: Add pagination
+ const study = await this.mustFind(requestContext, studyId, ['category']);
+ const prefix = await this.getFilesPrefix(requestContext, studyId, study.category);
+ const params = {
+ Bucket: this.studyDataBucket,
+ Prefix: prefix,
+ };
+
+ // Return results, removing zero-byte prefix object and only including certain fields
+ return (await this.s3Client.listObjectsV2(params).promise()).Contents.filter(object => object.Key !== prefix).map(
+ object => ({
+ filename: object.Key.slice(prefix.length),
+ size: object.Size,
+ lastModified: object.LastModified,
+ }),
+ );
+ }
+
+ /**
+ * Private Methods
+ */
+ getFilesPrefix(requestContext, studyId, studyCategory) {
+ if (studyCategory === 'My Studies') {
+ return `users/${requestContext.principal.username}/${studyId}/`;
+ }
+ return `studies/${studyCategory}/${studyId}/`;
+ }
+
+ notFoundError(studyId) {
+ return this.boom.notFound(`Study with id "${studyId}" does not exist`, true);
+ }
+
+ // Do some properties renaming to prepare the object to be saved in the database
+ fromRawToDbObject(rawObject, overridingProps = {}) {
+ const dbObject = { ...rawObject, ...overridingProps };
+ return dbObject;
+ }
+
+ // Do some properties renaming to restore the object that was saved in the database
+ fromDbToDataObject(rawDb, overridingProps = {}) {
+ if (_.isNil(rawDb)) return rawDb; // important, leave this if statement here, otherwise, your update methods won't work correctly
+ if (!_.isObject(rawDb)) return rawDb;
+
+ const dataObject = { ...rawDb, ...overridingProps };
+ return dataObject;
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+module.exports = StudyService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/user-roles/user-roles-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/user-roles/user-roles-service.js
new file mode 100644
index 0000000000..ad8cad760c
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/user-roles/user-roles-service.js
@@ -0,0 +1,230 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { allowIfActive, allowIfAdmin } = require('@aws-ee/base-services/lib/authorization/authorization-utils');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const createSchema = require('../schema/create-user-roles');
+const updateSchema = require('../schema/update-user-roles');
+
+const settingKeys = {
+ tableName: 'dbTableUserRoles',
+};
+
+class UserRolesService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'dbService', 'authorizationService', 'auditWriterService']);
+ }
+
+ async init() {
+ await super.init();
+ const [dbService] = await this.service(['dbService']);
+ const table = this.settings.get(settingKeys.tableName);
+
+ this._getter = () => dbService.helper.getter().table(table);
+ this._updater = () => dbService.helper.updater().table(table);
+ this._query = () => dbService.helper.query().table(table);
+ this._deleter = () => dbService.helper.deleter().table(table);
+ this._scanner = () => dbService.helper.scanner().table(table);
+ }
+
+ async find(requestContext, { id, fields = [] }) {
+ const result = await this._getter()
+ .key({ id })
+ .projection(fields)
+ .get();
+
+ return this._fromDbToDataObject(result);
+ }
+
+ async mustFind(requestContext, { id, fields = [] }) {
+ const result = await this.find(requestContext, { id, fields });
+ if (!result) throw this.boom.notFound(`userRoles with id "${id}" does not exist`, true);
+ return result;
+ }
+
+ async create(requestContext, userRole) {
+ // ensure that the caller has permissions to create the user role
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'create', conditions: [allowIfActive, allowIfAdmin] },
+ userRole,
+ );
+
+ // Validate input
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+ await validationService.ensureValid(userRole, createSchema);
+
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const { id } = userRole;
+
+ // Prepare the db object
+ const dbObject = this._fromRawToDbObject(userRole, { rev: 0, createdBy: by, updatedBy: by });
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_not_exists(id)') // yes we need this
+ .key({ id })
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`userRoles with id "${id}" already exists`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-user-role', body: result });
+
+ return result;
+ }
+
+ async update(requestContext, rawData) {
+ // ensure that the caller has permissions to update the user role information
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'update', conditions: [allowIfActive, allowIfAdmin] },
+ rawData,
+ );
+
+ // Validate input
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+ await validationService.ensureValid(rawData, updateSchema);
+
+ // For now, we assume that 'updatedBy' is always a user and not a group
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const { id, rev } = rawData;
+
+ // Prepare the db object
+ const dbObject = _.omit(this._fromRawToDbObject(rawData, { updatedBy: by }), ['rev']);
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .rev(rev)
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ // There are two scenarios here:
+ // 1 - The userroles does not exist
+ // 2 - The "rev" does not match
+ // We will display the appropriate error message accordingly
+ const existing = await this.find(requestContext, { id, fields: ['id', 'updatedBy'] });
+ if (existing) {
+ throw this.boom.badRequest(
+ `userRoles information changed by "${
+ (existing.updatedBy || {}).username
+ }" just before your request is processed, please try again`,
+ true,
+ );
+ }
+ throw this.boom.notFound(`userRoles with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'update-user-role', body: result });
+
+ return result;
+ }
+
+ async delete(requestContext, { id }) {
+ // ensure that the caller has permissions to delete the user role
+ // Perform default condition checks to make sure the user is active and is admin
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'delete', conditions: [allowIfActive, allowIfAdmin] },
+ { id },
+ );
+
+ // Lets now remove the item from the database
+ const result = await runAndCatch(
+ async () => {
+ return this._deleter()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .delete();
+ },
+ async () => {
+ throw this.boom.notFound(`userRoles with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'delete-user-role', body: { id } });
+
+ return result;
+ }
+
+ async list({ fields = [] } = {}) {
+ // Remember doing a scanning is not a good idea if you billions of rows
+ return this._scanner()
+ .limit(1000)
+ .projection(fields)
+ .scan();
+ }
+
+ // Do some properties renaming to prepare the object to be saved in the database
+ _fromRawToDbObject(rawObject, overridingProps = {}) {
+ const dbObject = { ...rawObject, ...overridingProps };
+ return dbObject;
+ }
+
+ // Do some properties renaming to restore the object that was saved in the database
+ _fromDbToDataObject(rawDb, overridingProps = {}) {
+ if (_.isNil(rawDb)) return rawDb; // important, leave this if statement here, otherwise, your update methods won't work correctly
+ if (!_.isObject(rawDb)) return rawDb;
+
+ const dataObject = { ...rawDb, ...overridingProps };
+ return dataObject;
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+
+ async assertAuthorized(requestContext, { action, conditions }, ...args) {
+ const authorizationService = await this.service('authorizationService');
+
+ // The "authorizationService.assertAuthorized" below will evaluate permissions by calling the "conditions" functions first
+ // It will then give a chance to all registered plugins (if any) to perform their authorization.
+ // The plugins can even override the authorization decision returned by the conditions
+ // See "authorizationService.authorize" method for more details
+ await authorizationService.assertAuthorized(
+ requestContext,
+ { extensionPoint: 'user-role-management-authz', action, conditions },
+ ...args,
+ );
+ }
+}
+
+module.exports = UserRolesService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/user/helpers/user-authz-utils.js b/addons/addon-base-raas/packages/base-raas-services/lib/user/helpers/user-authz-utils.js
new file mode 100644
index 0000000000..ce7390630f
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/user/helpers/user-authz-utils.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.
+ */
+
+const _ = require('lodash');
+const { allow, deny } = require('@aws-ee/base-services/lib/authorization/authorization-utils');
+
+function allowIfHasRole(requestContext, { action, resource }, allowedUserRoles) {
+ const userRole = _.get(requestContext, 'principal.userRole');
+ if (!_.includes(allowedUserRoles, userRole)) {
+ const resourceDisplayName = resource || 'resource';
+ return deny(
+ `Cannot ${action} ${resourceDisplayName}. The user's role "${userRole}" is not allowed to ${action} ${resourceDisplayName}`,
+ false,
+ );
+ }
+ return allow();
+}
+
+module.exports = {
+ allowIfHasRole,
+};
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/user/user-attributes-mapper-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/user/user-attributes-mapper-service.js
new file mode 100644
index 0000000000..cd56226ac6
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/user/user-attributes-mapper-service.js
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+const BaseAttribMapperService = require('@aws-ee/base-api-services/lib/authentication-providers/built-in-providers/cogito-user-pool/user-attributes-mapper-service');
+
+class UserAttributesMapperService extends BaseAttribMapperService {
+ mapAttributes(decodedToken) {
+ const userAttributes = super.mapAttributes(decodedToken);
+ // For RaaS solution, the user's email address should be used as his/her username
+ // so set username to be email address
+ userAttributes.username = userAttributes.email;
+
+ return userAttributes;
+ }
+}
+
+module.exports = UserAttributesMapperService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/user/user-authz-plugin.js b/addons/addon-base-raas/packages/base-raas-services/lib/user/user-authz-plugin.js
new file mode 100644
index 0000000000..601ae34530
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/user/user-authz-plugin.js
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+const authorizationPluginFactory = require('@aws-ee/base-services/lib/authorization/authorization-plugin-factory');
+
+module.exports = authorizationPluginFactory('raasUserAuthzService');
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/user/user-authz-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/user/user-authz-service.js
new file mode 100644
index 0000000000..91fb852581
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/user/user-authz-service.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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const {
+ isDeny,
+ allowIfActive,
+ allowIfAdmin,
+ allowIfCurrentUserOrAdmin,
+ allowIfRoot,
+ allow,
+ deny,
+} = require('@aws-ee/base-services/lib/authorization/authorization-utils');
+const { toUserNamespace } = require('@aws-ee/base-services/lib/user/helpers/user-namespace');
+
+class UserAuthzService extends Service {
+ async authorize(requestContext, { resource, action, effect, reason }, ...args) {
+ const permissionSoFar = { resource, action, effect, reason };
+
+ // in base-raas-services authorization logic needs to be customized only for "createBulk", "update", and
+ // "updateAttributes" action, for all other actions let it return permissions evaluated by base authorization plugin
+ // See "raas/addons/addon-base/packages/services/lib/plugins/authorization-plugin.js" for base authorization plugin implementation and
+ // See "raas/addons/addon-base/packages/services/lib/user/user-authorization-service.js" that implements base authorization logic for "user" resource
+ switch (action) {
+ case 'createBulk':
+ // if effect is "deny" already (due to any of the previous plugins returning "deny") then return "deny" right away
+ if (isDeny({ effect })) return permissionSoFar;
+ return this.authorizeCreateBulk(requestContext, { action, effect }, ...args);
+ case 'update':
+ // For "update", DO NOT return "deny" if other plugins returned deny, instead use our own authorization logic.
+ // This is because, the base authorization impl denies "update" by non-active users but we need to allow
+ // self-update in "pending" status to support self-enrollment application feature
+ return this.authorizeUpdate(requestContext, { action, effect }, ...args);
+ case 'updateAttributes':
+ // Just like the "update" case, DO NOT return "deny" if other plugins returned deny, instead use our own authorization logic.
+ return this.authorizeUpdateAttributes(requestContext, { resource, action, effect, reason }, ...args);
+ default:
+ return permissionSoFar;
+ }
+ }
+
+ async authorizeUpdate(requestContext, { action, effect }, user) {
+ // Allow update to "pending" status even if the caller is inactive to support self-enrollment application
+ let permissionSoFar = { action, effect };
+ if (user.status !== 'pending') {
+ // When updating user's status to anything other than "pending" make sure the caller is active
+ // Make sure the caller is active
+ permissionSoFar = await allowIfActive(requestContext, { action });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+ }
+
+ // User can update only their own attributes unless the user is an admin
+ const { username, authenticationProviderId, identityProviderName } = user;
+ const ns = toUserNamespace(authenticationProviderId, identityProviderName);
+ permissionSoFar = await allowIfCurrentUserOrAdmin(requestContext, { action }, { username, ns });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ return allow();
+ }
+
+ async authorizeCreateBulk(requestContext, { action }) {
+ // Make sure the caller is active
+ let permissionSoFar = await allowIfActive(requestContext, { action });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ // Only admins can create users in bulk by default
+ permissionSoFar = await allowIfAdmin(requestContext, { action });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ // If code reached here then allow this call
+ return allow();
+ }
+
+ async authorizeUpdateAttributes(requestContext, { action }, user, existingUser) {
+ const isBeingUpdated = attribName => {
+ const oldValue = _.get(existingUser, attribName);
+ const newValue = _.get(user, attribName);
+ // The update ignores undefined values during update (i.e., it retains existing values for those)
+ // so compare for only if the new value is undefined
+ return !_.isUndefined(newValue) && oldValue !== newValue;
+ };
+
+ let permissionSoFar;
+ // In addition to the permissions ascertained by the base class,
+ // make sure that we allow updating "userRole" only by admins
+ if (isBeingUpdated('isExternalUser') || isBeingUpdated('userRole')) {
+ // The "isExternalUser" and "userRole" properties should be updated only by admins
+ permissionSoFar = await allowIfAdmin(requestContext, { action });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+ }
+
+ // Similarly, in addition to the permissions ascertained by the base,
+ // make sure the following properties on root are immutable
+ if (existingUser.userType === 'root') {
+ permissionSoFar = await allowIfRoot(requestContext, { action });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ if (
+ isBeingUpdated('authenticationProviderId') ||
+ isBeingUpdated('identityProviderName') ||
+ isBeingUpdated('isAdmin') ||
+ isBeingUpdated('userRole') ||
+ isBeingUpdated('projectId')
+ ) {
+ return deny('You are not authorized to alter these fields on the root user', true);
+ }
+ }
+
+ // If code reached here then allow this call
+ return allow();
+ }
+}
+
+module.exports = UserAuthzService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/lib/user/user-service.js b/addons/addon-base-raas/packages/base-raas-services/lib/user/user-service.js
new file mode 100644
index 0000000000..858761da1d
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/lib/user/user-service.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.
+ */
+
+const _ = require('lodash');
+const { ensureCurrentUser } = require('@aws-ee/base-services/lib/authorization/assertions');
+const { toUserNamespace } = require('@aws-ee/base-services/lib/user/helpers/user-namespace');
+const BaseUserService = require('@aws-ee/base-services/lib/user/user-service');
+const { processInBatches } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const createUserJsonSchema = require('../schema/create-user');
+const updateUserJsonSchema = require('../schema/update-user');
+
+class UserService extends BaseUserService {
+ constructor() {
+ super();
+ this.dependency(['userRolesService']);
+ }
+
+ /**
+ * Method to create users in bulk in the specified number of batches. The method will try to create users in parallel
+ * within a given batch but will not start new batch until a previous batch is complete.
+ *
+ * @param requestContext
+ * @param users
+ * @param defaultAuthNProviderId
+ * @param batchSize
+ * @returns {Promise}
+ */
+ async createUsers(requestContext, users, defaultAuthNProviderId, batchSize = 5) {
+ // ensure that the caller has permissions to create user
+ await this.assertAuthorized(requestContext, { action: 'createBulk' });
+
+ const errors = [];
+ let successCount = 0;
+ let errorCount = 0;
+ const createUser = async curUser => {
+ try {
+ const isAdmin = curUser.isAdmin === true;
+ const authenticationProviderId =
+ curUser.authenticationProviderId ||
+ defaultAuthNProviderId ||
+ requestContext.principal.authenticationProviderId;
+ const name = await this.toDefaultName(curUser.email);
+ const userType = await this.toUserType(requestContext, curUser.userRole);
+ const userToCreate = {
+ firstName: _.isEmpty(curUser.firstName) ? name : curUser.firstName,
+ lastName: _.isEmpty(curUser.lastName) ? name : curUser.lastName,
+ username: curUser.email,
+ email: curUser.email,
+ isAdmin,
+ userRole: curUser.userRole,
+ authenticationProviderId,
+ identityProviderName: curUser.identityProviderName,
+ status: 'active',
+ isExternalUser: userType === 'EXTERNAL',
+ };
+ if (!_.isEmpty(userToCreate.email)) {
+ const user = await this.findUser({
+ username: userToCreate.username,
+ authenticationProviderId: userToCreate.authenticationProviderId,
+ identityProviderName: userToCreate.identityProviderName,
+ });
+ if (user) {
+ throw this.boom.alreadyExists('Cannot add user. The user already exists.', true);
+ } else {
+ await this.createUser(requestContext, userToCreate);
+ successCount += 1;
+ }
+ }
+ } catch (e) {
+ const errorMsg = e.safe // if error is boom error then see if it is safe to propagate it's message
+ ? `Error creating user ${curUser.email}. ${e.message}`
+ : `Error creating user ${curUser.email}`;
+
+ this.log.error(errorMsg);
+ this.log.error(e);
+ errors.push(errorMsg);
+
+ errorCount += 1;
+ }
+ };
+ // Create users in parallel in the specified batches
+ await processInBatches(users, batchSize, createUser);
+ if (!_.isEmpty(errors)) {
+ throw this.boom.internalError(`Errors creating users in bulk`, true).withPayload(errors);
+ }
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-users-batch', body: { totalUsers: _.size(users) } });
+
+ return { successCount, errorCount };
+ }
+
+ async listUsers(requestContext, { fields = [] } = {}) {
+ const users = await super.listUsers(requestContext, fields);
+
+ const isAdmin = _.get(requestContext, 'principal.isAdmin', false);
+
+ const fieldsToOmit = isAdmin ? ['encryptedCreds'] : ['encryptedCreds', 'userRole'];
+ const sanitizedUsers = users.map(user => _.omit(user, fieldsToOmit));
+ return sanitizedUsers;
+ }
+
+ async getCreateUserJsonSchema() {
+ return createUserJsonSchema;
+ }
+
+ async getUpdateUserJsonSchema() {
+ return updateUserJsonSchema;
+ }
+
+ async assertValidProjectId(requestContext, input, existingUser = {}) {
+ // in case of updateUser, there may be an existingUser with existing projectId, in that case, the input must
+ // specify a valid internal userRole or also make projectId empty array
+ const user = { ...existingUser, ...input };
+ // if projectId is not specified or if it's empty array then nothing to validate just return
+ if (!user.projectId || _.isEmpty(user.projectId)) {
+ return;
+ }
+
+ // Only internal users (i.e., user with userRole.userType === INTERNAL) can be assigned projects,
+ const userRoleId = input.userRole;
+ if (userRoleId) {
+ if (_.toLower(userRoleId) === 'internal-guest') {
+ throw this.boom.forbidden('Guest users cannot be assigned a project', true);
+ }
+
+ const userRolesService = await this.service('userRolesService');
+ const userRole = await userRolesService.mustFind(requestContext, { id: userRoleId, fields: ['userType'] });
+ if (_.toLower(userRole.userType) !== 'internal') {
+ throw this.boom.forbidden('External users cannot be assigned a project', true);
+ }
+ }
+ }
+
+ async validateCreateUser(requestContext, input) {
+ await super.validateCreateUser(requestContext, input);
+
+ // Make sure that the projectId(s) are not specified for user with any external role
+ await this.assertValidProjectId(requestContext, input);
+ }
+
+ async setDefaultAttributes(requestContext, user) {
+ const setDefaultIf = checkFn => {
+ return (attribName, defaultValue) => {
+ if (checkFn(user[attribName])) {
+ user[attribName] = defaultValue;
+ }
+ };
+ };
+ const setDefaultIfNil = setDefaultIf(_.isNil);
+ const setDefaultIfEmpty = setDefaultIf(_.isEmpty);
+
+ // Set default values for "status", and "userRole" if they are not specified in the user
+ if (user.userType === 'root') {
+ // default userRole to 'admin' if it's root user
+ setDefaultIfNil('userRole', 'admin');
+ } else {
+ // for all other users (non-root users) set "status" to "inactive" by default
+ setDefaultIfNil('status', 'inactive');
+
+ // for all other users (non-root users) set "userRole" to "guest" by default
+ setDefaultIfNil('userRole', 'guest');
+ }
+ const { email, userRole } = user;
+ const name = await this.toDefaultName(email);
+ const userType = await this.toUserType(requestContext, userRole);
+
+ setDefaultIfEmpty('username', email);
+ setDefaultIfEmpty('firstName', name);
+ setDefaultIfEmpty('lastName', name);
+ setDefaultIfEmpty('encryptedCreds', 'N/A');
+ setDefaultIfEmpty('applyReason', 'N/A');
+ setDefaultIfEmpty('projectId', []);
+
+ user.isAdmin = userRole === 'admin';
+ user.isExternalUser = userType === 'EXTERNAL';
+
+ // Give super class a chance to set it's defaults after we are done setting default values
+ await super.setDefaultAttributes(requestContext, user);
+ }
+
+ async validateUpdateAttributes(requestContext, user, existingUser) {
+ // call base impl first
+ await super.validateUpdateAttributes(requestContext, user, existingUser);
+
+ await this.assertValidProjectId(requestContext, user, existingUser);
+ }
+
+ async selfServiceUpdateUser(requestContext, user = {}) {
+ // user can only update his/her own info via self-service update
+ const { username, authenticationProviderId, identityProviderName } = user;
+ const ns = toUserNamespace(authenticationProviderId, identityProviderName);
+ await ensureCurrentUser(requestContext, username, ns);
+
+ return this.updateUser(requestContext, user);
+ }
+
+ // Private methods
+ async toUserType(requestContext, userRoleId) {
+ const userRolesService = await this.service('userRolesService');
+ let userType;
+ if (userRoleId) {
+ const { userType: userTypeInRole } = await userRolesService.mustFind(requestContext, { id: userRoleId });
+ userType = userTypeInRole;
+ }
+ return userType;
+ }
+
+ async toDefaultName(userEmail) {
+ return userEmail ? userEmail.substring(0, userEmail.lastIndexOf('@')) : '';
+ }
+}
+
+module.exports = UserService;
diff --git a/addons/addon-base-raas/packages/base-raas-services/package.json b/addons/addon-base-raas/packages/base-raas-services/package.json
new file mode 100644
index 0000000000..737c896dde
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-services/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@aws-ee/base-raas-services",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A library containing a set of base RaaS related services and utilities",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-services": "workspace:*",
+ "@aws-ee/base-api-services": "workspace:*",
+ "@aws-ee/base-services-container": "workspace:*",
+ "lodash": "^4.17.15",
+ "node-cache": "^4.2.1",
+ "uuid": "^3.4.0",
+ "xml": "^1.0.1"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; yarn run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/.eslintrc.json b/addons/addon-base-raas/packages/base-raas-workflow-steps/.eslintrc.json
new file mode 100644
index 0000000000..c700cf03a8
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/.eslintrc.json
@@ -0,0 +1,26 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "import/no-unresolved": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/.gitignore b/addons/addon-base-raas/packages/base-raas-workflow-steps/.gitignore
new file mode 100644
index 0000000000..659959de8f
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/.gitignore
@@ -0,0 +1,16 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/.prettierrc.json b/addons/addon-base-raas/packages/base-raas-workflow-steps/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/jest.config.js b/addons/addon-base-raas/packages/base-raas-workflow-steps/jest.config.js
new file mode 100644
index 0000000000..70544476d7
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/jest.config.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+};
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/jsconfig.json b/addons/addon-base-raas/packages/base-raas-workflow-steps/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/plugins/workflow-steps-plugin.js b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/plugins/workflow-steps-plugin.js
new file mode 100644
index 0000000000..7f9b98326c
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/plugins/workflow-steps-plugin.js
@@ -0,0 +1,41 @@
+/*
+ * 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 global-require */
+const deleteEnvironment = require('../steps/delete-environment/delete-environment');
+const deleteEnvironmentYaml = require('../steps/delete-environment/delete-environment.yml');
+const provisionAccount = require('../steps/provision-account/provision-account');
+const provisionAccountYaml = require('../steps/provision-account/provision-account.yml');
+const provisionEnvironment = require('../steps/provision-environment/provision-environment');
+const provisionEnvironmentYaml = require('../steps/provision-environment/provision-environment.yml');
+
+const add = (implClass, yaml) => ({ implClass, yaml });
+
+// The order is important, add your steps here
+const steps = [
+ add(deleteEnvironment, deleteEnvironmentYaml),
+ add(provisionAccount, provisionAccountYaml),
+ add(provisionEnvironment, provisionEnvironmentYaml),
+];
+
+async function registerWorkflowSteps(registry) {
+ // eslint-disable-next-line no-restricted-syntax
+ for (const step of steps) {
+ const { implClass, yaml } = step;
+ await registry.add({ implClass, yaml }); // eslint-disable-line no-await-in-loop
+ }
+}
+
+module.exports = { registerWorkflowSteps };
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/delete-environment/delete-environment.js b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/delete-environment/delete-environment.js
new file mode 100644
index 0000000000..ac2561a9a3
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/delete-environment/delete-environment.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.
+ */
+
+const StepBase = require('@aws-ee/base-workflow-core/lib/workflow/helpers/step-base');
+
+const STACK_FAILED = [
+ 'CREATE_FAILED',
+ 'ROLLBACK_FAILED',
+ 'DELETE_FAILED',
+ 'UPDATE_ROLLBACK_FAILED',
+ 'ROLLBACK_COMPLETE',
+ 'UPDATE_ROLLBACK_COMPLETE',
+];
+const STACK_SUCCESS = ['CREATE_COMPLETE', 'DELETE_COMPLETE', 'UPDATE_COMPLETE'];
+
+class DeleteEnvironment extends StepBase {
+ async start() {
+ // Get services
+ const [environmentService, environmentMountService] = await this.mustFindServices([
+ 'environmentService',
+ 'environmentMountService',
+ ]);
+
+ const cfn = await this.getCloudFormationService();
+
+ // Get common payload params and pull environment info
+ const [environmentId, requestContext] = await Promise.all([
+ this.payload.string('environmentId'),
+ this.payload.object('requestContext'),
+ ]);
+ const environment = await environmentService.mustFind(requestContext, { id: environmentId });
+
+ // Set initial state
+ this.state.setKey('STATE_ENVIRONMENT_ID', environmentId);
+ this.state.setKey('STATE_REQUEST_CONTEXT', requestContext);
+ this.state.setKey('STATE_STACK_ID', environment.stackId);
+
+ // Confirm that the environment has an associated CFN stack ID
+ this.print(`Deleting stack ${environment.stackId} for environment ${environment.id}`);
+ if (!environment.stackId) {
+ // if there is no stack id, then there's nothing else to clean up.
+ return this.updateEnvironmentStatusToTerminated();
+ }
+
+ // Perform updates/termination
+ // Remove from policies before deleting resources to prevent trying to save the principal ID
+ // because the role was removed before the policy update. See
+ // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html
+ // under IAM Roles
+ await environmentMountService.removeRoleArnFromLocalResourcePolicies(
+ environment.instanceInfo.WorkspaceInstanceRoleArn,
+ environment.instanceInfo.s3Prefixes,
+ );
+ await Promise.all([
+ this.deleteKeypair(),
+ this.updateEnvironmentStatus('TERMINATING'),
+ cfn.deleteStack({ StackName: environment.stackId }).promise(),
+ ]);
+
+ // Poll until the stack has been deleted
+ return this.wait(20)
+ .maxAttempts(120)
+ .until('checkCfnCompleted')
+ .thenCall('updateEnvironmentStatusToTerminated');
+ }
+
+ /**
+ * CloudFormation and Workflow-Related Methods
+ */
+ async checkCfnCompleted() {
+ const stackId = await this.state.string('STATE_STACK_ID');
+ this.print(`stack id is ${stackId}`);
+ const cfn = await this.getCloudFormationService();
+ const stackInfo = (await cfn.describeStacks({ StackName: stackId }).promise()).Stacks[0];
+
+ if (STACK_FAILED.includes(stackInfo.StackStatus)) {
+ throw new Error(`Stack operation failed with message: ${stackInfo.StackStatusReason}`);
+ } else {
+ return !!STACK_SUCCESS.includes(stackInfo.StackStatus);
+ }
+ }
+
+ async getCloudFormationService() {
+ const [aws] = await this.mustFindServices(['aws']);
+ const [requestContext, RoleArn, ExternalId] = await Promise.all([
+ this.payload.object('requestContext'),
+ this.payload.string('cfnExecutionRole'),
+ this.payload.string('roleExternalId'),
+ ]);
+
+ const sts = new aws.sdk.STS();
+ const {
+ Credentials: { AccessKeyId: accessKeyId, SecretAccessKey: secretAccessKey, SessionToken: sessionToken },
+ } = await sts
+ .assumeRole({
+ RoleArn,
+ RoleSessionName: `RaaS-${requestContext.principalIdentifier.username}`,
+ ExternalId,
+ })
+ .promise();
+
+ return new aws.sdk.CloudFormation({ accessKeyId, secretAccessKey, sessionToken });
+ }
+
+ async updateEnvironmentStatusToTerminated() {
+ return this.updateEnvironmentStatus('TERMINATED');
+ }
+
+ async updateEnvironmentStatus(status) {
+ const environmentService = await this.mustFindServices('environmentService');
+ const id = await this.state.string('STATE_ENVIRONMENT_ID');
+ const requestContext = await this.state.optionalObject('STATE_REQUEST_CONTEXT');
+ await environmentService.update(requestContext, { id, status });
+ }
+
+ async onFail() {
+ return this.updateEnvironmentStatus('TERMINATING_FAILED');
+ }
+
+ /**
+ * External Resource-Related Methods
+ */
+ async deleteKeypair() {
+ const environmentKeypairService = await this.mustFindServices('environmentKeypairService');
+ const id = await this.state.string('STATE_ENVIRONMENT_ID');
+ const requestContext = await this.state.optionalObject('STATE_REQUEST_CONTEXT');
+ try {
+ await environmentKeypairService.delete(requestContext, id);
+ } catch (error) {
+ // Ignore ParameterNotFound errors from Parameter Store if the key was already
+ // deleted or no key was ever created (e.g., for SageMaker environments)
+ if (!('code' in error) || error.code !== 'ParameterNotFound') {
+ throw error;
+ }
+ }
+ }
+}
+
+module.exports = DeleteEnvironment;
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/delete-environment/delete-environment.yml b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/delete-environment/delete-environment.yml
new file mode 100644
index 0000000000..4de6903677
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/delete-environment/delete-environment.yml
@@ -0,0 +1,8 @@
+id: st-delete-environment
+v: 1
+title: Delete Environment
+desc: |
+ Remove an analytics environment
+
+skippable: true # this means that if there is an error in a previous step, then this step will be skipped
+hidden: true
\ No newline at end of file
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/provision-account/provision-account.js b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/provision-account/provision-account.js
new file mode 100644
index 0000000000..7bfa29c5f3
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/provision-account/provision-account.js
@@ -0,0 +1,452 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const StepBase = require('@aws-ee/base-workflow-core/lib/workflow/helpers/step-base');
+
+const STACK_FAILED = [
+ 'CREATE_FAILED',
+ 'ROLLBACK_FAILED',
+ 'DELETE_FAILED',
+ 'UPDATE_ROLLBACK_FAILED',
+ 'ROLLBACK_COMPLETE',
+ 'UPDATE_ROLLBACK_COMPLETE',
+];
+
+const STACK_SUCCESS = ['CREATE_COMPLETE', 'DELETE_COMPLETE', 'UPDATE_COMPLETE'];
+
+const settingKeys = {
+ artifactsBucketName: 'artifactsBucketName',
+};
+
+class ProvisionAccount extends StepBase {
+ // ======================================================
+ // PLEASE NOTE: the following account provision is assuming the account organization is already been setup
+ // TODOs: create organization in master account, the role access limitation needs to be changed as well
+ // ======================================================
+ async start() {
+ this.print('start provisioning accounts');
+ const [requestContext] = await Promise.all([this.payload.object('requestContext')]);
+ this.state.setKey('STATE_REQUEST_CONTEXT', requestContext);
+ // provision new aws account in existing Organization in master account
+ this.print('getting awsorgservice');
+ const awsOrgService = await this.getOrganizationService();
+ this.print('getting accountname and email');
+ const [AccountName, Email] = await Promise.all([
+ this.payload.string('accountName'),
+ this.payload.string('accountEmail'),
+ ]);
+ const params = {
+ AccountName,
+ Email,
+ };
+ this.print(`Attempting to create AWS account with ${JSON.stringify(params)}`);
+ let creationResult;
+ try {
+ // TODO: handle if email address is already been used for another aws account
+ creationResult = await awsOrgService.createAccount(params).promise();
+ this.print(`setting the request id as ${creationResult.CreateAccountStatus.Id}`);
+ this.state.setKey('REQUEST_ID', creationResult.CreateAccountStatus.Id);
+ } catch (e) {
+ this.print(`Error in creating account ${e.stack}`);
+ throw new Error(`Create AWS Account operation error: ${e.stack}`);
+ }
+
+ this.print(`Waiting for Account creation process with requestID: ${await this.state.string('REQUEST_ID')}`);
+ // wait until the account creation is finished
+ return this.wait(10)
+ .maxAttempts(120)
+ .until('checkAccountCreationCompleted')
+ .thenCall('saveAccountToDb');
+ }
+
+ async saveAccountToDb() {
+ this.print('start to save account info into Dynamo');
+ const requestContext = await this.payload.object('requestContext');
+ const accountName = await this.payload.string('accountName');
+ const email = await this.payload.string('accountEmail');
+ // After the account is created
+ await this.describeAccount();
+ const accountId = await this.state.string('ACCOUNT_ID');
+ const accountArn = await this.state.string('ACCOUNT_ARN');
+
+ this.print(`Account creation process finish. New aws accountID: ${accountId}. ARN: ${accountArn}`);
+ const [accountService] = await this.mustFindServices(['accountService']);
+ this.print('got accountservice');
+ const data = {
+ accountName,
+ email,
+ accountArn,
+ // TODO: check if roleName and iamUserAccessToBillion is specified by user, keep them default and skip saving them for now
+ };
+ this.print(`saving account data into dynamo: ${JSON.stringify(data)}`);
+ await accountService.saveAccountToDb(requestContext, data, accountId);
+ // THIS IS NEEDED, we should wait AWS to setup the account, even if we can fetch the account ID
+ this.print('start to wait for 5min for AWS getting the account ready.');
+ return this.wait(60 * 5).thenCall('deployStack');
+ }
+
+ async deployStack() {
+ this.print('start to deploy initial stacks in newly created AWS account');
+
+ const requestContext = await this.payload.object('requestContext');
+ const by = _.get(requestContext, 'principalIdentifier');
+ const ExternalId = await this.payload.string('externalId');
+ const [workflowRoleArn, apiHandlerArn] = await Promise.all([
+ this.payload.string('workflowRoleArn'),
+ this.payload.string('apiHandlerArn'),
+ ]);
+ const centralAccountId = await this.state.string('ACCOUNT_ID');
+ // deploy basic stacks to the account just created
+ const [cfnTemplateService] = await this.mustFindServices(['cfnTemplateService']);
+ const cfn = await this.getCloudFormationService();
+
+ const [template] = await Promise.all([cfnTemplateService.getTemplate('onboard-account')]);
+ const stackName = `initial-stack-${new Date().getTime()}`;
+ const cfnParams = [];
+ const addParam = (key, v) => cfnParams.push({ ParameterKey: key, ParameterValue: v });
+
+ addParam('Namespace', stackName);
+ addParam('CentralAccountId', centralAccountId);
+ addParam('ExternalId', ExternalId);
+ // TODO: consider if following params are needed
+ // addParam('TrustUserArn', userArn);
+ addParam('WorkflowRoleArn', workflowRoleArn);
+ addParam('ApiHandlerArn', apiHandlerArn);
+
+ const input = {
+ StackName: stackName,
+ Parameters: cfnParams,
+ Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
+ TemplateBody: template,
+ Tags: [
+ {
+ Key: 'Description',
+ Value: `Created by ${by.username} for newly created AWS account`,
+ },
+ {
+ Key: 'CreatedBy',
+ Value: by.username,
+ },
+ ],
+ };
+ const response = await cfn.createStack(input).promise();
+
+ // Update workflow state and poll for stack creation completion
+ this.state.setKey('STATE_STACK_ID', response.StackId);
+ await this.updateAccount({ stackId: response.StackId });
+ return this.wait(20)
+ .maxAttempts(120)
+ .until('checkCfnCompleted');
+ }
+
+ async checkCfnCompleted() {
+ const requestContext = await this.payload.object('requestContext');
+ const stackId = await this.state.string('STATE_STACK_ID');
+ const cfn = await this.getCloudFormationService();
+ const stackInfo = (await cfn.describeStacks({ StackName: stackId }).promise()).Stacks[0];
+
+ if (STACK_FAILED.includes(stackInfo.StackStatus)) {
+ throw new Error(`Stack operation failed with message: ${stackInfo.StackStatusReason}`);
+ } else if (STACK_SUCCESS.includes(stackInfo.StackStatus)) {
+ // handle the case where the cloudformation is deleted before the creation could finish
+ if (stackInfo.StackStatus !== 'DELETE_COMPLETE') {
+ const cfnOutputs = this.getCfnOutputs(stackInfo);
+ this.print('updating stack deployed completed');
+
+ // create S3 and KMS resources access for newly created account
+ await this.updateLocalResourcePolicies();
+
+ await this.updateAccount({
+ status: 'COMPLETED',
+ cfnInfo: {
+ stackId,
+ vpcId: cfnOutputs.VPC,
+ subnetId: cfnOutputs.VpcPublicSubnet1,
+ crossAccountExecutionRoleArn: cfnOutputs.CrossAccountExecutionRoleArn,
+ encryptionKeyArn: cfnOutputs.EncryptionKeyArn,
+ },
+ });
+ // TODO: after the account is deployed and all useful info is fetched,
+ // try to update aws account table, it might be able to skip that table with all info already
+ // exsiting in the account table instead of aws-account table. but for now, update the info to aws-account
+ // ddb as well for code consistant reason.
+ const [description, externalId, name] = await Promise.all([
+ this.payload.string('description'),
+ this.payload.string('externalId'),
+ this.payload.string('accountName'),
+ ]);
+ this.print('saving account info into aws account table');
+ const accountId = await this.state.string('ACCOUNT_ID');
+ const awsAccountData = {
+ accountId,
+ description,
+ externalId,
+ name,
+ roleArn: cfnOutputs.CrossAccountExecutionRoleArn,
+ subnetId: cfnOutputs.VpcPublicSubnet1,
+ vpcId: cfnOutputs.VPC,
+ encryptionKeyArn: cfnOutputs.EncryptionKeyArn,
+ };
+ await this.addAwsAccountTable(requestContext, awsAccountData);
+ }
+ return true;
+ } // else CFN is still pending
+ return false;
+ }
+
+ async addAwsAccountTable(requestContext, awsAccountData) {
+ const [awsAccountsService] = await this.mustFindServices(['awsAccountsService']);
+ await awsAccountsService.create(requestContext, awsAccountData);
+ }
+
+ async checkAccountCreationCompleted() {
+ this.print('checking if the AWS account is created');
+ const awsOrgService = await this.getOrganizationService();
+
+ const requestId = await this.state.string('REQUEST_ID');
+ const params = {
+ CreateAccountRequestId: requestId,
+ };
+ let CreateAccountStatus;
+ try {
+ CreateAccountStatus = await awsOrgService.describeCreateAccountStatus(params).promise();
+ } catch (e) {
+ throw new Error(`checkAccountCreationCompleted operation error: ${e.stack}`);
+ }
+ /* response example:
+ data = {
+ CreateAccountStatus: {
+ AccountId: "333333333333",
+ Id: "car-exampleaccountcreationrequestid",
+ State: "SUCCEEDED"
+ }
+ }
+ */
+
+ if (CreateAccountStatus.CreateAccountStatus.State === 'SUCCEEDED') {
+ this.print(
+ `account is successfully created with account_id: ${CreateAccountStatus.CreateAccountStatus.AccountId}`,
+ );
+ this.state.setKey('ACCOUNT_ID', CreateAccountStatus.CreateAccountStatus.AccountId);
+ return true;
+ }
+
+ return false;
+ }
+
+ async updateAccount(obj) {
+ const accountService = await this.mustFindServices('accountService');
+ const id = await this.state.string('ACCOUNT_ID');
+ const requestContext = await this.state.optionalObject('STATE_REQUEST_CONTEXT');
+ const account = _.clone(obj);
+ account.id = id;
+ this.print(`updating account table with info: ${JSON.stringify(account)}`);
+ await accountService.update(requestContext, account);
+ }
+
+ async onFail() {
+ await this.updateAccount({ status: 'FAILED' });
+ }
+
+ async describeAccount() {
+ const awsOrgService = await this.getOrganizationService();
+ const AccountId = await this.state.string('ACCOUNT_ID');
+ const params = {
+ AccountId,
+ };
+ let account;
+ try {
+ account = await awsOrgService.describeAccount(params).promise();
+ this.print(`setting account_arn as ${account.Account.Arn}`);
+ this.state.setKey('ACCOUNT_ARN', account.Account.Arn);
+ } catch (e) {
+ throw new Error(`Describe AWS Account operation error: ${e.stack}`);
+ }
+
+ /* response example
+ data = {
+ Account: {
+ Arn: "arn:aws:organizations::111111111111:account/o-exampleorgid/555555555555",
+ Email: "anika@example.com",
+ Id: "555555555555",
+ Name: "Beta Account"
+ }
+ }
+ */
+ }
+
+ async getCloudFormationService() {
+ const [aws] = await this.mustFindServices(['aws']);
+ const credential = await this.getCredentials();
+ const [requestContext, ExternalId] = await Promise.all([
+ this.payload.object('requestContext'),
+ this.payload.string('externalId'),
+ ]);
+ const accountId = await this.state.string('ACCOUNT_ID');
+ // TODO: pass user customized role name, for now it's fixed as OrganizationAccountAccessRole
+ const RoleArn = `arn:aws:iam::${accountId}:role/OrganizationAccountAccessRole`;
+ const sts = new aws.sdk.STS(credential);
+ const {
+ Credentials: { AccessKeyId: accessKeyId, SecretAccessKey: secretAccessKey, SessionToken: sessionToken },
+ } = await sts
+ .assumeRole({
+ RoleArn,
+ RoleSessionName: `RaaS-${requestContext.principalIdentifier.username}-CfnRole`,
+ ExternalId,
+ })
+ .promise();
+
+ return new aws.sdk.CloudFormation({ accessKeyId, secretAccessKey, sessionToken });
+ }
+
+ async getOrganizationService() {
+ const [aws] = await this.mustFindServices(['aws']);
+ const [requestContext, ExternalId, RoleArn] = await Promise.all([
+ this.payload.object('requestContext'),
+ this.payload.string('externalId'),
+ this.payload.string('masterRoleArn'),
+ ]);
+ const sts = new aws.sdk.STS();
+ const {
+ Credentials: { AccessKeyId: accessKeyId, SecretAccessKey: secretAccessKey, SessionToken: sessionToken },
+ } = await sts
+ .assumeRole({
+ RoleArn,
+ RoleSessionName: `RaaS-${requestContext.principalIdentifier.username}-OrgRole`,
+ ExternalId,
+ })
+ .promise();
+
+ return new aws.sdk.Organizations({ accessKeyId, secretAccessKey, sessionToken });
+ }
+
+ async getCredentials() {
+ const [aws] = await this.mustFindServices(['aws']);
+ const [requestContext, RoleArn, ExternalId] = await Promise.all([
+ this.payload.object('requestContext'),
+ this.payload.string('masterRoleArn'),
+ this.payload.string('externalId'),
+ ]);
+
+ const sts = new aws.sdk.STS();
+ const {
+ Credentials: { AccessKeyId: accessKeyId, SecretAccessKey: secretAccessKey, SessionToken: sessionToken },
+ } = await sts
+ .assumeRole({
+ RoleArn,
+ RoleSessionName: `RaaS-${requestContext.principalIdentifier.username}-OrgRole`,
+ ExternalId,
+ })
+ .promise();
+
+ return { accessKeyId, secretAccessKey, sessionToken };
+ }
+
+ getCfnOutputs(stackInfo) {
+ const details = {};
+ stackInfo.Outputs.forEach(option => {
+ _.set(details, option.OutputKey, option.OutputValue);
+ });
+ return details;
+ }
+
+ async getPolicy(s3Client, s3BucketName) {
+ try {
+ return JSON.parse((await s3Client.getBucketPolicy({ Bucket: s3BucketName }).promise()).Policy);
+ } catch (error) {
+ // if no policy yet, return an empty one
+ if (error.code === 'NoSuchBucketPolicy') {
+ return {
+ Id: 'Policy',
+ Version: '2012-10-17',
+ Statement: [],
+ };
+ }
+ throw error;
+ }
+ }
+
+ /**
+ * Post-Launch Tasks
+ */
+ async updateLocalResourcePolicies() {
+ /**
+ * Update S3 bucket policy and KMS key policy to grant access for newly created account
+ * TODO: Consolidate some of this logic with the associated function in the DeleteEnvironment
+ * workflow step if possible
+ */
+ // Get new account ID
+ const accountId = await this.state.string('ACCOUNT_ID');
+ const remoteAccountArn = `arn:aws:iam::${accountId}:root`;
+ // Get S3 and KMS resource names
+ const s3BucketName = this.settings.get(settingKeys.artifactsBucketName);
+
+ // Setup services and SDK clients
+ const [aws, lockService] = await this.mustFindServices(['aws', 'lockService']);
+ const s3Client = new aws.sdk.S3();
+
+ // Define function to handle updating resource policy principals where the current principals
+ // may be an array or a string
+ const updateAwsPrincipals = (awsPrincipals, newPrincipal) => {
+ if (Array.isArray(awsPrincipals)) {
+ awsPrincipals.push(newPrincipal);
+ } else {
+ awsPrincipals = [awsPrincipals, newPrincipal];
+ }
+ return awsPrincipals;
+ };
+
+ // Perform locked updates to prevent inconsistencies from race conditions
+ const s3LockKey = `s3|bucket-policy|${accountId}`;
+ await Promise.all([
+ // Update S3 bucket policy
+ lockService.tryWriteLockAndRun({ id: s3LockKey }, async () => {
+ // Get existing policy
+ const s3Policy = await this.getPolicy(s3Client, s3BucketName);
+ // Get statements for listing and reading study data, respectively
+ const statements = s3Policy.Statement;
+ const accessSid = `accessId:${accountId}`;
+ // Define default statements to be used if we can't find existing ones
+ let accessStatement = {
+ Sid: accessSid,
+ Effect: 'Allow',
+ Principal: { AWS: [] },
+ Action: ['s3:GetObject', 's3:PutObject', 's3:GetObjectAcl'],
+ Resource: [`arn:aws:s3:::${s3BucketName}/*`], // The previous star-slash confuses some syntax highlighers */
+ };
+
+ // Pull out existing statements if available
+ statements.forEach(statement => {
+ if (statement.Sid === accessSid) {
+ accessStatement = statement;
+ }
+ });
+
+ // Update statement and policy
+ // NOTE: The S3 API *should* remove duplicate principals, if any
+ accessStatement.Principal.AWS = updateAwsPrincipals(accessStatement.Principal.AWS, remoteAccountArn);
+
+ s3Policy.Statement = s3Policy.Statement.filter(statement => ![accessSid].includes(statement.Sid));
+ s3Policy.Statement.push(accessStatement);
+
+ // Update policy
+ await s3Client.putBucketPolicy({ Bucket: s3BucketName, Policy: JSON.stringify(s3Policy) }).promise();
+ }),
+ ]);
+ }
+}
+
+module.exports = ProvisionAccount;
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/provision-account/provision-account.yml b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/provision-account/provision-account.yml
new file mode 100644
index 0000000000..6c45dee9e1
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/provision-account/provision-account.yml
@@ -0,0 +1,8 @@
+id: st-provision-account
+v: 1
+title: Provision Account
+desc: |
+ Execute the creation of an AWS Account
+
+skippable: true # this means that if there is an error in a previous step, then this step will be skipped
+hidden: true
\ No newline at end of file
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/provision-environment/provision-environment.js b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/provision-environment/provision-environment.js
new file mode 100644
index 0000000000..e92576c76a
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/provision-environment/provision-environment.js
@@ -0,0 +1,312 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const StepBase = require('@aws-ee/base-workflow-core/lib/workflow/helpers/step-base');
+
+const STACK_FAILED = [
+ 'CREATE_FAILED',
+ 'ROLLBACK_FAILED',
+ 'DELETE_FAILED',
+ 'UPDATE_ROLLBACK_FAILED',
+ 'ROLLBACK_COMPLETE',
+ 'UPDATE_ROLLBACK_COMPLETE',
+];
+const STACK_SUCCESS = ['CREATE_COMPLETE', 'DELETE_COMPLETE', 'UPDATE_COMPLETE'];
+
+class ProvisionEnvironment extends StepBase {
+ async start() {
+ // start workflow that starts the CFN Template, updates status to PROCESSING, waits for CFN to finish, updates env status to COMPLETED/FAILED
+
+ // Get services
+ const [
+ environmentService,
+ cfnTemplateService,
+ environmentKeypairService,
+ environmentMountService,
+ ] = await this.mustFindServices([
+ 'environmentService',
+ 'cfnTemplateService',
+ 'environmentKeypairService',
+ 'environmentMountService',
+ ]);
+
+ // Get common payload params and pull environment info
+ const [type, environmentId, requestContext, vpcId, vpcSubnet, encryptionKeyArn] = await Promise.all([
+ this.payload.string('type'),
+ this.payload.string('environmentId'),
+ this.payload.object('requestContext'),
+ this.payload.string('vpcId'),
+ this.payload.string('subnetId'),
+ this.payload.string('encryptionKeyArn'),
+ ]);
+ const environment = await environmentService.mustFind(requestContext, { id: environmentId });
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const stackName = `analysis-${new Date().getTime()}`;
+
+ // Set initial state
+ this.state.setKey('STATE_ENVIRONMENT_ID', environmentId);
+ this.state.setKey('STATE_REQUEST_CONTEXT', requestContext);
+
+ // Define array for collecting CloudFormation functions
+ const cfnParams = [];
+ const addParam = (key, value) => cfnParams.push({ ParameterKey: key, ParameterValue: value });
+
+ // Add parameters unique to each environment type
+ let template;
+ switch (type) {
+ case 'ec2-linux':
+ template = await cfnTemplateService.getTemplate('ec2-linux-instance');
+ break;
+ case 'ec2-windows':
+ template = await cfnTemplateService.getTemplate('ec2-windows-instance');
+ break;
+ case 'sagemaker':
+ template = await cfnTemplateService.getTemplate('sagemaker-notebook-instance');
+ break;
+ case 'emr': {
+ template = await cfnTemplateService.getTemplate('emr-cluster');
+
+ addParam('DiskSizeGB', environment.instanceInfo.config.diskSizeGb.toString());
+ addParam('MasterInstanceType', environment.instanceInfo.size);
+ addParam('WorkerInstanceType', environment.instanceInfo.config.workerInstanceSize);
+ addParam('CoreNodeCount', environment.instanceInfo.config.workerInstanceCount.toString());
+
+ // Add parameters to support spot instance pricing if specified
+ // TODO this needs to be parameterized
+ const isOnDemand = !environment.instanceInfo.config.spotBidPrice;
+ // The spot bid price can only have 3 decimal places maximum
+ const spotBidPrice = isOnDemand ? '0' : environment.instanceInfo.config.spotBidPrice.toFixed(3);
+
+ addParam('Market', isOnDemand ? 'ON_DEMAND' : 'SPOT');
+ addParam('WorkerBidPrice', spotBidPrice);
+
+ this.print(
+ isOnDemand
+ ? 'Launching on demand core nodes'
+ : `Launching spot core nodes with a bid price of ${spotBidPrice}`,
+ );
+
+ break;
+ }
+ default:
+ throw new Error(`Unknown environment type requested: ${type}`);
+ }
+
+ // Handle CFN parameters that need to be excluded from certain environment types
+ if (type !== 'ec2-windows') {
+ const {
+ s3Mounts,
+ iamPolicyDocument,
+ environmentInstanceFiles,
+ s3Prefixes,
+ } = await environmentMountService.getCfnMountParameters(requestContext, environment);
+
+ addParam('S3Mounts', s3Mounts);
+ addParam('IamPolicyDocument', iamPolicyDocument);
+ addParam('EnvironmentInstanceFiles', environmentInstanceFiles);
+
+ // Only save the prefixes for the local resources, otherwise we add list and get access for
+ // potentially the whole study bucket
+ this.state.setKey('ENV_S3_STUDY_PREFIXES', s3Prefixes);
+ }
+
+ if (type !== 'sagemaker') {
+ const credential = await this.getCredentials();
+ const [amiImage, cidr, keyName] = await Promise.all([
+ this.payload.string('amiImage'),
+ this.payload.string('cidr'),
+ environmentKeypairService.create(requestContext, environmentId, credential),
+ ]);
+
+ addParam('AmiId', amiImage);
+ addParam('AccessFromCIDRBlock', cidr);
+ addParam('KeyName', keyName);
+ }
+
+ if (type !== 'emr') {
+ addParam('InstanceType', environment.instanceInfo.size);
+ }
+
+ // Add rest of parameters
+ addParam('Namespace', stackName);
+ addParam('VPC', vpcId);
+ addParam('Subnet', vpcSubnet);
+ addParam('EncryptionKeyArn', encryptionKeyArn);
+
+ const input = {
+ StackName: stackName,
+ Parameters: cfnParams,
+ Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
+ TemplateBody: template,
+ Tags: [
+ {
+ Key: 'Description',
+ Value: `Created by ${by.username}`,
+ },
+ {
+ Key: 'Env',
+ Value: environmentId,
+ },
+ {
+ Key: 'Proj',
+ Value: environment.indexId,
+ },
+ {
+ Key: 'CreatedBy',
+ Value: by.username,
+ },
+ ],
+ };
+
+ // Create stack
+ const cfn = await this.getCloudFormationService();
+ const response = await cfn.createStack(input).promise();
+
+ // Update workflow state and poll for stack creation completion
+ this.state.setKey('STATE_STACK_ID', response.StackId);
+ await this.updateEnvironment({ stackId: response.StackId });
+ return this.wait(20)
+ .maxAttempts(120)
+ .until('checkCfnCompleted');
+ }
+
+ async getCloudFormationService() {
+ const [aws] = await this.mustFindServices(['aws']);
+ const [requestContext, RoleArn, ExternalId] = await Promise.all([
+ this.payload.object('requestContext'),
+ this.payload.string('cfnExecutionRole'),
+ this.payload.string('roleExternalId'),
+ ]);
+
+ const sts = new aws.sdk.STS();
+ const {
+ Credentials: { AccessKeyId: accessKeyId, SecretAccessKey: secretAccessKey, SessionToken: sessionToken },
+ } = await sts
+ .assumeRole({
+ RoleArn,
+ RoleSessionName: `RaaS-${requestContext.principalIdentifier.username}`,
+ ExternalId,
+ })
+ .promise();
+
+ return new aws.sdk.CloudFormation({ accessKeyId, secretAccessKey, sessionToken });
+ }
+
+ async getCredentials() {
+ const [aws] = await this.mustFindServices(['aws']);
+ const [requestContext, RoleArn, ExternalId] = await Promise.all([
+ this.payload.object('requestContext'),
+ this.payload.string('cfnExecutionRole'),
+ this.payload.string('roleExternalId'),
+ ]);
+
+ const sts = new aws.sdk.STS();
+ const {
+ Credentials: { AccessKeyId: accessKeyId, SecretAccessKey: secretAccessKey, SessionToken: sessionToken },
+ } = await sts
+ .assumeRole({
+ RoleArn,
+ RoleSessionName: `RaaS-${requestContext.principalIdentifier.username}-OrgRole`,
+ ExternalId,
+ })
+ .promise();
+
+ return { accessKeyId, secretAccessKey, sessionToken };
+ }
+
+ /**
+ * CloudFormation and Workflow-Related Methods
+ */
+ async checkCfnCompleted() {
+ const environmentMountService = await this.mustFindServices('environmentMountService');
+ const stackId = await this.state.string('STATE_STACK_ID');
+ this.print(`checking status of cfn stack: ${stackId}`);
+
+ // const cfnTemplateService = await this.mustFindServices('')
+ const cfn = await this.getCloudFormationService();
+ const stackInfo = (await cfn.describeStacks({ StackName: stackId }).promise()).Stacks[0];
+
+ if (STACK_FAILED.includes(stackInfo.StackStatus)) {
+ throw new Error(`Stack operation failed with message: ${stackInfo.StackStatusReason}`);
+ } else if (STACK_SUCCESS.includes(stackInfo.StackStatus)) {
+ // handle the case where the cloudformation is deleted before the creation could finish
+ if (stackInfo.StackStatus !== 'DELETE_COMPLETE') {
+ const cfnOutputs = this.getCfnOutputs(stackInfo);
+
+ // Update S3 and KMS resources if needed
+ const s3Prefixes = await this.state.optionalArray('ENV_S3_STUDY_PREFIXES');
+ if (s3Prefixes.length > 0) {
+ await environmentMountService.addRoleArnToLocalResourcePolicies(
+ cfnOutputs.WorkspaceInstanceRoleArn,
+ s3Prefixes,
+ );
+ }
+
+ // Update environment metadata
+ await this.updateEnvironment({
+ status: 'COMPLETED',
+ instanceInfo: {
+ ...cfnOutputs,
+ s3Prefixes,
+ },
+ });
+ }
+ return true;
+ } // else CFN is still pending
+ return false;
+ }
+
+ getCfnOutputs(stackInfo) {
+ const details = {};
+ stackInfo.Outputs.forEach(option => {
+ _.set(details, option.OutputKey, option.OutputValue);
+ });
+ return details;
+ }
+
+ async updateEnvironment(environment) {
+ const environmentService = await this.mustFindServices('environmentService');
+ const id = await this.state.string('STATE_ENVIRONMENT_ID');
+ const requestContext = await this.state.optionalObject('STATE_REQUEST_CONTEXT');
+ environment.id = id;
+ await environmentService.update(requestContext, environment);
+ }
+
+ async onFail() {
+ const error = await this.getDeploymentError();
+ await this.updateEnvironment({ status: 'FAILED', error });
+ }
+
+ async getDeploymentError() {
+ const id = await this.state.string('STATE_ENVIRONMENT_ID');
+ const requestContext = await this.state.optionalObject('STATE_REQUEST_CONTEXT');
+
+ const environmentService = await this.mustFindServices('environmentService');
+ const existingEnvironment = await environmentService.mustFind(requestContext, { id });
+
+ const { stackId } = existingEnvironment;
+ const cfn = await this.getCloudFormationService();
+
+ const events = await cfn.describeStackEvents({ StackName: stackId }).promise();
+ const failReasons = events.StackEvents.filter(e => STACK_FAILED.includes(e.ResourceStatus)).map(
+ e => e.ResourceStatusReason || '',
+ );
+
+ return failReasons.join(' ');
+ }
+}
+
+module.exports = ProvisionEnvironment;
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/provision-environment/provision-environment.yml b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/provision-environment/provision-environment.yml
new file mode 100644
index 0000000000..557e584b05
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/lib/steps/provision-environment/provision-environment.yml
@@ -0,0 +1,8 @@
+id: st-provision-environment
+v: 1
+title: Provision Environment
+desc: |
+ Provision a new environment
+
+skippable: true # this means that if there is an error in a previous step, then this step will be skipped
+hidden: true
diff --git a/addons/addon-base-raas/packages/base-raas-workflow-steps/package.json b/addons/addon-base-raas/packages/base-raas-workflow-steps/package.json
new file mode 100644
index 0000000000..507027cdfe
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflow-steps/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@aws-ee/base-raas-workflow-steps",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A collection of base RaaS workflow steps",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-workflow-core": "workspace:*",
+ "lodash": "^4.17.15",
+ "shortid": "^2.2.15",
+ "slugify": "^1.4.0"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.1.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; yarn run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-raas/packages/base-raas-workflows/.eslintrc.json b/addons/addon-base-raas/packages/base-raas-workflows/.eslintrc.json
new file mode 100644
index 0000000000..c700cf03a8
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflows/.eslintrc.json
@@ -0,0 +1,26 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "import/no-unresolved": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-raas/packages/base-raas-workflows/.gitignore b/addons/addon-base-raas/packages/base-raas-workflows/.gitignore
new file mode 100644
index 0000000000..659959de8f
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflows/.gitignore
@@ -0,0 +1,16 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/addons/addon-base-raas/packages/base-raas-workflows/.prettierrc.json b/addons/addon-base-raas/packages/base-raas-workflows/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflows/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-raas/packages/base-raas-workflows/jest.config.js b/addons/addon-base-raas/packages/base-raas-workflows/jest.config.js
new file mode 100644
index 0000000000..70544476d7
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflows/jest.config.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+};
diff --git a/addons/addon-base-raas/packages/base-raas-workflows/jsconfig.json b/addons/addon-base-raas/packages/base-raas-workflows/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflows/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-raas/packages/base-raas-workflows/lib/plugins/workflows-plugin.js b/addons/addon-base-raas/packages/base-raas-workflows/lib/plugins/workflows-plugin.js
new file mode 100644
index 0000000000..713de528aa
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflows/lib/plugins/workflows-plugin.js
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+const createEnvironmentYaml = require('../workflows/create-environment.yml');
+const deleteEnvironmentYaml = require('../workflows/delete-environment.yml');
+const provisionAccountYaml = require('../workflows/provision-account.yml');
+
+const add = yaml => ({ yaml });
+
+// The order is important, add your templates here
+const workflows = [add(createEnvironmentYaml), add(deleteEnvironmentYaml), add(provisionAccountYaml)];
+
+async function registerWorkflows(registry) {
+ // eslint-disable-next-line no-restricted-syntax
+ for (const workflow of workflows) {
+ await registry.add(workflow); // eslint-disable-line no-await-in-loop
+ }
+}
+
+module.exports = { registerWorkflows };
diff --git a/addons/addon-base-raas/packages/base-raas-workflows/lib/workflows/create-environment.yml b/addons/addon-base-raas/packages/base-raas-workflows/lib/workflows/create-environment.yml
new file mode 100644
index 0000000000..f1b57793fa
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflows/lib/workflows/create-environment.yml
@@ -0,0 +1,17 @@
+id: wf-create-environment
+v: 1
+workflowTemplateId: wt-empty
+workflowTemplateVer: 1
+title: Create New Environment
+desc: |
+ Create new environment
+hidden: false
+builtin: true
+instanceTtl: 30 # In days, however, empty value means that it is indefinite
+selectedSteps:
+ - stepTemplateId: st-provision-environment
+ stepTemplateVer: 1
+ id: wf-step_1_1576783029739_20 # Randomly generated id, no need to change it
+ # configs: # add configuration key/value pairs (if needed)
+ # key1: value1
+ # key2: value2
diff --git a/addons/addon-base-raas/packages/base-raas-workflows/lib/workflows/delete-environment.yml b/addons/addon-base-raas/packages/base-raas-workflows/lib/workflows/delete-environment.yml
new file mode 100644
index 0000000000..07b67dcafc
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflows/lib/workflows/delete-environment.yml
@@ -0,0 +1,17 @@
+id: wf-delete-environment
+v: 1
+workflowTemplateId: wt-empty
+workflowTemplateVer: 1
+title: Delete an Environment
+desc: |
+ Delete an environment
+hidden: false
+builtin: true
+instanceTtl: 30 # In days, however, empty value means that it is indefinite
+selectedSteps:
+ - stepTemplateId: st-delete-environment
+ stepTemplateVer: 1
+ id: wf-step_1_1576786737060_55 # Randomly generated id, no need to change it
+ # configs: # add configuration key/value pairs (if needed)
+ # key1: value1
+ # key2: value2
diff --git a/addons/addon-base-raas/packages/base-raas-workflows/lib/workflows/provision-account.yml b/addons/addon-base-raas/packages/base-raas-workflows/lib/workflows/provision-account.yml
new file mode 100644
index 0000000000..54b8ba519a
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflows/lib/workflows/provision-account.yml
@@ -0,0 +1,17 @@
+id: wf-provision-account
+v: 1
+workflowTemplateId: wt-empty
+workflowTemplateVer: 1
+title: Provision Account
+desc: |
+ Provision Amazon Web Service Account
+hidden: false
+builtin: true
+instanceTtl: 30 # In days, however, empty value means that it is indefinite
+selectedSteps:
+ - stepTemplateId: st-provision-account
+ stepTemplateVer: 1
+ id: wf-step_1_1574277701354_97 # Randomly generated id, no need to change it
+ # configs: # add configuration key/value pairs (if needed)
+ # key1: value1
+ # key2: value2
diff --git a/addons/addon-base-raas/packages/base-raas-workflows/package.json b/addons/addon-base-raas/packages/base-raas-workflows/package.json
new file mode 100644
index 0000000000..4c9f42bfde
--- /dev/null
+++ b/addons/addon-base-raas/packages/base-raas-workflows/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@aws-ee/base-raas-workflows",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A collection of base RaaS workflows",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-workflow-core": "workspace:*",
+ "lodash": "^4.17.15",
+ "shortid": "^2.2.15",
+ "slugify": "^1.4.0"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.1.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; yarn run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-raas/packages/serverless-packer/.eslintrc.json b/addons/addon-base-raas/packages/serverless-packer/.eslintrc.json
new file mode 100644
index 0000000000..a3d178c4c9
--- /dev/null
+++ b/addons/addon-base-raas/packages/serverless-packer/.eslintrc.json
@@ -0,0 +1,22 @@
+{
+ "extends": ["airbnb-base", "prettier"],
+ "plugins": ["prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ }
+}
diff --git a/addons/addon-base-raas/packages/serverless-packer/.gitignore b/addons/addon-base-raas/packages/serverless-packer/.gitignore
new file mode 100644
index 0000000000..285328f831
--- /dev/null
+++ b/addons/addon-base-raas/packages/serverless-packer/.gitignore
@@ -0,0 +1,16 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+/coverage/
+.build
diff --git a/addons/addon-base-raas/packages/serverless-packer/.prettierrc.json b/addons/addon-base-raas/packages/serverless-packer/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-raas/packages/serverless-packer/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-raas/packages/serverless-packer/README.md b/addons/addon-base-raas/packages/serverless-packer/README.md
new file mode 100644
index 0000000000..da04628f93
--- /dev/null
+++ b/addons/addon-base-raas/packages/serverless-packer/README.md
@@ -0,0 +1,18 @@
+## Prerequisites
+
+#### Tools
+
+- Node 12
+- [Hashicorp Packer](https://www.packer.io/)
+
+#### Project variables
+
+All variables in the settings will be added as -var parameters into the packer build command
+
+## Usage
+
+When installed as a [Serverless plugin](https://serverless.com/framework/docs/providers/aws/guide/plugins/), this provides the following CLI commands:
+
+### `pnpx sls build-image -s [--file]`
+
+By convention, this looks in the `./config/infra` directory for a json file that starts with packer. This file is then used by packer to build the AMI.
diff --git a/addons/addon-base-raas/packages/serverless-packer/index.js b/addons/addon-base-raas/packages/serverless-packer/index.js
new file mode 100644
index 0000000000..24ea4c05f1
--- /dev/null
+++ b/addons/addon-base-raas/packages/serverless-packer/index.js
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+const fs = require('fs');
+const _ = require('lodash');
+const { runCommand } = require('./lib/utils/command.js');
+
+const PACKER_FILE_DIR = './config/infra';
+
+class ServerlessPackerPlugin {
+ constructor(serverless, options) {
+ this.serverless = serverless;
+ this.options = options;
+
+ this.commands = {
+ 'build-image': {
+ usage: 'Build an AMI using packer',
+ lifecycleEvents: ['build'],
+ options: {
+ file: {
+ usage:
+ 'Override the packer file used to build the AMI' +
+ '(e.g. "--file \'packer.json\'" or "-m \'packer.json\'")',
+ required: false,
+ shortcut: 'f',
+ },
+ },
+ },
+ };
+
+ this.hooks = {
+ 'build-image:build': this.buildImages.bind(this),
+ };
+ }
+
+ async buildImages() {
+ let filePaths;
+ if (this.options.files) {
+ // Parse files passed via CLI arg
+ filePaths = this.options.files.split(',');
+ } else {
+ // Look for files in default location
+ filePaths = await this.getPackerFiles();
+ }
+
+ this.serverless.cli.log(`Building packer images: ${filePaths.join(', ')}`);
+ return Promise.all(
+ filePaths.map(async filePath => {
+ this.serverless.cli.log(`${filePath}: Building packer image`);
+
+ const args = _.concat('build', this.packageVarArgs(), `${PACKER_FILE_DIR}/${filePath}`);
+
+ try {
+ await runCommand({
+ command: 'packer',
+ args,
+ stdout: {
+ log: this.serverless.cli.consoleLog,
+ raw: msg => {
+ this.serverless.cli.log(`${filePath}: ${msg}`);
+ },
+ },
+ });
+ } catch (err) {
+ throw new Error(`${filePath}: Error running packer build command: ${err}`);
+ }
+
+ this.serverless.cli.log(`${filePath}: Finished packer image`);
+ }),
+ );
+ }
+
+ async getPackerFiles() {
+ return new Promise((resolve, reject) => {
+ fs.readdir(PACKER_FILE_DIR, (err, files) => {
+ if (err || !files) {
+ return reject(new Error('Missing config/infra directory.'));
+ }
+
+ const packerFiles = [];
+ files.forEach(file => {
+ if (file.match('packer.*.json')) {
+ packerFiles.push(file);
+ }
+ });
+ if (packerFiles.length > 0) {
+ return resolve(packerFiles);
+ }
+
+ return reject(new Error('No packer file found'));
+ });
+ });
+ }
+
+ packageVarArgs() {
+ const varArgs = [];
+ _.forEach(this.serverless.service.custom.settings, (value, key) => {
+ varArgs.push('-var');
+ varArgs.push(`${key}=${value}`);
+ });
+ return varArgs;
+ }
+}
+
+module.exports = ServerlessPackerPlugin;
diff --git a/addons/addon-base-raas/packages/serverless-packer/jest.config.js b/addons/addon-base-raas/packages/serverless-packer/jest.config.js
new file mode 100644
index 0000000000..3f7ffc8068
--- /dev/null
+++ b/addons/addon-base-raas/packages/serverless-packer/jest.config.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-raas/packages/serverless-packer/lib/utils/command.js b/addons/addon-base-raas/packages/serverless-packer/lib/utils/command.js
new file mode 100644
index 0000000000..d0b1e91ad6
--- /dev/null
+++ b/addons/addon-base-raas/packages/serverless-packer/lib/utils/command.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.
+ */
+
+/**
+ * Helpers for executing an external command.
+ * */
+const _ = require('lodash');
+const chalk = require('chalk');
+const spawn = require('cross-spawn');
+
+const runCommand = ({ command, args, successCodes = [0], cwd, stdout }) => {
+ const child = spawn(command, args, { stdio: 'pipe', cwd });
+ stdout.log(`${chalk.bgGreen('>>')} ${command} ${args.join(' ')}`);
+ return new Promise((resolve, reject) => {
+ // we are using _.once() because the error and exit events might be fired one after the other
+ // see https://nodejs.org/api/child_process.html#child_process_event_error
+ const rejectOnce = _.once(reject);
+ const resolveOnce = _.once(resolve);
+ const errors = [];
+
+ child.stdout.on('data', data => {
+ stdout.raw(data.toString().trim());
+ });
+
+ child.stderr.on('data', data => {
+ errors.push(data.toString().trim());
+ });
+
+ child.on('exit', code => {
+ if (successCodes.includes(code)) {
+ callLater(resolveOnce);
+ } else {
+ callLater(rejectOnce, new Error(`process exited with code ${code}: ${errors.join('\n')}`));
+ }
+ });
+ child.on('error', () => callLater(rejectOnce, new Error('Failed to start child process.')));
+ });
+};
+
+// to help avoid unleashing Zalgo see http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony
+const callLater = (callback, ...args) => {
+ setImmediate(() => {
+ callback(...args);
+ });
+};
+
+module.exports = { runCommand };
diff --git a/addons/addon-base-raas/packages/serverless-packer/package.json b/addons/addon-base-raas/packages/serverless-packer/package.json
new file mode 100644
index 0000000000..df4180c049
--- /dev/null
+++ b/addons/addon-base-raas/packages/serverless-packer/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@aws-ee/serverless-packer",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A serverless framework plugin to help with using packer",
+ "main": "index.js",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "chalk": "^2.4.2",
+ "cross-spawn": "^6.0.5",
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; yarn run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true"
+ }
+}
diff --git a/addons/addon-base-rest-api/packages/api-handler-factory/.eslintrc.json b/addons/addon-base-rest-api/packages/api-handler-factory/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/api-handler-factory/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-rest-api/packages/api-handler-factory/.gitignore b/addons/addon-base-rest-api/packages/api-handler-factory/.gitignore
new file mode 100644
index 0000000000..659959de8f
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/api-handler-factory/.gitignore
@@ -0,0 +1,16 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/addons/addon-base-rest-api/packages/api-handler-factory/.prettierrc.json b/addons/addon-base-rest-api/packages/api-handler-factory/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/api-handler-factory/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-rest-api/packages/api-handler-factory/jest.config.js b/addons/addon-base-rest-api/packages/api-handler-factory/jest.config.js
new file mode 100644
index 0000000000..3f7ffc8068
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/api-handler-factory/jest.config.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-rest-api/packages/api-handler-factory/jsconfig.json b/addons/addon-base-rest-api/packages/api-handler-factory/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/api-handler-factory/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-rest-api/packages/api-handler-factory/lib/app-context.js b/addons/addon-base-rest-api/packages/api-handler-factory/lib/app-context.js
new file mode 100644
index 0000000000..85c51194d3
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/api-handler-factory/lib/app-context.js
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const express = require('express');
+const Boom = require('@aws-ee/base-services-container/lib/boom');
+
+class AppContext {
+ constructor({ app, settings, log, servicesContainer }) {
+ this.app = app;
+ this.settings = settings;
+ this.log = log;
+ this.container = servicesContainer;
+ this.boom = new Boom();
+ }
+
+ async service(nameOrNames) {
+ const result = [];
+ /* eslint-disable no-restricted-syntax, no-await-in-loop */
+ for (const name of _.concat(nameOrNames)) {
+ const service = await this.container.find(name);
+ if (!service) throw new Error(`The "${name}" service is not available.`);
+ result.push(service);
+ }
+ /* eslint-enable no-restricted-syntax, no-await-in-loop */
+
+ if (!_.isArray(nameOrNames)) return _.head(result);
+ return result;
+ }
+
+ async optionalService(nameOrNames) {
+ const result = [];
+ /* eslint-disable no-restricted-syntax, no-await-in-loop */
+ for (const name of _.concat(nameOrNames)) {
+ const service = await this.container.find(name);
+ result.push(service);
+ }
+ /* eslint-enable no-restricted-syntax, no-await-in-loop */
+
+ if (!_.isArray(nameOrNames)) return _.head(result);
+ return result;
+ }
+
+ wrap(fn) {
+ return async (req, res, next) => {
+ try {
+ await fn(req, res, next);
+ } catch (err) {
+ next(err);
+ }
+ };
+ }
+
+ router() {
+ return express.Router();
+ }
+}
+
+module.exports = AppContext;
diff --git a/addons/addon-base-rest-api/packages/api-handler-factory/lib/error-handler.js b/addons/addon-base-rest-api/packages/api-handler-factory/lib/error-handler.js
new file mode 100644
index 0000000000..4f78853906
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/api-handler-factory/lib/error-handler.js
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const logError = console.error; // eslint-disable-line no-console
+
+module.exports = () => (err, req, res, next) => {
+ if (!_.isError(err)) {
+ next();
+ return;
+ }
+
+ const httpStatus = _.get(err, 'status', 500);
+
+ // see https://github.com/dougmoscrop/serverless-http/blob/master/docs/ADVANCED.md
+ const requestId = _.get(req, 'x-request-id', '');
+ const code = _.get(err, 'code', 'UNKNOWN');
+ const root = _.get(err, 'root');
+ const safe = _.get(err, 'safe', false);
+
+ if (httpStatus >= 500) {
+ // we print the error only if it is an internal server error
+ if (root) logError(root);
+ logError(err);
+ }
+ const errorMessage = err.message;
+
+ const responseBody = {
+ requestId,
+ code,
+ // if there is error message and if it is safe to include then include it in http response
+ message: safe && errorMessage ? errorMessage : 'Something went wrong',
+ };
+ const payload = err.payload;
+ // if there is error payload object and if it is safe to include then include it in http response
+ if (safe && payload) {
+ responseBody.payload = payload;
+ }
+
+ res.set('X-Request-Id-2', requestId);
+ res.status(httpStatus).json(responseBody);
+};
diff --git a/addons/addon-base-rest-api/packages/api-handler-factory/lib/handler.js b/addons/addon-base-rest-api/packages/api-handler-factory/lib/handler.js
new file mode 100644
index 0000000000..2893f6979c
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/api-handler-factory/lib/handler.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.
+ */
+
+const _ = require('lodash');
+const express = require('express');
+const serverless = require('serverless-http');
+const compression = require('compression');
+const bodyParser = require('body-parser');
+const cors = require('cors');
+const ServicesContainer = require('@aws-ee/base-services-container/lib/services-container');
+
+const errorHandler = require('./error-handler');
+const AppContext = require('./app-context');
+
+let cachedHandler;
+
+// registerServices = fn (required)
+// registerRoutes = fn (required)
+function handlerFactory({ registerServices, registerRoutes }) {
+ return async (event, context) => {
+ if (cachedHandler) return cachedHandler(event, context);
+
+ const apiRouter = express.Router({ mergeParams: true });
+ const app = express();
+ app.disable('x-powered-by');
+
+ // register services
+ const servicesContainer = new ServicesContainer(['settings', 'log']);
+ await registerServices(servicesContainer);
+ await servicesContainer.initServices();
+
+ // check circular dependencies
+ const servicesList = servicesContainer.validate();
+
+ // resolve settings and log services
+ const logger = await servicesContainer.find('log');
+ const settingsService = await servicesContainer.find('settings');
+
+ // create app context
+ const appContext = new AppContext({ app, settings: settingsService, log: logger, servicesContainer });
+
+ // register routes
+ await registerRoutes(appContext, apiRouter);
+
+ // setup CORS, compression and body parser
+ const isDev = settingsService.get('envType') === 'dev';
+ let whitelist = settingsService.optionalObject('corsWhitelist', []);
+ if (isDev) whitelist = _.concat(whitelist, settingsService.optionalObject('corsWhitelistLocal', []));
+ const corsOptions = {
+ origin: (origin, callback) => {
+ if (whitelist.indexOf(origin) !== -1) {
+ callback(null, true);
+ } else {
+ callback(null, false);
+ }
+ },
+ optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
+ };
+
+ app.use(compression());
+ app.use(cors(corsOptions));
+ app.use(bodyParser.json({ limit: '50mb' })); // see https://stackoverflow.com/questions/19917401/error-request-entity-too-large
+ app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); // for parsing application/x-www-form-urlencoded
+
+ // mount all routes under /
+ app.use('/', apiRouter);
+
+ // add global error handler
+ app.use(errorHandler());
+
+ // allow options for all
+ app.options('*');
+
+ // prepare the handler
+ cachedHandler = serverless(app, {
+ callbackWaitsForEmptyEventLoop: true,
+ request(req, { requestContext = {} }) {
+ // expose the lambda event request context
+ req.context = requestContext;
+ },
+ });
+
+ // print useful information
+ const settingsList = settingsService.entries;
+
+ logger.info('Settings available are :');
+ logger.info(settingsList);
+
+ logger.info('Services available are :');
+ logger.info(servicesList);
+
+ return cachedHandler(event, context);
+ };
+}
+
+module.exports = handlerFactory;
diff --git a/addons/addon-base-rest-api/packages/api-handler-factory/package.json b/addons/addon-base-rest-api/packages/api-handler-factory/package.json
new file mode 100644
index 0000000000..9c141d67ae
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/api-handler-factory/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@aws-ee/base-api-handler-factory",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A library to help prepare the lambda handler function",
+ "author": "aws-ee",
+ "main": "lib/handler",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-services-container": "workspace:*",
+ "body-parser": "^1.19.0",
+ "compression": "^1.7.4",
+ "cors": "^2.8.5",
+ "express": "^4.17.1",
+ "lodash": "^4.17.15",
+ "serverless-http": "^2.3.1"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-rest-api/packages/base-api-handler/.eslintrc.json b/addons/addon-base-rest-api/packages/base-api-handler/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-api-handler/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-rest-api/packages/base-api-handler/.gitignore b/addons/addon-base-rest-api/packages/base-api-handler/.gitignore
new file mode 100644
index 0000000000..f15d856fe2
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-api-handler/.gitignore
@@ -0,0 +1,22 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# Webpack generated directories
+.webpack
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# IntelliJ Idea Module Files
+*.iml
diff --git a/addons/addon-base-rest-api/packages/base-api-handler/.prettierrc.json b/addons/addon-base-rest-api/packages/base-api-handler/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-api-handler/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-rest-api/packages/base-api-handler/jest.config.js b/addons/addon-base-rest-api/packages/base-api-handler/jest.config.js
new file mode 100644
index 0000000000..3f7ffc8068
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-api-handler/jest.config.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-rest-api/packages/base-api-handler/jsconfig.json b/addons/addon-base-rest-api/packages/base-api-handler/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-api-handler/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-rest-api/packages/base-api-handler/lib/plugins/services-plugin.js b/addons/addon-base-rest-api/packages/base-api-handler/lib/plugins/services-plugin.js
new file mode 100644
index 0000000000..dcfd14864b
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-api-handler/lib/plugins/services-plugin.js
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+
+const AwsService = require('@aws-ee/base-services/lib/aws/aws-service');
+const DbService = require('@aws-ee/base-services/lib/db-service');
+const DbAuthenticationService = require('@aws-ee/base-api-services/lib/db-authentication-service');
+const JsonSchemaValidationService = require('@aws-ee/base-services/lib/json-schema-validation-service');
+const InputManifestValidationService = require('@aws-ee/base-services/lib/input-manifest/input-manifest-validation-service');
+const S3Service = require('@aws-ee/base-services/lib/s3-service');
+const LockService = require('@aws-ee/base-services/lib/lock/lock-service');
+const PluginRegistryService = require('@aws-ee/base-services/lib/plugin-registry/plugin-registry-service');
+const AuditWriterService = require('@aws-ee/base-services/lib/audit/audit-writer-service');
+const AuthorizationService = require('@aws-ee/base-services/lib/authorization/authorization-service');
+const UserAuthzService = require('@aws-ee/base-services/lib/user/user-authz-service');
+const UserService = require('@aws-ee/base-services/lib/user/user-service');
+const AuthenticationProviderConfigService = require('@aws-ee/base-api-services/lib/authentication-providers/authentication-provider-config-service');
+const AuthenticationProviderTypeService = require('@aws-ee/base-api-services/lib/authentication-providers/authentication-provider-type-service');
+const DbPasswordService = require('@aws-ee/base-services/lib/db-password/db-password-service');
+const JwtService = require('@aws-ee/base-api-services/lib/jwt-service');
+const registerBuiltInAuthProviders = require('@aws-ee/base-api-services/lib/authentication-providers/register-built-in-provider-services');
+const registerBuiltInAuthProvisioners = require('@aws-ee/base-api-services/lib/authentication-providers/register-built-in-provisioner-services');
+const ApiKeyService = require('@aws-ee/base-api-services/lib/authentication-providers/built-in-providers/internal/api-key-service');
+const TokenRevocationService = require('@aws-ee/base-api-services/lib/token-revocation-service');
+
+const settingKeys = {
+ tablePrefix: 'dbTablePrefix',
+};
+
+/**
+ * A function that registers base services required by the base addon for api handler lambda function
+ * @param container An instance of ServicesContainer to register services to
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+async function registerServices(container, pluginRegistry) {
+ container.register('aws', new AwsService(), { lazy: false });
+
+ container.register('authenticationProviderConfigService', new AuthenticationProviderConfigService());
+ container.register('authenticationProviderTypeService', new AuthenticationProviderTypeService());
+ // Register all the built in authentication providers supported by the base out of the box
+ registerBuiltInAuthProviders(container);
+ registerBuiltInAuthProvisioners(container);
+
+ container.register('dbService', new DbService(), { lazy: false });
+ container.register('dbAuthenticationService', new DbAuthenticationService());
+ container.register('dbPasswordService', new DbPasswordService());
+ container.register('jsonSchemaValidationService', new JsonSchemaValidationService());
+ container.register('inputManifestValidationService', new InputManifestValidationService());
+ container.register('jwtService', new JwtService());
+ container.register('userService', new UserService());
+ container.register('s3Service', new S3Service());
+ container.register('lockService', new LockService());
+ container.register('apiKeyService', new ApiKeyService());
+ container.register('tokenRevocationService', new TokenRevocationService());
+ container.register('auditWriterService', new AuditWriterService());
+ container.register('pluginRegistryService', new PluginRegistryService(pluginRegistry), { lazy: false });
+
+ // Authorization Services from base addon
+ container.register('authorizationService', new AuthorizationService());
+ container.register('userAuthzService', new UserAuthzService());
+}
+
+/**
+ * A function that registers base static settings required by the base addon for api handler lambda function
+ * @param existingStaticSettings An existing static settings plain javascript object containing settings as key/value contributed by other plugins
+ * @param settings Default instance of settings service that resolves settings from environment variables
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point
+ *
+ * @returns {Promise<*>} A promise that resolves to static settings object
+ */
+// eslint-disable-next-line no-unused-vars
+function getStaticSettings(existingStaticSettings, settings, pluginRegistry) {
+ const staticSettings = {
+ ...existingStaticSettings,
+ };
+
+ // Register all dynamodb table names used by the base rest api addon
+ const tablePrefix = settings.get(settingKeys.tablePrefix);
+ const table = (key, suffix) => {
+ staticSettings[key] = `${tablePrefix}-${suffix}`;
+ };
+ table('dbTableAuthenticationProviderTypes', 'DbAuthenticationProviderTypes');
+ table('dbTableAuthenticationProviderConfigs', 'DbAuthenticationProviderConfigs');
+ table('dbTablePasswords', 'DbPasswords');
+ table('dbTableUserApiKeys', 'DbUserApiKeys');
+ table('dbTableRevokedTokens', 'DbRevokedTokens');
+ table('dbTableUsers', 'DbUsers');
+ table('dbTableLocks', 'DbLocks');
+
+ return staticSettings;
+}
+
+const plugin = {
+ getStaticSettings,
+ // getLoggingContext, // not implemented, the default behavior provided by addon-base is sufficient
+ // getLoggingContext, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerSettingsService, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerLoggerService, // not implemented, the default behavior provided by addon-base is sufficient
+ registerServices,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-rest-api/packages/base-api-handler/lib/routes-registration-util.js b/addons/addon-base-rest-api/packages/base-api-handler/lib/routes-registration-util.js
new file mode 100644
index 0000000000..222566fa38
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-api-handler/lib/routes-registration-util.js
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+/**
+ * Configures the given express router by collecting routes contributed by all route plugins.
+ * @param {*} context An instance of AppContext from api-handler-factory
+ * @param {*} router Top level Express router
+ * @param {getPlugins} pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ * Each 'route' plugin in the returned array is an object containing "getRoutes" method.
+ *
+ * @returns {Promise}
+ */
+async function registerRoutes(context, router, pluginRegistry) {
+ // Get all routes plugins from the routes plugin registry
+ // Each plugin is an object containing "getRoutes" method
+ const plugins = await pluginRegistry.getPlugins('route');
+
+ const initialRoutes = new Map();
+ // Ask each plugin to return their routes. Each plugin is passed a Map containing the routes collected so
+ // far from other plugins. The plugins are called in the same order as returned by the registry.
+ // Each plugin gets a chance to add, remove, update, or delete routes by mutating the provided routesMap object.
+ // This routesMap is a Map that has route paths as keys and an array of functions that configure the router as value.
+ // Each function in the array is expected have the following signature. The function accepts context and router
+ // arguments and returns a configured router.
+ //
+ // (context, router) => configured router
+ //
+ const routesMap = await _.reduce(
+ plugins,
+ async (routesSoFarPromise, plugin) => plugin.getRoutes(await routesSoFarPromise, pluginRegistry),
+ Promise.resolve(initialRoutes),
+ );
+
+ const configuredRoutes = [];
+ const entries = Array.from(routesMap || new Map());
+ // Register routes to the parent level express "router" and call each function from the routes configuration
+ // functions to give them a chance to configure their routes by either adding routes directly to the parent router
+ // or by returning a child router
+ for (let i = 0; i < entries.length; i += 1) {
+ const entry = entries[i];
+ const [routePath, routerConfigurers] = entry;
+ for (let j = 0; j < routerConfigurers.length; j += 1) {
+ const configurerFn = routerConfigurers[j];
+ // Need to await configurerFn in sequence so awaiting in loop
+ // eslint-disable-next-line no-await-in-loop
+ const childRouter = await configurerFn(context, router);
+ // The router configurer function may create a child router.
+ // In that case, configure the child router on the parent router.
+ // If the function does not return a router then assume it configured
+ // the route directly on the parent router given to it
+ if (childRouter) {
+ router.use(routePath, childRouter);
+ }
+ configuredRoutes.push(routePath);
+ }
+ }
+ return configuredRoutes;
+}
+
+module.exports = { registerRoutes };
diff --git a/addons/addon-base-rest-api/packages/base-api-handler/package.json b/addons/addon-base-rest-api/packages/base-api-handler/package.json
new file mode 100644
index 0000000000..768842ea4d
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-api-handler/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@aws-ee/base-api-handler",
+ "version": "1.0.0",
+ "private": true,
+ "description": "A library containing some utilities to be used for an api-handler lambda function based on addons",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-api-services": "workspace:*",
+ "@aws-ee/base-services": "workspace:*",
+ "@aws-ee/base-services-container": "workspace:*",
+ "aws-sdk": "^2.647.0",
+ "generate-password": "^1.5.0",
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-rest-api/packages/base-authn-handler/.eslintrc.json b/addons/addon-base-rest-api/packages/base-authn-handler/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-authn-handler/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-rest-api/packages/base-authn-handler/.gitignore b/addons/addon-base-rest-api/packages/base-authn-handler/.gitignore
new file mode 100644
index 0000000000..f15d856fe2
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-authn-handler/.gitignore
@@ -0,0 +1,22 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# Webpack generated directories
+.webpack
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# IntelliJ Idea Module Files
+*.iml
diff --git a/addons/addon-base-rest-api/packages/base-authn-handler/.prettierrc.json b/addons/addon-base-rest-api/packages/base-authn-handler/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-authn-handler/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-rest-api/packages/base-authn-handler/jest.config.js b/addons/addon-base-rest-api/packages/base-authn-handler/jest.config.js
new file mode 100644
index 0000000000..3f7ffc8068
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-authn-handler/jest.config.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-rest-api/packages/base-authn-handler/jsconfig.json b/addons/addon-base-rest-api/packages/base-authn-handler/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-authn-handler/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-rest-api/packages/base-authn-handler/lib/plugins/services-plugin.js b/addons/addon-base-rest-api/packages/base-authn-handler/lib/plugins/services-plugin.js
new file mode 100644
index 0000000000..de6f88e7f2
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-authn-handler/lib/plugins/services-plugin.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.
+ */
+
+const AwsService = require('@aws-ee/base-services/lib/aws/aws-service');
+const DbService = require('@aws-ee/base-services/lib/db-service');
+const S3Service = require('@aws-ee/base-services/lib/s3-service');
+const JsonSchemaValidationService = require('@aws-ee/base-services/lib/json-schema-validation-service');
+const PluginRegistryService = require('@aws-ee/base-services/lib/plugin-registry/plugin-registry-service');
+const AuditWriterService = require('@aws-ee/base-services/lib/audit/audit-writer-service');
+const AuthorizationService = require('@aws-ee/base-services/lib/authorization/authorization-service');
+const UserAuthzService = require('@aws-ee/base-services/lib/user/user-authz-service');
+const UserService = require('@aws-ee/base-services/lib/user/user-service');
+const AuthenticationService = require('@aws-ee/base-api-services/lib/authentication-service');
+const AuthenticationProviderConfigService = require('@aws-ee/base-api-services/lib/authentication-providers/authentication-provider-config-service');
+const AuthenticationProviderTypeService = require('@aws-ee/base-api-services/lib/authentication-providers/authentication-provider-type-service');
+const DbAuthenticationService = require('@aws-ee/base-api-services/lib/db-authentication-service');
+const DbPasswordService = require('@aws-ee/base-services/lib/db-password/db-password-service');
+const JwtService = require('@aws-ee/base-api-services/lib/jwt-service');
+const TokenRevocationService = require('@aws-ee/base-api-services/lib/token-revocation-service');
+const registerBuiltInAuthProviders = require('@aws-ee/base-api-services/lib/authentication-providers/register-built-in-provider-services');
+const registerBuiltInAuthProvisioners = require('@aws-ee/base-api-services/lib/authentication-providers/register-built-in-provisioner-services');
+
+const settingKeys = {
+ tablePrefix: 'dbTablePrefix',
+};
+
+/**
+ * A function that registers base services required by the base addon for api handler lambda function
+ * @param container An instance of ServicesContainer to register services to
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerServices(container, pluginRegistry) {
+ container.register('aws', new AwsService());
+ container.register('s3Service', new S3Service());
+
+ container.register('authenticationProviderConfigService', new AuthenticationProviderConfigService());
+ container.register('authenticationProviderTypeService', new AuthenticationProviderTypeService());
+ container.register('authenticationService', new AuthenticationService());
+ container.register('tokenRevocationService', new TokenRevocationService());
+
+ // Register all the built in authentication providers supported by the data lake out of the box
+ registerBuiltInAuthProviders(container);
+ registerBuiltInAuthProvisioners(container);
+
+ container.register('dbService', new DbService(), { lazy: false });
+ container.register('dbAuthenticationService', new DbAuthenticationService());
+ container.register('dbPasswordService', new DbPasswordService());
+ container.register('jsonSchemaValidationService', new JsonSchemaValidationService());
+ container.register('jwtService', new JwtService());
+ container.register('userService', new UserService());
+ container.register('auditWriterService', new AuditWriterService());
+ container.register('pluginRegistryService', new PluginRegistryService(pluginRegistry), { lazy: false });
+
+ // Authorization Services from base addon
+ container.register('authorizationService', new AuthorizationService());
+ container.register('userAuthzService', new UserAuthzService());
+}
+
+/**
+ * A function that registers base static settings required by the base addon for api handler lambda function
+ * @param existingStaticSettings
+ * @param settings
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to static settings object
+ */
+// eslint-disable-next-line no-unused-vars
+function getStaticSettings(existingStaticSettings, settings, pluginRegistry) {
+ const staticSettings = {
+ ...existingStaticSettings,
+ };
+
+ // Register all dynamodb table names used by the base rest api addon
+ const tablePrefix = settings.get(settingKeys.tablePrefix);
+ const table = (key, suffix) => {
+ staticSettings[key] = `${tablePrefix}-${suffix}`;
+ };
+ table('dbTableAuthenticationProviderTypes', 'DbAuthenticationProviderTypes');
+ table('dbTableAuthenticationProviderConfigs', 'DbAuthenticationProviderConfigs');
+ table('dbTablePasswords', 'DbPasswords');
+ table('dbTableUserApiKeys', 'DbUserApiKeys');
+ table('dbTableRevokedTokens', 'DbRevokedTokens');
+ table('dbTableUsers', 'DbUsers');
+ table('dbTableLocks', 'DbLocks');
+
+ return staticSettings;
+}
+
+const plugin = {
+ getStaticSettings,
+ // getLoggingContext, // not implemented, the default behavior provided by addon-base is sufficient
+ // getLoggingContext, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerSettingsService, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerLoggerService, // not implemented, the default behavior provided by addon-base is sufficient
+ registerServices,
+};
+module.exports = plugin;
diff --git a/addons/addon-base-rest-api/packages/base-authn-handler/package.json b/addons/addon-base-rest-api/packages/base-authn-handler/package.json
new file mode 100644
index 0000000000..934543ceda
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-authn-handler/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@aws-ee/base-authn-handler",
+ "version": "1.0.0",
+ "private": true,
+ "description": "A library containing some utilities to be used for an custom lambda authorizer lambda function based on addons",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-api-services": "workspace:*",
+ "@aws-ee/base-services": "workspace:*",
+ "@aws-ee/base-services-container": "workspace:*",
+ "aws-sdk": "^2.647.0",
+ "generate-password": "^1.5.0",
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-rest-api/packages/base-controllers/.eslintrc.json b/addons/addon-base-rest-api/packages/base-controllers/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-rest-api/packages/base-controllers/.gitignore b/addons/addon-base-rest-api/packages/base-controllers/.gitignore
new file mode 100644
index 0000000000..659959de8f
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/.gitignore
@@ -0,0 +1,16 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/addons/addon-base-rest-api/packages/base-controllers/.prettierrc.json b/addons/addon-base-rest-api/packages/base-controllers/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-rest-api/packages/base-controllers/jest.config.js b/addons/addon-base-rest-api/packages/base-controllers/jest.config.js
new file mode 100644
index 0000000000..3f7ffc8068
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/jest.config.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-rest-api/packages/base-controllers/jsconfig.json b/addons/addon-base-rest-api/packages/base-controllers/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/api-key-controller.js b/addons/addon-base-rest-api/packages/base-controllers/lib/api-key-controller.js
new file mode 100644
index 0000000000..b9db557e0b
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/lib/api-key-controller.js
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ const apiKeyService = await context.service('apiKeyService');
+
+ // ===============================================================
+ // GET / (mounted to /api/api-keys)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const { username, ns } = requestContext.principalIdentifier;
+ // Is user is specified then perform operation for that user or else for current user
+ const usernameToUse = req.query.username || username;
+ const nsToUse = req.query.ns || ns;
+ const apiKeys = await apiKeyService.getApiKeys(requestContext, { username: usernameToUse, ns: nsToUse });
+ res.status(200).json(apiKeys);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id (mounted to /api/api-keys)
+ // ===============================================================
+ router.get(
+ '/:id',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const { username, ns } = requestContext.principalIdentifier;
+ // Is user is specified then perform operation for that user or else for current user
+ const usernameToUse = req.query.username || username;
+ const nsToUse = req.query.ns || ns;
+ const keyId = req.params.id;
+ const apiKey = await apiKeyService.getApiKey(requestContext, { username: usernameToUse, ns: nsToUse, keyId });
+ res.status(200).json(apiKey);
+ }),
+ );
+
+ // ===============================================================
+ // PUT /:id/revoke (mounted to /api/api-keys)
+ // ===============================================================
+ router.put(
+ '/:id/revoke',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const { username, ns } = requestContext.principalIdentifier;
+ // Is user is specified then perform operation for that user or else for current user
+ const usernameToUse = req.query.username || username;
+ const nsToUse = req.query.ns || ns;
+ const keyId = req.params.id;
+ const apiKey = await apiKeyService.revokeApiKey(requestContext, { username: usernameToUse, ns: nsToUse, keyId });
+ res.status(200).json(apiKey);
+ }),
+ );
+
+ // ===============================================================
+ // POST / (mounted to /api/api-keys)
+ // ===============================================================
+ router.post(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const { username, ns } = requestContext.principalIdentifier;
+ // Is user is specified then perform operation for that user or else for current user
+ const usernameToUse = req.query.username || username;
+ const nsToUse = req.query.ns || ns;
+ const apiKey = await apiKeyService.issueApiKey(requestContext, {
+ username: usernameToUse,
+ ns: nsToUse,
+ expiryTime: req.body.expiryTime,
+ });
+ res.status(200).json(apiKey);
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/authentication-provider-controller.js b/addons/addon-base-rest-api/packages/base-controllers/lib/authentication-provider-controller.js
new file mode 100644
index 0000000000..35013d62e5
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/lib/authentication-provider-controller.js
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const { ensureAdmin } = require('@aws-ee/base-services/lib/authorization/assertions');
+const { newInvoker } = require('@aws-ee/base-api-services/lib/authentication-providers/helpers/invoker');
+const authProviderConstants = require('@aws-ee/base-api-services/lib/authentication-providers/constants')
+ .authenticationProviders;
+
+/**
+ * Function to remove impl information from authentication provider config or authentication provider type configuration as that is not useful on the client side \
+ * and should not be transmitted
+ *
+ * @param authConfigOrTypeConfig Authentication provider config or authentication provider type configuration
+ * @returns {{impl}}
+ */
+const sanitize = authConfigOrTypeConfig => {
+ const sanitizeOne = config => {
+ if (_.get(config, 'config.impl')) {
+ // When the auth provider type config is passed the impl is at 'config.impl' path
+ delete config.config.impl;
+ } else if (_.get(config, 'type.config.impl')) {
+ // When the auth provider config is passed the impl is at 'type.config.impl' path
+ delete config.type.config.impl;
+ }
+ return config;
+ };
+ return _.isArray(authConfigOrTypeConfig)
+ ? _.map(authConfigOrTypeConfig, sanitizeOne)
+ : sanitizeOne(authConfigOrTypeConfig);
+};
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ // const settings = context.settings;
+ const boom = context.boom;
+ const invoke = newInvoker(context.service.bind(context));
+
+ const [authenticationProviderTypeService, authenticationProviderConfigService] = await context.service([
+ 'authenticationProviderTypeService',
+ 'authenticationProviderConfigService',
+ ]);
+
+ const saveAuthenticationProvider = async (res, req, action) => {
+ const requestContext = res.locals.requestContext;
+ const { providerTypeId, providerConfig } = req.body;
+
+ // Make sure the current user is an admin user
+ // Only admins are allowed to add authentication providers
+ await ensureAdmin(requestContext);
+
+ if (!providerTypeId) {
+ throw boom.badRequest('Missing providerTypeId in the request', true);
+ }
+ if (!providerConfig) {
+ throw boom.badRequest('Missing providerConfig in the request', true);
+ }
+ if (!providerConfig.id) {
+ throw boom.badRequest('Missing id in the providerConfig', true);
+ }
+
+ const providerTypeConfig = await authenticationProviderTypeService.getAuthenticationProviderType(
+ requestContext,
+ providerTypeId,
+ );
+
+ if (_.isEmpty(providerTypeConfig)) {
+ throw boom.badRequest(
+ `Invalid providerTypeId specified. No authentication provider type with id = "${providerTypeId}" found`,
+ true,
+ );
+ }
+
+ const provisionerLocator = _.get(providerTypeConfig, 'config.impl.provisionerLocator');
+ const result = await invoke(provisionerLocator, {
+ providerTypeConfig,
+ providerConfig,
+ action,
+ });
+
+ res.status(200).json(sanitize(result));
+ };
+
+ // ===============================================================
+ // GET /configs (mounted to /api/authentication/provider)
+ // ===============================================================
+ router.get(
+ '/configs',
+ wrap(async (req, res) => {
+ const result = await authenticationProviderConfigService.getAuthenticationProviderConfigs();
+ res.status(200).json(sanitize(result));
+ }),
+ );
+
+ // ===============================================================
+ // POST /configs (mounted to /api/authentication/provider)
+ // ===============================================================
+ router.post(
+ '/configs',
+ wrap(async (req, res) => {
+ await saveAuthenticationProvider(res, req, authProviderConstants.provisioningAction.create);
+ }),
+ );
+
+ // ===============================================================
+ // PUT /configs (mounted to /api/authentication/provider)
+ // ===============================================================
+ router.put(
+ '/configs',
+ wrap(async (req, res) => {
+ await saveAuthenticationProvider(res, req, authProviderConstants.provisioningAction.update);
+ }),
+ );
+
+ // ===============================================================
+ // GET /types (mounted to /api/authentication/provider)
+ // ===============================================================
+ router.get(
+ '/types',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const result = await authenticationProviderTypeService.getAuthenticationProviderTypes(requestContext);
+ res.status(200).json(sanitize(result));
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/authentication-provider-public-controller.js b/addons/addon-base-rest-api/packages/base-controllers/lib/authentication-provider-public-controller.js
new file mode 100644
index 0000000000..368ce36c93
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/lib/authentication-provider-public-controller.js
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+const cognitoAuthType = require('@aws-ee/base-api-services/lib/authentication-providers/built-in-providers/cogito-user-pool/type')
+ .type;
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ // const settings = context.settings;
+ // const boom = context.boom;
+
+ const authenticationProviderConfigService = await context.service('authenticationProviderConfigService');
+
+ // ===============================================================
+ // GET / (mounted to /api/authentication/public/provider/configs)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const providers = await authenticationProviderConfigService.getAuthenticationProviderConfigs();
+
+ // Construct/filter results based on info that's needed client-side
+ const result = [];
+ providers.forEach(provider => {
+ const basePublicInfo = {
+ id: provider.config.id,
+ title: provider.config.title,
+ type: provider.config.type.type,
+ credentialHandlingType: provider.config.type.config.credentialHandlingType,
+ signInUri: provider.config.signInUri,
+ signOutUri: provider.config.signOutUri,
+ };
+
+ if (provider.config.type.type !== cognitoAuthType) {
+ // For non-Cognito providers, just return their info as-is
+ result.push(basePublicInfo);
+ } else {
+ // If native users are enabled for a Cognito user pool, add the pool's info
+ // NOTE: The pool info is still needed by the frontend even if native users
+ // are disabled. When a user is federated by Cognito, the JWT issuer
+ // is defined as the user pool itself. The frontend uses the JWT issuer
+ // to determine which provider was used so that it can facilitate logout.
+ const cognitoPublicInfo = {
+ ...basePublicInfo,
+ userPoolId: provider.config.userPoolId,
+ clientId: provider.config.clientId,
+ enableNativeUserPoolUsers: provider.config.enableNativeUserPoolUsers,
+ };
+
+ if (cognitoPublicInfo.enableNativeUserPoolUsers) {
+ cognitoPublicInfo.signInUri = `${basePublicInfo.signInUri}&identity_provider=COGNITO`;
+ } else {
+ delete cognitoPublicInfo.signInUri;
+ }
+
+ result.push(cognitoPublicInfo);
+
+ // Add IdPs federating via Cognito as their own entries
+ provider.config.federatedIdentityProviders.forEach(idp => {
+ result.push({
+ ...basePublicInfo,
+ id: idp.id,
+ title: idp.displayName,
+ type: 'cognito_user_pool_federated_idp',
+ signInUri: `${basePublicInfo.signInUri}&idp_identifier=${idp.id}`,
+ });
+ });
+ }
+ });
+ res.status(200).json(result);
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/middlewares/ensure-active.js b/addons/addon-base-rest-api/packages/base-controllers/lib/middlewares/ensure-active.js
new file mode 100644
index 0000000000..39b99183fe
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/lib/middlewares/ensure-active.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ const boom = context.boom;
+ const userService = await context.service('userService');
+ // ===============================================================
+ // A middleware
+ // ===============================================================
+ // Ensure the logged in user is Active before allowing this route access
+ router.all(
+ '*',
+ wrap(async (req, res, next) => {
+ const requestContext = res.locals.requestContext;
+
+ const isActive = await userService.isCurrentUserActive(requestContext);
+ if (!isActive) {
+ // Do not allow any access if the logged in user is marked inactive in the system
+ throw boom.unauthorized('Inactive user', true);
+ }
+ next();
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/middlewares/ensure-admin.js b/addons/addon-base-rest-api/packages/base-controllers/lib/middlewares/ensure-admin.js
new file mode 100644
index 0000000000..89aa81ae76
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/lib/middlewares/ensure-admin.js
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+const { ensureAdmin } = require('@aws-ee/base-services/lib/authorization/assertions');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ // ===============================================================
+ // A middleware
+ // ===============================================================
+ // Ensure the logged in user is Admin before allowing this route access
+ router.all(
+ '*',
+ wrap(async (req, res, next) => {
+ const requestContext = res.locals.requestContext;
+ await ensureAdmin(requestContext);
+ next();
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/middlewares/prepare-context.js b/addons/addon-base-rest-api/packages/base-controllers/lib/middlewares/prepare-context.js
new file mode 100644
index 0000000000..69fd8e943b
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/lib/middlewares/prepare-context.js
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+const RequestContext = require('@aws-ee/base-services-container/lib/request-context');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ const userService = await context.service('userService');
+
+ // ===============================================================
+ // A middleware
+ // ===============================================================
+ // populate request context, if user is authenticated
+ router.all(
+ '*',
+ wrap(async (req, res, next) => {
+ const requestContext = new RequestContext();
+ res.locals.requestContext = requestContext;
+ const authenticated = res.locals.authenticated;
+ const username = res.locals.username;
+ const authenticationProviderId = res.locals.authenticationProviderId;
+ const identityProviderName = res.locals.identityProviderName;
+
+ if (!authenticated || !username) return next();
+
+ const user = await userService.mustFindUser({
+ username,
+ authenticationProviderId,
+ identityProviderName,
+ });
+ requestContext.authenticated = authenticated;
+ requestContext.principal = user;
+ requestContext.principalIdentifier = { username, ns: user.ns };
+
+ return next();
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/middlewares/setup-auth-context.js b/addons/addon-base-rest-api/packages/base-controllers/lib/middlewares/setup-auth-context.js
new file mode 100644
index 0000000000..4cfa250ae3
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/lib/middlewares/setup-auth-context.js
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+module.exports = async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+
+ // ===============================================================
+ // A middleware
+ // ===============================================================
+ router.all(
+ '*',
+ wrap(async (req, res, next) => {
+ res.locals.authenticated = false; // start with false;
+ const { context: { authorizer } = {} } = req;
+ if (authorizer) {
+ const { token, isApiKey, username, identityProviderName, authenticationProviderId } = authorizer;
+ res.locals.token = token;
+ res.locals.isApiKey = isApiKey; // may be undefined if the token is not an api key
+ res.locals.username = username;
+ res.locals.identityProviderName = identityProviderName;
+ res.locals.authenticationProviderId = authenticationProviderId;
+ res.locals.authenticated = true;
+ }
+ next();
+ }),
+ );
+ return router;
+};
diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/plugins/routes-plugin.js b/addons/addon-base-rest-api/packages/base-controllers/lib/plugins/routes-plugin.js
new file mode 100644
index 0000000000..2d584c5d02
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/lib/plugins/routes-plugin.js
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+// base middlewares
+const newSetupAuthContextMiddleware = require('../middlewares/setup-auth-context');
+const prepareContextFn = require('../middlewares/prepare-context');
+const ensureActiveFn = require('../middlewares/ensure-active');
+const ensureAdminFn = require('../middlewares/ensure-admin');
+// base controllers
+const authenticationProviderController = require('../authentication-provider-controller');
+const authenticationProviderPublicController = require('../authentication-provider-public-controller');
+const signInController = require('../sign-in-controller');
+const signOutController = require('../sign-out-controller');
+const apiKeyController = require('../api-key-controller');
+const usersController = require('../users-controller');
+const userController = require('../user-controller');
+/**
+ * Adds base routes to the given routesMap.
+ * @param routesMap A Map containing routes. This object is a Map that has route paths as
+ * keys and an array of functions that configure the router as value. Each function in the
+ * array is expected have the following signature. The function accepts context and router
+ * arguments and returns a configured router.
+ *
+ * (context, router) => configured router
+ *
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} Returns a Map with the mapping of base routes vs their router configurer functions
+ */
+// eslint-disable-next-line no-unused-vars
+async function getBaseRoutes(routesMap, pluginRegistry) {
+ const routes = new Map([
+ ...routesMap,
+ // PUBLIC APIS - No base middlewares to configure
+ ['/api/authentication/id-tokens', [signInController]],
+ ['/api/authentication/public/provider/configs', [authenticationProviderPublicController]],
+
+ // PROTECTED APIS accessible only to logged in admin users
+ [
+ '/api/authentication/provider',
+ [
+ newSetupAuthContextMiddleware,
+ prepareContextFn,
+ ensureActiveFn,
+ ensureAdminFn,
+ authenticationProviderController,
+ ],
+ ],
+
+ // PROTECTED API accessible to logged in (but not necessarily active) users
+ ['/api/authentication/logout', [newSetupAuthContextMiddleware, prepareContextFn, signOutController]],
+
+ // Other PROTECTED APIS accessible only to logged in active users
+ ['/api/api-keys', [newSetupAuthContextMiddleware, prepareContextFn, ensureActiveFn, apiKeyController]],
+ ['/api/users', [newSetupAuthContextMiddleware, prepareContextFn, ensureActiveFn, usersController]],
+ ['/api/user', [newSetupAuthContextMiddleware, prepareContextFn, ensureActiveFn, userController]],
+ ]);
+
+ return routes;
+}
+
+const plugin = {
+ getRoutes: getBaseRoutes,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/sign-in-controller.js b/addons/addon-base-rest-api/packages/base-controllers/lib/sign-in-controller.js
new file mode 100644
index 0000000000..aad61aa6d2
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/lib/sign-in-controller.js
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const { newInvoker } = require('@aws-ee/base-api-services/lib/authentication-providers/helpers/invoker');
+const authProviderConstants = require('@aws-ee/base-api-services/lib/authentication-providers/constants')
+ .authenticationProviders;
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ // const settings = context.settings;
+ // const boom = context.boom;
+
+ const authenticationProviderConfigService = await context.service('authenticationProviderConfigService');
+ const invoke = newInvoker(context.service.bind(context));
+ // ===============================================================
+ // POST / (mounted to /api/authentication/id-tokens)
+ // ===============================================================
+ router.post(
+ '/',
+ wrap(async (req, res) => {
+ const { username, password, authenticationProviderId } = req.body;
+
+ // If no authentication provider id is specified in the request then assume this to be authenticated by the
+ // internal authentication provider
+ const authenticationProviderIdToUse = authenticationProviderId || authProviderConstants.internalAuthProviderId;
+
+ const authProviderConfig = await authenticationProviderConfigService.getAuthenticationProviderConfig(
+ authenticationProviderIdToUse,
+ );
+ const tokenIssuerLocator = _.get(authProviderConfig, 'config.type.config.impl.tokenIssuerLocator');
+ const idToken = await invoke(tokenIssuerLocator, { username, password }, authProviderConfig);
+ res.status(200).json({ idToken });
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/sign-out-controller.js b/addons/addon-base-rest-api/packages/base-controllers/lib/sign-out-controller.js
new file mode 100644
index 0000000000..a6f2326035
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/lib/sign-out-controller.js
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const { newInvoker } = require('@aws-ee/base-api-services/lib/authentication-providers/helpers/invoker');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ // const settings = context.settings;
+ const boom = context.boom;
+
+ const providerConfigService = await context.service('authenticationProviderConfigService');
+ const invoke = newInvoker(context.service.bind(context));
+ // ===============================================================
+ // POST / (mounted to /api/authentication/logout)
+ // ===============================================================
+ router.post(
+ '/',
+ wrap(async (req, res) => {
+ if (res.locals.isApiKey) {
+ throw boom.badRequest('Logout API is not supported using api key', true);
+ }
+ const providerId = res.locals.authenticationProviderId;
+ const token = res.locals.token;
+ const requestContext = res.locals.requestContext;
+
+ const providerConfig = await providerConfigService.getAuthenticationProviderConfig(providerId);
+ const tokenRevokerLocator = _.get(providerConfig, 'config.type.config.impl.tokenRevokerLocator');
+ if (!tokenRevokerLocator) {
+ throw boom.badRequest(
+ `Error logging out. The authentication provider with id = '${providerId}' does not support token revocation`,
+ false,
+ );
+ }
+
+ // invoke the token revoker and pass the token that needs to be revoked
+ await invoke(tokenRevokerLocator, requestContext, { token }, providerConfig);
+ res.status(200).json({ revoked: true });
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/user-controller.js b/addons/addon-base-rest-api/packages/base-controllers/lib/user-controller.js
new file mode 100644
index 0000000000..19ae42b166
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/lib/user-controller.js
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ const [userService] = await context.service(['userService']);
+
+ // ===============================================================
+ // GET / (mounted to /api/user)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const user = res.locals.requestContext.principal;
+ res.status(200).json(user);
+ }),
+ );
+
+ // ===============================================================
+ // PUT / (mounted to /api/user)
+ // ===============================================================
+ // This is for self-service update
+ router.put(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const currentUser = requestContext.principal;
+ // Get current user's attributes to identify the user in the system
+ const { username, authenticationProviderId, identityProviderName } = currentUser;
+ const userToUpdate = req.body;
+ const updatedUser = await userService.updateUser(requestContext, {
+ ...userToUpdate,
+ username,
+ authenticationProviderId,
+ identityProviderName,
+ });
+ res.status(200).json(updatedUser);
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-rest-api/packages/base-controllers/lib/users-controller.js b/addons/addon-base-rest-api/packages/base-controllers/lib/users-controller.js
new file mode 100644
index 0000000000..d8a8e65c18
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/lib/users-controller.js
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const authProviderConstants = require('@aws-ee/base-api-services/lib/authentication-providers/constants')
+ .authenticationProviders;
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ const boom = context.boom;
+ const [userService, dbPasswordService] = await context.service(['userService', 'dbPasswordService']);
+
+ // ===============================================================
+ // GET / (mounted to /api/users)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const users = await userService.listUsers(requestContext);
+ res.status(200).json(users);
+ }),
+ );
+
+ // ===============================================================
+ // POST / (mounted to /api/users)
+ // ===============================================================
+ router.post(
+ '/',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const authenticationProviderId =
+ req.query.authenticationProviderId || authProviderConstants.internalAuthProviderId;
+ const identityProviderName = req.query.identityProviderName; // This is currently always null or undefined for "internal" auth provider
+ if (authenticationProviderId !== authProviderConstants.internalAuthProviderId) {
+ throw boom.badRequest(
+ `Cannot create user for authentication provider ${authenticationProviderId}. Currently adding users is only supported for internal authentication provider.`,
+ true,
+ );
+ }
+ const { username, firstName, lastName, email, isAdmin, status, password } = req.body;
+
+ const createdUser = await userService.createUser(requestContext, {
+ username,
+ authenticationProviderId,
+ identityProviderName,
+ firstName,
+ lastName,
+ email,
+ isAdmin: _.isNil(isAdmin) ? false : isAdmin,
+ status,
+ password,
+ });
+
+ res.status(200).json(createdUser);
+ }),
+ );
+
+ // ===============================================================
+ // PUT /:username (mounted to /api/users)
+ // ===============================================================
+ router.put(
+ '/:username',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const username = req.params.username;
+ const authenticationProviderId =
+ req.query.authenticationProviderId || authProviderConstants.internalAuthProviderId;
+ const identityProviderName = req.query.identityProviderName;
+ const { firstName, lastName, email, isAdmin, status, rev } = req.body;
+ const user = await userService.updateUser(requestContext, {
+ username,
+ authenticationProviderId,
+ identityProviderName,
+ firstName,
+ lastName,
+ email,
+ isAdmin: _.isNil(isAdmin) ? false : isAdmin,
+ status: _.isNil(status) ? 'active' : status,
+ rev,
+ });
+ res.status(200).json(user);
+ }),
+ );
+
+ // ===============================================================
+ // PUT /:username/password (mounted to /api/users)
+ // ===============================================================
+ router.put(
+ '/:username/password',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const username = req.params.username;
+ const authenticationProviderId =
+ req.query.authenticationProviderId || authProviderConstants.internalAuthProviderId;
+ if (authenticationProviderId !== authProviderConstants.internalAuthProviderId) {
+ throw boom.badRequest(
+ `Cannot create user for authentication provider ${authenticationProviderId}. Currently adding users is only supported for internal authentication provider.`,
+ true,
+ );
+ }
+ const { password } = req.body;
+
+ // Save password salted hash for the user in internal auth provider (i.e., in passwords table)
+ await dbPasswordService.savePassword(requestContext, { username, password });
+ res.status(200).json({ username, message: `Password successfully updated for user ${username}` });
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-rest-api/packages/base-controllers/package.json b/addons/addon-base-rest-api/packages/base-controllers/package.json
new file mode 100644
index 0000000000..648f56177d
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/base-controllers/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "@aws-ee/base-controllers",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A library containing a set of base controllers",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-services": "workspace:*",
+ "@aws-ee/base-api-services": "workspace:*",
+ "@aws-ee/base-services-container": "workspace:*",
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-rest-api/packages/services/.eslintrc.json b/addons/addon-base-rest-api/packages/services/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-rest-api/packages/services/.gitignore b/addons/addon-base-rest-api/packages/services/.gitignore
new file mode 100644
index 0000000000..05bd7c6845
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/.gitignore
@@ -0,0 +1,20 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+**/.webpack
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+/coverage/
+.build
diff --git a/addons/addon-base-rest-api/packages/services/.prettierrc.json b/addons/addon-base-rest-api/packages/services/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-rest-api/packages/services/jest.config.js b/addons/addon-base-rest-api/packages/services/jest.config.js
new file mode 100644
index 0000000000..60c221fc1d
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/jest.config.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-rest-api/packages/services/jsconfig.json b/addons/addon-base-rest-api/packages/services/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/authentication-provider-config-service.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/authentication-provider-config-service.js
new file mode 100644
index 0000000000..e1adddc83f
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/authentication-provider-config-service.js
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+const _ = require('lodash');
+const authProviderConstants = require('./constants').authenticationProviders;
+
+const settingKeys = {
+ tableName: 'dbTableAuthenticationProviderConfigs',
+};
+
+const serializeProviderConfig = providerConfig => JSON.stringify(providerConfig);
+const deSerializeProviderConfig = providerConfigStr => JSON.parse(providerConfigStr);
+
+const toProviderConfig = dbResultItem =>
+ _.assign({}, dbResultItem, {
+ config: dbResultItem && deSerializeProviderConfig(dbResultItem.config),
+ });
+
+class AuthenticationProviderConfigService extends Service {
+ constructor() {
+ super();
+ this.dependency(['dbService', 'jsonSchemaValidationService']);
+ }
+
+ async getAuthenticationProviderConfigs(fields = []) {
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+ const dbResults = await dbService.helper
+ .scanner()
+ .table(table)
+ .projection(fields)
+ .scan();
+ return _.map(dbResults, toProviderConfig);
+ }
+
+ async getAuthenticationProviderConfig(providerId, fields = []) {
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+ const dbResult = await dbService.helper
+ .getter()
+ .table(table)
+ .key({ id: providerId })
+ .projection(fields)
+ .get();
+ return dbResult && toProviderConfig(dbResult);
+ }
+
+ async exists(providerId) {
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+ const item = await dbService.helper
+ .getter()
+ .table(table)
+ .key({ id: providerId })
+ .get();
+
+ if (item === undefined) return false;
+ return true;
+ }
+
+ async saveAuthenticationProviderConfig({
+ providerTypeConfig,
+ providerConfig,
+ status = authProviderConstants.status.initializing,
+ }) {
+ const jsonSchemaValidationService = await this.service('jsonSchemaValidationService');
+ const providerConfigJsonSchema = _.get(providerTypeConfig, 'config.inputSchema');
+
+ // Validate input
+ await jsonSchemaValidationService.ensureValid(providerConfig, providerConfigJsonSchema);
+
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+
+ providerConfig.type = providerTypeConfig;
+
+ const dbResult = await dbService.helper
+ .updater()
+ .table(table)
+ .key({ id: providerConfig.id })
+ // save serialized providerConfig as JSON string
+ // also set the "status" of the authentication provider as "initializing", by default
+ // once the provisioning is complete, the status should be set to "active" by the subclasses
+ .item({ config: serializeProviderConfig(providerConfig), status })
+ .update();
+ return dbResult && toProviderConfig(dbResult);
+ }
+}
+
+module.exports = AuthenticationProviderConfigService;
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/authentication-provider-type-service.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/authentication-provider-type-service.js
new file mode 100644
index 0000000000..0a56d398fb
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/authentication-provider-type-service.js
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+const _ = require('lodash');
+
+const internalAuthenticationProviderType = require('./built-in-providers/internal/type');
+const cognitoUserPoolAuthenticationProviderType = require('./built-in-providers/cogito-user-pool/type');
+
+class AuthenticationProviderTypeService extends Service {
+ constructor() {
+ super();
+ this.dependency(['dbService', 'pluginRegistryService']);
+ }
+
+ async getAuthenticationProviderTypes(requestContext) {
+ const types = [internalAuthenticationProviderType, cognitoUserPoolAuthenticationProviderType];
+
+ // Give all plugins a chance in registering their authentication provider types
+ // Each plugin will receive the following payload object with the shape {requestContext, container, types}
+ const pluginRegistryService = await this.service('pluginRegistryService');
+ const result = await pluginRegistryService.visitPlugins('authentication-provider-type', 'registerTypes', {
+ payload: { requestContext, container: this.container, types },
+ });
+ return result ? result.types : [];
+ }
+
+ async getAuthenticationProviderType(requestContext, providerTypeId) {
+ const providerTypes = await this.getAuthenticationProviderTypes(requestContext);
+ return _.find(providerTypes, { type: providerTypeId });
+ }
+}
+
+module.exports = AuthenticationProviderTypeService;
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/cognito-token-verifier.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/cognito-token-verifier.js
new file mode 100644
index 0000000000..ecefde5f17
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/cognito-token-verifier.js
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+const request = require('request');
+const jwkToPem = require('jwk-to-pem');
+const _ = require('lodash');
+const jwt = require('jsonwebtoken');
+
+async function getCognitoTokenVerifier(userPoolUri, logger = console) {
+ function toPem(keyDictionary) {
+ const modulus = keyDictionary.n;
+ const exponent = keyDictionary.e;
+ const keyType = keyDictionary.kty;
+ const jwk = { kty: keyType, n: modulus, e: exponent };
+ const pem = jwkToPem(jwk);
+ return pem;
+ }
+
+ // build key cache from cognito user pools
+ const jwtKeySetUri = `${userPoolUri}/.well-known/jwks.json`;
+ const pemKeyCache = await new Promise((resolve, reject) => {
+ request({ url: jwtKeySetUri, json: true }, (error, response, body) => {
+ if (!error && response && response.statusCode === 200) {
+ const keys = body.keys;
+ const keyCache = {};
+ _.forEach(keys, key => {
+ // kid = key id
+ const kid = key.kid;
+ keyCache[kid] = toPem(key);
+ });
+ resolve(keyCache);
+ } else {
+ logger.error('Failed to retrieve the keys from the well known user-pool URI');
+ reject(error);
+ }
+ });
+ });
+
+ const verify = async token => {
+ // First attempt to decode token before attempting to verify the signature
+ const decodedJwt = jwt.decode(token, { complete: true });
+ if (!decodedJwt) {
+ throw new Error('Not valid JWT token. Could not decode the token');
+ }
+
+ // Fail if token is not from your User Pool
+ if (decodedJwt.payload.iss !== userPoolUri) {
+ throw new Error('Not valid JWT token. The token is not issued by the trusted source');
+ }
+
+ // Reject the jwt if it's not an 'Identity Token'
+ if (decodedJwt.payload.token_use !== 'id') {
+ throw new Error('Not valid JWT token. The token is not the identity token');
+ }
+
+ const keyId = decodedJwt.header.kid;
+ const pem = pemKeyCache[keyId];
+ if (!pem) {
+ throw new Error('Not valid JWT token. No valid key available for verifying the token.');
+ }
+
+ const payload = await jwt.verify(token, pem, { issuer: userPoolUri });
+ return payload;
+ };
+
+ return { verify };
+}
+
+module.exports = { getCognitoTokenVerifier };
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/create-cognito-user-pool-input-manifest.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/create-cognito-user-pool-input-manifest.js
new file mode 100644
index 0000000000..898e13d6f8
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/create-cognito-user-pool-input-manifest.js
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+const inputManifestForCreate = {
+ sections: [
+ {
+ title: 'General Information',
+ children: [
+ {
+ name: 'id',
+ type: 'stringInput',
+ title: 'ID',
+ rules: 'required|string|between:2,64|regex:/^[a-zA-Z][a-zA-Z0-9_-]+$/',
+ desc:
+ 'This is a required field. This is used for uniquely identifying the authentication provider. ' +
+ 'It must be between 2 to 64 characters long and must start with an alphabet and may contain alpha numeric ' +
+ 'characters, underscores, and dashes. No other special symbols are allowed.',
+ },
+ {
+ name: 'title',
+ type: 'stringInput',
+ title: 'Title',
+ rules: 'required|between:3,255',
+ desc: 'This is a required field and must be between 3 and 255 characters long.',
+ },
+ ],
+ },
+ {
+ title: 'Congito User Pool Information',
+ children: [
+ {
+ name: 'connectExistingCognitoUserPool',
+ type: 'yesNoInput',
+ title: 'Connect to Existing or Create New',
+ yesLabel: 'Connect to Existing',
+ noLabel: 'Create New',
+ rules: 'required|boolean',
+ desc: 'Do you want to connect to an existing cognito user pool or create a new one?',
+ },
+ {
+ name: 'configureFedIdps',
+ type: 'yesNoInput',
+ title: 'Identity Federation',
+ yesLabel: 'yes',
+ noLabel: 'no',
+ rules: 'required|boolean',
+ desc: 'Do you want to configure SAML identity federation with other SAML identity providers?',
+ },
+ ],
+ },
+ {
+ title: 'Existing Cognito User Pool Information (Optional)',
+ condition: '<%= connectExistingCognitoUserPool === true %>',
+ children: [
+ {
+ name: 'userPoolId',
+ type: 'stringInput',
+ title: 'Cognito User Pool ID',
+ rules: 'required|string',
+ desc: 'Enter the ID of the cognito user pool you want to connect to.',
+ },
+ {
+ name: 'userPoolName',
+ type: 'stringInput',
+ title: 'Cognito User Pool Name',
+ desc: 'Enter name of the cognito user pool you want to connect to.',
+ },
+ ],
+ },
+ {
+ title: 'Configure Identity Federation (Optional)',
+ condition: '<%= configureFedIdps === true %>',
+ // TODO: Add support for array input types in input manifest
+ // this is required for allowing to dynamically configure multiple federatedIdentityProviders
+ // The children in the input manifest sections tree are all expected to be flat key,value pairs
+ // The names below are based on object path
+ // For example, the authentication providers create API expects structure to be
+ // {
+ // ...
+ // federatedIdentityProviders: [
+ // {
+ // id, // This is named federatedIdentitiyProviders_0_id below as this is the "id" of the first element (i.e., at "0" index) in the array "federatedIdentityProviders"
+ // name,
+ // displayName,
+ // metadata
+ // }
+ // ]
+ // }
+ children: [
+ {
+ name: 'federatedIdentityProviders[0].id',
+ type: 'stringInput',
+ title: 'Identity Provider ID (IdP Id)',
+ rules: 'required|string',
+ desc:
+ 'An identifier for the federated identity provider. This will be used for identifying the IdP. Usually this is configured to be same as the domain name of the IdP (E.g., amazonaws.com).',
+ },
+ {
+ name: 'federatedIdentityProviders[0].name',
+ type: 'stringInput',
+ title: 'Identity Provider Name',
+ rules: 'required|string',
+ desc: 'Name for the identity provider.',
+ },
+ {
+ name: 'federatedIdentityProviders[0].displayName',
+ type: 'stringInput',
+ title: 'Identity Provider Display Name',
+ desc: 'Optional display name for the identity provider.',
+ },
+ {
+ name: 'federatedIdentityProviders[0].metadata',
+ type: 'textAreaInput',
+ title: 'Identity Provider SAML Metadata XML',
+ desc: 'Enter identity provider SAML metadata XML document for setting up trust.',
+ },
+ ],
+ },
+ ],
+};
+
+module.exports = { inputManifestForCreate };
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/create-cognito-user-pool-schema.json b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/create-cognito-user-pool-schema.json
new file mode 100644
index 0000000000..aa6d2922bc
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/create-cognito-user-pool-schema.json
@@ -0,0 +1,78 @@
+{
+ "definitions": {},
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://example.com/root.json",
+ "type": "object",
+ "required": ["title"],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "$id": "#/properties/id",
+ "type": "string"
+ },
+ "title": {
+ "$id": "#/properties/title",
+ "type": "string"
+ },
+ "userPoolName": {
+ "$id": "#/properties/userPoolName",
+ "type": "string"
+ },
+ "userPoolId": {
+ "$id": "#/properties/userPoolId",
+ "type": "string"
+ },
+ "clientName": {
+ "$id": "#/properties/clientName",
+ "type": "string"
+ },
+ "clientId": {
+ "$id": "#/properties/clientId",
+ "type": "string"
+ },
+ "userPoolDomain": {
+ "$id": "#/properties/userPoolDomain",
+ "type": "string"
+ },
+ "signInUri": {
+ "$id": "#/properties/signInUri",
+ "type": "string"
+ },
+ "signOutUri": {
+ "$id": "#/properties/signOutUri",
+ "type": "string"
+ },
+ "enableNativeUserPoolUsers": {
+ "$id": "#/properties/enableNativeUserPoolUsers",
+ "type": "boolean"
+ },
+ "federatedIdentityProviders": {
+ "$id": "#/properties/providerConfig/properties/federatedIdentityProviders",
+ "type": "array",
+ "items": {
+ "$id": "#/properties/providerConfig/properties/federatedIdentityProviders/items",
+ "type": "object",
+ "title": "The Items Schema",
+ "required": ["id", "name", "metadata"],
+ "properties": {
+ "id": {
+ "$id": "#/properties/federatedIdentityProviders/properties/id",
+ "type": "string"
+ },
+ "name": {
+ "$id": "#/properties/federatedIdentityProviders/properties/name",
+ "type": "string"
+ },
+ "displayName": {
+ "$id": "#/properties/federatedIdentityProviders/properties/displayName",
+ "type": "string"
+ },
+ "metadata": {
+ "$id": "#/properties/federatedIdentityProviders/properties/metadata",
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/provider-service.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/provider-service.js
new file mode 100644
index 0000000000..44b5bbad88
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/provider-service.js
@@ -0,0 +1,153 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context');
+
+const { getCognitoTokenVerifier } = require('./cognito-token-verifier');
+
+class ProviderService extends Service {
+ constructor() {
+ super();
+ this.dependency(['userService', 'userAttributesMapperService', 'tokenRevocationService']);
+ this.cognitoTokenVerifiersCache = {}; // Cache object containing token verifier objects. Each token verifier is keyed by the userPoolUri
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ async validateToken({ token, issuer }, providerConfig) {
+ if (_.isEmpty(token)) {
+ throw this.boom.forbidden('no jwt token was provided', true);
+ }
+ // -- Check if this token is revoked (may be due to an earlier logout)
+ const tokenRevocationService = await this.service('tokenRevocationService');
+ const isRevoked = await tokenRevocationService.isRevoked({ token });
+ if (isRevoked) {
+ throw this.boom.invalidToken('The token is revoked', true);
+ }
+
+ // In case of cognito, the issuer is the cognito userPoolUri
+ const userPoolUri = issuer;
+ let cognitoTokenVerifier = this.cognitoTokenVerifiersCache[userPoolUri];
+ if (!cognitoTokenVerifier) {
+ // No cognitoTokenVerifier in the cache so create a new one
+ cognitoTokenVerifier = await getCognitoTokenVerifier(userPoolUri, this.log);
+ // Add newly created cognitoTokenVerifier to the cache
+ this.cognitoTokenVerifiersCache[userPoolUri] = cognitoTokenVerifier;
+ }
+ // User the cognitoTokenVerifier to validate cognito token
+ const verifiedToken = await cognitoTokenVerifier.verify(token);
+ const { username, identityProviderName } = await this.saveUser(verifiedToken, providerConfig.config.id);
+ return { verifiedToken, username, identityProviderName };
+ }
+
+ async saveUser(decodedToken, authenticationProviderId) {
+ const userAttributesMapperService = await this.service('userAttributesMapperService');
+ // Ask user attributes mapper service to read information from the decoded token and map them to user attributes
+ const userAttributes = await userAttributesMapperService.mapAttributes(decodedToken);
+ if (userAttributes.isSamlAuthenticatedUser) {
+ // If this user is authenticated via SAML then we need to add it to our user table if it doesn't exist already
+ const userService = await this.service('userService');
+
+ const user = await userService.findUser({
+ username: userAttributes.username,
+ authenticationProviderId,
+ identityProviderName: userAttributes.identityProviderName,
+ });
+ if (user) {
+ await this.updateUser(authenticationProviderId, userAttributes, user);
+ } else {
+ await this.createUser(authenticationProviderId, userAttributes);
+ }
+ }
+ return userAttributes;
+ }
+
+ /**
+ * Creates a user in the system based on the user attributes provided by the SAML Identity Provider (IdP)
+ * @param authenticationProviderId ID of the authentication provider
+ * @param userAttributes An object containing attributes mapped from SAML IdP
+ * @returns {Promise}
+ */
+ async createUser(authenticationProviderId, userAttributes) {
+ const userService = await this.service('userService');
+ try {
+ await userService.createUser(getSystemRequestContext(), {
+ authenticationProviderId,
+ ...userAttributes,
+ });
+ } catch (err) {
+ this.log.error(err);
+ throw this.boom.internalError('error creating user');
+ }
+ }
+
+ /**
+ * Updates user in the system based on the user attributes provided by the SAML Identity Provider (IdP).
+ * This base implementation updates only those user attributes in the system which are missing but are available in
+ * the SAML user attributes. Subclasses can override this method to provide different implementation (for example,
+ * update all user attributes in the system if they are updated in SAML IdP etc)
+ *
+ * @param authenticationProviderId ID of the authentication provider
+ * @param userAttributes An object containing attributes mapped from SAML IdP
+ * @param existingUser The existing user in the system
+ *
+ * @returns {Promise}
+ */
+ async updateUser(authenticationProviderId, userAttributes, existingUser) {
+ // Find all attributes present in the userAttributes but missing in existingUser
+ const missingAttribs = {};
+ const keys = _.keys(userAttributes);
+ if (!_.isEmpty(keys)) {
+ _.forEach(keys, key => {
+ const value = userAttributes[key];
+ const existingValue = existingUser[key];
+
+ // check if the attribute is missing in the existingUser object but present in
+ // userAttributes (i.e., the user attributes mapped from SAML assertions)
+ if (_.isNil(existingValue)) {
+ missingAttribs[key] = value;
+ }
+ });
+ }
+
+ // If there are any attributes that are present in the userAttributes but missing in existingUser
+ // then update the user in the system to set the missing attributes
+ if (!_.isEmpty(missingAttribs)) {
+ const userService = await this.service('userService');
+ const { username, identityProviderName, rev } = existingUser;
+ try {
+ await userService.updateUser(getSystemRequestContext(), {
+ username,
+ authenticationProviderId,
+ identityProviderName,
+ rev,
+ ...missingAttribs,
+ });
+ } catch (err) {
+ this.log.error(err);
+ throw this.boom.internalError('error updating user');
+ }
+ }
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ async revokeToken(requestContext, { token }, providerConfig) {
+ const tokenRevocationService = await this.service('tokenRevocationService');
+ await tokenRevocationService.revoke(requestContext, { token });
+ }
+}
+
+module.exports = ProviderService;
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/provisioner-service.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/provisioner-service.js
new file mode 100644
index 0000000000..83954b01b0
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/provisioner-service.js
@@ -0,0 +1,434 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const authProviderConstants = require('../../constants').authenticationProviders;
+
+const settingKeys = {
+ awsRegion: 'awsRegion',
+ envName: 'envName',
+ envType: 'envType',
+ solutionName: 'solutionName',
+ websiteUrl: 'websiteUrl',
+};
+
+class ProvisionerService extends Service {
+ constructor() {
+ super();
+ this.dependency(['aws', 's3Service', 'jsonSchemaValidationService', 'authenticationProviderConfigService']);
+ this.boom.extend(['authProviderAlreadyExists', 400]);
+ this.boom.extend(['noAuthProviderFound', 400]);
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ async provision({ providerTypeConfig, providerConfig, action }) {
+ if (!action) {
+ throw this.boom.badRequest('Can not provision Cognito User Pool. Missing required parameter "action"', false);
+ }
+
+ this.log.info('Provisioning Cognito User Pool Authentication Provider');
+
+ // Validate input
+ const jsonSchemaValidationService = await this.service('jsonSchemaValidationService');
+ const providerConfigJsonSchema = _.get(providerTypeConfig, 'config.inputSchema');
+ await jsonSchemaValidationService.ensureValid(providerConfig, providerConfigJsonSchema);
+
+ const authenticationProviderConfigService = await this.service('authenticationProviderConfigService');
+ let existingProviderConfig;
+ if (providerConfig.id) {
+ existingProviderConfig = await authenticationProviderConfigService.getAuthenticationProviderConfig(
+ providerConfig.id,
+ );
+ }
+ if (action === authProviderConstants.provisioningAction.create && !_.isNil(existingProviderConfig)) {
+ // The authentication provider with same config id already exists.
+ throw this.boom.authProviderAlreadyExists(
+ 'Can not create the specified authentication provider. An authentication provider with the same id already exists',
+ true,
+ );
+ }
+ if (action === authProviderConstants.provisioningAction.update && _.isNil(existingProviderConfig)) {
+ // The authentication provider with the specified config id does not exist.
+ throw this.boom.noAuthProviderFound(
+ 'Can not update the specified authentication provider. No authentication provider with the specified id found',
+ true,
+ );
+ }
+
+ // Each provisioning step function below takes providerConfig and performs it's own provisioning work
+ // and also updates (enriches) providerConfig with additional information (referred to as outputs)
+ // For example, when creating Cognito User Pool, the ID of the created cognito user pool (i.e., userPoolId) is
+ // added to the providerConfig.
+ // The providerConfigWithOutputs variable below is the updated providerConfig with these outputs
+ let providerConfigWithOutputs = providerConfig;
+ providerConfigWithOutputs = await this.saveCognitoUserPool(providerConfigWithOutputs);
+
+ if (existingProviderConfig) {
+ providerConfigWithOutputs.clientId = existingProviderConfig.config.clientId;
+ } else {
+ providerConfigWithOutputs = await this.createUserPoolClient(providerConfigWithOutputs);
+ }
+
+ providerConfigWithOutputs = await this.createUserPoolClient(providerConfigWithOutputs);
+ providerConfigWithOutputs = await this.configureCognitoIdentityProviders(providerConfigWithOutputs);
+ providerConfigWithOutputs = await this.updateUserPoolClient(providerConfigWithOutputs);
+ providerConfigWithOutputs = await this.configureUserPoolDomain(providerConfigWithOutputs);
+
+ const userPoolDomain = providerConfigWithOutputs.userPoolDomain;
+ const awsRegion = this.settings.get(settingKeys.awsRegion);
+ const clientId = providerConfigWithOutputs.clientId;
+ const websiteUrl = this.settings.get(settingKeys.websiteUrl);
+
+ providerConfigWithOutputs.id = `https://cognito-idp.${awsRegion}.amazonaws.com/${providerConfigWithOutputs.userPoolId}`;
+
+ const baseAuthUri = `https://${userPoolDomain}.auth.${awsRegion}.amazoncognito.com`;
+ providerConfigWithOutputs.signInUri = `${baseAuthUri}/oauth2/authorize?response_type=token&client_id=${clientId}&redirect_uri=${websiteUrl}`;
+ providerConfigWithOutputs.signOutUri = `${baseAuthUri}/logout?client_id=${clientId}&logout_uri=${websiteUrl}`;
+
+ this.log.info('Saving Cognito User Pool Authentication Provider Configuration.');
+
+ // Save auth provider configuration and make it active
+ const result = await authenticationProviderConfigService.saveAuthenticationProviderConfig({
+ providerTypeConfig,
+ providerConfig: providerConfigWithOutputs,
+ status: authProviderConstants.status.active,
+ });
+ return result;
+ }
+
+ /* ************** Provisioning Steps ************** */
+ async saveCognitoUserPool(providerConfig) {
+ this.log.info('Creating or configuring Cognito User Pool');
+
+ const aws = await this.service('aws');
+ const cognitoIdentityServiceProvider = new aws.sdk.CognitoIdentityServiceProvider();
+
+ const envType = this.settings.get(settingKeys.envType);
+ const envName = this.settings.get(settingKeys.envName);
+ const solutionName = this.settings.get(settingKeys.solutionName);
+ const userPoolName = providerConfig.userPoolName || `${envName}-${envType}-${solutionName}-userpool`;
+ const params = {
+ AdminCreateUserConfig: { AllowAdminCreateUserOnly: true },
+ AutoVerifiedAttributes: ['email'],
+ Schema: [
+ {
+ Name: 'name',
+ Mutable: true,
+ Required: true,
+ },
+ {
+ Name: 'family_name',
+ Mutable: true,
+ Required: true,
+ },
+ {
+ Name: 'middle_name',
+ Mutable: true,
+ Required: false,
+ },
+ ],
+ };
+ if (providerConfig.userPoolId) {
+ // If userPoolId is specified then this must be for update so make sure it points to a valid cognito user pool
+ try {
+ await cognitoIdentityServiceProvider.describeUserPool({ UserPoolId: providerConfig.userPoolId }).promise();
+ } catch (err) {
+ if (err.code === 'ResourceNotFoundException') {
+ throw this.boom.badRequest(
+ 'Can not update Cognito User Pool. No Cognito User Pool with the given userPoolId exists.',
+ true,
+ );
+ }
+ // In case of any other error, let it propagate
+ throw err;
+ }
+ } else {
+ // userPoolId is not specified so create new user pool
+ params.PoolName = userPoolName;
+ const data = await cognitoIdentityServiceProvider.createUserPool(params).promise();
+ providerConfig.userPoolId = data.UserPool.Id;
+ }
+ providerConfig.userPoolName = userPoolName;
+ return providerConfig;
+ }
+
+ async createUserPoolClient(providerConfig) {
+ this.log.info('Creating or configuring Cognito User Pool Client');
+ if (!providerConfig.userPoolId) {
+ throw this.boom.badRequest('Can not create Cognito User Pool Client. Missing userPoolId.', true);
+ }
+ const aws = await this.service('aws');
+ const cognitoIdentityServiceProvider = new aws.sdk.CognitoIdentityServiceProvider();
+
+ function getUrls() {
+ const websiteUrl = this.settings.get(settingKeys.websiteUrl);
+ const envType = this.settings.get(settingKeys.envType);
+ const callbackUrls = [websiteUrl];
+ const localUrl = 'http://localhost:3000';
+ let defaultRedirectUri;
+ if (envType === 'dev') {
+ defaultRedirectUri = localUrl;
+ // add localhost for callback url for local development in case of 'dev' environment
+ callbackUrls.push(localUrl);
+ } else {
+ defaultRedirectUri = websiteUrl;
+ }
+ // The logout urls are same as callback urls in our case
+ const logoutUrls = callbackUrls;
+ return { callbackUrls, defaultRedirectUri, logoutUrls };
+ }
+
+ let isClientConfiguredAlready = false;
+ if (providerConfig.clientId) {
+ try {
+ // If clientId is specified then make sure it exists in the given user pool
+ const result = await cognitoIdentityServiceProvider
+ .describeUserPoolClient({
+ ClientId: providerConfig.clientId,
+ UserPoolId: providerConfig.userPoolId,
+ })
+ .promise();
+
+ isClientConfiguredAlready = !!result.UserPoolClient;
+ } catch (e) {
+ if (e.code !== 'ResourceNotFoundException') {
+ // Swallow ResourceNotFoundException. In that case, the flag isClientConfiguredAlready will stay false.
+ // Propagate any other exception
+ throw e;
+ }
+ }
+ }
+
+ if (!isClientConfiguredAlready) {
+ // if client is not configured for the given user pool yet then create a new one
+ const { callbackUrls, defaultRedirectUri, logoutUrls } = getUrls.call(this);
+
+ const clientName = providerConfig.clientName || 'DataLakeClient';
+ const params = {
+ ClientName: clientName,
+ UserPoolId: providerConfig.userPoolId,
+ AllowedOAuthFlows: ['implicit'],
+ AllowedOAuthFlowsUserPoolClient: true,
+ AllowedOAuthScopes: ['email', 'openid', 'profile'],
+ CallbackURLs: callbackUrls,
+ DefaultRedirectURI: defaultRedirectUri,
+ ExplicitAuthFlows: ['ADMIN_NO_SRP_AUTH'],
+ LogoutURLs: logoutUrls,
+ // Make certain attributes readable and writable by this client.
+ // See "https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html" for list of attributes Cognito supports by default
+ ReadAttributes: [
+ 'address',
+ 'birthdate',
+ 'family_name',
+ 'gender',
+ 'given_name',
+ 'locale',
+ 'middle_name',
+ 'name',
+ 'nickname',
+ 'phone_number',
+ 'phone_number_verified',
+ 'picture',
+ 'preferred_username',
+ 'profile',
+ 'updated_at',
+ 'website',
+ 'zoneinfo',
+ 'email',
+ 'email_verified',
+ ],
+ WriteAttributes: ['email', 'family_name', 'given_name', 'middle_name', 'name'],
+ RefreshTokenValidity: 7, // Allowing refresh token to be used for one week
+ };
+ const data = await cognitoIdentityServiceProvider.createUserPoolClient(params).promise();
+ providerConfig.clientId = data.UserPoolClient.ClientId;
+ }
+ return providerConfig;
+ }
+
+ async updateUserPoolClient(providerConfig) {
+ this.log.info('Updating Cognito User Pool Client');
+ if (!providerConfig.userPoolId) {
+ throw this.boom.badRequest('Can not update Cognito User Pool Client. Missing userPoolId.', true);
+ }
+ if (!providerConfig.clientId) {
+ throw this.boom.badRequest('Can not update Cognito User Pool Client. Missing clientId.', true);
+ }
+ const aws = await this.service('aws');
+ const cognitoIdentityServiceProvider = new aws.sdk.CognitoIdentityServiceProvider();
+
+ // At this point the cognito client should have already been created.
+ const result = await cognitoIdentityServiceProvider
+ .describeUserPoolClient({
+ ClientId: providerConfig.clientId,
+ UserPoolId: providerConfig.userPoolId,
+ })
+ .promise();
+ const existingClientConfig = result.UserPoolClient;
+
+ let supportedIdpNames = [];
+ if (!_.isEmpty(providerConfig.federatedIdentityProviders)) {
+ // federatedIdentityProviders are provided so assume SAML federation
+ //
+ // federatedIdentityProviders -- an array of federated identity provider info objects with following shape
+ // [{
+ // id: 'some-id-of-the-idp' (such as 'com.amazonaws' etc. The usual practice is to keep this same as the domain name of the idp.)
+ // name: 'some-idp-name' (such as 'com.amazonaws', 'AmazonAWSEmployees' etc)
+ // displayName: 'some-displayable-name-for-the-idp' (such as 'Internal Users', 'External Users' etc)
+ // metadata: 'SAML XML Metadata blob for the identity provider or a URI pointing to a location that will provide the SAML metadata'
+ // }]
+ const idpNames = _.map(providerConfig.federatedIdentityProviders, idp => idp.name);
+ supportedIdpNames = idpNames;
+ }
+
+ // Enable Cognito as an auth provider for the app client if configured
+ if (providerConfig.enableNativeUserPoolUsers) {
+ supportedIdpNames.push('COGNITO');
+ }
+
+ const params = {
+ ClientId: existingClientConfig.ClientId,
+ UserPoolId: existingClientConfig.UserPoolId,
+ AllowedOAuthFlows: existingClientConfig.AllowedOAuthFlows,
+ AllowedOAuthFlowsUserPoolClient: existingClientConfig.AllowedOAuthFlowsUserPoolClient,
+ AllowedOAuthScopes: existingClientConfig.AllowedOAuthScopes,
+ CallbackURLs: existingClientConfig.CallbackURLs,
+ ClientName: existingClientConfig.ClientName,
+ DefaultRedirectURI: existingClientConfig.DefaultRedirectURI,
+ ExplicitAuthFlows: existingClientConfig.ExplicitAuthFlows,
+ LogoutURLs: existingClientConfig.LogoutURLs,
+ ReadAttributes: existingClientConfig.ReadAttributes,
+ RefreshTokenValidity: existingClientConfig.RefreshTokenValidity,
+ WriteAttributes: existingClientConfig.WriteAttributes,
+ SupportedIdentityProviders: supportedIdpNames,
+ };
+ // The following update call with SupportedIdentityProviders must be made only AFTER creating the identity providers (happening in "configureCognitoIdentityProviders")
+ // The below call will fail without that. The idp names specified in SupportedIdentityProviders must match the ones created in "configureCognitoIdentityProviders"
+ await cognitoIdentityServiceProvider.updateUserPoolClient(params).promise();
+ return providerConfig;
+ }
+
+ async configureCognitoIdentityProviders(providerConfig) {
+ // federatedIdentityProviders -- an array of federated identity provider info objects with following shape
+ // [{
+ // id: 'some-id-of-the-idp' (such as 'com.amazonaws' etc. The usual practice is to keep this same as the domain name of the idp.
+ // For example, when connecting with an IdP that has users "user1@domain1.com", "user2@domain1.com" etc then the "id" should
+ // be set to "domain1.com")
+ //
+ // name: 'some-idp-name' (such as 'com.amazonaws', 'AmazonAWSEmployees' etc)
+ //
+ // displayName: 'some-displayable-name-for-the-idp' (such as 'Internal Users', 'External Users' etc)
+ //
+ // metadata: 'SAML XML Metadata blob for the identity provider or a URI pointing to a location that will provide the SAML metadata'
+ // }]
+ if (_.isEmpty(providerConfig.federatedIdentityProviders)) {
+ // No IdPs to add. Just exit.
+ return providerConfig;
+ }
+
+ this.log.info('Configuring Cognito Identity Providers');
+ const aws = await this.service('aws');
+ const cognitoIdentityServiceProvider = new aws.sdk.CognitoIdentityServiceProvider();
+
+ const idpCreationPromises = _.map(providerConfig.federatedIdentityProviders, async idp => {
+ let metadata = idp.metadata;
+
+ if (metadata.startsWith('s3://')) {
+ const s3Service = await this.service('s3Service');
+ const { s3BucketName, s3Key } = s3Service.parseS3Details(metadata);
+ const result = await s3Service.api.getObject({ Bucket: s3BucketName, Key: s3Key }).promise();
+ metadata = result.Body.toString('utf8');
+ }
+
+ const metaDataInfo = { IDPSignout: 'true' };
+ if (/^https?:\/\//.test(metadata)) {
+ metaDataInfo.MetadataURL = metadata;
+ } else {
+ metaDataInfo.MetadataFile = metadata;
+ }
+
+ const params = {
+ ProviderDetails: metaDataInfo,
+ ProviderName: idp.name /* required */,
+ ProviderType: 'SAML' /* required */, // TODO: Add support for other Federation providers
+ UserPoolId: providerConfig.userPoolId /* required */,
+ AttributeMapping: {
+ // TODO: Add support for configurable attributes mapping
+ name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name',
+ given_name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
+ family_name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
+ email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
+ },
+ IdpIdentifiers: [idp.id],
+ };
+
+ // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityServiceProvider.html#createIdentityProvider-property
+ try {
+ await cognitoIdentityServiceProvider.createIdentityProvider(params).promise();
+ } catch (err) {
+ if (err.code === 'DuplicateProviderException') {
+ // the identity provider already exists so update it instead of creating it
+ await cognitoIdentityServiceProvider
+ .updateIdentityProvider({
+ ProviderName: params.ProviderName,
+ UserPoolId: params.UserPoolId,
+ AttributeMapping: params.AttributeMapping,
+ IdpIdentifiers: params.IdpIdentifiers,
+ ProviderDetails: params.ProviderDetails,
+ })
+ .promise();
+ } else {
+ // In case of any other error just rethrow
+ throw err;
+ }
+ }
+ });
+ await Promise.all(idpCreationPromises);
+ return providerConfig;
+ }
+
+ async configureUserPoolDomain(providerConfig) {
+ this.log.info('Configuring Cognito User Pool Domain');
+ const userPoolId = providerConfig.userPoolId;
+ // The Domain Name Prefix for the Cogito User Pool. This will be used as a prefix to form the Fully Qualified Domain Name (FQDN)
+ // for the Cognito User Pool. The Conito User Pool FQDN URL is passed to SAML IdP. The SAML IdP then returns the SAML assertion
+ // back by redirecting the client to this URL.
+ const envType = this.settings.get(settingKeys.envType);
+ const envName = this.settings.get(settingKeys.envName);
+ const solutionName = this.settings.get(settingKeys.solutionName);
+ const userPoolDomain = providerConfig.userPoolDomain || `${envName}-${envType}-${solutionName}`;
+ const params = {
+ Domain: userPoolDomain,
+ UserPoolId: userPoolId,
+ };
+
+ const aws = await this.service('aws');
+ const cognitoIdentityServiceProvider = new aws.sdk.CognitoIdentityServiceProvider();
+
+ try {
+ await cognitoIdentityServiceProvider.createUserPoolDomain(params).promise();
+ } catch (err) {
+ if (err.code === 'InvalidParameterException' && err.message.indexOf('already exists') >= 0) {
+ // The domain already exists so nothing to do. Just log and move on.
+ this.log.info(`The Cognito User Pool Domain with Prefix "${userPoolDomain}" already exists. Nothing to do.`);
+ }
+ }
+ providerConfig.userPoolDomain = userPoolDomain;
+ return providerConfig;
+ }
+}
+
+module.exports = ProvisionerService;
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/type.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/type.js
new file mode 100644
index 0000000000..8870ee183b
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/type.js
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+const { inputManifestForCreate } = require('./create-cognito-user-pool-input-manifest');
+const { inputManifestForUpdate } = require('./update-cognito-user-pool-input-manifest');
+const inputSchema = require('./create-cognito-user-pool-schema');
+
+module.exports = {
+ type: 'cognito_user_pool',
+ title: 'Cognito User Pool',
+ description: 'Authentication provider for Amazon Cognito User Pool',
+ config: {
+ // credentialHandlingType indicating credential handling for the authentication provider
+ // Possible values:
+ // 'submit' -- The credentials should be submitted to the URL provided by the authentication provider
+ // 'redirect' -- The credentials should be NOT be collected and the user should be redirected directly to the
+ // URL provided by the authentication provider. For example, in case of SAML auth, the username/password
+ // should not be collected by the service provider but the user should be redirected to the identity provider
+ credentialHandlingType: 'redirect',
+
+ // "inputSchema": JSON schema representing inputs required from user when configuring an authentication provider of this type.
+ inputSchema,
+
+ // The "inputManifest*" will be used on the UI to ask configuration inputs from the user when registering new
+ // authentication provider
+ inputManifestForCreate,
+ inputManifestForUpdate,
+
+ impl: {
+ // In case of Cognito User Pools, the ID token is issued by the User Pool
+ // the tokenIssuerLocator is not applicable in this case
+ // tokenIssuerLocator: '',
+
+ // Similar to the tokenIssuerLocator mentioned above but used for token validation instead of issuing token.
+ // The token validation locator is used to validate token upon each request.
+ // Unlike the tokenIssuerLocator which is only used for authentication being performed via application APIs, the
+ // tokenValidatorLocator is used in all cases
+ tokenValidatorLocator: 'locator:service:cognitoUserPoolAuthenticationProviderService/validateToken',
+
+ // Similar to the tokenIssuerLocator mentioned above but used for token revocation instead of issuing token.
+ // The token revocation locator is used to revoke a token upon logout.
+ tokenRevokerLocator: 'locator:service:cognitoUserPoolAuthenticationProviderService/revokeToken',
+
+ // Similar to above locators. The provisionerLocator identifies an implementation that takes care of provisioning the authentication provider.
+ // In case of Internal Authentication Provider this "provisioning" step may be as simple as adding authentication provider configuration in Data Base.
+ // In case of other auth providers, this step may be more elaborate (for example, in case of Cognito + SAML, the provisioner has to create Cognito User Pool,
+ // configure cognito client application, configure SAML identity providers in the Cognito User Pool etc.
+ provisionerLocator: 'locator:service:cognitoUserPoolAuthenticationProvisionerService/provision',
+ },
+ },
+};
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/update-cognito-user-pool-input-manifest.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/update-cognito-user-pool-input-manifest.js
new file mode 100644
index 0000000000..03a9ec2115
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/update-cognito-user-pool-input-manifest.js
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+
+const inputManifestForUpdate = {
+ sections: [
+ {
+ title: 'General Information',
+ children: [
+ {
+ name: 'id',
+ type: 'stringInput',
+ title: 'ID',
+ rules: 'required|string|between:2,64|regex:/^[a-zA-Z][a-zA-Z0-9_-]+$/',
+ desc:
+ 'This is a required field. This is used for uniquely identifying the authentication provider. ' +
+ 'This must be same as the Cognito User Pool Provider URL in ' +
+ '"https://cognito-idp.{aws-region}.amazonaws.com/{user-pool-id}" format',
+ },
+ {
+ name: 'title',
+ type: 'stringInput',
+ title: 'Title',
+ rules: 'required|between:3,255',
+ desc: 'This is a required field and must be between 3 and 255 characters long.',
+ },
+ ],
+ },
+ {
+ title: 'Existing Cognito User Pool Information (Optional)',
+ children: [
+ {
+ name: 'userPoolId',
+ type: 'stringInput',
+ title: 'Cognito User Pool ID',
+ desc: 'Enter the ID of the cognito user pool you want to connect to.',
+ },
+ {
+ name: 'userPoolName',
+ type: 'stringInput',
+ title: 'Cognito User Pool Name',
+ desc: 'Enter name of the cognito user pool you want to connect to.',
+ },
+ ],
+ },
+ {
+ title: 'Configure Identity Federation (Optional)',
+ // TODO: Add support for array input types in input manifest
+ // this is required for allowing to dynamically configure multiple federatedIdentityProviders
+ // The children in the input manifest sections tree are all expected to be flat key,value pairs
+ // The names below are based on object path
+ // For example, the authentication providers create API expects structure to be
+ // {
+ // ...
+ // federatedIdentityProviders: [
+ // {
+ // id, // This is named federatedIdentityProviders_0_id below as this is the "id" of the first element (i.e., at "0" index) in the array "federatedIdentityProviders"
+ // name,
+ // displayName,
+ // metadata
+ // }
+ // ]
+ // }
+ children: [
+ {
+ name: 'federatedIdentityProviders|-0-|/id',
+ type: 'stringInput',
+ title: 'Identity Provider ID (IdP Id)',
+ desc:
+ 'An identifier for the federated identity provider. This will be used for identifying the IdP. Usually this is configured to be same as the domain name of the IdP (E.g., amazonaws.com).',
+ },
+ {
+ name: 'federatedIdentityProviders|-0-|/name',
+ type: 'stringInput',
+ title: 'Identity Provider Name',
+ desc: 'Name for the identity provider.',
+ },
+ {
+ name: 'federatedIdentityProviders|-0-|/displayName',
+ type: 'stringInput',
+ title: 'Identity Provider Display Name',
+ desc: 'Optional display name for the identity provider.',
+ },
+ {
+ name: 'federatedIdentityProviders|-0-|/metadata',
+ type: 'textAreaInput',
+ title: 'Identity Provider SAML Metadata XML',
+ desc: 'Enter identity provider SAML metadata XML document for setting up trust.',
+ },
+ ],
+ },
+ ],
+};
+
+module.exports = { inputManifestForUpdate };
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/user-attributes-mapper-service.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/user-attributes-mapper-service.js
new file mode 100644
index 0000000000..a1923ebef2
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/cogito-user-pool/user-attributes-mapper-service.js
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+class UserAttributesMapperService extends Service {
+ mapAttributes(decodedToken) {
+ const { username, usernameInIdp } = this.getUsername(decodedToken);
+ const identityProviderName = this.getIdpName(decodedToken);
+ const isSamlAuthenticatedUser = this.isSamlAuthenticatedUser(decodedToken);
+ const firstName = this.getFirstName(decodedToken);
+ const lastName = this.getLastName(decodedToken);
+ const email = this.getEmail(decodedToken);
+
+ return {
+ username,
+ usernameInIdp,
+ identityProviderName,
+ isSamlAuthenticatedUser,
+
+ firstName,
+ lastName,
+ email,
+ };
+ }
+
+ getEmail(decodedToken) {
+ return decodedToken.email;
+ }
+
+ getLastName(decodedToken) {
+ return decodedToken.family_name;
+ }
+
+ getFirstName(decodedToken) {
+ return decodedToken.given_name;
+ }
+
+ isSamlAuthenticatedUser(decodedToken) {
+ const isSamlAuthenticatedUser =
+ decodedToken.identities &&
+ decodedToken.identities[0] &&
+ _.toUpper(decodedToken.identities[0].providerType) === 'SAML';
+ return isSamlAuthenticatedUser;
+ }
+
+ getIdpName(decodedToken) {
+ let identityProviderName = '';
+ if (decodedToken.identities && decodedToken.identities[0] && decodedToken.identities[0].providerName) {
+ identityProviderName = decodedToken.identities[0].providerName;
+ }
+ return identityProviderName;
+ }
+
+ getUsername(decodedToken) {
+ let username = decodedToken['cognito:username'];
+ let usernameInIdp = username;
+ if (username.indexOf('\\') > -1) {
+ // the cognito username may contain backslash (in case the user is authenticated via some other identity provider
+ // via federation - such as SAML replace backslash with underscore in such case to satisfy various naming
+ // constraints in our code base this is because we use the username for automatically naming various dependent
+ // resources (such as IAM roles, policies, unix user groups etc) The backslash would not work in most of those
+ // cases
+ // Grab raw username on the IDP side. This is needed in certain situations
+ // For example, when creating user home directories on jupyter for LDAP users, the directory name needs to match
+ // username in IDP (i.e., AD or LDAP)
+ usernameInIdp = _.split(username, '\\')[1];
+ username = username.replace('\\', '_');
+ }
+ return { username, usernameInIdp };
+ }
+}
+
+module.exports = UserAttributesMapperService;
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/internal/api-key-service.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/internal/api-key-service.js
new file mode 100644
index 0000000000..7b7fd62b80
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/internal/api-key-service.js
@@ -0,0 +1,262 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const uuid = require('uuid');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { ensureCurrentUserOrAdmin, isCurrentUser } = require('@aws-ee/base-services/lib/authorization/assertions');
+
+const authProviderConstants = require('../../constants').authenticationProviders;
+
+const settingKeys = {
+ tableName: 'dbTableUserApiKeys',
+};
+const maxActiveApiKeysPerUser = 5;
+
+const redactIfNotForCurrentUser = (requestContext, apiKey, username, ns) => {
+ if (!isCurrentUser(requestContext, username, ns)) {
+ // if the api key is issued for some other user then redact the api key material as user should be
+ // only able to read his/her own api key value (even if the user is an admin)
+ apiKey.key = undefined;
+ }
+ return apiKey;
+};
+const encode = (username, ns) => `${ns}/${username}`;
+
+class ApiKeyService extends Service {
+ constructor() {
+ super();
+ this.dependency(['dbService', 'jwtService']);
+ this.boom.extend(['invalidCredentials', 401]);
+ this.boom.extend(['maxApiKeysLimitReached', 400]);
+ }
+
+ async init() {
+ await super.init();
+ const createInternals = async () => {
+ const [dbService] = await this.service(['dbService']);
+ const table = this.settings.get(settingKeys.tableName);
+ const dbGetter = () => dbService.helper.getter().table(table);
+ const dbUpdater = () => dbService.helper.updater().table(table);
+ const dbQuery = () => dbService.helper.query().table(table);
+ const ensureMaxApiKeysLimitNotReached = async (requestContext, { username, ns }) => {
+ const existingApiKeys = await this.getApiKeys(requestContext, {
+ username,
+ ns,
+ });
+ const existingActiveApiKeys = _.filter(existingApiKeys, apiKey => {
+ const isActive = apiKey.status === 'active';
+ const isExpired = apiKey.expiryTime && _.now() > apiKey.expiryTime;
+ return isActive && !isExpired;
+ });
+ if (existingActiveApiKeys.length >= maxActiveApiKeysPerUser) {
+ throw this.boom.maxApiKeysLimitReached(
+ `Cannot create API Key. Maximum ${maxActiveApiKeysPerUser} active API keys per user is allowed.`,
+ true,
+ );
+ }
+ };
+ return {
+ dbGetter,
+ dbUpdater,
+ dbQuery,
+ ensureMaxApiKeysLimitNotReached,
+ };
+ };
+ this.internals = await createInternals();
+ }
+
+ async createApiKeyMaterial(requestContext, { apiKeyId, username, ns, expiryTime }) {
+ if (!username) {
+ throw this.boom.badRequest(
+ "Cannot issue API Key. Missing username. Don't know who to issue the API key for.",
+ true,
+ );
+ }
+ if (expiryTime && !_.isNumber(expiryTime)) {
+ // Make sure if "expiryTime" is specified then it is a valid "NumericDate" i.e., epoch time as number
+ throw this.boom.badRequest(
+ 'Cannot issue API Key. Invalid expiryTime specified. expiryTime is optional. ' +
+ 'If it is specified then it must be a number indicating epoch time',
+ true,
+ );
+ }
+
+ const authenticationProviderId = _.get(requestContext, 'principal.authenticationProviderId');
+ const identityProviderName = _.get(requestContext, 'principal.identityProviderName');
+
+ // The JWT service sets "expiresIn" by default based on the settings.
+ // Make sure JWT service does not set it here as we are controlling that via the "exp" claim directly
+ const apiKeyJwtToken = {
+ 'sub': username,
+ 'iss': authProviderConstants.internalAuthProviderId, // This is validated by internal auth provider so set issuer as internal
+ // Add private claims under "custom:". These claims are then used at the time of verifying token
+ 'custom:tokenType': 'api',
+ 'custom:apiKeyId': apiKeyId,
+ 'custom:userNs': ns,
+ 'custom:authenticationProviderId': authenticationProviderId,
+ 'custom:identityProviderName': identityProviderName,
+ };
+ // If expiryTime is specified then set it
+ if (expiryTime) {
+ // If code reached here then it means valid expiryTime is specified
+ apiKeyJwtToken.exp = expiryTime;
+ }
+
+ const jwtService = await this.service('jwtService');
+ return jwtService.sign(apiKeyJwtToken, { expiresIn: undefined });
+ }
+
+ async revokeApiKey(requestContext, { username, ns, keyId }) {
+ // ensure the caller is asking to revoke api key for him/herself or is admin
+ await ensureCurrentUserOrAdmin(requestContext, username, ns);
+
+ // ensure that the key exists and set it's status to "reovked", if it does
+ const unameWithNs = encode(username, ns);
+ const apiKey = await this.internals
+ .dbGetter()
+ .key({ unameWithNs, id: keyId })
+ .get();
+ if (!apiKey) {
+ throw this.boom.badRequest('Cannot revoke API Key. The API key does not exist.', true);
+ }
+ apiKey.status = 'revoked';
+
+ // Update the key with "revoked" status
+ const revokedApiKey = await this.internals
+ .dbUpdater()
+ .key({ unameWithNs, id: apiKey.id })
+ .item(apiKey)
+ .update();
+
+ return redactIfNotForCurrentUser(requestContext, revokedApiKey, username, ns);
+ }
+
+ async issueApiKey(requestContext, { username, ns, expiryTime }) {
+ // ensure the caller is asking to issue new api key for him/herself or is admin
+ await ensureCurrentUserOrAdmin(requestContext, username, ns);
+
+ // ensure max active api keys limit is not reached before issuing new key
+ await this.internals.ensureMaxApiKeysLimitNotReached(requestContext, {
+ username,
+ ns,
+ });
+
+ // create new api key object
+ const apiKeyId = uuid.v4();
+ const apiKeyMaterial = await this.createApiKeyMaterial(requestContext, {
+ apiKeyId,
+ username,
+ ns,
+ expiryTime,
+ });
+ const unameWithNs = encode(username, ns);
+ // TODO: Add TTL based on revocation status and expiry time
+ const newApiKey = {
+ unameWithNs,
+ username,
+ ns,
+ id: apiKeyId,
+ key: apiKeyMaterial,
+ status: 'active',
+ };
+ if (expiryTime) {
+ newApiKey.expiryTime = expiryTime;
+ }
+
+ // save api key to db
+ const apiKey = await this.internals
+ .dbUpdater()
+ .key({ unameWithNs, id: newApiKey.id })
+ .item(newApiKey)
+ .update();
+
+ // sanitize
+ return redactIfNotForCurrentUser(requestContext, apiKey, username, ns);
+ }
+
+ async validateApiKey(signedApiKey) {
+ // Make sure the key is specified
+ if (_.isEmpty(signedApiKey)) {
+ throw this.boom.forbidden('no api key was provided', true);
+ }
+
+ // Make sure the key is a valid non-expired JWT token and has correct signature
+ const jwtService = await this.service('jwtService');
+ const verifiedApiKeyJwtToken = await jwtService.verify(signedApiKey);
+ const { 'sub': username, 'custom:userNs': ns } = verifiedApiKeyJwtToken;
+ if (_.isEmpty(username)) {
+ throw this.boom.invalidToken('No "sub" is provided in the api key', true);
+ }
+
+ // Make sure the key is an API key (and not the regular JWT token)
+ if (!this.isApiKeyToken(verifiedApiKeyJwtToken)) {
+ throw this.boom.invalidToken('The given key is not valid for API access', true);
+ }
+
+ // Make sure the key is an active key (by checking it against the DB)
+ const apiKeyId = _.get(verifiedApiKeyJwtToken, 'custom:apiKeyId');
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+ const unameWithNs = encode(username, ns);
+ const apiKey = await dbService.helper
+ .getter()
+ .table(table)
+ .key({ unameWithNs, id: apiKeyId })
+ .get();
+
+ if (!apiKey) {
+ throw this.boom.invalidToken('The given key is not valid for API access', true);
+ }
+ if (apiKey.status !== 'active') {
+ throw this.boom.invalidToken('The given API key is not active', true);
+ }
+ // If code reached here then this is a valid key
+ return { verifiedToken: verifiedApiKeyJwtToken, username, ns };
+ }
+
+ async getApiKeys(requestContext, { username, ns }) {
+ // ensure the caller is asking retrieve api keys for him/herself or is admin
+ await ensureCurrentUserOrAdmin(requestContext, username, ns);
+ const unameWithNs = encode(username, ns);
+ const apiKeys = await this.internals
+ .dbQuery()
+ .key('unameWithNs', unameWithNs)
+ .query();
+
+ return _.map(apiKeys, apiKey => redactIfNotForCurrentUser(requestContext, apiKey, username, ns));
+ }
+
+ async getApiKey(requestContext, { username, ns, keyId }) {
+ // ensure the caller is asking to retrieve api key for him/herself or is admin
+ await ensureCurrentUserOrAdmin(requestContext, username, ns);
+
+ const unameWithNs = encode(username, ns);
+ const apiKey = await this.internals
+ .dbGetter()
+ .key({ unameWithNs, id: keyId })
+ .get();
+
+ return redactIfNotForCurrentUser(requestContext, apiKey, username, ns);
+ }
+
+ async isApiKeyToken(decodedToken) {
+ const tokenType = _.get(decodedToken, 'custom:tokenType');
+ const apiKeyId = _.get(decodedToken, 'custom:apiKeyId');
+ return tokenType === 'api' && !!apiKeyId;
+ }
+}
+
+module.exports = ApiKeyService;
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/internal/provider-service.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/internal/provider-service.js
new file mode 100644
index 0000000000..25b0b4d3b8
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/internal/provider-service.js
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const authProviderConstants = require('../../constants').authenticationProviders;
+
+class ProviderService extends Service {
+ constructor() {
+ super();
+ this.dependency(['dbAuthenticationService', 'jwtService', 'tokenRevocationService']);
+ }
+
+ async init() {
+ await super.init();
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ async issueToken({ username, password }, providerConfig) {
+ const [dbAuthenticationService, jwtService] = await this.service(['dbAuthenticationService', 'jwtService']);
+
+ await dbAuthenticationService.authenticate({
+ username,
+ password,
+ });
+ const idToken = await jwtService.sign({
+ sub: username,
+
+ // The "iss" (i.e., the issuer) below is used for selecting appropriate authentication provider
+ // for validating JWT tokens on subsequent requests.
+ // See "issuer" claim in JWT RFC - https://tools.ietf.org/html/rfc7519#section-4.1 for
+ // information about this claim
+ iss: authProviderConstants.internalAuthProviderId,
+ });
+ return idToken;
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ async validateToken({ token, issuer }, providerConfig) {
+ if (_.isEmpty(token)) {
+ throw this.boom.forbidden('no jwt token was provided', true);
+ }
+ const jwtService = await this.service('jwtService');
+ const verifiedToken = await jwtService.verify(token);
+ const { sub: username } = verifiedToken;
+
+ if (_.isEmpty(username)) {
+ throw this.boom.invalidToken('No "sub" is provided in the token', true);
+ }
+
+ // -- Check if this token is revoked (may be due to an earlier logout)
+ const tokenRevocationService = await this.service('tokenRevocationService');
+ const isRevoked = await tokenRevocationService.isRevoked({ token });
+ if (isRevoked) {
+ throw this.boom.invalidToken('The token is revoked', true);
+ }
+
+ return { verifiedToken, username };
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ async revokeToken(requestContext, { token }, providerConfig) {
+ const tokenRevocationService = await this.service('tokenRevocationService');
+ await tokenRevocationService.revoke(requestContext, { token });
+ }
+}
+
+module.exports = ProviderService;
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/internal/provisioner-service.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/internal/provisioner-service.js
new file mode 100644
index 0000000000..f3fbab27df
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/internal/provisioner-service.js
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+const authProviderConstants = require('../../constants').authenticationProviders;
+
+class ProvisionerService extends Service {
+ constructor() {
+ super();
+ this.dependency(['authenticationProviderConfigService']);
+ }
+
+ async provision({ providerTypeConfig, providerConfig }) {
+ // There is nothing to do for internal auth provider provisioning
+ // except for saving the configuration in DB which is handled by authenticationProviderPublicConfigService
+ this.log.info('Provisioning internal authentication provider');
+ const authenticationProviderConfigService = await this.service('authenticationProviderConfigService');
+ const result = await authenticationProviderConfigService.saveAuthenticationProviderConfig({
+ providerTypeConfig,
+ providerConfig,
+ status: authProviderConstants.status.active,
+ });
+ return result;
+ }
+}
+
+module.exports = ProvisionerService;
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/internal/type.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/internal/type.js
new file mode 100644
index 0000000000..368f502647
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/built-in-providers/internal/type.js
@@ -0,0 +1,165 @@
+/*
+ * 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.
+ */
+
+module.exports = {
+ type: 'internal',
+ title: 'Internal',
+ description:
+ 'This is a built-in internal authentication provider. ' +
+ 'The internal authentication provider uses an internal user directory for authenticating ' +
+ 'the users. This provider is only intended to be used for development and testing. It currently ' +
+ 'lacks many features required for production usage such as ability to force password rotations, ability ' +
+ 'to reset passwords, and support "forgot password" etc. For production use, please add other ' +
+ 'authentication provider with identity federation for production use.',
+ config: {
+ // credentialHandlingType indicating credential handling for the authentication provider
+ // Possible values:
+ // 'submit' -- The credentials should be submitted to the URL provided by the authentication provider
+ // 'redirect' -- The credentials should be NOT be collected and the user should be redirected directly to the
+ // URL provided by the authentication provider. For example, in case of SAML auth, the username/password
+ // should not be collected by the service provider but the user should be redirected to the identity provider
+ credentialHandlingType: 'submit',
+
+ // "inputSchema": JSON schema representing inputs required from user when configuring an authentication provider of this type.
+ inputSchema: {
+ definitions: {},
+ $schema: 'http://json-schema.org/draft-07/schema#',
+ $id: 'http://example.com/root.json',
+ type: 'object',
+ required: ['id', 'title', 'signInUri'],
+ properties: {
+ id: {
+ $id: '#/properties/id',
+ type: 'string',
+ },
+ title: {
+ $id: '#/properties/title',
+ type: 'string',
+ },
+ signInUri: {
+ $id: '#/properties/signInUri',
+ type: 'string',
+ },
+ signOutUri: {
+ $id: '#/properties/signOutUri',
+ type: 'string',
+ },
+ },
+ },
+
+ // The "inputManifest" will be used on the UI to ask configuration inputs from the user when registering new
+ // authentication provider
+ inputManifestForCreate: {
+ sections: [
+ {
+ title: 'General Information',
+ children: [
+ {
+ name: 'id',
+ type: 'stringInput',
+ title: 'ID',
+ rules: 'required|string|between:2,64|regex:/^[a-zA-Z][a-zA-Z0-9_-]+$/',
+ desc:
+ 'This is a required field. This is used for uniquely identifying the authentication provider. ' +
+ 'It must be between 2 to 64 characters long and must start with an alphabet and may contain alpha numeric ' +
+ 'characters, underscores, and dashes. No other special symbols are allowed.',
+ },
+ {
+ name: 'title',
+ type: 'stringInput',
+ title: 'Title',
+ rules: 'required|between:3,255',
+ desc: 'This is a required field and must be between 3 and 255 characters long.',
+ },
+ {
+ name: 'signInUri',
+ type: 'stringInput',
+ title: 'Sign In URI',
+ rules: 'required|between:3,255',
+ desc: 'The Sign In URI that accepts username/password for signing in.',
+ },
+ {
+ name: 'signOutUri',
+ type: 'stringInput',
+ title: 'Sign Out URI',
+ rules: 'required|between:3,255',
+ desc: 'The Sign Out URI to log out user.',
+ },
+ ],
+ },
+ ],
+ },
+ inputManifestForUpdate: {
+ sections: [
+ {
+ title: 'General Information',
+ children: [
+ {
+ name: 'id',
+ type: 'stringInput',
+ title: 'ID',
+ rules: 'required|string|between:2,64|regex:/^[a-zA-Z][a-zA-Z0-9_-]+$/',
+ desc:
+ 'This is a required field. This is used for uniquely identifying the authentication provider. ' +
+ 'It must be between 2 to 64 characters long and must start with an alphabet and may contain alpha numeric ' +
+ 'characters, underscores, and dashes. No other special symbols are allowed.',
+ },
+ {
+ name: 'title',
+ type: 'stringInput',
+ title: 'Title',
+ rules: 'required|between:3,255',
+ desc: 'This is a required field and must be between 3 and 255 characters long.',
+ },
+ ],
+ },
+ ],
+ },
+
+ impl: {
+ // A locator that identifies the authentication provider implementation
+ //
+ // The implementation may be internal to the code base or could be provided by some external system via APIs (in future)
+ // In case of internal implementation, the below locator should start with 'locator:service:/' pointing to the
+ // service and method for issuing token. The specified method will be invoked with the authentication request body
+ // and the authentication provide configuration
+ //
+ // In case of external implementation provided via APIs, the below locator could be 'locator:external:' pointing to API for issuing token.
+ //
+ // The tokenIssuerLocator will be used only in cases the APIs are receiving credentials
+ // If the authentication is performed outside of the APIs directly, for example, when submitting
+ // credentials to Cognito User Pools directly or when using Cognito User Pool federation to some external identity providers
+ // (e.g., via SAML) then the JWT token is issued by Cognito User Pool outside of the the application code.
+ // The "tokenIssuerLocator" below will not be used in those cases.
+ tokenIssuerLocator: 'locator:service:internalAuthenticationProviderService/issueToken',
+
+ // Similar to the tokenIssuerLocator mentioned above but used for token validation instead of issuing token.
+ // The token validation locator is used to validate token upon each request.
+ // Unlike the tokenIssuerLocator which is only used for authentication being performed via application APIs, the
+ // tokenValidatorLocator is used in all cases
+ tokenValidatorLocator: 'locator:service:internalAuthenticationProviderService/validateToken',
+
+ // Similar to the tokenIssuerLocator mentioned above but used for token revocation instead of issuing token.
+ // The token revocation locator is used to revoke a token upon logout.
+ tokenRevokerLocator: 'locator:service:internalAuthenticationProviderService/revokeToken',
+
+ // Similar to above locators. The provisionerLocator identifies an implementation that takes care of provisioning the authentication provider.
+ // In case of Internal Authentication Provider this "provisioning" step may be as simple as adding authentication provider configuration in Data Base.
+ // In case of other auth providers, this step may be more elaborate (for example, in case of Cognito + SAML, the provisioner has to create Cognito User Pool,
+ // configure Cognito client application, configure SAML identity providers in the Cognito User Pool etc.)
+ provisionerLocator: 'locator:service:internalAuthenticationProvisionerService/provision',
+ },
+ },
+};
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/constants.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/constants.js
new file mode 100644
index 0000000000..ada4358a85
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/constants.js
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+// TODO - move this to base-services in base addon
+const constants = {
+ authenticationProviders: {
+ internalAuthProviderTypeId: 'internal',
+ internalAuthProviderId: 'internal',
+ cognitoAuthProviderTypeId: 'cognito_user_pool',
+ status: {
+ initializing: 'initializing',
+ active: 'active',
+ inactive: 'inactive',
+ },
+ provisioningAction: {
+ create: 'create',
+ update: 'update',
+ activate: 'activate',
+ deactivate: 'deactivate',
+ },
+ },
+};
+
+module.exports = constants;
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/helpers/invoker.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/helpers/invoker.js
new file mode 100644
index 0000000000..12f76deb7d
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/helpers/invoker.js
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+const resolve = require('./resolver').resolve;
+
+function newInvoker(findServiceByName) {
+ return async (locator, ...args) => {
+ const resolvedLocator = resolve(locator);
+ switch (resolvedLocator.type) {
+ case 'service': {
+ const { serviceName, methodName } = resolvedLocator;
+ const instance = await findServiceByName(serviceName);
+ if (!instance) {
+ throw new Error(`unknown service: ${serviceName}`);
+ }
+ return instance[methodName].call(instance, ...args);
+ }
+ default:
+ throw new Error(`unsupported locator type: ${resolve.type}`);
+ }
+ };
+}
+
+module.exports = { newInvoker };
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/helpers/resolver.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/helpers/resolver.js
new file mode 100644
index 0000000000..abef020054
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/helpers/resolver.js
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+function resolve(locator) {
+ // The authentication provider locators would be on of the following formats
+ //
+ // locator:service:/ -- for locator pointing to a method of a specific service
+ // (the service here refers to a service instance loaded by services container)
+ //
+ // locator:external-url: -- for locators pointing to a URL of a specific API
+ //
+ // Make sure the given locator is in one of the expected format
+ if (!_.startsWith(locator, 'locator:service:') && !_.startsWith(locator, 'locator:external-url:')) {
+ throw new Error(`Malformed locator: ${locator}`);
+ }
+
+ const locatorParts = _.split(locator, ':');
+ if (locatorParts.length < 3) {
+ throw new Error(
+ `Malformed locator: ${locator}. Supported locator formats are either
+ - "locator:service:/" or
+ - "locator:external-url:".`,
+ );
+ }
+
+ const typeIndicator = locatorParts[1];
+ const path = locatorParts[2]; // will be / or
+
+ const typeParsers = {
+ 'service': () => {
+ const [serviceName, methodName] = _.split(path, '/');
+ return {
+ type: typeIndicator,
+ serviceName,
+ methodName,
+ };
+ },
+ 'external-url': () => {
+ return {
+ type: typeIndicator,
+ url: path,
+ };
+ },
+ };
+ const typeParserFn = typeParsers[typeIndicator];
+ if (!typeParserFn) {
+ throw new Error(
+ `Malformed locator: ${locator}. Currently only supporting locators that resolve to a 'service' or and 'external-url'.`,
+ );
+ }
+ return typeParserFn();
+}
+
+module.exports = { resolve };
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/register-built-in-provider-services.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/register-built-in-provider-services.js
new file mode 100644
index 0000000000..44a21629de
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/register-built-in-provider-services.js
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+const InternalAuthenticationProviderService = require('./built-in-providers/internal/provider-service');
+const CognitoUserPoolAuthenticationProviderService = require('./built-in-providers/cogito-user-pool/provider-service');
+const UserAttributesMapperService = require('./built-in-providers/cogito-user-pool/user-attributes-mapper-service');
+const ApiKeyService = require('./built-in-providers/internal/api-key-service');
+
+function registerBuiltInAuthProviders(container) {
+ // --- INTERNAL AUTHENTICATION PROVIDER RELATED --- //
+ // internal - provider
+ container.register('internalAuthenticationProviderService', new InternalAuthenticationProviderService());
+ // The api key authentication is always through the internal provider
+ container.register('apiKeyService', new ApiKeyService());
+
+ // --- COGNITO USER POOL AUTHENTICATION PROVIDER RELATED --- //
+ // cognito user pool - provider
+ container.register(
+ 'cognitoUserPoolAuthenticationProviderService',
+ new CognitoUserPoolAuthenticationProviderService(),
+ );
+ container.register('userAttributesMapperService', new UserAttributesMapperService());
+}
+
+module.exports = registerBuiltInAuthProviders;
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-providers/register-built-in-provisioner-services.js b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/register-built-in-provisioner-services.js
new file mode 100644
index 0000000000..6b4eb4a2f8
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-providers/register-built-in-provisioner-services.js
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+const InternalAuthenticationProvisionerService = require('./built-in-providers/internal/provisioner-service');
+const CognitoUserPoolAuthenticationProvisionerService = require('./built-in-providers/cogito-user-pool/provisioner-service');
+
+function registerBuiltInAuthProvisioners(container) {
+ // --- INTERNAL AUTHENTICATION PROVIDER RELATED --- //
+ // internal - provisioner
+ container.register('internalAuthenticationProvisionerService', new InternalAuthenticationProvisionerService());
+
+ // --- COGNITO USER POOL AUTHENTICATION PROVIDER RELATED --- //
+ // cognito user pool - provider
+ container.register(
+ 'cognitoUserPoolAuthenticationProvisionerService',
+ new CognitoUserPoolAuthenticationProvisionerService(),
+ );
+}
+
+module.exports = registerBuiltInAuthProvisioners;
diff --git a/addons/addon-base-rest-api/packages/services/lib/authentication-service.js b/addons/addon-base-rest-api/packages/services/lib/authentication-service.js
new file mode 100644
index 0000000000..1712ee77c8
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/authentication-service.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.
+ */
+
+const jwtDecode = require('jwt-decode');
+const _ = require('lodash');
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+const internalAuthProviderId = require('./authentication-providers/constants').authenticationProviders
+ .internalAuthProviderId;
+const { newInvoker } = require('./authentication-providers/helpers/invoker');
+
+const notAuthenticated = claims => ({ ...claims, authenticated: false });
+const authenticated = claims => ({ ...claims, authenticated: true });
+
+class AuthenticationService extends Service {
+ constructor() {
+ super();
+ this.dependency(['authenticationProviderConfigService', 'apiKeyService', 'pluginRegistryService']);
+ }
+
+ async init() {
+ this.invoke = newInvoker(this.container.find.bind(this.container));
+ this.pluginRegistryService = await this.service('pluginRegistryService');
+ }
+
+ /**
+ * type AuthenticationResult = AuthenticationSuccess | AuthenticationFailed;
+ * type AuthenticationSuccess = {
+ * authenticated: true
+ * verifiedToken: Object
+ * username: string
+ * authenticationProviderId: string
+ * identityProviderName?: string
+ * }
+ * type AuthenticationError = {
+ * authenticated: false
+ * error: Error | string
+ * username?: string
+ * authenticationProviderId?: string
+ * identityProviderName?: string
+ * }
+ *
+ * @returns AuthenticationResult
+ */
+ // TODO return username even if authentication fails.
+ async authenticateMain(token) {
+ const [authenticationProviderConfigService, apiKeyService] = await this.service([
+ 'authenticationProviderConfigService',
+ 'apiKeyService',
+ ]);
+ if (!token) {
+ return notAuthenticated({ error: 'empty token' });
+ }
+ let claims;
+ try {
+ claims = jwtDecode(token);
+ } catch (error) {
+ return notAuthenticated({
+ error: `jwt decode error: ${error.toString()}`,
+ });
+ }
+ const isApiKey = await apiKeyService.isApiKeyToken(claims);
+ if (isApiKey) {
+ try {
+ const { verifiedToken, username } = await apiKeyService.validateApiKey(token);
+ return authenticated({
+ token,
+ isApiKey,
+ verifiedToken,
+ username,
+ authenticationProviderId: _.get(claims, 'custom:authenticationProviderId', internalAuthProviderId),
+ identityProviderName: _.get(claims, 'custom:identityProviderName', ''),
+ });
+ } catch (error) {
+ return notAuthenticated({
+ username: claims.sub,
+ authenticationProviderId: _.get(claims, 'custom:authenticationProviderId', internalAuthProviderId),
+ identityProviderName: _.get(claims, 'custom:identityProviderName', ''),
+ error,
+ });
+ }
+ }
+ const providerId = claims.iss || internalAuthProviderId;
+ const providerConfig = await authenticationProviderConfigService.getAuthenticationProviderConfig(providerId);
+ if (!providerConfig) {
+ return notAuthenticated({
+ username: claims.sub,
+ authenticationProviderId: claims.iss,
+ error: `unknown provider id: '${providerId}'`,
+ });
+ }
+ let tokenValidatorLocator;
+ try {
+ tokenValidatorLocator = providerConfig.config.type.config.impl.tokenValidatorLocator;
+ } catch (error) {
+ // exceptional circumstance, throw an actual error
+ throw new Error(`malformed provider config for provider id '${providerId}'`);
+ }
+ try {
+ const { verifiedToken, username, identityProviderName } = await this.invoke(
+ tokenValidatorLocator,
+ { token, issuer: claims.iss },
+ providerConfig,
+ );
+ return authenticated({
+ token,
+ verifiedToken,
+ username,
+ identityProviderName,
+ authenticationProviderId: providerId,
+ });
+ } catch (error) {
+ return notAuthenticated({
+ username: claims.sub,
+ authenticationProviderId: claims.iss,
+ error,
+ });
+ }
+ }
+
+ async authenticate(token) {
+ const originalAuthResult = await this.authenticateMain(token);
+ // Give all plugins a chance to customize the authentication result
+ return this.checkWithPlugins(token, originalAuthResult);
+ }
+
+ async checkWithPlugins(token, authResult) {
+ // Give all plugins a chance to customize the authentication result
+ // This gives plugins registered to 'authentication' a chance to participate in the token
+ // validation/authentication process
+ const result = await this.pluginRegistryService.visitPlugins('authentication', 'authenticate', {
+ payload: { token, container: this.container, authResult },
+ });
+ const effectiveAuthResult = _.get(result, 'authResult', authResult);
+ return effectiveAuthResult;
+ }
+}
+
+module.exports = AuthenticationService;
diff --git a/addons/addon-base-rest-api/packages/services/lib/db-authentication-service.js b/addons/addon-base-rest-api/packages/services/lib/db-authentication-service.js
new file mode 100644
index 0000000000..607e4f3865
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/db-authentication-service.js
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const inputSchema = require('./schema/username-password-credentials');
+
+class DbAuthenticationService extends Service {
+ constructor() {
+ super();
+ this.boom.extend(['invalidCredentials', 401]);
+ this.dependency(['jsonSchemaValidationService', 'dbPasswordService']);
+ }
+
+ async authenticate(credentials) {
+ const [jsonSchemaValidationService, dbPasswordService] = await this.service([
+ 'jsonSchemaValidationService',
+ 'dbPasswordService',
+ ]);
+
+ // Validate input
+ await jsonSchemaValidationService.ensureValid(credentials, inputSchema);
+
+ const { username, password } = credentials;
+ const exists = await dbPasswordService.exists({ username, password });
+ if (!exists) {
+ throw this.boom.invalidCredentials('Either the password is incorrect or the user does not exist', true);
+ }
+ }
+}
+
+module.exports = DbAuthenticationService;
diff --git a/addons/addon-base-rest-api/packages/services/lib/jwt-service.js b/addons/addon-base-rest-api/packages/services/lib/jwt-service.js
new file mode 100644
index 0000000000..e62af0b67e
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/jwt-service.js
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+const jwt = require('jsonwebtoken'); // https://github.com/auth0/node-jsonwebtoken/tree/v8.3.0
+const _ = require('lodash');
+
+const settingKeys = {
+ paramStoreJwtSecret: 'paramStoreJwtSecret',
+ jwtOptions: 'jwtOptions',
+};
+
+function removeNils(obj) {
+ return _.transform(
+ obj,
+ (result, value, key) => {
+ if (!_.isNil(value)) {
+ result[key] = value;
+ }
+ },
+ {},
+ );
+}
+
+class JwtService extends Service {
+ constructor() {
+ super();
+ this.boom.extend(['invalidToken', 403]);
+ this.dependency('aws');
+ }
+
+ async init() {
+ await super.init();
+ const keyName = this.settings.get(settingKeys.paramStoreJwtSecret);
+ this.secret = await this.getSecret(keyName);
+ }
+
+ async sign(payload, optionsOverride = {}) {
+ const defaultOptions = this.settings.getObject(settingKeys.jwtOptions);
+
+ // Create resultant options and remove Nil values (null or undefined) from the resultant options object.
+ // This is done to allow removing an option using "optionsOverride"
+ // For example, the defaultOptions "expiresIn": "2 days" but the we want to issue non-expiring token
+ // we can pass optionsOverride with "expiresIn": undefined.
+ // This will result in removing the "expiresIn" from the resultant options
+ const options = removeNils(_.assign({}, defaultOptions, optionsOverride));
+
+ return jwt.sign(payload, this.secret, options);
+ }
+
+ async verify(token) {
+ try {
+ const payload = await jwt.verify(token, this.secret);
+ return payload;
+ } catch (err) {
+ throw this.boom.invalidToken('Invalid Token', true).cause(err);
+ }
+ }
+
+ /**
+ * Decodes a token and either returns the token payload or returns the complete decoded token as
+ * { payload, header, signature } based on the "complete" flag.
+ *
+ * @param token The JWT token to decode
+ *
+ * @param complete A flag indicating whether to return just the payload or return the whole token in
+ * { payload, header, signature } format after decoding. Defaults to true i.e., it returns the whole token.
+ *
+ * @param ignoreExpiration A flag indicating whether the decoding should ignore token expiration. If this flag is
+ * false, the decoding will throw exception if an expired token is being decoded. Defaults to true i.e., it ignores expiration.
+ *
+ * @returns {Promise<*|boolean|undefined>}
+ */
+ async decode(token, { complete = true, ignoreExpiration = true } = {}) {
+ try {
+ // using verify method here instead of "decode" method because the "decode" method does not return signature
+ // we want to return signature also when complete === true
+ return jwt.verify(token, this.secret, { complete, ignoreExpiration });
+ } catch (err) {
+ throw this.boom.invalidToken('Invalid Token', true).cause(err);
+ }
+ }
+
+ async getSecret(keyName) {
+ const aws = await this.service('aws');
+ const ssm = new aws.sdk.SSM({ apiVersion: '2014-11-06' });
+
+ this.log.info(`Getting the "${keyName}" key from the parameter store`);
+ const result = await ssm.getParameter({ Name: keyName, WithDecryption: true }).promise();
+ return result.Parameter.Value;
+ }
+}
+
+module.exports = JwtService;
diff --git a/addons/addon-base-rest-api/packages/services/lib/schema/username-password-credentials.json b/addons/addon-base-rest-api/packages/services/lib/schema/username-password-credentials.json
new file mode 100644
index 0000000000..b4cec06041
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/schema/username-password-credentials.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "properties": {
+ "username": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ }
+ },
+ "required": [ "username", "password"]
+}
diff --git a/addons/addon-base-rest-api/packages/services/lib/token-revocation-service.js b/addons/addon-base-rest-api/packages/services/lib/token-revocation-service.js
new file mode 100644
index 0000000000..c27c3c9c28
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/lib/token-revocation-service.js
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const jwtDecode = require('jwt-decode');
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const settingKeys = {
+ tableName: 'dbTableRevokedTokens',
+};
+class TokenRevocationService extends Service {
+ constructor() {
+ super();
+ this.boom.extend(['invalidToken', 403]);
+ this.dependency(['dbService']);
+ }
+
+ async revoke(requestContext, { token }) {
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+
+ const record = await this.toTokenRevocationRecord(token);
+ return dbService.helper
+ .updater()
+ .table(table)
+ .key({ id: record.id })
+ .item(record)
+ .update();
+ }
+
+ async isRevoked({ token }) {
+ // if the given token exists in the database table of revoked tokens then it is "revoked"
+ return this.exists({ token });
+ }
+
+ async exists({ token }) {
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+ const record = await this.toTokenRevocationRecord(token);
+ const item = await dbService.helper
+ .getter()
+ .table(table)
+ .key({ id: record.id })
+ .get();
+
+ return !!item;
+ }
+
+ /**
+ * A method responsible for translating token into a token revocation record in {id, ttl} format.
+ *
+ * @param token
+ * @returns {Promise<{id, ttl}>}
+ */
+ // All other methods in this class treat token as an opaque string. The knowledge of mapping token to it's identifier
+ // and TTL is abstracted in this method.
+ async toTokenRevocationRecord(token) {
+ try {
+ const payload = jwtDecode(token);
+ const signature = token.split('.')[2];
+
+ // use token's signature as the ID of the record for hash key (partition key)
+ // Note that the max limit for partition key is 2048 bytes.
+ // The JWT signatures are SHA256 (so always 256 bits) which are Base64 URL encoded so should fit in 2048 bytes.
+
+ // Set the record's TTL as the token's expiry (i.e., let DynamoDB clear the record from the revocation table
+ // after it is expired)
+ return { id: signature, ttl: _.get(payload, 'exp', 0) };
+ } catch (error) {
+ throw this.boom.invalidToken('Invalid Token', true).cause(error);
+ }
+ }
+}
+
+module.exports = TokenRevocationService;
diff --git a/addons/addon-base-rest-api/packages/services/package.json b/addons/addon-base-rest-api/packages/services/package.json
new file mode 100644
index 0000000000..25a5627c5b
--- /dev/null
+++ b/addons/addon-base-rest-api/packages/services/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@aws-ee/base-api-services",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A library containing base set of services to be used with solutions based on addons",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-services-container": "workspace:*",
+ "@aws-ee/base-services": "workspace:*",
+ "ajv": "^6.11.0",
+ "aws-sdk": "^2.647.0",
+ "jsonwebtoken": "^8.5.1",
+ "jwk-to-pem": "^2.0.3",
+ "jwt-decode": "^2.2.0",
+ "lodash": "^4.17.15",
+ "request": "^2.88.2",
+ "underscore": "^1.9.2",
+ "uuid": "^3.4.0",
+ "validatorjs": "^3.18.1"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-ui/packages/base-ui/.babelrc b/addons/addon-base-ui/packages/base-ui/.babelrc
new file mode 100644
index 0000000000..83064a1e60
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/.babelrc
@@ -0,0 +1,4 @@
+{
+ "plugins": [["@babel/plugin-proposal-class-properties", { "loose": true }]],
+ "presets": ["@babel/preset-react"]
+}
diff --git a/addons/addon-base-ui/packages/base-ui/.eslintrc.json b/addons/addon-base-ui/packages/base-ui/.eslintrc.json
new file mode 100644
index 0000000000..90ac399dcf
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/.eslintrc.json
@@ -0,0 +1,42 @@
+{
+ "extends": ["airbnb", "prettier", "prettier/react", "plugin:jest/recommended", "eslint-config-prettier"],
+ "parser": "babel-eslint",
+ "plugins": ["jest", "prettier", "react"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "no-console": 0,
+ "no-plusplus": 0,
+ "no-nested-ternary": 0,
+ "import/no-named-as-default": 0,
+ "import/no-named-as-default-member": 0,
+ "react/jsx-props-no-spreading": 0,
+ "react/jsx-one-expression-per-line": 0,
+ "react/jsx-filename-extension": 0,
+ "react/destructuring-assignment": 0,
+ "react/sort-comp": 0,
+ "react/prop-types": 0,
+ "jsx-a11y/no-static-element-interactions": 0,
+ "jsx-a11y/click-events-have-key-events": 0
+ },
+ "env": {
+ "browser": true,
+ "node": true,
+ "es6": true,
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-ui/packages/base-ui/.gitignore b/addons/addon-base-ui/packages/base-ui/.gitignore
new file mode 100644
index 0000000000..49cc63674e
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/.gitignore
@@ -0,0 +1,45 @@
+# See https://help.github.com/ignore-files/ for more about ignoring files.
+
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+**/npm-debug.log*
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# dependencies
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# transpiled code
+dist
+
+# misc
+.env.local
+.env.development
+.env.development.local
+.env.test.local
+.env.production
+.env.production.local
+
+yarn-debug.log*
+yarn-error.log*
+
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
diff --git a/addons/addon-base-ui/packages/base-ui/.prettierrc.json b/addons/addon-base-ui/packages/base-ui/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-ui/packages/base-ui/images/login-image.gif b/addons/addon-base-ui/packages/base-ui/images/login-image.gif
new file mode 100644
index 0000000000..ad97652fb3
Binary files /dev/null and b/addons/addon-base-ui/packages/base-ui/images/login-image.gif differ
diff --git a/addons/addon-base-ui/packages/base-ui/jest.config.js b/addons/addon-base-ui/packages/base-ui/jest.config.js
new file mode 100644
index 0000000000..60c221fc1d
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/jest.config.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-ui/packages/base-ui/jsconfig.json b/addons/addon-base-ui/packages/base-ui/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-ui/packages/base-ui/package.json b/addons/addon-base-ui/packages/base-ui/package.json
new file mode 100644
index 0000000000..83e1e84eb5
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/package.json
@@ -0,0 +1,90 @@
+{
+ "name": "@aws-ee/base-ui",
+ "version": "0.1.0",
+ "private": true,
+ "author": "aws-ee",
+ "dependencies": {
+ "aws-sdk": "^2.647.0",
+ "chart.js": "^2.9.3",
+ "classnames": "^2.2.6",
+ "jwt-decode": "^2.2.0",
+ "lodash": "^4.17.15",
+ "mobx": "^5.15.4",
+ "mobx-react": "^6.1.7",
+ "mobx-react-form": "^2.0.8",
+ "mobx-state-tree": "^3.15.0",
+ "numeral": "^2.0.6",
+ "react": "^16.12.0",
+ "react-avatar": "^3.9.0",
+ "react-beautiful-dnd": "^11.0.5",
+ "react-chartjs-2": "^2.9.0",
+ "react-dom": "^16.12.0",
+ "react-dotdotdot": "^1.3.1",
+ "react-idle-timer": "^4.2.12",
+ "react-responsive-carousel": "^3.1.51",
+ "react-router-dom": "^5.1.2",
+ "react-select": "^3.0.8",
+ "react-table": "^6.11.5",
+ "react-timeago": "^4.4.0",
+ "semantic-ui-react": "^0.88.2",
+ "showdown": "^1.9.1",
+ "toastr": "^2.1.4",
+ "typeface-lato": "0.0.75",
+ "validatorjs": "^3.18.1"
+ },
+ "devDependencies": {
+ "@aws-ee/base-serverless-settings-helper": "workspace:*",
+ "@aws-ee/base-serverless-ui-tools": "workspace:*",
+ "@babel/cli": "^7.8.4",
+ "@babel/core": "^7.8.6",
+ "@babel/plugin-proposal-class-properties": "^7.8.3",
+ "@babel/plugin-transform-react-jsx": "^7.8.3",
+ "@babel/preset-env": "^7.8.6",
+ "@babel/preset-react": "^7.8.3",
+ "babel-eslint": "^10.0.3",
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.0.1",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-jsx-a11y": "^6.2.3",
+ "eslint-plugin-prettier": "^3.1.2",
+ "eslint-plugin-react": "^7.18.3",
+ "eslint-plugin-react-hooks": "^1.7.0",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "prop-types": "^15.7.2",
+ "serverless": "^1.63.0",
+ "serverless-deployment-bucket": "^1.1.0",
+ "typescript": "^3.7.5",
+ "webpack": "4.41.2"
+ },
+ "scripts": {
+ "babel": "babel src/ --out-dir dist/ --source-maps",
+ "babel:watch": "babel src/ --out-dir dist/ --source-maps --watch",
+ "build": "pnpm run babel",
+ "build:watch": "pnpm run babel:watch",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "prepare": "pnpm run build"
+ },
+ "files": [
+ "LICENSE",
+ "README.md",
+ "dist/",
+ "src/"
+ ],
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-ui/packages/base-ui/src/App.js b/addons/addon-base-ui/packages/base-ui/src/App.js
new file mode 100644
index 0000000000..8e076e3e01
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/App.js
@@ -0,0 +1,76 @@
+/*
+ * 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 } from 'mobx';
+import { inject, observer } from 'mobx-react';
+import { getEnv } from 'mobx-state-tree';
+import { Switch, Redirect, withRouter } from 'react-router-dom';
+
+import withAuth from './withAuth';
+import { getRoutes, getMenuItems, getDefaultRouteLocation } from './helpers/plugins-util';
+import MainLayout from './parts/MainLayout';
+
+// expected props
+// - app model (via injection)
+// - location (from react router)
+class App extends React.Component {
+ get appContext() {
+ return getEnv(this.props.app) || {};
+ }
+
+ getRoutes() {
+ const { location } = this.props;
+ const appContext = this.appContext;
+ return getRoutes({ location, appContext });
+ }
+
+ getMenuItems() {
+ const { location } = this.props;
+ const appContext = this.appContext;
+ return getMenuItems({ location, appContext });
+ }
+
+ getDefaultRouteLocation() {
+ // See https://reacttraining.com/react-router/web/api/withRouter
+ const { location } = this.props;
+ const appContext = this.appContext;
+
+ return getDefaultRouteLocation({ location, appContext });
+ }
+
+ renderApp() {
+ const defaultLocation = this.getDefaultRouteLocation();
+ return (
+
+
+
+ {this.getRoutes()}
+
+
+ );
+ }
+
+ render() {
+ return this.renderApp();
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(App, {
+ appContext: computed,
+});
+
+export default withAuth(inject('app', 'userStore')(withRouter(observer(App))));
diff --git a/addons/addon-base-ui/packages/base-ui/src/AppContainer.js b/addons/addon-base-ui/packages/base-ui/src/AppContainer.js
new file mode 100644
index 0000000000..aef0edf986
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/AppContainer.js
@@ -0,0 +1,79 @@
+/*
+ * 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, { Component } from 'react';
+import _ from 'lodash';
+import { withRouter } from 'react-router-dom';
+import { inject, observer } from 'mobx-react';
+import { getEnv } from 'mobx-state-tree';
+import { Message, Container } from 'semantic-ui-react';
+
+import { branding } from './helpers/settings';
+
+// expected props
+// - pluginRegistry (via injection)
+// - app (via injection)
+// - location (from react router)
+class AppContainer extends Component {
+ componentDidMount() {
+ document.title = branding.page.title;
+ }
+
+ render() {
+ const { location, pluginRegistry, app } = this.props;
+ let plugins = _.reverse(pluginRegistry.getPluginsWithMethod('app-component', 'getAppComponent') || []);
+ let App = this.renderError();
+
+ // We ask each plugin in reverse order if they have the App component
+ _.forEach(plugins, plugin => {
+ const result = plugin.getAppComponent({ location, appContext: getEnv(app) });
+ if (_.isUndefined(result)) return;
+ App = result;
+ // eslint-disable-next-line consistent-return
+ return false; // This will stop lodash from continuing the forEach loop
+ });
+
+ plugins = _.reverse(pluginRegistry.getPluginsWithMethod('app-component', 'getAutoLogoutComponent') || []);
+ let AutoLogout = () => <>>;
+ // We ask each plugin in reverse order if they have the AutoLogout component
+ _.forEach(plugins, plugin => {
+ const result = plugin.getAutoLogoutComponent({ location, appContext: getEnv(app) });
+ if (_.isUndefined(result)) return;
+ AutoLogout = result;
+ // eslint-disable-next-line consistent-return
+ return false; // This will stop lodash from continuing the forEach loop
+ });
+
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ renderError() {
+ return (
+
+
+ A problem was encountered
+ No plugins provided the App react component!
+
+
+ );
+ }
+}
+
+export default inject('pluginRegistry', 'app')(withRouter(observer(AppContainer)));
diff --git a/addons/addon-base-ui/packages/base-ui/src/app-context/app-context.js b/addons/addon-base-ui/packages/base-ui/src/app-context/app-context.js
new file mode 100644
index 0000000000..41c38424dd
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/app-context/app-context.js
@@ -0,0 +1,57 @@
+/*
+ * 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 { observable } from 'mobx';
+
+import { PluginRegistry } from '../models/PluginRegistry';
+
+const appContext = observable({});
+
+/**
+ * Initializes the given appContext (application context containing various MobX stores etc) by calling each plugin's
+ * "registerAppContextItems" and "postRegisterAppContextItems" methods.
+ *
+ * @param {Object} pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ * Each 'contextItems' plugin in the returned array is an object containing "registerAppContextItems" and "postRegisterAppContextItems" plugin methods.
+ *
+ * @returns {{intervalIds: {}, disposers: {}, pluginRegistry: {}}}
+ */
+const initializeAppContext = pluginRegistry => {
+ const registry = new PluginRegistry(pluginRegistry);
+ const appContextHolder = {
+ disposers: {},
+ intervalIds: {},
+ pluginRegistry: registry,
+ assets: {
+ images: {},
+ },
+ };
+
+ const registerAppContextItems = registry.getPluginsWithMethod('app-context-items', 'registerAppContextItems');
+ _.forEach(registerAppContextItems, plugin => {
+ plugin.registerAppContextItems(appContextHolder);
+ });
+
+ const postRegisterAppContextItems = registry.getPluginsWithMethod('app-context-items', 'postRegisterAppContextItems');
+ _.forEach(postRegisterAppContextItems, plugin => {
+ plugin.postRegisterAppContextItems(appContextHolder);
+ });
+
+ Object.assign(appContext, appContextHolder); // this is to ensure that it is the same appContext reference whether initializeAppContext is called or not
+ return appContextHolder;
+};
+
+export { appContext, initializeAppContext };
diff --git a/addons/addon-base-ui/packages/base-ui/src/bootstrap-app.js b/addons/addon-base-ui/packages/base-ui/src/bootstrap-app.js
new file mode 100644
index 0000000000..8fab9f0bbb
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/bootstrap-app.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 { configure } from 'mobx';
+import 'mobx-react/batchingForReactDom';
+import { initializeAppContext } from './app-context/app-context';
+
+import * as serviceWorker from './service-worker';
+import AppContainer from './AppContainer';
+
+function bootstrapApp({ renderAppContainer, renderError, renderProgress, pluginRegistry }) {
+ // Disabling service worker
+ serviceWorker.unregister();
+
+ // Enable mobx strict mode, changes to state must be contained in actions
+ configure({ enforceActions: 'always' });
+
+ // Initialize appContext object registering various Mobx stores etc
+ const appContext = initializeAppContext(pluginRegistry);
+
+ // Render page loading message
+ renderProgress();
+
+ // Trigger the app startup sequence
+ (async () => {
+ try {
+ await appContext.appRunner.run();
+ renderAppContainer(AppContainer, appContext);
+ } catch (err) {
+ console.log(err);
+ // TODO - check if the code = tokenExpired, then
+ // - render a notification error
+ // - call cleaner cleanup, this is IMPORTANT
+ // - render the app and skip the rest of the renderError logic
+ // - doing the above logic will help us have a smooth user experience
+ // when the token has expired. NOTE: this might not be applicable for the
+ // cases where the app requires midway before anything is displayed to the user
+ renderError(err);
+ try {
+ appContext.cleaner.cleanup();
+ } catch (error) {
+ // ignore
+ console.log(error);
+ }
+ }
+ })();
+}
+
+export default bootstrapApp;
diff --git a/addons/addon-base-ui/packages/base-ui/src/helpers/__tests__/utils.test.js b/addons/addon-base-ui/packages/base-ui/src/helpers/__tests__/utils.test.js
new file mode 100644
index 0000000000..b47a21c42c
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/helpers/__tests__/utils.test.js
@@ -0,0 +1,107 @@
+/*
+ * 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 { flattenObject, unFlattenObject } from '../utils';
+
+describe('helpers/utils', () => {
+ describe('flattenObject --- is working fine if,', () => {
+ it('it leaves already flat object of key/value pairs as is', () => {
+ const input = { someKey: 'someValue' };
+ const expectedOutput = { someKey: 'someValue' };
+ const output = flattenObject(input);
+
+ expect(output).toEqual(expectedOutput);
+ });
+ it('it flattens a simple object graph into a flat object with key/value pairs', () => {
+ const input = { someKey: { someNestedKey: 'someValue' } };
+ const expectedOutput = { 'someKey.someNestedKey': 'someValue' };
+ const output = flattenObject(input);
+
+ expect(output).toEqual(expectedOutput);
+ });
+ it('it flattens an object graph with arrays correctly', () => {
+ const input = { someKey: ['someValue1', 'someValue2'] };
+ const expectedOutput = { 'someKey[0]': 'someValue1', 'someKey[1]': 'someValue2' };
+ const output = flattenObject(input);
+
+ expect(output).toEqual(expectedOutput);
+ });
+ it('it flattens an object graph with nested arrays correctly', () => {
+ const input = { someKey: ['someValue1', ['someValue2', 'someValue3'], 'someValue4'] };
+ const expectedOutput = {
+ 'someKey[0]': 'someValue1',
+ 'someKey[1][0]': 'someValue2',
+ 'someKey[1][1]': 'someValue3',
+ 'someKey[2]': 'someValue4',
+ };
+ const output = flattenObject(input);
+
+ expect(output).toEqual(expectedOutput);
+ });
+ it('it flattens an object graph with arrays containing nested object graphs correctly', () => {
+ const input = { someKey: [{ someNestedKey: 'someValue', nestedArr: [1, 2, { nestedArrKey: 'value' }] }] };
+ const expectedOutput = {
+ 'someKey[0].someNestedKey': 'someValue',
+ 'someKey[0].nestedArr[0]': 1,
+ 'someKey[0].nestedArr[1]': 2,
+ 'someKey[0].nestedArr[2].nestedArrKey': 'value',
+ };
+ const output = flattenObject(input);
+
+ expect(output).toEqual(expectedOutput);
+ });
+ });
+ describe('unFlattenObject --- is working fine if it a correct inverse of the flattenObject, it is correct inverse if', () => {
+ it('it leaves object with keys without any delimiters as is', () => {
+ const expectedOutput = { someKey: 'someValue' };
+ const input = flattenObject(expectedOutput);
+ const output = unFlattenObject(input);
+
+ expect(output).toEqual(expectedOutput);
+ });
+ it('it unFlattens a simple object graph into from a flat object with key/value pairs', () => {
+ const expectedOutput = { someKey: { someNestedKey: 'someValue' } };
+ const input = flattenObject(expectedOutput);
+ const output = unFlattenObject(input);
+
+ expect(output).toEqual(expectedOutput);
+ });
+ it('it unFlattens to an object graph with arrays correctly', () => {
+ const expectedOutput = { someKey: ['someValue1', 'someValue2', 'someValue3'] };
+ const input = flattenObject(expectedOutput);
+ const output = unFlattenObject(input);
+
+ expect(output).toEqual(expectedOutput);
+ });
+ it('it unFlattens to an object graph with nested arrays correctly', () => {
+ const expectedOutput = { someKey: ['someValue1', ['someValue2', 'someValue3'], 'someValue4'] };
+ const input = flattenObject(expectedOutput);
+ // input = { "someKey_0": "someValue1", "someKey_1_0": "someValue2", "someKey_1_1": "someValue3", "someKey_2": "someValue4" };
+ const output = unFlattenObject(input);
+
+ expect(output).toEqual(expectedOutput);
+ });
+ it('it unFlattens to an object graph with arrays containing nested object graphs correctly', () => {
+ const expectedOutput = {
+ someKey: [{ someNestedKey: 'someValue', nestedArr: [1, 2, { nestedArrKey: 'value' }] }],
+ };
+ const input = flattenObject(expectedOutput);
+ // input = { 'someKey[0].someNestedKey': 'someValue', 'someKey[0].nestedArr[0]': 1, 'someKey[0].nestedArr[1]': 2, 'someKey[0].nestedArr[2].nestedArrKey': 'value' };
+ const output = unFlattenObject(input);
+
+ expect(output).toEqual(expectedOutput);
+ });
+ });
+});
diff --git a/addons/addon-base-ui/packages/base-ui/src/helpers/api.js b/addons/addon-base-ui/packages/base-ui/src/helpers/api.js
new file mode 100644
index 0000000000..c6b0c001de
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/helpers/api.js
@@ -0,0 +1,312 @@
+/*
+ * 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 { parseError, delay, removeNulls } from './utils';
+import { apiPath } from './settings';
+
+/* eslint-disable import/no-mutable-exports */
+let config = {
+ apiPath,
+ fetchMode: 'cors',
+ maxRetryCount: 4,
+};
+
+let token;
+let decodedIdToken;
+const authHeader = tok => ({ Authorization: `${tok}` });
+
+function setIdToken(idToken, decodedToken) {
+ token = idToken;
+ decodedIdToken = decodedToken;
+}
+
+function getDecodedIdToken() {
+ return decodedIdToken;
+}
+
+function isTokenExpired() {
+ // Date.now() returns epoch time in MILLISECONDS
+ const expiresAt = _.get(decodedIdToken, 'exp', 0) * 1000;
+ return Date.now() >= expiresAt;
+}
+
+function forgetIdToken() {
+ token = undefined;
+ decodedIdToken = undefined;
+}
+
+function configure(obj) {
+ config = { ...config, ...obj };
+}
+
+function fetchJson(url, options = {}, retryCount = 0) {
+ // see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
+ let isOk = false;
+ let httpStatus;
+
+ const headers = {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ };
+ const body = '';
+ const merged = {
+ method: 'GET',
+ cache: 'no-cache',
+ mode: config.fetchMode,
+ redirect: 'follow',
+ body,
+ ...options,
+ headers: { ...headers, ...options.headers },
+ };
+
+ if (merged.method === 'GET') delete merged.body; // otherwise fetch will throw an error
+
+ if (merged.params) {
+ // if query string parameters are specified then add them to the URL
+ // The merged.params here is just a plain JavaScript object with key and value
+ // For example {key1: value1, key2: value2}
+
+ // Get keys from the params object such as [key1, key2] etc
+ const paramKeys = _.keys(merged.params);
+
+ // Filter out params with undefined or null values
+ const paramKeysToPass = _.filter(paramKeys, key => !_.isNil(_.get(merged.params, key)));
+ const query = _.map(
+ paramKeysToPass,
+ key => `${encodeURIComponent(key)}=${encodeURIComponent(_.get(merged.params, key))}`,
+ ).join('&');
+ url = query ? `${url}?${query}` : url;
+ }
+
+ return Promise.resolve()
+ .then(() => fetch(url, merged))
+ .catch(err => {
+ // this will capture network/timeout errors, because fetch does not consider http Status 5xx or 4xx as errors
+ if (retryCount < config.maxRetryCount) {
+ let backoff = retryCount * retryCount;
+ if (backoff < 1) backoff = 1;
+
+ return Promise.resolve()
+ .then(() => console.log(`Retrying count = ${retryCount}, Backoff = ${backoff}`))
+ .then(() => delay(backoff))
+ .then(() => fetchJson(url, options, retryCount + 1));
+ }
+ throw parseError(err);
+ })
+ .then(response => {
+ isOk = response.ok;
+ httpStatus = response.status;
+ return response;
+ })
+ .then(response => {
+ if (_.isFunction(response.text)) return response.text();
+ return response;
+ })
+ .then(text => {
+ let json;
+ try {
+ if (_.isObject(text)) {
+ json = text;
+ } else {
+ json = JSON.parse(text);
+ }
+ } catch (err) {
+ if (httpStatus >= 400) {
+ if (httpStatus >= 501 && retryCount < config.maxRetryCount) {
+ let backoff = retryCount * retryCount;
+ if (backoff < 1) backoff = 1;
+
+ return Promise.resolve()
+ .then(() => console.log(`Retrying count = ${retryCount}, Backoff = ${backoff}`))
+ .then(() => delay(backoff))
+ .then(() => fetchJson(url, options, retryCount + 1));
+ }
+ throw parseError({
+ message: text,
+ status: httpStatus,
+ });
+ } else {
+ throw parseError(new Error('The server did not return a json response.'));
+ }
+ }
+
+ return json;
+ })
+ .then(json => {
+ if (_.isBoolean(isOk) && !isOk) {
+ throw parseError({ ...json, status: httpStatus });
+ } else {
+ return json;
+ }
+ });
+}
+
+// ---------- helper functions ---------------
+
+function httpApiGet(urlPath, { params } = {}) {
+ return fetchJson(`${config.apiPath}/${urlPath}`, {
+ method: 'GET',
+ headers: authHeader(token),
+ params,
+ });
+}
+
+function httpApiPost(urlPath, { data, params } = {}) {
+ return fetchJson(`${config.apiPath}/${urlPath}`, {
+ method: 'POST',
+ headers: authHeader(token),
+ params,
+ body: JSON.stringify(data),
+ });
+}
+
+function httpApiPut(urlPath, { data, params } = {}) {
+ return fetchJson(`${config.apiPath}/${urlPath}`, {
+ method: 'PUT',
+ headers: authHeader(token),
+ params,
+ body: JSON.stringify(data),
+ });
+}
+
+// eslint-disable-next-line no-unused-vars
+function httpApiDelete(urlPath, { data, params } = {}) {
+ return fetchJson(`${config.apiPath}/${urlPath}`, {
+ method: 'DELETE',
+ headers: authHeader(token),
+ params,
+ body: JSON.stringify(data),
+ });
+}
+
+// ---------- api calls ---------------
+
+function authenticate(authenticationUrl, username, password, authenticationProviderId) {
+ return fetchJson(authenticationUrl, {
+ method: 'POST',
+ body: JSON.stringify({
+ username,
+ password,
+ authenticationProviderId,
+ }),
+ });
+}
+
+function logout() {
+ if (isTokenExpired()) {
+ // if token is already expired then no need to call logout API to revoke token just return
+ return { expired: true, revoked: false };
+ }
+ return httpApiPost('api/authentication/logout');
+}
+
+function getApiKeys({ username, ns } = {}) {
+ return httpApiGet('api/api-keys', { params: { username, ns } });
+}
+
+function createNewApiKey({ username, ns } = {}) {
+ return httpApiPost('api/api-keys', { params: { username, ns } });
+}
+
+function revokeApiKey(apiKeyId, { username, ns } = {}) {
+ return httpApiPut(`api/api-keys/${apiKeyId}/revoke`, { params: { username, ns } });
+}
+
+function getUser() {
+ return httpApiGet('api/user');
+}
+
+function addUser(user) {
+ const params = {};
+ if (user.authenticationProviderId) {
+ params.authenticationProviderId = user.authenticationProviderId;
+ }
+ if (user.identityProviderName) {
+ params.identityProviderName = user.identityProviderName;
+ }
+ const data = removeNulls(_.clone(user));
+ delete data.ns; // Server derives ns based on "authenticationProviderId" and "identityProviderName"
+ // on server side so remove it from request body
+ delete data.createdBy; // Similarly, createdBy and updatedBy are derived on server side
+ delete data.updatedBy;
+ if (!data.userType) {
+ // if userType is specified as empty string then make sure to delete it
+ // the api requires this to be only one of the supported values (currently only supported value is 'root')
+ delete data.userType;
+ }
+ return httpApiPost('api/users', { data, params });
+}
+
+function updateUser(user) {
+ const params = {};
+ if (user.authenticationProviderId) {
+ params.authenticationProviderId = user.authenticationProviderId;
+ }
+ if (user.identityProviderName) {
+ params.identityProviderName = user.identityProviderName;
+ }
+ const data = removeNulls(_.clone(user));
+ delete data.ns; // Server derives ns based on "authenticationProviderId" and "identityProviderName"
+ // on server side so remove it from request body
+ delete data.createdBy; // Similarly, createdBy and updatedBy are derived on server side
+ delete data.updatedBy;
+ if (!data.userType) {
+ // if userType is specified as empty string then make sure to delete it
+ // the api requires this to be only one of the supported values (currently only supported value is 'root')
+ delete data.userType;
+ }
+ return httpApiPut(`api/users/${user.username}`, { data, params });
+}
+
+function getUsers() {
+ return httpApiGet('api/users');
+}
+
+function getAuthenticationProviderPublicConfigs() {
+ return httpApiGet('api/authentication/public/provider/configs');
+}
+function getAuthenticationProviderConfigs() {
+ return httpApiGet('api/authentication/provider/configs');
+}
+
+function updateAuthenticationProviderConfig(authenticationProvider) {
+ return httpApiPut('api/authentication/provider/configs', { data: authenticationProvider });
+}
+
+export {
+ configure,
+ setIdToken,
+ getDecodedIdToken,
+ fetchJson,
+ httpApiGet,
+ httpApiPost,
+ httpApiPut,
+ httpApiDelete,
+ getAuthenticationProviderPublicConfigs,
+ getAuthenticationProviderConfigs,
+ updateAuthenticationProviderConfig,
+ forgetIdToken,
+ getApiKeys,
+ createNewApiKey,
+ revokeApiKey,
+ getUser,
+ addUser,
+ updateUser,
+ getUsers,
+ authenticate,
+ logout,
+ config,
+};
diff --git a/addons/addon-base-ui/packages/base-ui/src/helpers/errors.js b/addons/addon-base-ui/packages/base-ui/src/helpers/errors.js
new file mode 100644
index 0000000000..37c17c8abc
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/helpers/errors.js
@@ -0,0 +1,65 @@
+/*
+ * 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';
+
+const codes = ['apiError', 'notFound', 'badRequest', 'tokenExpired', 'incorrectImplementation', 'timeout'];
+
+const boom = {
+ error: (friendlyOrErr, code, friendly = '') => {
+ if (_.isString(friendlyOrErr)) {
+ const e = new Error(friendlyOrErr);
+ e.isBoom = true;
+ e.code = code;
+ e.friendly = friendlyOrErr; // the friendly argument is ignored and friendlyOrErr is used instead
+ return e;
+ }
+ if (_.isError(friendlyOrErr)) {
+ friendlyOrErr.code = code; // eslint-disable-line no-param-reassign
+ friendlyOrErr.isBoom = true; // eslint-disable-line no-param-reassign
+ friendlyOrErr.friendly = friendly || _.startCase(code);
+ return friendlyOrErr;
+ }
+
+ // if we are here, it means that the msgOrErr is an object
+ const err = new Error(JSON.stringify(friendlyOrErr));
+ err.isBoom = true;
+ err.code = code;
+ err.friendly = friendly || _.startCase(code);
+
+ return err;
+ },
+};
+
+// inject all the codes array elements as properties for the boom
+// example 'apiError' injected => produces boom.apiError(errOrFriendlyMsg, friendlyMsg)
+// then you can call boom.apiError(err, 'Error fetching user info')
+codes.forEach(code => {
+ boom[code] = (errOrFriendlyMsg, friendlyMsg) => boom.error(errOrFriendlyMsg, code, friendlyMsg);
+});
+
+const isNotFound = error => {
+ return _.get(error, 'code') === 'notFound';
+};
+
+const isTokenExpired = error => {
+ return _.get(error, 'code') === 'tokenExpired';
+};
+
+const isForbidden = error => {
+ return _.get(error, 'code') === 'forbidden';
+};
+
+export { boom, isNotFound, isTokenExpired, isForbidden };
diff --git a/addons/addon-base-ui/packages/base-ui/src/helpers/form.js b/addons/addon-base-ui/packages/base-ui/src/helpers/form.js
new file mode 100644
index 0000000000..ce948af8ed
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/helpers/form.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 dvr from 'mobx-react-form/lib/validators/DVR';
+import validatorjs from 'validatorjs';
+import MobxReactForm from 'mobx-react-form';
+
+const formPlugins = Object.freeze({
+ dvr: dvr(validatorjs),
+});
+
+const formOptions = Object.freeze({
+ showErrorsOnReset: false,
+});
+
+function createForm(fields, pluginsParam, optionsParam) {
+ const plugins = pluginsParam || formPlugins;
+ const options = optionsParam || formOptions;
+ return new MobxReactForm({ fields }, { plugins, options });
+}
+
+/**
+ * Creates a MobxReactForm specific to the field identified by the specified fieldName from the given fields.
+ * @param fieldName Name of the field to create MobxReactForm for
+ * @param fields An array of MobxReactForm fields OR an object containing the form fields.
+ * See MobxReactForm documentation about fields https://foxhound87.github.io/mobx-react-form/docs/fields/ for more details.
+ *
+ * @param value Optional value for the field
+ * @param pluginsParam Optional plugin parameters for the MobxReactForm
+ * @param optionsParam Optional options parameters for the MobxReactForm
+ */
+function createSingleFieldForm(fieldName, fields, value, pluginsParam, optionsParam) {
+ // An array of MobxReactForm fields OR an object containing the form fields
+ // Find field with the given fieldName from the fields
+ // In case of Array: It has shape [ {fieldName1:field1}, {fieldName2:field2} ]
+ // In case of Object: It has shape { fieldName1:field1, fieldName2:field2 }
+ const fieldsObj = _.isArray(fields) ? _.find(fields, field => _.keys(field)[0] === fieldName) : fields;
+ const fieldOfInterest = _.get(fieldsObj, fieldName);
+
+ if (!fieldOfInterest) {
+ throw new Error(`Field not found. Can not create form for field ${fieldName}.`);
+ }
+ const fieldWithValue = _.assign({}, { value }, fieldOfInterest);
+ const fieldsToUse = { [fieldName]: fieldWithValue };
+ return createForm(fieldsToUse, pluginsParam, optionsParam);
+}
+
+export { formPlugins, formOptions, createForm, createSingleFieldForm };
diff --git a/addons/addon-base-ui/packages/base-ui/src/helpers/notification.js b/addons/addon-base-ui/packages/base-ui/src/helpers/notification.js
new file mode 100644
index 0000000000..e81cc125e8
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/helpers/notification.js
@@ -0,0 +1,127 @@
+/*
+ * 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 toastr from 'toastr';
+
+function displayError(msg, error, timeOut = '20000') {
+ toastr.error(toMessage(msg, error), 'We have a problem!', { ...toasterErrorOptions, timeOut });
+ if (error) console.error(msg, error);
+ if (_.isError(msg)) console.error(msg);
+}
+
+function displayWarning(msg, error, timeOut = '20000') {
+ toastr.warning(toMessage(msg, error), 'Warning!', { ...toasterWarningOptions, timeOut });
+ if (error) console.error(msg, error);
+ if (_.isError(msg)) console.error(msg);
+}
+
+function displaySuccess(msg, title = 'Submitted!') {
+ toastr.success(toMessage(msg), title, toasterSuccessOptions);
+}
+
+function displayFormErrors(form) {
+ const map = form.errors();
+ const lines = [];
+ Object.keys(map).forEach(key => {
+ if (map[key]) lines.push(map[key]);
+ });
+
+ if (lines.length === 0) return displayError('The form submission has a problem.', undefined, 3000);
+ const isPlural = lines.length > 1;
+ const message = `There ${isPlural ? 'are issues' : 'is an issue'} with the form:`;
+ return displayError([message, ...lines], undefined, 3000);
+}
+
+function toMessage(msg, error) {
+ if (_.isError(msg)) {
+ return `${msg.message || msg.friendly} `;
+ }
+
+ if (_.isError(error)) {
+ return `${msg} - ${error.message} `;
+ }
+
+ if (_.isArray(msg)) {
+ const messages = msg;
+ const size = _.size(messages);
+
+ if (size === 0) {
+ return 'Unknown error ';
+ }
+ if (size === 1) {
+ return `${messages[0]} `;
+ }
+ const result = [];
+ result.push(' ');
+ result.push('');
+ _.forEach(messages, message => {
+ result.push(`${message} `);
+ });
+ result.push(' ');
+
+ return result.join('');
+ }
+
+ if (_.isEmpty(msg)) return 'Unknown error ';
+
+ return `${msg} `;
+}
+
+// For details of options, see https://github.com/CodeSeven/toastr
+//
+// closeButton: Enable a close button
+// debug: Emit debug logs to the console
+// newestOnTop: Show newest toast at top or bottom (top is default)
+// progressBar: Visually indicate how long before a toast expires
+// positionClass: CSS position style e.g. toast-top-center, toast-bottom-left
+// preventDuplicates: Prevent identical toasts appearing (based on content)
+// timeOut: How long the toast will display without user interaction (ms)
+// extendedTimeOut: How long the toast will display after a user hovers over it (ms)
+
+const toasterErrorOptions = {
+ closeButton: true,
+ debug: false,
+ newestOnTop: true,
+ progressBar: true,
+ positionClass: 'toast-top-right',
+ preventDuplicates: true,
+ timeOut: '20000', // 1000000
+ extendedTimeOut: '50000', // 1000000
+};
+
+const toasterWarningOptions = {
+ closeButton: true,
+ debug: false,
+ newestOnTop: true,
+ progressBar: true,
+ positionClass: 'toast-top-right',
+ preventDuplicates: true,
+ timeOut: '20000', // 1000000
+ extendedTimeOut: '50000', // 1000000
+};
+
+const toasterSuccessOptions = {
+ closeButton: true,
+ debug: false,
+ newestOnTop: true,
+ progressBar: true,
+ positionClass: 'toast-top-right',
+ preventDuplicates: true,
+ timeOut: '3000',
+ extendedTimeOut: '10000',
+};
+
+export { displayError, displayWarning, displaySuccess, displayFormErrors };
diff --git a/addons/addon-base-ui/packages/base-ui/src/helpers/plugins-util.js b/addons/addon-base-ui/packages/base-ui/src/helpers/plugins-util.js
new file mode 100644
index 0000000000..3a918e789f
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/helpers/plugins-util.js
@@ -0,0 +1,86 @@
+/*
+ * 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 _ from 'lodash';
+import { Route, Switch } from 'react-router-dom';
+
+/**
+ * Configures the given React Router by collecting routes contributed by all route plugins.
+ *
+ * @returns {*} A React.Router or Switch Component
+ */
+// eslint-disable-next-line no-unused-vars
+function getRoutes({ location, appContext }) {
+ const plugins = appContext.pluginRegistry.getPluginsWithMethod('routes', 'registerRoutes');
+ const initial = new Map();
+ // Ask each plugin to return their routes. Each plugin is passed a Map containing the routes collected so
+ // far from other plugins. The plugins are called in the same order as returned by the registry.
+ // Each plugin gets a chance to add, remove, update, or delete routes by mutating the provided routesMap object.
+ // This routesMap is a Map that has route paths as keys and React Component as value.
+ const routesMap = _.reduce(plugins, (routesFar, plugin) => plugin.registerRoutes(routesFar, appContext), initial);
+
+ const entries = Array.from(routesMap || new Map());
+ let routeIdx = 0;
+ return (
+
+ {_.map(entries, ([routePath, reactComponent]) => {
+ routeIdx += 1;
+ return ;
+ })}
+
+ );
+}
+
+/**
+ * Returns menu items for navigation by collecting items contributed by all menu item plugins.
+ *
+ * @param {*} appContext An application context object containing all MobX store objects
+ *
+ * @returns {*}
+ */
+function getMenuItems({ location, appContext }) {
+ const plugins = appContext.pluginRegistry.getPluginsWithMethod('menu-items', 'registerMenuItems');
+ const initial = new Map();
+ // Ask each plugin to return their nav items. Each plugin is passed a Map containing the nav items collected so
+ // far from other plugins. The plugins are called in the same order as returned by the registry.
+ // Each plugin gets a chance to add, remove, update, or delete items by mutating the provided itemsMap object.
+ // This itemsMap is a Map that has route paths (urls) as keys and menu item object as values.
+ const itemsMap = _.reduce(
+ plugins,
+ (itemsSoFar, plugin) => plugin.registerMenuItems(itemsSoFar, { location, appContext }),
+ initial,
+ );
+
+ const entries = Array.from(itemsMap || new Map());
+ return _.map(entries, ([url, menuItem]) => ({ url, ...menuItem }));
+}
+
+function getDefaultRouteLocation({ location, appContext }) {
+ const plugins = _.reverse(appContext.pluginRegistry.getPluginsWithMethod('routes', 'getDefaultRouteLocation') || []);
+ // We ask each plugin in reverse order if they have a default route
+ let defaultRoute;
+ _.forEach(plugins, plugin => {
+ const result = plugin.getDefaultRouteLocation({ location, appContext });
+ if (_.isUndefined(result)) return;
+ defaultRoute = result;
+ // eslint-disable-next-line consistent-return
+ return false; // This will stop lodash from continuing the forEach loop
+ });
+
+ return defaultRoute;
+}
+
+export { getRoutes, getMenuItems, getDefaultRouteLocation };
diff --git a/addons/addon-base-ui/packages/base-ui/src/helpers/routing.js b/addons/addon-base-ui/packages/base-ui/src/helpers/routing.js
new file mode 100644
index 0000000000..1135d62a0a
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/helpers/routing.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.
+ */
+
+function createLinkWithSearch({ location, pathname, search }) {
+ return {
+ pathname,
+ search: search || location.search,
+ hash: location.hash,
+ state: location.state,
+ };
+}
+
+function createLink({ location, pathname }) {
+ return {
+ pathname,
+ hash: location.hash,
+ state: location.state,
+ };
+}
+
+function reload() {
+ setTimeout(() => {
+ window.location.reload();
+ }, 150);
+}
+
+/**
+ * A generic goto function creator function that returns a go to function bound to the given react component.
+ *
+ * See below snippet as an example for using this function from within some react component
+ * containing "location" and "history" props.
+ *
+ * const goto = gotoFn(this);
+ * goto('/some-path');
+ *
+ * @param reactComponent A react component that has "location" and "history" props as injected via the "withRouter" function.
+ * @returns {{new(...args: any[]): any} | ((...args: any[]) => any) | OmitThisParameter | goto | any | {new(...args: any[]): any} | ((...args: any[]) => any)}
+ */
+function gotoFn(reactComponent) {
+ function goto(pathname) {
+ const location = reactComponent.props.location;
+ const link = createLink({ location, pathname });
+
+ reactComponent.props.history.push(link);
+ }
+ return goto.bind(reactComponent);
+}
+
+export { createLink, createLinkWithSearch, reload, gotoFn };
diff --git a/addons/addon-base-ui/packages/base-ui/src/helpers/settings.js b/addons/addon-base-ui/packages/base-ui/src/helpers/settings.js
new file mode 100644
index 0000000000..d92105994f
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/helpers/settings.js
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+const isLocalDev = process.env.REACT_APP_LOCAL_DEV === 'true';
+const awsRegion = process.env.REACT_APP_AWS_REGION;
+const apiPath = process.env.REACT_APP_API_URL;
+const websiteUrl = process.env.REACT_APP_WEBSITE_URL;
+const stage = process.env.REACT_APP_STAGE;
+const region = process.env.REACT_APP_REGION;
+const autoLogoutTimeoutInMinutes = process.env.REACT_APP_AUTO_LOGOUT_TIMEOUT_IN_MINUTES || 5;
+
+const branding = {
+ login: {
+ title: process.env.REACT_APP_BRAND_LOGIN_TITLE,
+ subtitle: process.env.REACT_APP_BRAND_LOGIN_SUBTITLE,
+ },
+ main: {
+ title: process.env.REACT_APP_BRAND_MAIN_TITLE,
+ },
+ page: {
+ title: process.env.REACT_APP_BRAND_PAGE_TITLE,
+ },
+};
+
+export { awsRegion, apiPath, isLocalDev, websiteUrl, stage, region, branding, autoLogoutTimeoutInMinutes };
diff --git a/addons/addon-base-ui/packages/base-ui/src/helpers/utils.js b/addons/addon-base-ui/packages/base-ui/src/helpers/utils.js
new file mode 100644
index 0000000000..f12cefd61d
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/helpers/utils.js
@@ -0,0 +1,557 @@
+/*
+ * 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 numeral from 'numeral';
+import { observable } from 'mobx';
+
+/**
+ * Converts the given Map object to an array of values from the map
+ */
+function mapToArray(map) {
+ const result = [];
+ // converting map to result array
+ map.forEach(value => result.push(value));
+ return result;
+}
+
+function parseError(err) {
+ const message = _.get(err, 'message', 'Something went wrong');
+ const code = _.get(err, 'code');
+ const status = _.get(err, 'status');
+ const requestId = _.get(err, 'requestId');
+ const error = new Error(message);
+
+ error.code = code;
+ error.requestId = requestId;
+ error.root = err;
+ error.status = status;
+
+ return error;
+}
+
+function swallowError(promise, fn = () => ({})) {
+ try {
+ return Promise.resolve()
+ .then(() => promise)
+ .catch(err => fn(err));
+ } catch (err) {
+ return fn(err);
+ }
+}
+
+const storage = observable({
+ clear() {
+ try {
+ if (localStorage) return localStorage.clear();
+ return window.localStorage.clear();
+ } catch (err) {
+ console.log(err);
+ try {
+ if (sessionStorage) return sessionStorage.clear();
+ return window.sessionStorage.clear();
+ } catch (error) {
+ // if we get here, it means no support for localStorage nor sessionStorage, which is a problem
+ return console.log(error);
+ }
+ }
+ },
+
+ getItem(key) {
+ try {
+ if (localStorage) return localStorage.getItem(key);
+ return window.localStorage.getItem(key);
+ } catch (err) {
+ console.log(err);
+ try {
+ if (sessionStorage) return sessionStorage.getItem(key);
+ return window.sessionStorage.getItem(key);
+ } catch (error) {
+ // if we get here, it means no support for localStorage nor sessionStorage, which is a problem
+ return console.log(error);
+ }
+ }
+ },
+
+ setItem(key, value) {
+ try {
+ if (localStorage) return localStorage.setItem(key, value);
+ return window.localStorage.setItem(key, value);
+ } catch (err) {
+ console.log(err);
+ try {
+ if (sessionStorage) return sessionStorage.setItem(key, value);
+ return window.sessionStorage.setItem(key, value);
+ } catch (error) {
+ // if we get here, it means no support for localStorage nor sessionStorage, which is a problem
+ return console.log(error);
+ }
+ }
+ },
+
+ removeItem(key) {
+ try {
+ if (localStorage) return localStorage.removeItem(key);
+ return window.localStorage.removeItem(key);
+ } catch (err) {
+ console.log(err);
+ try {
+ if (sessionStorage) return sessionStorage.removeItem(key);
+ return window.sessionStorage.removeItem(key);
+ } catch (error) {
+ // if we get here, it means no support for localStorage nor sessionStorage, which is a problem
+ return console.log(error);
+ }
+ }
+ },
+});
+
+// a promise friendly delay function
+function delay(seconds) {
+ return new Promise(resolve => {
+ _.delay(resolve, seconds * 1000);
+ });
+}
+
+function niceNumber(value) {
+ if (_.isNil(value)) return 'N/A';
+ if (_.isString(value) && _.isEmpty(value)) return 'N/A';
+ return numeral(value).format('0,0');
+}
+
+function nicePrice(value) {
+ if (_.isNil(value)) return 'N/A';
+ if (_.isString(value) && _.isEmpty(value)) return 'N/A';
+ return numeral(value).format('0,0.00');
+}
+
+// super basic plural logic, laughable
+function plural(singleStr, pluralStr, count) {
+ if (count === 1) return singleStr;
+ return pluralStr;
+}
+
+function getQueryParam(location, key) {
+ const queryParams = new URL(location).searchParams;
+ return queryParams.get(key);
+}
+
+function addQueryParams(location, params) {
+ const url = new URL(location);
+ const queryParams = url.searchParams;
+
+ const keys = _.keys(params);
+ keys.forEach(key => {
+ queryParams.append(key, params[key]);
+ });
+
+ let newUrl = url.origin + url.pathname;
+
+ if (queryParams.toString()) {
+ newUrl += `?${queryParams.toString()}`;
+ }
+
+ newUrl += url.hash;
+ return newUrl;
+}
+
+function removeQueryParams(location, keys) {
+ const url = new URL(location);
+ const queryParams = url.searchParams;
+
+ keys.forEach(key => {
+ queryParams.delete(key);
+ });
+
+ let newUrl = url.origin + url.pathname;
+
+ if (queryParams.toString()) {
+ newUrl += `?${queryParams.toString()}`;
+ }
+
+ newUrl += url.hash;
+ return newUrl;
+}
+
+function getFragmentParam(location, key) {
+ const fragmentParams = new URL(location).hash;
+ const hashKeyValues = {};
+ const params = fragmentParams.substring(1).split('&');
+ if (params) {
+ params.forEach(param => {
+ const keyValueArr = param.split('=');
+ const currentKey = keyValueArr[0];
+ const value = keyValueArr[1];
+ if (value) {
+ hashKeyValues[currentKey] = value;
+ }
+ });
+ }
+ return hashKeyValues[key];
+}
+
+function removeFragmentParams(location, keyNamesToRemove) {
+ const url = new URL(location);
+ const fragmentParams = url.hash;
+ let hashStr = '#';
+ const params = fragmentParams.substring(1).split('&');
+ if (params) {
+ params.forEach(param => {
+ const keyValueArr = param.split('=');
+ const currentKey = keyValueArr[0];
+ const value = keyValueArr[1];
+ // Do not include the currentKey if it is the one specified in the array of keyNamesToRemove
+ if (value && _.indexOf(keyNamesToRemove, currentKey) < 0) {
+ hashStr = `${currentKey}${currentKey}=${value}`;
+ }
+ });
+ }
+ return `${url.protocol}//${url.host}${url.search}${hashStr === '#' ? '' : hashStr}`;
+}
+
+function isAbsoluteUrl(url) {
+ // return /^[a-z][a-z\d+.-]*:/.test(url);
+ return /^https?:/.test(url);
+}
+
+function removeNulls(obj = {}) {
+ Object.keys(obj).forEach(key => {
+ if (obj[key] === null) delete obj[key];
+ });
+
+ return obj;
+}
+
+// remove the "end" string from "str" if it exists
+function chopRight(str = '', end = '') {
+ if (!_.endsWith(str, end)) return str;
+ return str.substring(0, str.length - end.length);
+}
+
+const isFloat = n => {
+ return n % 1 !== 0;
+};
+
+// input [ { : { label, desc, ..} }, { : { label, desc } } ]
+// output { : { label, desc, ..}, : { label, desc } }
+function childrenArrayToMap(arr) {
+ const result = {};
+ arr.forEach(item => {
+ const key = _.keys(item)[0];
+ result[key] = item[key];
+ });
+ return result;
+}
+
+let idGeneratorCount = 0;
+
+function generateId(prefix = '') {
+ idGeneratorCount += 1;
+ return `${prefix}_${idGeneratorCount}_${Date.now()}_${_.random(0, 1000)}`;
+}
+
+// Given a Map and an array of items (each item MUST have an "id" prop), consolidate
+// the array in the following manner:
+// - if an existing item in the map is no longer in the array of items, remove the item from the map
+// - if an item in the array is not in the map, then add it to the map using the its "id" prop
+// - if an item in the array is also in the map, then call 'mergeExistingFn' with the existing item
+// and the new item. It is expected that this 'mergeExistingFn', will know how to merge the
+// properties of the new item into the existing item.
+function consolidateToMap(map, itemsArray, mergeExistingFn) {
+ const unprocessedKeys = {};
+
+ map.forEach((_value, key) => {
+ unprocessedKeys[key] = true;
+ });
+
+ itemsArray.forEach(item => {
+ const id = item.id;
+ const hasExisting = map.has(id);
+ const exiting = map.get(id);
+
+ if (!exiting) {
+ map.set(item.id, item);
+ } else {
+ mergeExistingFn(exiting, item);
+ }
+
+ if (hasExisting) {
+ delete unprocessedKeys[id];
+ }
+ });
+
+ _.forEach(unprocessedKeys, (_value, key) => {
+ map.delete(key);
+ });
+}
+
+/**
+ * Converts an object graph into flat object with key/value pairs.
+ * The rules of object graph to flat key value transformation are as follows.
+ * 1. An already flat attribute with primitive will not be transformed.
+ * For example,
+ * input = { someKey: 'someValue' } => output = { someKey: 'someValue' }
+ * 2. A nested object attribute will be flattened by adding full attribute path '.' (the paths are as per lodash's get and set functions)
+ * For example,
+ * input = { someKey: { someNestedKey: 'someValue' } } => output = { 'someKey.someNestedKey': 'someValue' }
+ * 3. An array attribute will be flattened by adding correct path '[]' prefix. (the paths are as per lodash's get and set functions)
+ * For example,
+ * input = { someKey: [ 'someValue1', 'someValue2' ] } => output = { 'someKey[0]': 'someValue1', 'someKey[1]': 'someValue2' }
+ * input = { someKey: [ 'someValue1', ['someValue2','someValue3'], 'someValue4' ] } => output = { 'someKey[0]': 'someValue1', 'someKey[1][0]': 'someValue2', 'someKey[1][1]': 'someValue3', 'someKey[2]': 'someValue4' }
+ * input = { someKey: [{ someNestedKey: 'someValue' }] } => output = { 'someKey[0].someNestedKey': 'someValue' }
+ *
+ * @param obj An object to flatten
+ * @param filterFn An optional filter function that allows filtering out certain attributes from being included in the flattened result object. The filterFn is called with 3 arguments (result, value, key) and is expected to return true to include
+ * the key in the result or false to exclude the key from the result.
+ * @param keyPrefix A optional key prefix to include in all keys in the resultant flattened object.
+ * @param accum An optional accumulator to use when performing the transformation
+ * @returns {*}
+ */
+function flattenObject(obj, filterFn = () => true, keyPrefix = '', accum = {}) {
+ function toFlattenedKey(key, idx) {
+ let flattenedKey;
+ if (_.isNil(idx)) {
+ if (_.isNumber(key)) {
+ flattenedKey = keyPrefix ? `${keyPrefix}[${key}]` : `[${key}]`;
+ } else {
+ flattenedKey = keyPrefix ? `${keyPrefix}.${key}` : key;
+ }
+ } else {
+ flattenedKey = keyPrefix ? `${keyPrefix}.${key}[${idx}]` : `${key}[${idx}]`;
+ }
+ return flattenedKey;
+ }
+
+ return _.transform(
+ obj,
+ (result, value, key) => {
+ if (filterFn(result, value, key)) {
+ if (_.isArray(value)) {
+ let idx = 0;
+ _.forEach(value, element => {
+ const flattenedKey = toFlattenedKey(key, idx);
+ if (_.isObject(element)) {
+ flattenObject(element, filterFn, flattenedKey, result);
+ } else {
+ result[flattenedKey] = element;
+ }
+ ++idx;
+ });
+ } else {
+ const flattenedKey = toFlattenedKey(key);
+ if (_.isObject(value)) {
+ flattenObject(value, filterFn, flattenedKey, result);
+ } else {
+ result[flattenedKey] = value;
+ }
+ }
+ }
+ return result;
+ },
+ accum,
+ );
+}
+
+/**
+ * Converts an object with key/value pairs into object graph. This function is inverse of flattenObject.
+ * i.e., unFlattenObject(flattenObject(obj)) = obj
+ *
+ * The rules of key/value pairs to object graph transformation are as follows.
+ * 1. Key that does not contain delimiter will not be transformed.
+ * For example,
+ * input = { someKey: 'someValue' } => output = { someKey: 'someValue' }
+ * 2. Key/Value containing delimiter will be transformed into object path
+ * For example,
+ * input = { someKey_someNestedKey: 'someValue' } => output = { someKey: { someNestedKey: 'someValue' } }
+ * 3. Key/Value containing delimiter and integer index will be transformed into object containing array.
+ * For example,
+ * input = { someKey_0: 'someValue1', someKey_1: 'someValue2' } => output = { someKey: [ 'someValue1', 'someValue2' ] }
+ * input = { "someKey_0": "someValue1", "someKey_1_0": "someValue2", "someKey_1_1": "someValue3", "someKey_2": "someValue4" } => output = { someKey: [ 'someValue1', ['someValue2','someValue3'], 'someValue4' ] }
+ * input = { someKey_0_someNestedKey: 'someValue' } => output = { someKey: [{ someNestedKey: 'someValue' }] }
+ *
+ * @param obj An object to flatten
+ * @param filterFn An optional filter function that allows filtering out certain attributes from being included in the flattened result object. The filterFn is called with 3 arguments (result, value, key) and is expected to return true to include
+ * the key in the result or false to exclude the key from the result.
+ * @param keyPrefix A optional key prefix to include in all keys in the resultant flattened object.
+ * @returns {*}
+ */
+function unFlattenObject(keyValuePairs, filterFn = () => true) {
+ return _.transform(
+ keyValuePairs,
+ (result, value, key) => {
+ if (filterFn(result, value, key)) {
+ _.set(result, key, value);
+ }
+ return result;
+ },
+ {},
+ );
+}
+
+function toUTCDate(str) {
+ if (_.isEmpty(str)) return str;
+ if (!_.isString(str)) return str;
+ if (_.endsWith(str, 'Z')) return str;
+
+ return `${str}Z`;
+}
+
+/**
+ * Given a list of validatorjs rules for a form element, returns valid options
+ * by returning elements defined by the "in" rule as valid Semantic UI options objects.
+ *
+ * @param {Array} formRules Rules for the validatorjs library
+ * @returns {Array} Options formatted for a Semantic UI Dropdown
+ */
+function getOptionsFromRules(formRules) {
+ let options = [];
+ formRules.forEach(rule => {
+ if (typeof rule === 'object' && 'in' in rule) {
+ options = rule.in.map(option => ({ key: option, text: option, value: option }));
+ }
+ });
+ return options;
+}
+
+function validRegions() {
+ return [
+ 'us-east-2',
+ 'us-east-1',
+ 'us-west-1',
+ 'us-west-2',
+ 'ap-east-1',
+ 'ap-south-1',
+ 'ap-northeast-3',
+ 'ap-northeast-2',
+ 'ap-southeast-1',
+ 'ap-southeast-2',
+ 'ap-northeast-1',
+ 'ca-central-1',
+ 'cn-north-1',
+ 'cn-northwest-1',
+ 'eu-central-1',
+ 'eu-west-1',
+ 'eu-west-2',
+ 'eu-west-3',
+ 'eu-north-1',
+ 'me-south-1',
+ 'sa-east-1',
+ 'us-gov-east-1',
+ 'us-gov-west-1',
+ ].sort();
+}
+
+/**
+ * Converts bytes to a human-friendly string by representing them as KB, MB, GB,
+ * etc., depending on how large the size is.
+ * Adapted from https://stackoverflow.com/a/18650828
+ *
+ * @param {number} bytes The number of bytes to be converted
+ * @param {number} decimals How many decimal places should be maintained
+ * @returns {string} The human-friendly string form of the passed bytes
+ */
+function formatBytes(bytes, decimals = 2) {
+ if (bytes === 0) return '0 Bytes';
+
+ const k = 1024;
+ const dm = decimals < 0 ? 0 : decimals;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+ return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
+}
+
+/**
+ * A utility function to process given items in sequence of batches. Items in one batch are processed in-parallel but
+ * all batches are processed sequentially i..e, processing of the next batch is not started until the previous batch is
+ * complete.
+ *
+ * @param items Array of items to process
+ * @param batchSize Number of items in a batch
+ * @param processorFn A function to process the batch. The function is called with the item argument.
+ * The function is expected to return a Promise with some result of processing the item.
+ *
+ * @returns {Promise}
+ */
+async function processInBatches(items, batchSize, processorFn) {
+ const itemBatches = _.chunk(items, batchSize);
+
+ let results = [];
+
+ // Process all items in one batch in parallel and wait for the batch to
+ // complete before moving on to the next batch
+ for (let i = 0; i <= itemBatches.length; i += 1) {
+ const itemsInThisBatch = itemBatches[i];
+ // We need to await for each batch in loop to make sure they are awaited in sequence instead of
+ // firing them in parallel disabling eslint for "no-await-in-loop" due to this
+ // eslint-disable-next-line no-await-in-loop
+ const resultsFromThisBatch = await Promise.all(
+ // Fire promise for each item in this batch and let it be processed in parallel
+ _.map(itemsInThisBatch, processorFn),
+ );
+
+ // push all results from this batch into the main results array
+ results = _.concat(results, resultsFromThisBatch);
+ }
+ return results;
+}
+
+/**
+ * A utility function that processes items sequentially. The function uses the specified processorFn to process
+ * items in the given order i.e., it does not process next item in the given array until the promise returned for
+ * the processing of the previous item is resolved. If the processorFn throws error (or returns a promise rejection)
+ * this functions stops processing next item and the error is bubbled up to the caller (via a promise rejection).
+ *
+ * @param items Array of items to process
+ * @param processorFn A function to process the item. The function is called with the item argument.
+ * The function is expected to return a Promise with some result of processing the item.
+ *
+ * @returns {Promise}
+ */
+async function processSequentially(items, processorFn) {
+ return processInBatches(items, 1, processorFn);
+}
+
+export {
+ mapToArray,
+ parseError,
+ swallowError,
+ storage,
+ delay,
+ niceNumber,
+ plural,
+ getQueryParam,
+ removeQueryParams,
+ addQueryParams,
+ getFragmentParam,
+ removeFragmentParams,
+ nicePrice,
+ isFloat,
+ removeNulls,
+ chopRight,
+ childrenArrayToMap,
+ isAbsoluteUrl,
+ generateId,
+ consolidateToMap,
+ flattenObject,
+ unFlattenObject,
+ toUTCDate,
+ getOptionsFromRules,
+ validRegions,
+ formatBytes,
+ processInBatches,
+ processSequentially,
+};
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/App.js b/addons/addon-base-ui/packages/base-ui/src/models/App.js
new file mode 100644
index 0000000000..7d8d9c1424
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/App.js
@@ -0,0 +1,52 @@
+/*
+ * 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 App = types
+ .model('BaseApp', {
+ userAuthenticated: false,
+ })
+ .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 => ({
+ init: async payload => {
+ const tokenNotExpired = _.get(payload, 'tokenInfo.status') === 'notExpired';
+ if (tokenNotExpired) {
+ self.setUserAuthenticated(true);
+ }
+ },
+
+ setUserAuthenticated(flag) {
+ self.userAuthenticated = flag;
+ },
+
+ // this method is called by the Cleaner
+ cleanup() {
+ self.setUserAuthenticated(false);
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.app = App.create({}, appContext);
+}
+
+export { App, registerContextItems };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/AppRunner.js b/addons/addon-base-ui/packages/base-ui/src/models/AppRunner.js
new file mode 100644
index 0000000000..e17a000887
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/AppRunner.js
@@ -0,0 +1,62 @@
+/*
+ * 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 */
+class AppRunner {
+ constructor(appContext) {
+ this.appContext = appContext;
+ }
+
+ async run() {
+ const appContext = this.appContext;
+ const registry = appContext.pluginRegistry;
+ const initPlugins = registry.getPluginsWithMethod('initialization', 'init');
+ const payload = {};
+
+ // Ask each plugin to run init()
+ // eslint-disable-next-line no-restricted-syntax
+ for (const plugin of initPlugins) {
+ await plugin.init(payload, appContext);
+ }
+
+ // Did any plugin want to do an external redirect?
+ if (payload.externalRedirectUrl) {
+ window.location = payload.externalRedirectUrl;
+ return;
+ }
+
+ const postInitPlugins = registry.getPluginsWithMethod('initialization', 'postInit');
+ // Ask each plugin to run postInit()
+ // eslint-disable-next-line no-restricted-syntax
+ for (const plugin of postInitPlugins) {
+ await plugin.postInit(payload, appContext);
+ }
+
+ // Did any plugin want to do an external redirect?
+ if (payload.externalRedirectUrl) {
+ window.location = payload.externalRedirectUrl;
+ return;
+ }
+
+ const app = appContext.app;
+ await app.init(payload);
+ }
+}
+
+function registerContextItems(appContext) {
+ appContext.appRunner = new AppRunner(appContext);
+}
+
+export { AppRunner, registerContextItems };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/BaseStore.js b/addons/addon-base-ui/packages/base-ui/src/models/BaseStore.js
new file mode 100644
index 0000000000..f67c15c71f
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/BaseStore.js
@@ -0,0 +1,172 @@
+/*
+ * 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 './Err';
+
+// A four-state model that has the following states:
+// +---------+ +-----------+
+// | initial | +----> | ready |
+// +---------+ | +-----------+
+// |
+// + ^ | + ^
+// | | | | | success
+// load | | error | load | | or error
+// v + | v +
+// |
+// +---------+ | +-----------+
+// | loading +------------+ | reloading |
+// +---------+ success +-----------+
+//
+// state:
+// error: if there is an error otherwise
+// empty: if state is ready or reloading and the content is considered empty
+const BaseStore = types
+ .model('BaseStore', {
+ state: 'initial',
+ error: types.maybe(Err),
+ tickPeriod: 7 * 1000, // 7 seconds
+ heartbeatInterval: 0,
+ })
+ .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 => {
+ let loadingPromise;
+
+ return {
+ load: (...args) => {
+ if (loadingPromise) return loadingPromise;
+
+ // self.error = undefined; we don't want to clear the error here
+ if (self.state === 'ready') self.state = 'reloading';
+ else self.state = 'loading';
+
+ loadingPromise = new Promise((resolve, reject) => {
+ // if ((self.state === 'loading') || (self.state === 'reloading')) return;
+ try {
+ self
+ .doLoad(...args)
+ .then(() => {
+ self.runInAction(() => {
+ self.state = 'ready';
+ self.error = undefined;
+ });
+ loadingPromise = undefined;
+ resolve();
+ })
+ .catch(err => {
+ self.runInAction(() => {
+ self.state = self.state === 'loading' ? 'initial' : 'ready';
+ self.error = toErr(err);
+ });
+ loadingPromise = undefined;
+ reject(err);
+ });
+ } catch (err) {
+ self.runInAction(() => {
+ self.state = self.state === 'loading' ? 'initial' : 'ready';
+ self.error = toErr(err);
+ });
+ loadingPromise = undefined;
+ reject(err);
+ }
+ });
+
+ return loadingPromise;
+ },
+
+ startHeartbeat: () => {
+ if (self.heartbeatInterval !== 0) return; // there is one running
+ if (!self.shouldHeartbeat()) return;
+ const id = setInterval(async () => {
+ if (!self.shouldHeartbeat()) return;
+ try {
+ await self.load();
+ } catch (err) {
+ /* ignore */
+ }
+ }, self.tickPeriod);
+ self.heartbeatInterval = id;
+ },
+ shouldHeartbeat: () => {
+ return true; // extender can override this method
+ },
+ stopHeartbeat: () => {
+ const id = self.heartbeatInterval;
+ if (id !== 0) {
+ clearInterval(id);
+ self.heartbeatInterval = undefined;
+ }
+ },
+ changeTickPeriod(period) {
+ const beating = self.heartBeating;
+ self.tickPeriod = period;
+ if (beating) {
+ self.stopHeartbeat();
+ self.startHeartbeat();
+ }
+ },
+ cleanup: () => {
+ self.stopHeartbeat();
+ self.state = 'initial';
+ self.error = undefined;
+ },
+ };
+ })
+
+ .views(self => ({
+ get heartBeating() {
+ return self.heartbeatInterval > 0;
+ },
+ get initial() {
+ return self.state === 'initial';
+ },
+ get ready() {
+ return self.state === 'ready';
+ },
+ get loading() {
+ return self.state === 'loading';
+ },
+ get reloading() {
+ return self.state === 'reloading';
+ },
+ get errorMessage() {
+ return self.error ? self.error.message || 'unknown error' : '';
+ },
+ }));
+
+const isStoreReady = obj => obj.ready || obj.reloading;
+const isStoreEmpty = obj => (obj.ready || obj.reloading) && obj.empty;
+const isStoreNotEmpty = obj => (obj.ready || obj.reloading) && !obj.empty;
+const isStoreLoading = obj => obj.loading;
+const isStoreReloading = obj => obj.reloading;
+const isStoreNew = obj => obj.initial;
+const isStoreError = obj => !!obj.error;
+
+export {
+ BaseStore,
+ isStoreReady,
+ isStoreEmpty,
+ isStoreNotEmpty,
+ isStoreLoading,
+ isStoreReloading,
+ isStoreNew,
+ isStoreError,
+};
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/Cleaner.js b/addons/addon-base-ui/packages/base-ui/src/models/Cleaner.js
new file mode 100644
index 0000000000..58937a9edd
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/Cleaner.js
@@ -0,0 +1,69 @@
+/*
+ * 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 { runInAction } from 'mobx';
+
+import { forgetIdToken } from '../helpers/api';
+
+// An object that captures all the clean up logic when the app is done or no jwt token
+// is found.
+class Cleaner {
+ constructor(appContext) {
+ this.appContext = appContext;
+ }
+
+ cleanup() {
+ const { disposers, intervalIds } = this.appContext;
+
+ // it is important that we start with cleaning the disposers, otherwise snapshots events will be fired
+ // for cleaned stores
+ let keys = _.keys(disposers);
+ _.forEach(keys, key => {
+ const fn = disposers[key];
+ if (_.isFunction(fn)) {
+ fn();
+ }
+ delete disposers[key];
+ });
+
+ keys = _.keys(intervalIds);
+ _.forEach(keys, key => {
+ const id = intervalIds[key];
+ if (!_.isNil(id)) {
+ clearInterval(id);
+ }
+ delete intervalIds[key];
+ });
+
+ runInAction(() => {
+ forgetIdToken();
+
+ _.forEach(this.appContext, obj => {
+ if (obj === this) return; // we don't want to end up in an infinite loop
+ if (_.isFunction(obj.cleanup)) {
+ // console.log(`Cleaner.cleanup() : calling ${key}.clear()`);
+ obj.cleanup();
+ }
+ });
+ });
+ }
+}
+
+function registerContextItems(appContext) {
+ appContext.cleaner = new Cleaner(appContext);
+}
+
+export { Cleaner, registerContextItems };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/Err.js b/addons/addon-base-ui/packages/base-ui/src/models/Err.js
new file mode 100644
index 0000000000..974f080610
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/Err.js
@@ -0,0 +1,35 @@
+/*
+ * 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 { parseError } from '../helpers/utils';
+
+const Err = types.model('Err', {
+ message: '',
+ code: '',
+ requestId: '',
+});
+
+const toErr = error => {
+ const parsed = parseError(error);
+ return Err.create({
+ message: parsed.message || '',
+ code: parsed.code || '',
+ requestId: parsed.toErr || '',
+ });
+};
+
+export { Err, toErr };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/PluginRegistry.js b/addons/addon-base-ui/packages/base-ui/src/models/PluginRegistry.js
new file mode 100644
index 0000000000..cbbf826f56
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/PluginRegistry.js
@@ -0,0 +1,44 @@
+/*
+ * 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';
+import { processSequentially } from '../helpers/utils';
+
+class PluginRegistry {
+ constructor(registry) {
+ this.registry = registry;
+ }
+
+ getPlugins(extensionPoint) {
+ return this.registry.getPlugins(extensionPoint);
+ }
+
+ getPluginsWithMethod(extensionPoint, methodName) {
+ const registry = this.registry;
+ const plugins = registry.getPlugins(extensionPoint);
+ return _.filter(plugins, plugin => _.isFunction(plugin[methodName]));
+ }
+
+ async runPlugins(extensionPoint, methodName, ...args) {
+ const plugins = this.getPluginsWithMethod(extensionPoint, methodName);
+
+ // Each plugin needs to be executed in order. The plugin method may be return a promise we need to await
+ // it in sequence.
+ return processSequentially(plugins, plugin => plugin[methodName](...args));
+ }
+}
+
+export { PluginRegistry };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/SessionStore.js b/addons/addon-base-ui/packages/base-ui/src/models/SessionStore.js
new file mode 100644
index 0000000000..a4454e4622
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/SessionStore.js
@@ -0,0 +1,91 @@
+/*
+ * 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';
+
+class EventBus {
+ constructor() {
+ this.listenersMap = {};
+ }
+
+ listenTo(channel, { id, listener }) {
+ const entries = this.listenersMap[channel] || [];
+ entries.push({ id, listener });
+
+ this.listenersMap[channel] = entries;
+ }
+
+ async fireEvent(channel, event) {
+ const keys = _.keys(this.listenersMap);
+
+ /* eslint-disable no-restricted-syntax, no-await-in-loop */
+ for (const key of keys) {
+ if (_.startsWith(key, channel)) {
+ const entries = this.listenersMap[key];
+ for (const entry of entries) {
+ await entry.listener(event, { entry, channel });
+ }
+ }
+ }
+ }
+ /* eslint-enable no-restricted-syntax, no-await-in-loop */
+ // TODO stopListening(id, channel) { }
+}
+
+const uiEventBus = new EventBus();
+
+// A simple key/value store that only exists while the browser tab is open.
+// You can choose to store your component ui states in here when applicable.
+class SessionStore {
+ constructor() {
+ this.map = new Map();
+ }
+
+ cleanup() {
+ this.map.clear();
+ }
+
+ // remove all keys that start with the prefix
+ removeStartsWith(prefix) {
+ // map api https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
+ // for of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
+ const keys = this.map.keys();
+ /* eslint-disable no-restricted-syntax */
+ for (const key of keys) {
+ if (_.startsWith(key, prefix)) {
+ this.map.delete(key);
+ }
+ }
+ /* eslint-enable no-restricted-syntax */
+ }
+
+ get(key) {
+ return this.map.get(key);
+ }
+
+ set(key, value) {
+ this.map.set(key, value);
+ }
+}
+
+const sessionStore = new SessionStore();
+
+function registerContextItems(appContext) {
+ appContext.sessionStore = sessionStore;
+ appContext.uiEventBus = uiEventBus;
+}
+
+export { sessionStore, uiEventBus, registerContextItems };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/Showdown.js b/addons/addon-base-ui/packages/base-ui/src/models/Showdown.js
new file mode 100644
index 0000000000..146cbe87e4
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/Showdown.js
@@ -0,0 +1,67 @@
+/*
+ * 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 showdown from 'showdown';
+
+const classMap = {
+ h1: 'ui large header clearfix',
+ h2: 'ui medium header clearfix',
+ ul: 'ui list undo-line-height',
+ li: 'ui item undo-line-height',
+ p: 'ui undo-line-height clearfix',
+ // img: 'ui fluid image'
+ img: 'ui left floated image clearfix mb2 mr2',
+};
+
+const bindings = Object.keys(classMap).map(key => ({
+ type: 'output',
+ regex: new RegExp(`<${key}(.*)>`, 'g'),
+ replace: `<${key} class="${classMap[key]}" $1>`,
+}));
+
+// A wrapper around the showdown.js library https://github.com/showdownjs/showdown
+class Showdown {
+ constructor(appContext) {
+ this.appContext = appContext;
+ // see https://github.com/showdownjs/showdown/wiki/Add-default-classes-for-each-HTML-element
+ this.converter = new showdown.Converter({
+ extensions: [...bindings],
+ });
+ // this.converter.setFlavor('github');
+ this.converter.setFlavor('vanilla');
+
+ // options are available here https://github.com/showdownjs/showdown/wiki/Showdown-options
+ this.converter.setOption('parseImgDimensions', true);
+ }
+
+ convert(markdown, assets = {}) {
+ // example of markdown http://demo.showdownjs.com/
+ // we will append all the assets as image references (this is not efficient at all), when we
+ // have time we can the correct image src mapping in an extension
+ let extended = `${markdown}\n\n`;
+ _.forEach(assets, (url, name) => {
+ extended = `${extended}[${name}]: ${url}\n`;
+ });
+
+ return this.converter.makeHtml(extended);
+ }
+}
+
+function registerContextItems(appContext) {
+ appContext.showdown = new Showdown(appContext);
+}
+
+export { Showdown, registerContextItems };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/Stores.js b/addons/addon-base-ui/packages/base-ui/src/models/Stores.js
new file mode 100644
index 0000000000..4e68712941
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/Stores.js
@@ -0,0 +1,90 @@
+/*
+ * 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, computed } from 'mobx';
+
+import { isStoreLoading, isStoreReady, isStoreError } from './BaseStore';
+import { swallowError } from '../helpers/utils';
+
+// A way to load multiple stores and get the errors, etc.
+class Stores {
+ constructor(stores = []) {
+ const result = [];
+ _.forEach(stores, store => {
+ if (_.isEmpty(store) || _.isNil(store)) return;
+ result.push(store);
+ });
+
+ this.stores = result;
+ }
+
+ // only if they are not loaded already, you can force loading if you want
+ async load({ forceLoad = false } = {}) {
+ _.forEach(this.stores, store => {
+ if (!forceLoad && isStoreReady(store)) return;
+ swallowError(store.load());
+ });
+ }
+
+ get ready() {
+ let answer = true;
+ _.forEach(this.stores, store => {
+ answer = answer && isStoreReady(store);
+ });
+ return answer;
+ }
+
+ get loading() {
+ let answer = false;
+ _.forEach(this.stores, store => {
+ if (isStoreLoading(store)) {
+ answer = true;
+ return false; // to stop the loop
+ }
+ return undefined;
+ });
+
+ return answer;
+ }
+
+ get hasError() {
+ return !!this.error;
+ }
+
+ get error() {
+ let error;
+ _.forEach(this.stores, store => {
+ if (isStoreError(store)) {
+ error = store.error;
+ return false; // to stop the loop
+ }
+ return undefined;
+ });
+
+ return error;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(Stores, {
+ stores: observable,
+ ready: computed,
+ loading: computed,
+ hasError: computed,
+ error: computed,
+});
+
+export default Stores;
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/api-keys/ApiKey.js b/addons/addon-base-ui/packages/base-ui/src/models/api-keys/ApiKey.js
new file mode 100644
index 0000000000..ccccde43f5
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/api-keys/ApiKey.js
@@ -0,0 +1,44 @@
+/*
+ * 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 _ from 'lodash';
+
+const ApiKey = types
+ .model('ApiKey', {
+ id: types.identifier,
+ ns: '',
+ username: '',
+ updatedAt: '',
+ status: '',
+ createdAt: '',
+ expiryTime: 0,
+ key: types.optional(types.string, ''),
+ })
+ .views(self => ({
+ get effectiveStatus() {
+ if (self.status !== 'active') {
+ // if status it not active then the effective status is same as status (such as "revoked")
+ return self.status;
+ }
+ // if status is active then make sure it is not expired
+ if (self.expiryTime > 0 && _.now() > self.expiryTime) {
+ return 'expired';
+ }
+ return self.status;
+ },
+ }));
+
+export default ApiKey;
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/api-keys/ApiKeysStore.js b/addons/addon-base-ui/packages/base-ui/src/models/api-keys/ApiKeysStore.js
new file mode 100644
index 0000000000..c82d12ec20
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/api-keys/ApiKeysStore.js
@@ -0,0 +1,94 @@
+/*
+ * 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 { getEnv, types } from 'mobx-state-tree';
+
+import { getApiKeys, createNewApiKey, revokeApiKey } from '../../helpers/api';
+import { BaseStore } from '../BaseStore';
+import ApiKey from './ApiKey';
+import UserIdentifier from '../users/UserIdentifier';
+
+const ApiKeysStore = BaseStore.named('ApiKeysStore')
+ .props({
+ userIdentifierStr: types.identifier,
+ userIdentifier: UserIdentifier,
+ apiKeys: types.optional(types.map(ApiKey), {}),
+ })
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+ return {
+ async doLoad() {
+ const username = self.userIdentifier.username;
+ const ns = self.userIdentifier.ns;
+ // do not pass username or ns params when loading api keys for current user
+ const apiKeys = await getApiKeys(!self.isStoreForCurrentUser() && { username, ns });
+ self.runInAction(() => {
+ const map = {};
+ apiKeys.forEach(apiKey => {
+ const apiKeyModel = ApiKey.create(apiKey);
+ map[apiKeyModel.id] = apiKeyModel;
+ });
+ self.apiKeys.replace(map);
+ });
+ },
+ async createNewApiKey() {
+ const username = self.userIdentifier.username;
+ const ns = self.userIdentifier.ns;
+ const apiKey = await createNewApiKey(!self.isStoreForCurrentUser() && { username, ns });
+ self.runInAction(() => {
+ // The put call below will automatically use the id from ApiKey
+ // (as it is marked "types.identifier") and add that as a key in the map and
+ // store the object as value against it
+ self.apiKeys.put(ApiKey.create(apiKey));
+ });
+ },
+ async revokeApiKey(apiKeyId) {
+ const username = self.userIdentifier.username;
+ const ns = self.userIdentifier.ns;
+ const apiKey = await revokeApiKey(apiKeyId, !self.isStoreForCurrentUser() && { username, ns });
+ self.runInAction(() => {
+ self.apiKeys.put(ApiKey.create(apiKey));
+ });
+ },
+ cleanup: () => {
+ self.user = undefined;
+ superCleanup();
+ },
+ };
+ })
+ .views(self => ({
+ get empty() {
+ return self.apiKeys.size === 0;
+ },
+ get list() {
+ const result = [];
+ // converting map self.apiKeys to result array
+ self.apiKeys.forEach(apiKey => result.push(apiKey));
+ return result;
+ },
+ isStoreForCurrentUser: () => {
+ const username = self.userIdentifier.username;
+ const ns = self.userIdentifier.ns;
+
+ const userStore = getEnv(self).userStore;
+ const currentUser = userStore.user;
+ return currentUser.username === username && currentUser.ns === ns;
+ },
+ }));
+
+// Note: Do NOT register this in the global context, if you want to gain access to an instance
+// use UserStore.apiKeysStore or UsersStore.getApiKeysStore(userIdentifier)
+export default ApiKeysStore;
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/api-keys/UserApiKeysStore.js b/addons/addon-base-ui/packages/base-ui/src/models/api-keys/UserApiKeysStore.js
new file mode 100644
index 0000000000..b91c459422
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/api-keys/UserApiKeysStore.js
@@ -0,0 +1,80 @@
+/*
+ * 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 { getEnv, types } from 'mobx-state-tree';
+
+import { BaseStore, isStoreReady } from '../BaseStore';
+import ApiKeysStore from './ApiKeysStore';
+
+const UserApiKeysStore = BaseStore.named('UserApiKeysStore')
+ .props({
+ // key = userIdentifierStr and value = ApiKeysStore for that user
+ userApiKeysStores: types.optional(types.map(ApiKeysStore), {}),
+ })
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+ return {
+ async doLoad() {
+ const userStore = getEnv(self).userStore;
+ if (!isStoreReady(userStore)) {
+ // Load current user information, if not loaded already
+ await userStore.load();
+ }
+
+ const currentUser = userStore.user;
+ const currentUserApiKeyStore = ApiKeysStore.create({ userIdentifierStr: currentUser.id });
+ if (!isStoreReady(currentUserApiKeyStore)) {
+ // Load API keys for the current user
+ await currentUserApiKeyStore.load();
+ }
+
+ self.runInAction(() => {
+ // The put call below will automatically use the id from currentUserApiKeyStore
+ // (as it is marked "types.identifier") and add that as a key in the map and
+ // store the object as value against it
+ self.userApiKeysStores.put(currentUserApiKeyStore);
+ });
+ },
+ getApiKeysStore: (userIdentifierStr, userIdentifier) => {
+ let entry = self.userApiKeysStores.get(userIdentifierStr);
+ if (!entry) {
+ self.userApiKeysStores.put(ApiKeysStore.create({ userIdentifierStr, userIdentifier }));
+ entry = self.userApiKeysStores.get(userIdentifierStr);
+ }
+ return entry;
+ },
+ getCurrentUserApiKeysStore: () => {
+ const userStore = getEnv(self).userStore;
+ const currentUser = userStore.user;
+ return self.getApiKeysStore(currentUser.id, currentUser.identifier);
+ },
+ cleanup: () => {
+ self.user = undefined;
+ superCleanup();
+ },
+ };
+ })
+ .views(self => ({
+ get empty() {
+ return self.userApiKeysStores.size === 0;
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.userApiKeysStore = UserApiKeysStore.create({}, appContext);
+}
+
+export { UserApiKeysStore, registerContextItems };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/authentication/Authentication.js b/addons/addon-base-ui/packages/base-ui/src/models/authentication/Authentication.js
new file mode 100644
index 0000000000..2f8beb4ade
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/authentication/Authentication.js
@@ -0,0 +1,172 @@
+/*
+ * 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 { getEnv, types } from 'mobx-state-tree';
+
+import jwtDecode from 'jwt-decode';
+import { storage, getFragmentParam, removeFragmentParams } from '../../helpers/utils';
+import { setIdToken } from '../../helpers/api';
+
+import localStorageKeys from '../constants/local-storage-keys';
+import { boom } from '../../helpers/errors';
+
+function removeTokensFromUrl() {
+ const newUrl = removeFragmentParams(document.location, ['id_token', 'access_token', 'token_type', 'expires_in']);
+ window.history.replaceState({}, document.title, newUrl);
+}
+
+// ==================================================================
+// Login model
+// ==================================================================
+const Authentication = types
+ .model('Authentication', {
+ processing: false,
+ selectedAuthenticationProviderId: '',
+ })
+ .actions(self => ({
+ runInAction(fn) {
+ return fn();
+ },
+
+ // this method is called by the Cleaner
+ cleanup() {
+ if (self.selectedAuthenticationProvider) {
+ // give selected authentication provider a chance to do its own cleanup
+ self.selectedAuthenticationProvider.cleanup();
+ }
+ self.clearTokens();
+ },
+
+ clearTokens() {
+ _.forEach(localStorageKeys, keyValue => storage.removeItem(keyValue));
+ },
+
+ setSelectedAuthenticationProviderId(authenticationProviderId) {
+ self.selectedAuthenticationProviderId = authenticationProviderId;
+ },
+
+ async getIdToken() {
+ // The id token would be in URL in case of SAML redirection.
+ // The name of the token param is "id_token" in that case (instead of "appIdToken"), if the token is
+ // issued by Cognito.
+ // Also the id_token is returned via URL fragment i.e, with # instead of query param something like
+ // https://web.site.url/#id_token=blabla instead of
+ // https://web.site.url?idToken=blabla
+ // TODO: Make the retrieval of id token from query string param or fragment param (or any other mechanism)
+ // dynamic based on the authentication provider. Without that, the following code will only work for
+ // any auth providers that set id token either in local storage as "appIdToken" or deliver to us
+ // via URL fragment parameter as "id_token".
+ // This code will NOT work for auth providers issuing id token and delivering via any other mechanism.
+ const idTokenFromUrl = getFragmentParam(document.location, 'id_token');
+ if (idTokenFromUrl) removeTokensFromUrl(); // we remove the idToken from the url for a good security measure
+
+ const idTokenFromLocal = storage.getItem(localStorageKeys.appIdToken);
+
+ const idToken = idTokenFromUrl || idTokenFromLocal;
+ return idToken;
+ },
+
+ async getIdTokenInfo() {
+ const idToken = await self.getIdToken();
+
+ let tokenStatus = 'notFound';
+ let decodedIdToken;
+ if (idToken) {
+ try {
+ decodedIdToken = jwtDecode(idToken);
+ // Check if the token is expired
+ // decodedIdToken.exp is epoch time in SECONDS
+ // ( - See "exp" claim JWT RFC - https://tools.ietf.org/html/rfc7519#section-4.1.4 for details
+ // - the claim is in "NumericDate" format.
+ // - NumericDate is Epoch in Seconds - https://ldapwiki.com/wiki/NumericDate )
+ //
+ // Date.now() returns epoch time in MILLISECONDS
+ const expiresAt = _.get(decodedIdToken, 'exp', 0) * 1000;
+ if (Date.now() >= expiresAt) {
+ tokenStatus = 'expired';
+ } else {
+ tokenStatus = 'notExpired';
+ }
+ } catch (e) {
+ // the token may not be a well-formed JWT toekn in case of any error
+ // decoding it
+ tokenStatus = 'corrupted';
+ }
+ }
+ return {
+ idToken,
+ decodedIdToken,
+ status: tokenStatus,
+ };
+ },
+
+ async saveIdToken(idToken) {
+ storage.setItem(localStorageKeys.appIdToken, idToken);
+ const decodedIdToken = idToken && jwtDecode(idToken);
+ setIdToken(idToken, decodedIdToken);
+ },
+
+ async login({ username, password }) {
+ if (self.shouldCollectUserNamePassword) {
+ const result = await self.selectedAuthenticationProvider.login({
+ username,
+ password,
+ authenticationProviderId: self.selectedAuthenticationProviderId,
+ });
+ const { idToken } = result || {};
+ if (_.isEmpty(idToken)) {
+ throw boom.incorrectImplementation(
+ `There is a problem with the implementation of the server side code. The id token is not returned.`,
+ );
+ }
+
+ await self.saveIdToken(idToken);
+
+ const appRunner = getEnv(self).appRunner;
+ await appRunner.run();
+ } else {
+ // If we do no need to collect credentials from the user then just call login method of the selected authentication provider without any arguments
+ // The selected auth provider will then take care of rest of the login flow (such as redirecting to other identity provider etc)
+ await self.selectedAuthenticationProvider.login();
+ }
+ },
+ async logout({ autoLogout = false } = {}) {
+ self.cleanup();
+ return self.selectedAuthenticationProvider.logout({ autoLogout });
+ },
+ }))
+ .views(self => ({
+ get isCognitoUserPool() {
+ return self.selectedAuthenticationProvider.type === 'cognito_user_pool';
+ },
+ get selectedAuthenticationProvider() {
+ const authenticationProviderPublicConfigsStore = getEnv(self).authenticationProviderPublicConfigsStore;
+ return authenticationProviderPublicConfigsStore.toAuthenticationProviderFromId(
+ self.selectedAuthenticationProviderId,
+ );
+ },
+
+ get shouldCollectUserNamePassword() {
+ const selectedAuthenticationProvider = self.selectedAuthenticationProvider;
+ return selectedAuthenticationProvider && selectedAuthenticationProvider.credentialHandlingType === 'submit';
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.authentication = Authentication.create({}, appContext);
+}
+
+export { Authentication, registerContextItems };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderConfigEditor.js b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderConfigEditor.js
new file mode 100644
index 0000000000..f76ac32a05
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderConfigEditor.js
@@ -0,0 +1,32 @@
+/*
+ * 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 ConfigurationEditor from '../configuration/ConfigurationEditor';
+
+const AuthenticationProviderConfigEditor = types
+ .model('AuthenticationProviderPublicConfig', {
+ id: types.identifier,
+ configEditor: types.optional(ConfigurationEditor, {}),
+ })
+ .actions(self => ({
+ setConfigEditor(configEditor) {
+ self.configEditor = configEditor;
+ },
+ }))
+ .views(_self => ({}));
+
+export default AuthenticationProviderConfigEditor;
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderConfigsStore.js b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderConfigsStore.js
new file mode 100644
index 0000000000..d7b1e47042
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderConfigsStore.js
@@ -0,0 +1,218 @@
+/*
+ * 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 _ from 'lodash';
+
+import { getAuthenticationProviderConfigs, updateAuthenticationProviderConfig } from '../../helpers/api';
+import { BaseStore } from '../BaseStore';
+import { flattenObject, mapToArray, unFlattenObject } from '../../helpers/utils';
+import ConfigurationEditor from '../configuration/ConfigurationEditor';
+import AuthenticationProviderConfigEditor from './AuthenticationProviderConfigEditor';
+
+const AuthenticationProviderConfigsStore = BaseStore.named('AuthenticationProviderConfigsStore')
+ .props({
+ // authenticationProviderConfigs: A map of authentication provider configurations. Key = id, Value = authn provider config
+ // Each config in the array has the following shape
+ /*
+ {
+ id: STRING // id of the authentication provider
+ title: STRING // title of the authentication provider
+ ..... // The rest of the fields which differ depending on the type of the authentication provider
+ type: { // An object containing information about the authentication provider type
+ type: STRING // authentication provider type
+ title: STRING // title of the authentication provider type
+ description: STRING // description about the authentication provider type
+ config: { // An object authentication provider type configuration
+ credentialHandlingType: STRING // credentialHandlingType indicating credential handling for the authentication provider
+ // Possible values:
+ // 'submit' -- The credentials should be submitted to the URL provided by the authentication provider
+ // 'redirect' -- The credentials should be NOT be collected and the user should be redirected directly to the
+ // URL provided by the authentication provider. For example, in case of SAML auth, the username/password
+ // should not be collected by the service provider but the user should be redirected to the identity provider
+
+ inputSchema: OBJECT // An object containing JSON schema that describes all properties of the authentication provider configuration that must be provided as
+ // input when creating this authentication provider.
+ // This schema will defer based on authentication provider type.
+ inputManifestForCreate: OBJECT // An object similar to inputSchema containing a manifest that describes all properties of the authentication provider configuration that must be provided as
+ // input when creating this authentication provider. In addition, the object also has information that can be used by the UI to display inputs
+ // forms such as which inputs to ask from user in which section of the wizard, which sections to show based on which conditions etc.
+ // This manifest will defer based on authentication provider type.
+ inputManifestForUpdate: OBJECT // Similar to inputManifestForCreate that describes inputs to be accepted from user when updating an existing authentication provider
+ }
+ }
+ }
+ */
+ authenticationProviderConfigs: types.optional(types.map(types.frozen()), {}),
+
+ /*
+ Key = authenticationProviderConfigId, Value = AuthenticationProviderConfigEditor
+ */
+ authenticationProviderConfigEditors: types.optional(types.map(AuthenticationProviderConfigEditor), {}),
+ })
+ .actions(self => ({
+ async doLoad() {
+ const authenticationProviderConfigs = await getAuthenticationProviderConfigs();
+ self.runInAction(() => {
+ const map = {};
+ authenticationProviderConfigs.forEach(authenticationProviderConfig => {
+ map[authenticationProviderConfig.id] = authenticationProviderConfig;
+ });
+ self.authenticationProviderConfigs.replace(map);
+ });
+ },
+
+ getUpdateAuthenticationProviderConfigEditor(authenticationProviderConfigId) {
+ let authenticationProviderConfigEditor = self.authenticationProviderConfigEditors.get(
+ authenticationProviderConfigId,
+ );
+ if (!authenticationProviderConfigEditor) {
+ authenticationProviderConfigEditor = AuthenticationProviderConfigEditor.create({
+ id: authenticationProviderConfigId,
+ });
+ const authenticationProviderConfig = self.getAuthenticationProviderConfig(authenticationProviderConfigId);
+ authenticationProviderConfigEditor.setConfigEditor(self.getConfigEditorForUpdate(authenticationProviderConfig));
+
+ self.authenticationProviderConfigEditors.put(authenticationProviderConfigEditor);
+ }
+ return authenticationProviderConfigEditor;
+ },
+
+ getConfigEditorForUpdate(authenticationProviderConfig) {
+ const inputManifestForUpdate = authenticationProviderConfig.config.type.config.inputManifestForUpdate;
+ if (inputManifestForUpdate) {
+ const inputManifest = _.cloneDeep(inputManifestForUpdate);
+ // "id" is read-only and should not be part of the inputManifestForUpdate when updating an existing provider so remove it
+ const filteredSections = _.map(inputManifest.sections, section => {
+ const filteredChildren = _.filter(section.children, child => child.name !== 'id');
+ section.children = filteredChildren;
+ return section;
+ });
+ inputManifest.sections = filteredSections;
+
+ const configuration = toConfiguration(authenticationProviderConfig.config);
+ return ConfigurationEditor.create({
+ currentSectionIndex: 0,
+ review: false,
+ inputManifest,
+ configuration,
+ mode: 'edit',
+ });
+ }
+ return undefined;
+ },
+
+ async updateAuthenticationProvider(authenticationProviderConfig) {
+ const updated = await updateAuthenticationProviderConfig(authenticationProviderConfig);
+ self.runInAction(() => {
+ self.authenticationProviderConfigs.set(updated.id, updated);
+ const authenticationProviderConfigEditor = self.authenticationProviderConfigEditors.get(updated.id);
+ authenticationProviderConfigEditor.setConfigEditor(self.getConfigEditorForUpdate(updated));
+ });
+ },
+
+ getCreateAuthenticationProviderConfigEditor(_authenticationProviderTypeConfig) {},
+ }))
+ .views(self => ({
+ get empty() {
+ return self.authenticationProviderConfigs.size === 0;
+ },
+ get list() {
+ return mapToArray(self.authenticationProviderConfigs);
+ },
+ getAuthenticationProviderConfig(authenticationProviderConfigId) {
+ return self.authenticationProviderConfigs.get(authenticationProviderConfigId);
+ },
+
+ /**
+ * Method that finds first authentication provider that has an idp with the given idp name
+ * @param idpName Name of the identity provider
+ * @returns {*}
+ */
+ getAuthenticationProviderConfigByIdpName(idpName) {
+ const providerConfig = _.find(self.list, authNProvider => {
+ const idps = _.get(authNProvider, 'config.federatedIdentityProviders');
+ const foundIdp = _.find(idps, { name: idpName });
+ // return true if idp is found under this authentication provider
+ return !!foundIdp;
+ });
+ return providerConfig;
+ },
+ }));
+/**
+ * Translates given authenticationProviderConfig into ConfigurationEditor compatible flat "configuration" object with key/value pairs.
+ * The authenticationProviderConfig may be an object graph but the returned "configuration" will be flat object with key/value pairs.
+ * @param authenticationProviderConfig
+ * @return configuration
+ */
+function toConfiguration(authenticationProviderConfig) {
+ // Authentication provider "type" information is not part of inputs and can be skipped from the configuration
+ const flatObj = flattenObject(authenticationProviderConfig, (_result, _value, key) => key !== 'type');
+
+ // MobX form tries to handle nested object notations using dots and and array notations using
+ // [] and expects nested field structure
+ // Here, we want the keys like 'a.b[0].c[1]' etc to be treated as opaque keys in MobX
+ // So replace . and [] to make sure mobx does not treat them as nested keys
+ const toOpaqueKey = key => {
+ let opaqueKey = _.replace(key, /\./g, '/');
+ opaqueKey = _.replace(opaqueKey, /\[/g, '|-');
+ opaqueKey = _.replace(opaqueKey, /]/g, '-|');
+ return opaqueKey;
+ };
+ return _.transform(
+ flatObj,
+ (result, value, key) => {
+ result[toOpaqueKey(key)] = value;
+ },
+ {},
+ );
+}
+
+/**
+ * Translates given configuration object containing key/value pairs into authenticationProviderConfig.
+ * This function is inverse of toConfiguration function above.
+ * @param configuration
+ * @return authenticationProviderConfig
+ */
+function fromConfiguration(configuration) {
+ // MobX form tries to handle nested object notations using dots and and array notations using
+ // [] and expects nested field structure
+ // Here, the configuration may have been translated to use opaque keys with dots replaced by / and
+ // [ replaced by |- and ] replaced with -|
+ // Convert those keys back to use dot and [] notations
+ const fromOpaqueKey = key => {
+ let opaqueKey = _.replace(key, /\//g, '.');
+ opaqueKey = _.replace(opaqueKey, /(\|-)/g, '[');
+ opaqueKey = _.replace(opaqueKey, /(-\|)/g, ']');
+ return opaqueKey;
+ };
+
+ const flatObj = _.transform(
+ configuration,
+ (result, value, key) => {
+ result[fromOpaqueKey(key)] = value;
+ },
+ {},
+ );
+
+ // Authentication provider "type" information is not part of inputs and can be skipped from the configuration
+ return unFlattenObject(flatObj, (_result, _value, key) => key !== 'type');
+}
+
+function registerContextItems(appContext) {
+ appContext.authenticationProviderConfigsStore = AuthenticationProviderConfigsStore.create({}, appContext);
+}
+
+export { AuthenticationProviderConfigsStore, registerContextItems, fromConfiguration };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfig.js b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfig.js
new file mode 100644
index 0000000000..678d509aad
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfig.js
@@ -0,0 +1,156 @@
+/*
+ * 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 { getEnv, types } from 'mobx-state-tree';
+import { authenticate, config } from '../../helpers/api';
+import { isAbsoluteUrl, getQueryParam, removeQueryParams, addQueryParams } from '../../helpers/utils';
+import { boom } from '../../helpers/errors';
+import { websiteUrl } from '../../helpers/settings';
+
+function toAbsoluteUrl(uri) {
+ return isAbsoluteUrl(uri) ? uri : `${config.apiPath}/${uri}`;
+}
+const AUTHN_EXTENSION_POINT = 'authentication';
+
+// TODO: Remove this temp adjustment method. See comments in "absoluteSignInUrl" getter below for more details.
+function adjustRedirectUri(uri, redirectType = 'login') {
+ // Adjust the name of the query param to update and determine whether to preserve the path
+ // based on whether the user is logging in or out
+ let redirectParamName = 'redirect_uri';
+ let preservePath = true;
+ if (redirectType === 'logout') {
+ redirectParamName = 'logout_uri';
+ preservePath = false;
+ }
+
+ // if the current uri contains redirect param and if it is not the same as websiteUrl then adjust it
+ // This is required during local development. For other envs, redirectUri and websiteUrl will be same.
+ const initialRedirectUri = getQueryParam(uri, [redirectParamName]);
+
+ let adjustedUri = uri;
+ if (initialRedirectUri !== websiteUrl) {
+ adjustedUri = removeQueryParams(uri, [redirectParamName]);
+ adjustedUri = addQueryParams(adjustedUri, {
+ [redirectParamName]: preservePath ? window.location : window.location.origin,
+ });
+ }
+
+ return adjustedUri;
+}
+
+const AuthenticationProviderPublicConfig = types
+ .model('AuthenticationProviderPublicConfig', {
+ id: '',
+ title: types.identifier,
+ type: '',
+ credentialHandlingType: '',
+ signInUri: '',
+ signOutUri: '',
+ enableNativeUserPoolUsers: types.maybeNull(types.boolean),
+ })
+ .actions(self => ({
+ cleanup() {
+ // No-op for now
+ },
+
+ login: async ({ username, password } = {}) => {
+ const pluginRegistry = getEnv(self).pluginRegistry;
+
+ const handleException = err => {
+ const code = _.get(err, 'code');
+ const isBoom = _.get(err, 'isBoom');
+ if (code === 'badRequest') throw boom.badRequest(err, err.message);
+ if (isBoom) throw err;
+ throw boom.apiError(err, 'Something went wrong while trying to contact the server.');
+ };
+
+ try {
+ // Notify each authentication plugins of explicit login attempt
+ await pluginRegistry.runPlugins(AUTHN_EXTENSION_POINT, 'loginInitiated');
+
+ if (self.credentialHandlingType === 'submit') {
+ // if the selectedAuthenticationProvider requires credentials to be submitted
+ // then submit the username/password to the specified URL
+ const authenticationProviderId = self.id;
+
+ const loginResult = await authenticate(self.absoluteSignInUrl, username, password, authenticationProviderId);
+
+ // If code reached here means the login was successful.
+ // (The above line would throw exception in case of failed login - in case of incorrect credentials or any other error)
+ // Notify each authentication plugins after explicit login.
+ await pluginRegistry.runPlugins(AUTHN_EXTENSION_POINT, 'loginDetected', { explicitLogin: true });
+
+ return loginResult;
+ }
+ if (self.credentialHandlingType === 'redirect') {
+ // if the selectedAuthenticationProvider requires us to redirect to identity provider
+ // instead of collecting credentials from user (for example, in case of SAML)
+ // just redirect to the specified url.
+ // The authentication plugins will be notified of 'loginDetected' in this case after the login process is
+ // complete by the "initialization-plugin"
+ window.location = self.absoluteSignInUrl;
+ }
+ } catch (err) {
+ handleException(err);
+ }
+ return undefined;
+ },
+
+ logout: async ({ autoLogout = false } = {}) => {
+ const pluginRegistry = getEnv(self).pluginRegistry;
+ // Notify each authentication plugins of explicit logout attempt.
+ // Explicit logout may be explicitly initiated
+ // - by user - "autoLogout: false" - (e.g., clicking on logout) OR
+ // - by application automatically - "autoLogout: true" - (e.g., app code initiating logout due to certain period
+ // of user inactivity
+ await pluginRegistry.runPlugins(AUTHN_EXTENSION_POINT, 'logoutInitiated', { autoLogout });
+
+ if (self.signOutUri) {
+ // if the selectedAuthenticationProvider requires us to redirect to some logout URL
+ // (such as SAML logout url in case of identity federation) just redirect to the specified url.
+ // The authentication plugins will be notified of 'logoutDetected' in this case after the logout process is
+ // complete by the "initialization-plugin"
+ window.location = self.absoluteSignOutUrl;
+ } else {
+ const cleaner = getEnv(self).cleaner;
+ await cleaner.cleanup();
+ window.history.pushState('', '', '/');
+
+ // Notify each authentication plugins after explicit logout.
+ await pluginRegistry.runPlugins(AUTHN_EXTENSION_POINT, 'logoutDetected', {
+ explicitLogout: true,
+ autoLogout,
+ });
+ }
+ },
+ }))
+ .views(self => ({
+ get absoluteSignInUrl() {
+ // The "signInUri" below contains redirectUrl that comes from server and points back to the actual websiteUrl
+ // (even on local machines during local development)
+ // TODO: Temp code: Adjust redirectUrl for local development.
+ // This will go away once we switch to the idea of "provider registry". Currently, the provider configs are retrieved
+ // from a central "AuthenticationProviderConfigService" on the server side and the providers do not get a chance to adjust "signInUri"
+ // (or any other config variables) before returning them during local development, once we move to "provider registry" the registry will
+ // pick appropriate auth provider impl and give it a chance to adjust variables or create derived variables
+ return adjustRedirectUri(toAbsoluteUrl(self.signInUri));
+ },
+ get absoluteSignOutUrl() {
+ return adjustRedirectUri(toAbsoluteUrl(self.signOutUri), 'logout');
+ },
+ }));
+
+export default AuthenticationProviderPublicConfig;
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfigsStore.js b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfigsStore.js
new file mode 100644
index 0000000000..e24246a98c
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderPublicConfigsStore.js
@@ -0,0 +1,69 @@
+/*
+ * 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 { getEnv, types } from 'mobx-state-tree';
+import _ from 'lodash';
+import { BaseStore } from '../BaseStore';
+import { getAuthenticationProviderPublicConfigs } from '../../helpers/api';
+import AuthenticationProviderPublicConfig from './AuthenticationProviderPublicConfig';
+
+const AuthenticationProviderPublicConfigsStore = BaseStore.named('AuthenticationProviderPublicConfigsStore')
+ .props({
+ authenticationProviderPublicConfigs: types.optional(types.array(AuthenticationProviderPublicConfig), []),
+ })
+ .actions(self => ({
+ async doLoad() {
+ const authenticationProviderPublicConfigs = await getAuthenticationProviderPublicConfigs();
+ self.runInAction(() => {
+ self.authenticationProviderPublicConfigs = authenticationProviderPublicConfigs;
+ if (self.authenticationProviderPublicConfigs && !_.isEmpty(self.authenticationProviderPublicConfigs)) {
+ const authentication = getEnv(self).authentication;
+ authentication.setSelectedAuthenticationProviderId(self.authenticationProviderPublicConfigs[0].id);
+ }
+ });
+ },
+ }))
+ .views(self => ({
+ get authenticationProviderOptions() {
+ if (self.authenticationProviderPublicConfigs && !_.isEmpty(self.authenticationProviderPublicConfigs)) {
+ const authProviderOptions = self.authenticationProviderPublicConfigs
+ // Remove user pools as an option if native users are disabled
+ .filter(
+ config =>
+ config.type !== 'cognito_user_pool' ||
+ (config.type === 'cognito_user_pool' && config.enableNativeUserPoolUsers),
+ )
+ .map(config => ({
+ key: config.id,
+ text: config.title,
+ value: config.id,
+ }));
+ return authProviderOptions;
+ }
+ return [];
+ },
+
+ toAuthenticationProviderFromId(authenticationProviderId) {
+ if (self.authenticationProviderPublicConfigs && !_.isEmpty(self.authenticationProviderPublicConfigs)) {
+ return _.find(self.authenticationProviderPublicConfigs, { id: authenticationProviderId });
+ }
+ return undefined;
+ },
+ }));
+function registerContextItems(appContext) {
+ appContext.authenticationProviderPublicConfigsStore = AuthenticationProviderPublicConfigsStore.create({}, appContext);
+}
+
+export { AuthenticationProviderPublicConfigsStore, registerContextItems };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderType.js b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderType.js
new file mode 100644
index 0000000000..ab0509af9e
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderType.js
@@ -0,0 +1,32 @@
+/*
+ * 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 AuthenticationProviderTypeConfig from './AuthenticationProviderTypeConfig';
+
+const AuthenticationProviderType = types
+ .model('AuthenticationProviderType', {
+ type: types.string,
+ title: types.string,
+ description: types.optional(types.string, ''),
+ config: AuthenticationProviderTypeConfig,
+ })
+ .actions(_self => ({
+ cleanup() {
+ // No-op for now
+ },
+ }));
+
+export default AuthenticationProviderType;
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderTypeConfig.js b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderTypeConfig.js
new file mode 100644
index 0000000000..5ff36a7870
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/authentication/AuthenticationProviderTypeConfig.js
@@ -0,0 +1,24 @@
+/*
+ * 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';
+
+const AuthenticationProviderTypeConfig = types.model('AuthenticationProviderTypeConfig', {}).actions(_self => ({
+ cleanup() {
+ // No-op for now
+ },
+}));
+
+export default AuthenticationProviderTypeConfig;
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/component-states/component-session-state.js b/addons/addon-base-ui/packages/base-ui/src/models/component-states/component-session-state.js
new file mode 100644
index 0000000000..e0492dbead
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/component-states/component-session-state.js
@@ -0,0 +1,36 @@
+/*
+ * 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 { sessionStore } from '../SessionStore';
+
+/**
+ * A function that returns component's state (as MST model).
+ * The function creates the component's state MST model if it doesn't exist in the SessionStore.
+ *
+ * @param uiStateModel The MST model containing the component's UI state
+ * @param id The identifier string for the model
+ * @param componentStateCreatorFn The function to create the component's state MST model if it doesn't exist in the SessionStore.
+ * The default "componentStateCreatorFn" just uses the "create()" method of the given model to create initial state.
+ *
+ * @returns {*}
+ */
+function getComponentSessionState(uiStateModel, id, componentStateCreatorFn = model => model.create()) {
+ const stateId = `${uiStateModel.name}-${id}`;
+ const entry = sessionStore.get(stateId) || componentStateCreatorFn(uiStateModel);
+ sessionStore.set(stateId, entry);
+ return entry;
+}
+
+export default getComponentSessionState;
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/configuration/ConfigurationEditor.js b/addons/addon-base-ui/packages/base-ui/src/models/configuration/ConfigurationEditor.js
new file mode 100644
index 0000000000..8ae1827536
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/configuration/ConfigurationEditor.js
@@ -0,0 +1,314 @@
+/*
+ * 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, getSnapshot, applySnapshot } from 'mobx-state-tree';
+
+import { createForm } from '../../helpers/form';
+import { InputManifest, toMobxFormFields, isConditionTrue } from '../forms/InputManifest';
+
+// ==================================================================
+// ConfigurationEditor
+// ==================================================================
+const ConfigurationEditor = types
+ .model('ConfigurationEditor', {
+ currentSectionIndex: 0, // IMPORTANT section index start from 0 not 1
+ review: false,
+ inputManifest: types.maybe(InputManifest),
+ configuration: types.optional(
+ types.map(types.union(types.null, types.undefined, types.integer, types.number, types.boolean, types.string)),
+ {},
+ ),
+ mode: types.optional(types.enumeration('Mode', ['create', 'edit']), 'create'), // mode - either "create" or "edit"
+ })
+
+ .volatile(_self => ({
+ originalConfig: undefined,
+ originalSectionConfig: undefined, // the key/value object for the original section config after next()
+ }))
+
+ .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 => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ // If the value of a form field is an object, then make the value a json string instead
+ const normalizeForm = obj => {
+ return _.transform(
+ obj,
+ (result, value, key) => {
+ result[key] = _.isObject(value) ? JSON.stringify(value) : value;
+ },
+ {},
+ );
+ };
+
+ // Returns a key/value object for configuration keys that are part of the given input manifest section
+ const getSectionConfig = inputManifestSection => {
+ const config = {};
+ const section = inputManifestSection;
+ if (section === undefined) return config;
+ const flattened = self.inputManifest.getSectionFlattened(section) || [];
+ flattened.forEach(item => {
+ const key = item.name;
+ if (self.configuration.has(key)) config[key] = _.cloneDeep(self.configuration.get(key));
+ });
+
+ return config;
+ };
+
+ const resetOriginalSectionConfig = () => {
+ self.originalSectionConfig = getSectionConfig(self.inputManifestSection);
+ };
+
+ // Returns all config keys (if any) that belong to input manifest sections after the given index
+ const configKeysAfter = index => {
+ const sections = _.slice(_.get(self.inputManifest, 'sections', []), Math.max(index + 1, 0));
+ const keys = [];
+ _.forEach(sections, section => {
+ const config = getSectionConfig(section);
+ const configKeys = _.keys(config) || [];
+ if (!_.isEmpty(configKeys)) keys.push(...configKeys);
+ });
+
+ return keys;
+ };
+
+ return {
+ afterCreate() {
+ // We keep the original values of the configuration object so that when we do cancel, we simply restore the original copy
+ self.originalConfig = getSnapshot(self.configuration);
+ resetOriginalSectionConfig();
+ },
+
+ cleanup() {
+ superCleanup();
+ },
+
+ next(form) {
+ const configuration = self.configuration;
+ configuration.merge(normalizeForm(form.values()));
+
+ const changed = !_.isEqual(self.originalSectionConfig, getSectionConfig(self.inputManifestSection));
+ const keysAfter = configKeysAfter(self.currentSectionIndex);
+ const nextSectionIndex = self.nextSectionIndex;
+ const before = self.currentSectionIndex;
+
+ if (nextSectionIndex !== -1) self.currentSectionIndex = nextSectionIndex;
+ const after = self.currentSectionIndex;
+
+ resetOriginalSectionConfig();
+
+ // If the configuration keys changed, then it is time to clear all configuration keys (if any) after the current section
+ // In case of edit mode, do not clear any section (we need to pre-populate all sections with existing values)
+ if (!self.isEditMode && changed) {
+ _.forEach(keysAfter, key => {
+ self.configuration.delete(key);
+ });
+ }
+
+ // If the section index didn't move forward, it means that we don't have any more sections
+ // for input and it is time to show the review content
+ self.review = before === after;
+ },
+
+ previous(_form) {
+ if (self.review) {
+ self.review = false;
+ return;
+ }
+ // const configuration = self.configuration;
+ // configuration.merge(normalizeForm(form.values()));
+ const previousSectionIndex = self.previousSectionIndex;
+ if (previousSectionIndex !== -1) self.currentSectionIndex = previousSectionIndex;
+ resetOriginalSectionConfig();
+ },
+
+ clearConfigs() {
+ self.configuration.clear();
+ },
+
+ clearSectionConfigs() {
+ // We only clear configuration keys that belong to the current section
+ if (self.empty) {
+ self.configuration.clear();
+ return;
+ }
+
+ const section = self.inputManifestSection;
+ if (section === undefined) return;
+ const flattened = self.inputManifest.getSectionFlattened(section) || [];
+ flattened.forEach(item => {
+ self.configuration.delete(item.name);
+ });
+ },
+
+ applyChanges() {
+ self.originalConfig = getSnapshot(self.configuration);
+ },
+
+ cancel() {
+ self.review = false;
+ self.currentSectionIndex = 0;
+ if (self.originalConfig) {
+ applySnapshot(self.configuration, self.originalConfig);
+ }
+
+ resetOriginalSectionConfig();
+ },
+
+ restart() {
+ self.cancel();
+ },
+ };
+ })
+
+ .views(self => ({
+ get isEditMode() {
+ return self.mode === 'edit';
+ },
+
+ get inputManifestSection() {
+ if (self.inputManifest === undefined) return undefined;
+ const sections = self.inputManifest.sections;
+ const index = self.currentSectionIndex;
+ if (index > self.totalSections) return undefined;
+ if (index >= sections.length) return undefined;
+ return sections[index];
+ },
+
+ // A list of objects, where each object represents a configuration name/entry that is not undefined
+ // [ {name: 'xyz', title: '...', value: 'true', etc}, {name: 'abc', title: '...', value: 'something', etc}, ... ]
+ get definedConfigList() {
+ if (self.inputManifest === undefined) return [];
+ const inputEntries = self.inputManifest.flattened;
+ const configMap = self.configuration;
+ const list = [];
+ _.forEach(inputEntries, entry => {
+ let value = configMap.get(entry.name);
+ if (_.isUndefined(value)) value = entry.value;
+ if (!_.isUndefined(value)) list.push({ ...entry, value });
+ });
+
+ return list;
+ },
+
+ // A map of all names in inputManifest with their values from the configuration object if they exist
+ // or from the inputManifest if they exist, otherwise undefined is given as the value for the key
+ // An example of returned object shape: { 'configName': 'demo', 'doYouWantThis': undefined }
+ get merged() {
+ const inputEntries = self.inputManifest.flattened;
+ const map = {};
+ _.forEach(inputEntries, entry => {
+ map[entry.name] = entry.value;
+ });
+
+ /* eslint-disable no-restricted-syntax */
+ for (const [key, value] of self.configuration.entries()) {
+ map[key] = value;
+ }
+ /* eslint-enable no-restricted-syntax */
+
+ return map;
+ },
+
+ get formFields() {
+ const index = self.currentSectionIndex;
+ if (self.totalSections < index) return [];
+ const input = self.inputManifestSection;
+ if (_.isUndefined(input)) return [];
+
+ return toMobxFormFields(input.children, self.merged);
+ },
+
+ get form() {
+ return createForm(self.formFields);
+ },
+
+ get totalSections() {
+ if (self.inputManifest === undefined) return 0;
+ return self.inputManifest.sections.length;
+ },
+
+ get hasNext() {
+ return self.nextSectionIndex !== -1 && !self.review;
+ },
+
+ get hasPrevious() {
+ return self.previousSectionIndex !== -1 || self.review;
+ },
+
+ // Returns the next section index
+ // if the current section is the last section, return -1
+ // walk through the remaining sections and return the index of the first one
+ // that has condition === true, otherwise return -1
+ get nextSectionIndex() {
+ if (self.totalSections < self.currentSectionIndex) return -1;
+ if (self.inputManifest === undefined) return -1;
+ const sections = self.inputManifest.sections;
+ const merged = self.merged;
+ let found = false;
+ let index = self.currentSectionIndex + 1;
+
+ while (!found && index < self.totalSections) {
+ const entry = sections[index];
+ found = isConditionTrue(entry.condition, merged);
+ if (!found) index += 1;
+ }
+
+ return found ? index : -1;
+ },
+
+ // Returns the previous section index
+ // if the current section is 0, return -1
+ // walk through the previous sections and return the index of the first one
+ // that has condition === true, otherwise return -1
+ get previousSectionIndex() {
+ if (self.currentSectionIndex === 0) return -1;
+ const sections = self.inputManifest.sections;
+ const merged = self.merged;
+ let found = false;
+ let index = self.currentSectionIndex - 1;
+
+ while (!found && index >= 0) {
+ const entry = sections[index];
+ found = isConditionTrue(entry.condition, merged);
+ if (!found) index -= 1;
+ }
+
+ return found ? index : -1;
+ },
+
+ get sectionsTitles() {
+ const sections = self.inputManifest.sections;
+ return _.map(sections, index => index.title);
+ },
+
+ get empty() {
+ if (self.inputManifest === undefined) return true;
+ return self.inputManifest.empty;
+ },
+ }));
+
+// Note: Do NOT register ConfigurationEditor in the global context
+
+export default ConfigurationEditor;
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/constants/local-storage-keys.js b/addons/addon-base-ui/packages/base-ui/src/models/constants/local-storage-keys.js
new file mode 100644
index 0000000000..a8ce4780d3
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/constants/local-storage-keys.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+const localStorageKeys = {
+ // Name of the id token for Data Lake APIs in local storage
+ appIdToken: 'appIdToken',
+};
+
+export default localStorageKeys;
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/forms/AddUserForm.js b/addons/addon-base-ui/packages/base-ui/src/models/forms/AddUserForm.js
new file mode 100644
index 0000000000..3080f6a6cb
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/forms/AddUserForm.js
@@ -0,0 +1,67 @@
+/*
+ * 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 dvr from 'mobx-react-form/lib/validators/DVR';
+import Validator from 'validatorjs';
+import MobxReactForm from 'mobx-react-form';
+
+const addUserFormFields = {
+ username: {
+ label: 'User Name',
+ placeholder: 'Type a unique username for the user',
+ explain: `The username must be between 3 and 300 characters long. Once the user is created, you can not change the username and can not delete the user. You will be able to de-activate/activate the user.`,
+ rules: 'required|string|between:3,300',
+ },
+ password: {
+ label: 'Password',
+ placeholder: 'Type default password for the user',
+ explain: `The password must be between 4 and 2048 characters long.`,
+ rules: 'required|string|between:4,2048',
+ },
+ email: {
+ label: 'Email',
+ placeholder: 'Type email address for the user',
+ rules: 'required|email|string',
+ },
+ firstName: {
+ label: 'First Name',
+ placeholder: 'Type first name of the user',
+ rules: 'required|string|between:1,500',
+ },
+ lastName: {
+ label: 'Last Name',
+ placeholder: 'Type last name of the user',
+ rules: 'required|string|between:4,500',
+ },
+ isAdmin: {
+ label: 'Admin',
+ explain: 'Select if the user should be admin user',
+ },
+ status: {
+ label: 'Status',
+ explain: 'Select if the user should be active user',
+ },
+};
+
+function getAddUserFormFields() {
+ return addUserFormFields;
+}
+
+function getAddUserForm() {
+ const plugins = { dvr: dvr(Validator) }; // , vjf: validator };
+ return new MobxReactForm({ fields: addUserFormFields }, { plugins });
+}
+
+export { getAddUserFormFields, getAddUserForm };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/forms/EditAuthenticationProviderForm.js b/addons/addon-base-ui/packages/base-ui/src/models/forms/EditAuthenticationProviderForm.js
new file mode 100644
index 0000000000..733464e8d5
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/forms/EditAuthenticationProviderForm.js
@@ -0,0 +1,47 @@
+/*
+ * 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';
+
+const formFields = {
+ title: {
+ label: 'Authentication Provider Title',
+ extra: {
+ explain: 'This is a required field and the number of characters must be between 3 and 255. ',
+ },
+ placeholder: 'Type a title for the Authentication Provider',
+ rules: 'required|between:3,255',
+ },
+ desc: {
+ label: 'Authentication Provider Description',
+ placeholder: 'Type a description of the Authentication Provider',
+ extra: {
+ explain:
+ 'The Authentication Provider description helps other administrators understand the details about the authentication provider. ' +
+ 'The description can have a maximum of 2048 characters.',
+ },
+ rules: 'max:2048',
+ },
+};
+
+function getEditAuthenticationProviderFormFields() {
+ return formFields;
+}
+
+function getEditAuthenticationProviderForm(fields = formFields) {
+ return createForm(fields);
+}
+
+export { getEditAuthenticationProviderForm, getEditAuthenticationProviderFormFields };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/forms/InputManifest.js b/addons/addon-base-ui/packages/base-ui/src/models/forms/InputManifest.js
new file mode 100644
index 0000000000..5d341fb834
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/forms/InputManifest.js
@@ -0,0 +1,200 @@
+/*
+ * 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, getSnapshot } from 'mobx-state-tree';
+
+// ==================================================================
+// InputManifest
+// ==================================================================
+const InputManifest = types
+ .model('InputManifest', {
+ sections: types.optional(types.array(types.frozen()), []),
+ })
+ .actions(_self => ({}))
+
+ .views(self => ({
+ // An array of all the input entries (excluding non-interactive ones). This is a convenient method that
+ // traverses the whole input manifest tree.
+ // [ { name, title, ... }, { name, title, ...} ]
+ get flattened() {
+ return _.flatten(_.map(self.sections, section => findEntries(section.children)));
+ },
+
+ get names() {
+ return _.map(self.flattened, entry => entry.name);
+ },
+
+ get empty() {
+ return self.flattened.length === 0;
+ },
+
+ getSectionFlattened(section = {}) {
+ return _.flatten(findEntries(section.children));
+ },
+ }));
+
+// ==================================================================
+// Helpers
+// ==================================================================
+
+// Does the entry represent an input that will interact with the user
+function isInteractive(entry) {
+ if (_.isUndefined(entry)) return false;
+ return _.isNil(entry.nonInteractive) || entry.nonInteractive === false;
+}
+
+// Condition is true if it is empty/undefined or if the lodash expression evaluates to the string "true"
+function isConditionTrue(condition, config) {
+ if (_.isEmpty(condition)) return true;
+ return _.template(condition)(config) === 'true';
+}
+
+// Given an inputManifestEntry returns a object that contains all the supported mobx form field props
+// For a list of all mobx form field props see https://foxhound87.github.io/mobx-react-form/docs/fields/defining-flat-fields/unified-properties.html
+function toMobxFormFieldProps(entry, value) {
+ if (!isInteractive(entry)) return {};
+ const map = {};
+ const add = (key, val) => {
+ if (!_.isUndefined(val)) map[key] = val;
+ };
+ const { name, title, placeholder, rules, extra = {}, desc, disabled, options, yesLabel, noLabel } = entry;
+
+ add('name', name);
+ add('value', _.isUndefined(value) ? entry.default : value);
+ add('label', title);
+ add('placeholder', placeholder);
+ add('rules', rules);
+ add('default', _.isUndefined(entry.default) ? value : entry.default);
+ add('extra', { ..._.cloneDeep(extra), explain: desc, options, yesLabel, noLabel });
+ add('disabled', disabled);
+
+ return map;
+}
+
+// Recursive function
+// input = an array of the input manifest section children or input manifest entry children
+// config = all names in inputManifest and their values (if they exist)
+function toMobxFormFields(input = [], config) {
+ const result = [];
+ if (input.length === 0) return result;
+ const queue = input.slice();
+
+ while (queue.length > 0) {
+ const entry = queue.shift();
+ const name = entry.name;
+ if (isInteractive(entry) && isConditionTrue(entry.condition, config)) {
+ const value = config[name];
+ const field = toMobxFormFieldProps(entry, value);
+ result.push(field);
+ const children = entry.children;
+ if (_.isObject(children)) {
+ const fields = toMobxFormFields(children, config); // recursive call
+ if (fields.length > 0) {
+ result.push(...fields);
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+// Given an instance of inputManifest, apply markdown on all 'desc' props and return
+// a new json object (NOT an instance of inputManifest)
+function applyMarkdown({ inputManifest, showdown, assets = {} }) {
+ const copy = _.cloneDeep(getSnapshot(inputManifest));
+
+ function transform(obj) {
+ if (_.isNil(obj)) return obj;
+ if (_.isArray(obj))
+ return _.map(obj, item => {
+ return transform(item);
+ });
+
+ if (!_.isObject(obj)) return obj;
+ const keys = Object.keys(obj);
+
+ keys.forEach(key => {
+ if (key !== 'desc') {
+ obj[key] = transform(obj[key]);
+ return;
+ }
+ const desc = obj[key];
+ if (_.isNil(desc)) return;
+ obj.desc = showdown.convert(desc, assets);
+ });
+
+ return obj;
+ }
+
+ copy.sections = transform(copy.sections);
+
+ return copy;
+}
+
+// Given an array of input entries, visit each one of them by passing the item
+// to the visitFn
+function visit(input = [], visitFn = obj => obj) {
+ const result = [];
+ if (input.length === 0) return result;
+ const queue = input.slice();
+
+ while (queue.length > 0) {
+ const entry = queue.shift();
+ result.push(visitFn(entry));
+ const children = entry.children;
+ if (_.isObject(children)) {
+ const entries = visit(children, visitFn); // recursive call
+ if (entries.length > 0) {
+ entries.forEach(field => {
+ result.push(visitFn(field));
+ });
+ }
+ }
+ }
+ return result;
+}
+
+// ==================================================================
+// Internal Helpers
+// ==================================================================
+
+// Find all names with their entries (such as titles). This is a recursive function.
+// Returns an array, [ { name, title, ... }, { name, title, ...} ]
+function findEntries(input = []) {
+ const result = [];
+ if (input.length === 0) return result;
+ const queue = input.slice();
+
+ while (queue.length > 0) {
+ const entry = queue.shift();
+ if (isInteractive(entry)) {
+ result.push(entry);
+ }
+ const children = entry.children;
+ if (_.isObject(children)) {
+ const entries = findEntries(children); // recursive call
+ if (entries.length > 0) {
+ entries.forEach(field => {
+ result.push(field);
+ });
+ }
+ }
+ }
+ return result;
+}
+
+export { InputManifest, isInteractive, toMobxFormFieldProps, isConditionTrue, toMobxFormFields, applyMarkdown, visit };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/forms/Validate.js b/addons/addon-base-ui/packages/base-ui/src/models/forms/Validate.js
new file mode 100644
index 0000000000..0708b41752
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/forms/Validate.js
@@ -0,0 +1,75 @@
+/*
+ * 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 Validator from 'validatorjs';
+
+/**
+ * Transforms fields object from
+ {
+ fieldName1: {
+ rules: string,
+ },
+ fieldName2: {
+ rules: string,
+ },
+ }
+
+ to
+
+ {
+ fieldName1: rulesString,
+ fieldName2: rulesString,
+ }
+ *
+ */
+function fieldsToValidationRules(fieldsConfig) {
+ return _.transform(
+ fieldsConfig,
+ (rules, config, fieldName) => {
+ if (config.rules) {
+ rules[fieldName] = config.rules;
+ }
+ return rules;
+ },
+ {},
+ );
+}
+
+/**
+ * Validates given input data using the form fields configuration
+ *
+ * @param input The object to validate
+ * @param fieldsConfig The field configuration to use for validation. The config must be in the following format.
+ {
+ fieldName1: {
+ rules: string,
+ },
+ fieldName2: {
+ rules: string,
+ },
+ }
+ * @returns {Promise}
+ */
+async function validate(input, fieldsConfig) {
+ const validationRules = fieldsToValidationRules(fieldsConfig);
+ let validation;
+ if (validationRules) {
+ validation = new Validator(input, validationRules);
+ }
+ return validation;
+}
+
+export default validate;
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/users/User.js b/addons/addon-base-ui/packages/base-ui/src/models/users/User.js
new file mode 100644
index 0000000000..c6a43945e7
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/users/User.js
@@ -0,0 +1,91 @@
+/*
+ * 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 _ from 'lodash';
+
+const User = types
+ .model('User', {
+ firstName: '',
+ lastName: '',
+ isAdmin: types.optional(types.boolean, false),
+ username: '',
+ ns: '',
+ email: '',
+ userType: '',
+ authenticationProviderId: '', // Id of the authentication provider this user is authenticated against (such as internal, cognito auth provider id etc)
+ identityProviderName: '', // Name of the identity provider this user belongs to (such as Identity Provider Id in cognito user pool in case of Federation etc)
+ status: 'active',
+ rev: 0,
+ })
+ .views(self => ({
+ get displayName() {
+ return `${self.firstName} ${self.lastName}`;
+ },
+
+ get longDisplayName() {
+ if (self.unknown) {
+ return `${self.username}??`;
+ }
+ const fullName = `${self.firstName} ${self.lastName}`;
+ if (self.email) {
+ return `${fullName} (${self.email})`;
+ }
+ return fullName;
+ },
+
+ get unknown() {
+ return !self.firstName && !self.lastName;
+ },
+
+ get isRootUser() {
+ return _.toLower(self.userType) === 'root';
+ },
+
+ get isActive() {
+ return _.toLower(self.status) === 'active';
+ },
+
+ get isSystem() {
+ const identifier = self.identifier;
+ return identifier.username === '_system_';
+ },
+
+ isSame({ username, ns }) {
+ return self.username === username && self.ns === ns;
+ },
+
+ get id() {
+ return self.identifierStr;
+ },
+
+ get identifier() {
+ return { username: self.username, ns: self.ns };
+ },
+
+ get identifierStr() {
+ return JSON.stringify(self.identifier);
+ },
+ }));
+
+function getIdentifierObjFromId(identifierStr) {
+ return JSON.parse(identifierStr);
+}
+
+function getIdFromObj({ username, ns }) {
+ return JSON.stringify({ username, ns });
+}
+
+export { User, getIdentifierObjFromId, getIdFromObj };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/users/UserDisplayName.js b/addons/addon-base-ui/packages/base-ui/src/models/users/UserDisplayName.js
new file mode 100644
index 0000000000..45a21ed9b4
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/users/UserDisplayName.js
@@ -0,0 +1,79 @@
+/*
+ * 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';
+
+// A convenient model that returns the display name or long display name given a user identifier
+const UserDisplayName = types.model('UserDisplayName', {}).views(self => ({
+ // identifier: can be an instance of the UserIdentifier, or a string or undefined
+ getDisplayName(identifier) {
+ // TODO deal with _systems_
+ let userStore;
+
+ if (_.isString(identifier)) return identifier;
+ if (_.isUndefined(identifier)) {
+ userStore = getEnv(self).userStore;
+ if (userStore.user) return userStore.displayName;
+ return 'Unknown';
+ }
+
+ const usersStore = getEnv(self).usersStore;
+ const user = usersStore.asUserObject(identifier);
+
+ if (_.isUndefined(user)) return 'unknown';
+ return user.displayName || 'unknown';
+ },
+
+ // identifier: can be an instance of the UserIdentifier, or a string or undefined
+ getLongDisplayName(identifier) {
+ // TODO deal with _systems_
+ let userStore;
+
+ if (_.isString(identifier)) return identifier;
+ if (_.isUndefined(identifier)) {
+ userStore = getEnv(self).userStore;
+ if (userStore.user) return userStore.longDisplayName;
+ return 'Unknown';
+ }
+
+ const usersStore = getEnv(self).usersStore;
+ const user = usersStore.asUserObject(identifier);
+
+ if (_.isUndefined(user)) return 'unknown';
+ return user.longDisplayName || 'unknown';
+ },
+
+ // identifier: can be an instance of the UserIdentifier, or a string or undefined
+ isSystem(identifier) {
+ let userStore;
+
+ if (_.isString(identifier)) return identifier === '_system_';
+ if (_.isUndefined(identifier)) {
+ userStore = getEnv(self).userStore;
+ if (userStore.user) return userStore.user.isSystem;
+ return false;
+ }
+ const username = _.get(identifier, 'username', '');
+
+ return username === '_system_';
+ },
+}));
+
+function registerContextItems(appContext) {
+ appContext.userDisplayName = UserDisplayName.create({}, appContext);
+}
+
+export { UserDisplayName, registerContextItems };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/users/UserIdentifier.js b/addons/addon-base-ui/packages/base-ui/src/models/users/UserIdentifier.js
new file mode 100644
index 0000000000..3d37a27cc3
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/users/UserIdentifier.js
@@ -0,0 +1,42 @@
+/*
+ * 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 { getSnapshot, types } from 'mobx-state-tree';
+
+// A user may be authenticated by different authentication providers due to this there is a
+// chance of collision of usernames across different authentication/identity providers.
+// Due to this, each user is uniquely identified by not just the username but "username" plus "ns" (i.e., namespace).
+// The MST model below represents this user identifier containing username and the namespace.
+const UserIdentifier = types
+ .model('UserIdentifier', {
+ username: '',
+ ns: '',
+ })
+ .views(self => ({
+ isSame({ username, ns }) {
+ return self.username === username && self.ns === ns;
+ },
+ get id() {
+ return self.identifierStr;
+ },
+ get identifier() {
+ return self;
+ },
+ get identifierStr() {
+ return JSON.stringify(getSnapshot(self));
+ },
+ }));
+
+export default UserIdentifier;
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/users/UserStore.js b/addons/addon-base-ui/packages/base-ui/src/models/users/UserStore.js
new file mode 100644
index 0000000000..9ccf822c1f
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/users/UserStore.js
@@ -0,0 +1,55 @@
+/*
+ * 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';
+
+import { getUser } from '../../helpers/api';
+import { BaseStore } from '../BaseStore';
+import { User } from './User';
+
+const UserStore = BaseStore.named('UserStore')
+ .props({
+ user: types.maybe(User),
+ })
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const user = await getUser();
+ self.runInAction(() => {
+ self.user = User.create(user);
+ });
+ },
+ cleanup: () => {
+ self.user = undefined;
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return _.isEmpty(self.user);
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.userStore = UserStore.create({}, appContext);
+}
+
+export { UserStore, registerContextItems };
diff --git a/addons/addon-base-ui/packages/base-ui/src/models/users/UsersStore.js b/addons/addon-base-ui/packages/base-ui/src/models/users/UsersStore.js
new file mode 100644
index 0000000000..879e00348d
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/models/users/UsersStore.js
@@ -0,0 +1,161 @@
+/*
+ * 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 { applySnapshot, getSnapshot, types } from 'mobx-state-tree';
+
+import { addUser, getUsers, updateUser } from '../../helpers/api';
+import { User, getIdFromObj } from './User';
+import { BaseStore } from '../BaseStore';
+
+const UsersStore = BaseStore.named('UsersStore')
+ .props({
+ users: types.optional(types.map(User), {}),
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const users = (await getUsers()) || [];
+ self.runInAction(() => {
+ const map = {};
+ users.forEach(user => {
+ const userId = getIdFromObj(user);
+ map[userId] = user;
+ });
+ self.users.replace(map);
+ });
+ },
+
+ cleanup: () => {
+ self.users.clear();
+ superCleanup();
+ },
+ addUser: async user => {
+ const addedUser = await addUser(user);
+ self.runInAction(() => {
+ // Added newly created user to users map
+ const addedUserModel = User.create(addedUser);
+ self.users.set(addedUserModel.id, addedUserModel);
+ });
+ },
+ updateUser: async user => {
+ const updatedUser = await updateUser(user);
+ const userModel = User.create(updatedUser);
+ const previousUser = self.users.get(userModel.id);
+ applySnapshot(previousUser, updatedUser);
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.users.size === 0;
+ },
+
+ get hasNonRootAdmins() {
+ const nonRootAdmins = _.filter(self.list, user => user.isAdmin && !user.isRootUser);
+ return !_.isEmpty(nonRootAdmins);
+ },
+
+ get hasNonRootUsers() {
+ return !_.isEmpty(self.nonRootUsers);
+ },
+
+ get nonRootUsers() {
+ return _.filter(self.list, user => !user.isRootUser);
+ },
+
+ get list() {
+ const result = [];
+ // converting map self.users to result array
+ self.users.forEach(user => result.push(user));
+ return result;
+ },
+
+ asSelectOptions({ nonClearables = [] } = {}) {
+ const result = [];
+ self.users.forEach(user =>
+ result.push({
+ value: user.id,
+ label: user.longDisplayName,
+ clearableValue: !nonClearables.includes(user.id),
+ }),
+ );
+ return result;
+ },
+
+ asDropDownOptions({ status = 'active' } = {}) {
+ const result = [];
+ self.users.forEach(user => {
+ if (user.status === status) {
+ result.push({
+ key: user.id,
+ value: user.id,
+ text: user.longDisplayName,
+ });
+ }
+ });
+ return result;
+ },
+
+ asUserObject(userIdentifier) {
+ if (userIdentifier) {
+ const user = self.users.get(userIdentifier.id);
+ return user || User.create({ username: userIdentifier.username, ns: userIdentifier.ns }); // this could happen in the employee is no longer active or with the company
+ }
+ return undefined;
+ },
+
+ asUserObjects(userIdentifiers = []) {
+ const result = [];
+ userIdentifiers.forEach(userIdentifier => {
+ if (userIdentifier) {
+ const user = self.users.get(userIdentifier.id);
+ if (user) {
+ result.push(user);
+ } else {
+ result.push(User.create(getSnapshot(userIdentifier)));
+ } // this could happen in the employee is no longer active or with the company
+ }
+ });
+
+ return result;
+ },
+ }));
+
+function toUserIds(userObjects) {
+ return _.map(userObjects, user => user.id);
+}
+
+function toLongNames(userObjects) {
+ return _.map(userObjects, user => user.longDisplayName);
+}
+
+function toLongName(object) {
+ if (object) {
+ return object.longDisplayName;
+ }
+ return 'Unknown';
+}
+
+function registerContextItems(appContext) {
+ appContext.usersStore = UsersStore.create({}, appContext);
+}
+
+export { UsersStore, toUserIds, toLongNames, toLongName, registerContextItems };
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/AutoLogout.js b/addons/addon-base-ui/packages/base-ui/src/parts/AutoLogout.js
new file mode 100644
index 0000000000..1aa14dc442
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/AutoLogout.js
@@ -0,0 +1,127 @@
+/*
+ * 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 { observable, action, decorate, runInAction, computed } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { Button, Modal, Header } from 'semantic-ui-react';
+import IdleTimer from 'react-idle-timer';
+
+import { autoLogoutTimeoutInMinutes } from '../helpers/settings';
+
+// expected props
+// - authentication
+// - app
+class AutoLogout extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.dialogCountDown = undefined;
+ this.intervalId = undefined;
+ });
+ }
+
+ get app() {
+ return this.props.app;
+ }
+
+ get authentication() {
+ return this.props.authentication;
+ }
+
+ get modalOpen() {
+ return this.dialogCountDown >= 0;
+ }
+
+ componentDidMount() {}
+
+ clearInterval() {
+ if (!_.isUndefined(this.intervalId)) {
+ clearInterval(this.intervalId);
+ this.intervalId = undefined;
+ }
+ this.dialogCountDown = undefined;
+ }
+
+ startDialogCountDown = () => {
+ if (!_.isUndefined(this.intervalId)) return;
+ this.dialogCountDown = 60;
+
+ this.intervalId = setInterval(async () => {
+ // eslint-disable-next-line consistent-return
+ runInAction(() => {
+ if (this.dialogCountDown <= 0) {
+ return this.doLogout();
+ }
+ this.dialogCountDown -= 1;
+ });
+ }, 1000);
+ };
+
+ cancelDialogCountDown = () => {
+ this.clearInterval();
+ };
+
+ doLogout = async () => {
+ this.clearInterval();
+ return this.authentication.logout({ autoLogout: true });
+ };
+
+ handleLogout = async event => {
+ event.preventDefault();
+ event.stopPropagation();
+ return this.doLogout();
+ };
+
+ render() {
+ const authenticated = this.app.userAuthenticated;
+ if (!authenticated) return null;
+ return (
+ <>
+
+
+ Are you still there?
+
+ For security purposes, you will be logged out in
+
+ seconds
+
+
+
+
+
+
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(AutoLogout, {
+ app: computed,
+ authentication: computed,
+ modalOpen: computed,
+ intervalId: observable,
+ dialogCountDown: observable,
+ startDialogCountDown: action,
+ doLogout: action,
+ handleLogout: action,
+ cancelDialogCountDown: action,
+ clearInterval: action,
+});
+
+export default inject('authentication', 'app')(observer(AutoLogout));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/Footer.js b/addons/addon-base-ui/packages/base-ui/src/parts/Footer.js
new file mode 100644
index 0000000000..4c2b09cd88
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/Footer.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 React from 'react';
+// from https://github.com/Semantic-Org/Semantic-UI-React/blob/master/docs/src/layouts/FixedMenuLayout.js
+// from https://react.semantic-ui.com/layouts/fixed-menu
+const Footer = () => (
+ <>
+ {/*
*/}
+
+
+
+
+
+
+
Footer Header
+
Extra space for a call to action inside the footer that could help re-engage users.
+
+
+
+
+ >
+);
+
+export default Footer;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/Login.js b/addons/addon-base-ui/packages/base-ui/src/parts/Login.js
new file mode 100644
index 0000000000..3921dd7830
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/Login.js
@@ -0,0 +1,262 @@
+/*
+ * 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 { observable, action, decorate, runInAction } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { Button, Form, Grid, Header, Segment, Label, Input, Select, Image } from 'semantic-ui-react';
+
+import { displayError } from '../helpers/notification';
+import { swallowError } from '../helpers/utils';
+import { branding } from '../helpers/settings';
+
+// From https://github.com/Semantic-Org/Semantic-UI-React/blob/master/docs/app/Layouts/LoginLayout.js
+// expected props
+// - authentication (via injection)
+// - authenticationProviderPublicConfigsStore (via injection)
+// - assets (via injection)
+class Login extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.username = '';
+ this.password = '';
+ this.loading = false;
+
+ this.authenticationProviderError = '';
+ this.usernameError = '';
+ this.passwordError = '';
+ });
+ }
+
+ getStore() {
+ return this.props.authenticationProviderPublicConfigsStore;
+ }
+
+ componentDidMount() {
+ const store = this.getStore();
+ swallowError(store.load());
+ }
+
+ handleChange = name =>
+ action(event => {
+ this[name] = event.target.value;
+ if (name === 'username') this.usernameError = '';
+ if (name === 'password') this.passwordError = '';
+ });
+
+ handleAuthenticationProviderChange = action((_event, { value }) => {
+ this.props.authentication.setSelectedAuthenticationProviderId(value);
+ });
+
+ handleLogin = action(event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ this.authenticationProviderError = '';
+ this.usernameError = '';
+ this.passwordError = '';
+ const username = _.trim(this.username) || '';
+ const password = this.password || '';
+ const selectedAuthenticationProviderId = this.props.authentication.selectedAuthenticationProviderId || '';
+ let error = false;
+
+ if (_.isEmpty(selectedAuthenticationProviderId)) {
+ this.authenticationProviderError = 'please select authentication provider';
+ error = true;
+ }
+
+ const collectUserNamePassword = this.props.authentication.shouldCollectUserNamePassword;
+ // Validate username and password fields only if the selected authentication provider requires
+ // username and password to be submitted.
+ // For example, in case of SAML we do not collect username/password and in that case,
+ // we won't validate username/password. It will be the responsibility of the Identity Provider
+ // Do we need to collect username/password or not is specified by the authentication provider configuration
+ // via "credentialHandlingType" field.
+ if (collectUserNamePassword) {
+ if (_.isEmpty(username)) {
+ this.usernameError = 'username is required';
+ error = true;
+ }
+
+ if (!_.isEmpty(username) && username.length < 3) {
+ this.usernameError = 'username must be at least 3 characters long';
+ error = true;
+ }
+
+ if (_.isEmpty(password)) {
+ this.passwordError = 'password is required';
+ error = true;
+ }
+ if (!_.isEmpty(password) && password.length < 4) {
+ this.passwordError = 'password must be at least 4 characters long';
+ error = true;
+ }
+ }
+
+ if (error) return;
+
+ const authentication = this.props.authentication;
+ this.loading = true;
+
+ Promise.resolve()
+ .then(() =>
+ authentication.login({
+ username,
+ password,
+ }),
+ )
+ .catch(err => displayError(err))
+ .finally(
+ action(() => {
+ this.loading = false;
+ }),
+ );
+ });
+
+ getAuthenticationProviderOptions = () => {
+ const authenticationProviderPublicConfigsStore = this.props.authenticationProviderPublicConfigsStore;
+ return authenticationProviderPublicConfigsStore.authenticationProviderOptions;
+ };
+
+ render() {
+ const error = !!(this.usernameError || this.passwordError || this.authenticationProviderError);
+
+ const authenticationProviderOptions = this.getAuthenticationProviderOptions();
+ const selectedAuthenticationProviderId = this.props.authentication.selectedAuthenticationProviderId;
+
+ const renderAuthenticationProviders = () => {
+ // Display authenticationProviderOptions only if there are more than one to choose from
+ // if there is only one authentication provider available then use that
+ if (authenticationProviderOptions && authenticationProviderOptions.length > 1) {
+ return (
+
+
+ {this.authenticationProviderError && (
+
+ {this.authenticationProviderError}
+
+ )}
+
+ );
+ }
+ return '';
+ };
+
+ const collectUserNamePassword = this.props.authentication.shouldCollectUserNamePassword;
+ const renderBrandingLogo = ;
+ return (
+
+ {/*
+ Heads up! The styles below are necessary for the correct render of this example.
+ You can do same with CSS, the main idea is that all the elements up to the `Grid`
+ below must have a height of 100%.
+ */}
+
+
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+
+ {renderBrandingLogo}
+
+ {branding.login.title}
+ {branding.login.subtitle}
+
+
+ {renderAuthenticationProviders()}
+
+ {collectUserNamePassword && (
+
+
+ {this.usernameError && (
+
+ {this.usernameError}
+
+ )}
+
+ )}
+
+ {collectUserNamePassword && (
+
+
+ {this.passwordError && (
+
+ {this.passwordError}
+
+ )}
+
+ )}
+
+
+ Login
+
+
+
+
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(Login, {
+ username: observable,
+ password: observable,
+ loading: observable,
+ authenticationProviderError: observable,
+ usernameError: observable,
+ passwordError: observable,
+});
+
+export default inject('authentication', 'authenticationProviderPublicConfigsStore', 'assets')(observer(Login));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/MainLayout.js b/addons/addon-base-ui/packages/base-ui/src/parts/MainLayout.js
new file mode 100644
index 0000000000..172114dbff
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/MainLayout.js
@@ -0,0 +1,113 @@
+/*
+ * 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, action } from 'mobx';
+import { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Menu, Icon } from 'semantic-ui-react';
+
+import { createLink } from '../helpers/routing';
+import { displayError } from '../helpers/notification';
+import { branding } from '../helpers/settings';
+
+// expected props
+// - userStore (via injection)
+class MainLayout extends React.Component {
+ goto = pathname => () => {
+ const location = this.props.location;
+ const link = createLink({
+ location,
+ pathname,
+ });
+
+ this.props.history.push(link);
+ };
+
+ handleLogout = async event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ try {
+ await this.props.authentication.logout();
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ getMenuItems() {
+ return this.props.menuItems || [];
+ }
+
+ render() {
+ const currentUser = this.props.userStore.user;
+ const displayName = currentUser ? currentUser.displayName : 'Not Logged In';
+ const pathname = _.get(this.props.location, 'pathname', '');
+ const is = value => _.startsWith(pathname, value);
+
+ const itemsArr = this.getMenuItems();
+ return [
+
+
+ {_.map(itemsArr, (item, index) => {
+ const show = (_.isFunction(item.shouldShow) && item.shouldShow()) || item.shouldShow;
+ return (
+ show &&
+ (item.body ? (
+ item.body
+ ) : (
+
+
+ {item.title}
+
+ ))
+ );
+ })}
+ ,
+
+
+
+ {/* */}
+ {branding.main.title}
+
+
+
+ {displayName}
+
+
+
+ ,
+
+ {this.props.children}
+
,
+ ];
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(MainLayout, {
+ handleLogout: action,
+});
+
+export default inject('authentication', 'userStore')(withRouter(observer(MainLayout)));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/api-keys/ApiKeysList.js b/addons/addon-base-ui/packages/base-ui/src/parts/api-keys/ApiKeysList.js
new file mode 100644
index 0000000000..75ee89f360
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/api-keys/ApiKeysList.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 React, { Component } from 'react';
+import { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Container, Header, Icon, Accordion, Table, Button, Label } from 'semantic-ui-react';
+
+import TimeAgo from 'react-timeago';
+import _ from 'lodash';
+import { swallowError } from '../../helpers/utils';
+import { isStoreEmpty, isStoreError, isStoreLoading, isStoreNotEmpty, isStoreReady } from '../../models/BaseStore';
+import Progress from '../helpers/Progress';
+import ErrorBox from '../helpers/ErrorBox';
+import { displayError } from '../../helpers/notification';
+
+// expected props
+// - userApiKeysStore (via injection)
+class ApiKeysList extends Component {
+ getStore() {
+ return this.props.userApiKeysStore.getCurrentUserApiKeysStore();
+ }
+
+ componentDidMount() {
+ const store = this.getStore();
+ swallowError(store.load());
+ }
+
+ render() {
+ const store = this.getStore();
+ let content;
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = (
+
+ );
+ } else if (isStoreReady(store) && isStoreEmpty(store)) {
+ content = this.renderEmpty();
+ } else if (isStoreReady(store) && isStoreNotEmpty(store)) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+ return {content}
;
+ }
+
+ renderEmpty() {
+ return (
+
+
+
+ );
+ }
+
+ handleCreateApiKey = async () => {
+ try {
+ await this.getStore().createNewApiKey();
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ handleRevokeApiKey = async apiKeyId => {
+ try {
+ await this.getStore().revokeApiKey(apiKeyId);
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ renderMain() {
+ const apiKeys = _.orderBy(this.getStore().list, ['createdAt', 'status'], ['desc', 'asc']);
+ const renderTotal = () => {
+ const count = apiKeys.length;
+ return (
+
+
+ You have{' '}
+
+ {count}
+ {' '}
+ API Keys
+
+
+ Create New API Key
+
+
+ );
+ };
+ const renderRow = (rowNum, apiKey) => {
+ const panels = [
+ {
+ key: `panel-${rowNum}`,
+ title: {
+ content: ,
+ },
+ content: {
+ content: (
+
+
+
+ ),
+ },
+ },
+ ];
+ return (
+
+
+ {rowNum}
+
+
+
+
+
+
+
+
+
+ {_.capitalize(apiKey.effectiveStatus)}
+
+
+
+ this.handleRevokeApiKey(apiKey.id)}>
+ Revoke
+
+
+
+ );
+ };
+ const renderTableBody = () => {
+ let rowNum = 0;
+ return _.map(apiKeys, apiKey => {
+ ++rowNum;
+ return renderRow(rowNum, apiKey);
+ });
+ };
+ return (
+
+ {renderTotal()}
+
+
+
+
+
+ #
+
+ Key
+
+ Issued
+
+
+ Status
+
+
+
+
+ {renderTableBody()}
+
+
+
+ );
+ }
+}
+
+export default inject('userApiKeysStore')(withRouter(observer(ApiKeysList)));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/authentication-providers/AddAuthenticationProvider.js b/addons/addon-base-ui/packages/base-ui/src/parts/authentication-providers/AddAuthenticationProvider.js
new file mode 100644
index 0000000000..84cd1345f5
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/authentication-providers/AddAuthenticationProvider.js
@@ -0,0 +1,27 @@
+/*
+ * 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 { inject, observer } from 'mobx-react';
+import { Component } from 'react';
+
+// expected props
+// - authenticationProviderConfigsStore (via injection)
+class AddAuthenticationProvider extends Component {
+ render() {
+ return 'TODO: IMPLEMENT';
+ }
+}
+
+export default inject('authenticationProviderConfigsStore')(observer(AddAuthenticationProvider));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/authentication-providers/AuthenticationProviderCard.js b/addons/addon-base-ui/packages/base-ui/src/parts/authentication-providers/AuthenticationProviderCard.js
new file mode 100644
index 0000000000..8342de2b2f
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/authentication-providers/AuthenticationProviderCard.js
@@ -0,0 +1,83 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import React, { Component } from 'react';
+import { withRouter } from 'react-router-dom';
+import { Button, Header, Label } from 'semantic-ui-react';
+import { gotoFn } from '../../helpers/routing';
+
+// expected props
+// - authenticationProviderConfig
+// - pos
+class AuthenticationProviderCard extends Component {
+ render() {
+ const authenticationProviderConfig = this.getAuthenticationProviderConfig();
+ return (
+ // The custom attribute "data-id" here is used for conveying the id of the virtualDatabase being clicked in the "handleVirtualDatabaseClick" handler
+
+
+
+ {this.renderIndexLabel()}
+
+
{this.renderActionButtons()}
+
+
{authenticationProviderConfig.config.type.description}
+
+
+ );
+ }
+
+ getAuthenticationProviderConfig() {
+ return this.props.authenticationProviderConfig;
+ }
+
+ renderStatus(authenticationProviderConfig) {
+ const isActive = _.toLower(authenticationProviderConfig.status) === 'active';
+ return {authenticationProviderConfig.status} ;
+ }
+
+ renderIndexLabel() {
+ const pos = this.props.pos;
+ return (
+
+ {pos}
+
+ );
+ }
+
+ renderActionButtons() {
+ return (
+
+
+
+ );
+ }
+
+ handleEditModeClick = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ const id = this.getAuthenticationProviderConfig().id;
+ const goto = gotoFn(this);
+ goto(`/authentication-providers/${encodeURIComponent(id)}/edit`);
+ };
+}
+
+export default withRouter(observer(AuthenticationProviderCard));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/authentication-providers/AuthenticationProvidersList.js b/addons/addon-base-ui/packages/base-ui/src/parts/authentication-providers/AuthenticationProvidersList.js
new file mode 100644
index 0000000000..ca4b9adc7f
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/authentication-providers/AuthenticationProvidersList.js
@@ -0,0 +1,120 @@
+/*
+ * 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, { Component } from 'react';
+import { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Container, Header, Icon, Label, Segment } from 'semantic-ui-react';
+import { gotoFn } from '../../helpers/routing';
+
+import { swallowError } from '../../helpers/utils';
+import { isStoreError, isStoreLoading, isStoreNotEmpty, isStoreReady } from '../../models/BaseStore';
+import BasicProgressPlaceholder from '../helpers/BasicProgressPlaceholder';
+import ErrorBox from '../helpers/ErrorBox';
+import AuthenticationProviderCard from './AuthenticationProviderCard';
+
+// expected props
+// - authenticationProviderConfigsStore (via injection)
+class AuthenticationProvidersList extends Component {
+ componentDidMount() {
+ const store = this.getStore();
+ swallowError(store.load());
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getStore();
+ store.stopHeartbeat();
+ }
+
+ getStore() {
+ return this.props.authenticationProviderConfigsStore;
+ }
+
+ handleAddAuthenticationProviderClick = _event => {
+ const goto = gotoFn(this);
+ goto('/authentication-providers/add');
+ };
+
+ render() {
+ const store = this.getStore();
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreReady(store)) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+
+ {this.renderTitle()}
+ {content}
+
+
+ );
+ }
+
+ renderTitle() {
+ const renderCount = () => {
+ const store = this.getStore();
+ const showCount = isStoreReady(store) && isStoreNotEmpty(store);
+ const list = store.list;
+ return (
+ showCount && (
+
+ {list.length}
+
+ )
+ );
+ };
+
+ return (
+
+
+
+
+ Authentication Providers
+ {renderCount()}
+
+
+ {/* Add Authentication Provider */}
+
+ );
+ }
+
+ renderMain() {
+ const store = this.getStore();
+ const list = store.list;
+
+ return (
+
+ {_.map(list, (authNProviderConfig, idx) => (
+
+
+
+ ))}
+
+ );
+ }
+}
+
+export default inject('authenticationProviderConfigsStore')(withRouter(observer(AuthenticationProvidersList)));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/authentication-providers/EditAuthenticationProvider.js b/addons/addon-base-ui/packages/base-ui/src/parts/authentication-providers/EditAuthenticationProvider.js
new file mode 100644
index 0000000000..6b12a78fa6
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/authentication-providers/EditAuthenticationProvider.js
@@ -0,0 +1,146 @@
+/*
+ * 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 { inject, observer } from 'mobx-react';
+import React, { Component } from 'react';
+import { Breadcrumb, Container, Header, Segment } from 'semantic-ui-react';
+import { displayError, displaySuccess } from '../../helpers/notification';
+
+import { gotoFn } from '../../helpers/routing';
+import { swallowError } from '../../helpers/utils';
+import { fromConfiguration } from '../../models/authentication/AuthenticationProviderConfigsStore';
+import { isStoreError, isStoreLoading, isStoreReady } from '../../models/BaseStore';
+import ConfigurationEditor from '../configuration/ConfigurationEditor';
+import ConfigurationReview from '../configuration/ConfigurationReview';
+import BasicProgressPlaceholder from '../helpers/BasicProgressPlaceholder';
+import ErrorBox from '../helpers/ErrorBox';
+
+// expected props
+// - authenticationProviderConfigId (via react router params)
+// - authenticationProviderConfigsStore (via injection)
+class EditAuthenticationProvider extends Component {
+ componentDidMount() {
+ const store = this.getStore();
+ swallowError(store.load());
+ }
+
+ render() {
+ const store = this.getStore();
+ 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 id = this.getAuthenticationProviderConfigId();
+ const authenticationProviderConfig = this.getAuthenticationProviderConfig();
+ if (!authenticationProviderConfig) return ;
+ const goto = gotoFn(this);
+ return (
+
+
+ goto('/authentication-providers')}>
+ Authentication Providers
+
+
+ Authentication Provider
+
+ {authenticationProviderConfig.id}
+
+
+
+ {this.renderTitle(authenticationProviderConfig)}
+ {this.renderDetails(authenticationProviderConfig.id)}
+
+
+
+ );
+ }
+
+ renderTitle(authenticationProviderConfig) {
+ return (
+
+ {authenticationProviderConfig.config.title}
+
+ );
+ }
+
+ renderDetails(authenticationProviderConfigId) {
+ const authenticationProviderConfigEditor = this.getStore().getUpdateAuthenticationProviderConfigEditor(
+ authenticationProviderConfigId,
+ );
+ const model = authenticationProviderConfigEditor.configEditor;
+ const review = model.review;
+ if (review) {
+ return ;
+ }
+ return ;
+ }
+
+ getStore() {
+ return this.props.authenticationProviderConfigsStore;
+ }
+
+ getAuthenticationProviderConfigId() {
+ return decodeURIComponent((this.props.match.params || {}).authenticationProviderConfigId);
+ }
+
+ getAuthenticationProviderConfig() {
+ const id = this.getAuthenticationProviderConfigId();
+ return this.getStore().getAuthenticationProviderConfig(id);
+ }
+
+ handleCancel = () => {
+ const goto = gotoFn(this);
+ goto('/authentication-providers');
+ };
+
+ handleSave = async configs => {
+ try {
+ const authenticationProviderConfigToUpdate = fromConfiguration(configs);
+ const original = this.getAuthenticationProviderConfig();
+
+ const typeObj = original.config.type;
+ const providerTypeId = typeObj.type;
+
+ await this.getStore().updateAuthenticationProvider({
+ providerTypeId,
+ providerConfig: authenticationProviderConfigToUpdate,
+ });
+ const goto = gotoFn(this);
+ goto('/authentication-providers');
+
+ displaySuccess(`The authentication provider is updated successfully`);
+ } catch (error) {
+ displayError(error);
+ }
+ };
+}
+
+export default inject('authenticationProviderConfigsStore')(observer(EditAuthenticationProvider));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/configuration/ConfigTable.js b/addons/addon-base-ui/packages/base-ui/src/parts/configuration/ConfigTable.js
new file mode 100644
index 0000000000..e52dfb3b49
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/configuration/ConfigTable.js
@@ -0,0 +1,67 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Table } from 'semantic-ui-react';
+
+// expected props
+// - rows (via props), an array of objects, [ { name, title, value }, { name, title, value }, ... ]
+// - className (via props)
+const Component = observer(({ rows = [], className = '' }) => {
+ if (rows.length === 0) return null;
+
+ return (
+
+
+
+ Key
+ Value
+
+
+
+ {_.map(rows, (item, index) => (
+
+ {renderKey(item)}
+ {renderValue(item)}
+
+ ))}
+
+
+ );
+});
+
+function renderValue({ value }) {
+ const isNil = _.isNil(value);
+ const isEmpty = _.isString(value) && _.isEmpty(value);
+ return isNil || isEmpty ? 'Not Provided' : value.toString();
+}
+
+function renderKey({ title = '', name }) {
+ const hasTitle = !_.isEmpty(title);
+
+ if (hasTitle) {
+ return (
+ <>
+ {title}
+ {name}
+ >
+ );
+ }
+ return {name}
;
+}
+
+export default Component;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/configuration/ConfigurationEditor.js b/addons/addon-base-ui/packages/base-ui/src/parts/configuration/ConfigurationEditor.js
new file mode 100644
index 0000000000..3b5edcbf8c
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/configuration/ConfigurationEditor.js
@@ -0,0 +1,165 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { decorate, action } from 'mobx';
+import { Button, Icon, Segment, Step } from 'semantic-ui-react';
+
+import Form from '../helpers/fields/Form';
+import InputEntriesRenderer from './InputEntriesRenderer';
+
+// expected props
+// - model - an instance of the ConfigurationEditor model instance (via props)
+// - onCancel (via props) is called after all the necessary clean up
+class ConfigurationEditor extends React.Component {
+ getModel() {
+ return this.props.model;
+ }
+
+ getForm() {
+ const model = this.getModel();
+ return model.form;
+ }
+
+ handleCancel = () => {
+ const onCancel = this.props.onCancel || _.noop;
+ const model = this.getModel();
+ model.cancel();
+
+ return onCancel();
+ };
+
+ handleNext = form => {
+ const model = this.getModel();
+ model.next(form);
+ };
+
+ handlePrevious = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ const form = this.getForm();
+ const model = this.getModel();
+
+ model.previous(form);
+ };
+
+ handleClear = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ const form = this.getForm();
+ const model = this.getModel();
+ model.clearSectionConfigs();
+ form.reset();
+ };
+
+ render() {
+ const form = this.getForm();
+ const model = this.getModel();
+ const hasPrevious = model.hasPrevious;
+ const inputManifestSection = model.inputManifestSection || {};
+ const inputEntries = inputManifestSection.children || [];
+ const empty = inputEntries.length === 0;
+
+ return (
+
+ {({ processing, errors, _onSubmit, onCancel }) => (
+ <>
+ {!empty && (
+
+ {this.renderSectionTitles(errors)}
+
+
+ )}
+ {empty && (
+
+ No configuration values are provided
+
+ )}
+
+
+
+ Next
+
+
+ {hasPrevious && (
+
+ Previous
+
+
+ )}
+
+ Clear
+
+
+ Cancel
+
+
+ >
+ )}
+
+ );
+ }
+
+ renderSectionTitles(errors) {
+ const model = this.getModel();
+ const totalSections = model.totalSections;
+ const currentSectionIndex = model.currentSectionIndex;
+ const showSectionTitles = totalSections > 1;
+ const sectionTitles = model.sectionsTitles;
+ const hasError = errors.length > 0;
+ if (!showSectionTitles) return null;
+ if (totalSections < 3) return null; // only show the titles when we have 3 or more sections
+
+ return (
+
+ {_.times(totalSections, index => (
+
+ {hasError && index === currentSectionIndex ? : }
+
+ {sectionTitles[index] || ''}
+
+
+ ))}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(ConfigurationEditor, {
+ handleNext: action,
+ handlePrevious: action,
+ handleCancel: action,
+ handleClear: action,
+});
+
+export default observer(ConfigurationEditor);
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/configuration/ConfigurationReview.js b/addons/addon-base-ui/packages/base-ui/src/parts/configuration/ConfigurationReview.js
new file mode 100644
index 0000000000..b116f925d2
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/configuration/ConfigurationReview.js
@@ -0,0 +1,159 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { decorate, action, runInAction, observable } from 'mobx';
+import { Button, Icon, Segment, Dimmer, Loader } from 'semantic-ui-react';
+
+import { displayError } from '../../helpers/notification';
+import ConfigTable from './ConfigTable';
+
+// expected props
+// - model - an instance of the ConfigurationEditor model instance (via props)
+// - onCancel (via props) is called after all the necessary clean up
+// - onSave (via props) is called with (configuration) which is just an object with key/value pairs
+// - dimmer (via props) default to true, set to false if you don't want to use the dimmer (buttons will still be disabled during processing)
+class ConfigurationReview extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.processing = false;
+ });
+ }
+
+ getModel() {
+ return this.props.model;
+ }
+
+ getForm() {
+ const model = this.getModel();
+ return model.form;
+ }
+
+ getDimmer() {
+ const dimmer = this.props.dimmer;
+ return _.isUndefined(dimmer) ? true : !!dimmer;
+ }
+
+ handleCancel = () => {
+ this.processing = false;
+ const onCancel = this.props.onCancel || _.noop;
+ const model = this.getModel();
+ model.cancel();
+
+ return onCancel();
+ };
+
+ handleSave = async () => {
+ const onSave = this.props.onSave || _.noop;
+ const model = this.getModel();
+ const configuration = {};
+
+ /* eslint-disable no-restricted-syntax */
+ for (const [key, value] of model.configuration.entries()) {
+ configuration[key] = value;
+ }
+ /* eslint-enable no-restricted-syntax */
+
+ try {
+ this.processing = true;
+ await onSave(configuration);
+ runInAction(() => {
+ this.processing = false;
+ });
+ model.applyChanges();
+ model.restart();
+ } catch (error) {
+ runInAction(() => {
+ this.processing = false;
+ });
+ displayError(error);
+ }
+ };
+
+ handlePrevious = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ const form = this.getForm();
+ const model = this.getModel();
+
+ this.processing = false;
+ model.previous(form);
+ };
+
+ render() {
+ const processing = this.processing;
+ const dimmer = this.getDimmer();
+ const model = this.getModel();
+ const configRows = model.definedConfigList || [];
+ const empty = configRows.length === 0;
+ const review = model.review;
+ const buttons = (
+
+ {review && (
+
+ )}
+
+ Previous
+
+
+
+ Cancel
+
+
+ );
+
+ let content = ;
+ if (empty) content = 'No configuration values are provided';
+
+ return (
+ <>
+ {review && (
+
+ {dimmer && (
+
+ Processing
+
+ )}
+ {content}
+
+ )}
+ {buttons}
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(ConfigurationReview, {
+ processing: observable,
+ handleSave: action,
+ handlePrevious: action,
+ handleCancel: action,
+});
+
+export default observer(ConfigurationReview);
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/configuration/InputEntriesRenderer.js b/addons/addon-base-ui/packages/base-ui/src/parts/configuration/InputEntriesRenderer.js
new file mode 100644
index 0000000000..ae7f01a0a1
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/configuration/InputEntriesRenderer.js
@@ -0,0 +1,55 @@
+/*
+ * 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 } from 'mobx';
+import { observer, inject } from 'mobx-react';
+
+import InputEntryRenderer from './InputEntryRenderer';
+
+// expected props
+// - form (via props)
+// - inputEntries (via props) (these are the input manifest input entries)
+// - processing (via props) (default to false)
+class InputEntriesRenderer extends React.Component {
+ getForm() {
+ return this.props.form;
+ }
+
+ getInputEntries() {
+ return this.props.inputEntries;
+ }
+
+ render() {
+ const processing = this.props.processing || false;
+ const form = this.getForm();
+ // entry is an object of this shape:
+ // { name: 'id', type: 'string/yesNo,..', label, children: [ ], .. }
+ const entries = this.getInputEntries();
+ return (
+ <>
+ {_.map(entries, entry => (
+
+ ))}
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(InputEntriesRenderer, {});
+
+export default inject()(observer(InputEntriesRenderer));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/configuration/InputEntryRenderer.js b/addons/addon-base-ui/packages/base-ui/src/parts/configuration/InputEntryRenderer.js
new file mode 100644
index 0000000000..a3e0e8f739
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/configuration/InputEntryRenderer.js
@@ -0,0 +1,102 @@
+/*
+ * 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 } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { Divider, Icon, Header } from 'semantic-ui-react';
+
+import Input from '../helpers/fields/Input';
+import YesNo from '../helpers/fields/YesNo';
+import DropDown from '../helpers/fields/DropDown';
+import TextArea from '../helpers/fields/TextArea';
+
+// expected props
+// - form (via props)
+// - inputEntry (via props)
+// - processing (via props) (default to false)
+class InputEntryRenderer extends React.Component {
+ getForm() {
+ return this.props.form;
+ }
+
+ getInputEntry() {
+ return this.props.inputEntry;
+ }
+
+ getProcessing() {
+ return this.props.processing || false;
+ }
+
+ render() {
+ // entry is an object of a shape like:
+ // { name: 'id', type: 'string/yesNo,..', label, children: [ ], .. }
+ const entry = this.getInputEntry();
+ const field = this.getField();
+
+ return (
+ <>
+ {this.renderDivider(entry)}
+ {field}
+ >
+ );
+ }
+
+ renderDivider(entry) {
+ if (entry.divider === undefined) return null;
+ const divider = entry.divider;
+ const hasIcon = !!divider.icon;
+
+ if (_.isBoolean(entry.divider)) return ;
+
+ return (
+
+
+ {hasIcon && }
+ {divider.title}
+
+
+ );
+ }
+
+ getField() {
+ const processing = this.getProcessing();
+ const form = this.getForm();
+ // entry is an object of a shape like:
+ // { name: 'id', type: 'string/yesNo,..', label, children: [ ], .. }
+ const entry = this.getInputEntry();
+ const field = form.$(entry.name);
+
+ switch (entry.type) {
+ case 'stringInput':
+ return ;
+ case 'yesNoInput':
+ return ;
+ case 'dropDownInput':
+ return ;
+ case 'textAreaInput':
+ return ;
+
+ default:
+ return <>>;
+ }
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(InputEntryRenderer, {});
+
+export default inject()(observer(InputEntryRenderer));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/dashboard/Dashboard.js b/addons/addon-base-ui/packages/base-ui/src/parts/dashboard/Dashboard.js
new file mode 100644
index 0000000000..23d1086f3c
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/dashboard/Dashboard.js
@@ -0,0 +1,114 @@
+/*
+ * 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 } from 'mobx';
+import { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import c from 'classnames';
+import { Container, Header, Segment, Icon, Divider, Label } from 'semantic-ui-react';
+
+import { blueDatasets } from './graphs/graph-options';
+import BarGraph from './graphs/BarGraph';
+
+// expected props
+// - location (from react router)
+class Dashboard extends React.Component {
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ render() {
+ return (
+
+ {this.renderTitle()}
+ {this.renderContent()}
+
+ );
+ }
+
+ renderTitle() {
+ return (
+
+
+
+ );
+ }
+
+ stat(title, label, value, color, className) {
+ return (
+
+
{title}
+
+ {value}
+
+
+ {label}
+
+
+ );
+ }
+
+ renderContent() {
+ return (
+
+
+
+ {this.stat('You have to complete', 'TASKS', '550', 'color-blue', 'mr4')}
+ {this.renderTaskCountGraph()}
+ {this.renderTaskDueGraph()}
+
+
+ There are{' '}
+
+ 100
+ {' '}
+ tasks due today. You have been assigned an additional 300 tasks since last month. There are
+ a total of
+ 10,000
+ tasks to complete.
+
+
+ );
+ }
+
+ renderTaskCountGraph() {
+ const title = 'Tasks';
+ const data = {
+ labels: ['Eat', 'Run', 'Walk', 'Sleep', 'Work'],
+ datasets: blueDatasets(title, [1, 8, 5, 6, 3]),
+ };
+
+ return ;
+ }
+
+ renderTaskDueGraph() {
+ const title = 'Due Date';
+ const data = {
+ labels: ['Today', 'Tomorrow', 'Yesterday', 'Last Year', 'No'],
+ datasets: blueDatasets(title, [1, 8, 5, 6, 3]),
+ };
+
+ return ;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(Dashboard, {});
+
+export default inject()(withRouter(observer(Dashboard)));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/dashboard/graphs/BarGraph.js b/addons/addon-base-ui/packages/base-ui/src/parts/dashboard/graphs/BarGraph.js
new file mode 100644
index 0000000000..ae4948c0a3
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/dashboard/graphs/BarGraph.js
@@ -0,0 +1,32 @@
+/*
+ * 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 { HorizontalBar } from 'react-chartjs-2';
+
+import { barOptions } from './graph-options';
+
+const BarGraph = ({ className, data, title, width = 250, height = 120 }) => {
+ return (
+
+ );
+};
+
+export default BarGraph;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/dashboard/graphs/graph-options.js b/addons/addon-base-ui/packages/base-ui/src/parts/dashboard/graphs/graph-options.js
new file mode 100644
index 0000000000..e513b01777
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/dashboard/graphs/graph-options.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.
+ */
+
+const barOptions = {
+ // see https://www.chartjs.org/docs/latest/configuration/legend.html
+ // see https://github.com/jerairrest/react-chartjs-2/blob/master/example/src/components/bar.js
+ // see https://stackoverflow.com/questions/36676263/chart-js-v2-hiding-grid-lines
+ // see https://github.com/jerairrest/react-chartjs-2
+ legend: {
+ display: false,
+ },
+ maintainAspectRatio: false,
+ scales: {
+ // see https://www.chartjs.org/docs/latest/charts/bar.html
+ xAxes: [
+ {
+ // barPercentage: 1,
+ // categoryPercentage: 1,
+ gridLines: {
+ display: false,
+ },
+ },
+ ],
+ yAxes: [
+ {
+ gridLines: {
+ display: false,
+ },
+ },
+ ],
+ },
+};
+
+function blueDatasets(label, data) {
+ return [
+ {
+ label: label || 'Patients Ages 1 to 5',
+ backgroundColor: 'rgba(33, 133, 208,0.2)',
+ borderColor: 'rgba(33, 133, 208,1)',
+ borderWidth: 1,
+ // hoverBackgroundColor: 'rgba(255,99,132,0.4)',
+ // hoverBorderColor: 'rgba(255,99,132,1)',
+ data: data || [1, 8, 5, 6, 3],
+ },
+ ];
+}
+export { barOptions, blueDatasets };
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/Age.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/Age.js
new file mode 100644
index 0000000000..075be03abe
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/Age.js
@@ -0,0 +1,37 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import c from 'classnames';
+import TimeAgo from 'react-timeago';
+
+// expected props
+// - date (via props)
+// - emptyMessage (via props) (a message to display when the date is empty)
+// - className (via props)
+const Component = observer(({ date, className, emptyMessage = 'Not Provided' }) => {
+ if (_.isEmpty(date)) return {emptyMessage} ;
+ const formatter = (_value, _unit, _suffix, _epochSeconds, nextFormatter) =>
+ (nextFormatter() || '').replace(/ago$/, 'old');
+ return (
+
+
+
+ );
+});
+
+export default Component;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/BasicProgressPlaceholder.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/BasicProgressPlaceholder.js
new file mode 100644
index 0000000000..51ee43da0d
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/BasicProgressPlaceholder.js
@@ -0,0 +1,44 @@
+/*
+ * 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 { Segment, Placeholder, Divider } from 'semantic-ui-react';
+
+// expected props
+// - segmentCount (via props)
+// - className (via props)
+const Component = ({ segmentCount = 1, className }) => {
+ const segment = index => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ return {_.map(_.times(segmentCount, String), index => segment(index))}
;
+};
+
+export default Component;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/By.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/By.js
new file mode 100644
index 0000000000..ad40218361
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/By.js
@@ -0,0 +1,53 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import c from 'classnames';
+
+// expected props
+// - user (via props)
+// - userDisplayName (via injection)
+// - className (via props)
+class By extends React.Component {
+ get user() {
+ return this.props.user;
+ }
+
+ get userDisplayNameService() {
+ return this.props.userDisplayName;
+ }
+
+ render() {
+ const user = this.user;
+ const displayNameService = this.userDisplayNameService;
+ const isSystem = displayNameService.isSystem(user);
+ return isSystem ? (
+ ''
+ ) : (
+
+ by
+ {displayNameService.getDisplayName(user)}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(By, {});
+
+export default inject('userDisplayName')(withRouter(observer(By)));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/ErrorBox.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/ErrorBox.js
new file mode 100644
index 0000000000..b387850d60
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/ErrorBox.js
@@ -0,0 +1,56 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Message, Button } from 'semantic-ui-react';
+
+// expected props
+// - error (an object with a "message" property or a string)
+// - className
+class ErrorBox extends React.Component {
+ handleRetry = () => {
+ Promise.resolve()
+ .then(() => this.props.onRetry())
+ .catch(_err => {
+ /* ignore */
+ });
+ };
+
+ render() {
+ const defaultMessage = 'Hmm... something went wrong';
+ const rawMessage = this.props.error || defaultMessage;
+ const message = _.isString(rawMessage) ? rawMessage : _.get(rawMessage, 'message', defaultMessage);
+ const shouldRetry = _.isFunction(this.props.onRetry);
+ const className = this.props.className ? this.props.className : 'p3';
+
+ return (
+
+
+ A problem was encountered
+ {message}
+ {shouldRetry && (
+
+ Retry
+
+ )}
+
+
+ );
+ }
+}
+
+export default observer(ErrorBox);
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/Progress.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/Progress.js
new file mode 100644
index 0000000000..6dc5920224
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/Progress.js
@@ -0,0 +1,25 @@
+/*
+ * 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 { Progress } from 'semantic-ui-react';
+
+export default ({ message = 'Loading...', className = 'p3' }) => (
+
+);
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/UserLabels.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/UserLabels.js
new file mode 100644
index 0000000000..da378e9177
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/UserLabels.js
@@ -0,0 +1,40 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Label } from 'semantic-ui-react';
+
+const UserLabels = props => {
+ const { color, className = '', users } = props;
+
+ return (
+
+ {_.map(users, user => (
+
+ {user.firstName}
+ {user.lastName}
+
+ {user.unknown && `${user.username}??`}
+ {!user.unknown && (user.email || user.username)}
+
+
+ ))}
+
+ );
+};
+
+export default observer(UserLabels);
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/WarningBox.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/WarningBox.js
new file mode 100644
index 0000000000..84561b983c
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/WarningBox.js
@@ -0,0 +1,52 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Message, Button } from 'semantic-ui-react';
+
+class WarningBox extends React.Component {
+ handleRetry = () => {
+ Promise.resolve()
+ .then(() => this.props.onRetry())
+ .catch(_err => {
+ /* ignore */
+ });
+ };
+
+ render() {
+ const defaultMessage = 'Hmm... something is needing your attention';
+ const rawMessage = this.props.warning || defaultMessage;
+ const message = _.isString(rawMessage) ? rawMessage : _.get(rawMessage, 'message', defaultMessage);
+ const shouldRetry = _.isFunction(this.props.onRetry);
+
+ return (
+
+
+ Warning
+ {message}
+ {shouldRetry && (
+
+ Retry
+
+ )}
+
+
+ );
+ }
+}
+
+export default observer(WarningBox);
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Description.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Description.js
new file mode 100644
index 0000000000..72a4afb24c
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Description.js
@@ -0,0 +1,44 @@
+/*
+ * 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 react/no-danger */
+import React from 'react';
+import { observer } from 'mobx-react';
+import c from 'classnames';
+import { Message } from 'semantic-ui-react';
+
+// expected props
+// - field (via props), this is the mobx form field object
+const Component = observer(({ field }) => {
+ const { extra = {} } = field;
+ const explain = (extra || {}).explain;
+ const warn = (extra || {}).warn;
+ const hasExplain = !!explain;
+ const hasWarn = !!warn;
+
+ return (
+ <>
+ {hasExplain &&
}
+ {hasWarn && (
+
+ Warning
+ {warn}
+
+ )}
+ >
+ );
+});
+
+export default Component;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/DropDown.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/DropDown.js
new file mode 100644
index 0000000000..7299acde4a
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/DropDown.js
@@ -0,0 +1,87 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Dropdown } from 'semantic-ui-react';
+import c from 'classnames';
+
+import Header from './Header';
+import Description from './Description';
+import ErrorPointer from './ErrorPointer';
+
+// expected props
+// - field (via props), this is the mobx form field object
+// - options (via props), an array of [ {text, value}, {text, value}, ...]
+// - onChange (via props), (optional) if provided, it will be given (value, field)
+// - className (via props)
+//
+// The following props are to support existing React Semantic UI props:
+// - selection (via props), default to false
+// - fluid (via props), default to false
+// - disabled (via props), default to false
+// - clearable (via props), default to false
+// - multiple (via props), default to false
+// - search (via props), default to false
+const Component = observer(
+ ({
+ field,
+ selection = false,
+ fluid = false,
+ disabled = false,
+ clearable = false,
+ multiple = false,
+ search = false,
+ className = 'mb4',
+ options = [],
+ onChange,
+ }) => {
+ const { id, value, sync, placeholder, error = '', extra = {} } = field;
+ const hasError = !_.isEmpty(error); // IMPORTANT do NOT use field.hasError
+ const mergeOptions = [...((extra && extra.options) || []), ...options];
+ const isDisabled = field.disabled || disabled;
+ const disabledClass = isDisabled ? 'disabled' : '';
+ const errorClass = hasError ? 'error' : '';
+ const attrs = {
+ id,
+ value,
+ onChange: (e, data = {}) => {
+ sync(data.value);
+ field.validate({ showErrors: true });
+ if (onChange) onChange(data.value, field);
+ },
+ placeholder,
+ selection,
+ clearable,
+ multiple,
+ search,
+ fluid,
+ disabled: isDisabled,
+ error: hasError,
+ };
+
+ return (
+
+
+
+
+
+
+ );
+ },
+);
+
+export default Component;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/EditableField.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/EditableField.js
new file mode 100644
index 0000000000..2c80f8426b
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/EditableField.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 { observer } from 'mobx-react';
+import React from 'react';
+import { decorate, action, observable, runInAction } from 'mobx';
+import _ from 'lodash';
+
+import { displayError } from '../../../helpers/notification';
+import Form from './Form';
+
+// expected props
+// - form -- A single field Mobx Form specific to this field.
+// - renderFieldForView -- Called to render the field in "view" mode.
+// - renderFieldForEdit -- Called to render the field in "edit" mode.
+// - onSubmit - optional -- Called when form specific to this field is submitted
+// - onCancel - optional -- Called when the field is being canceled for edit (i.e., transitioning from edit mode to view mode)
+// - onError - optional -- Called when any error occurs when processing the form (may be validation errors)
+/**
+ * A field component that can be used for places where you require single field edits (such as inline edits).
+ * The field handles switching between "view" mode and "edit" mode.
+ */
+class EditableField extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.editorOn = false;
+ });
+ }
+
+ render() {
+ if (this.editorOn) return this.renderEditMode();
+ return this.renderViewMode();
+ }
+
+ renderEditMode() {
+ const form = this.props.form;
+ const renderFieldForEdit = this.props.renderFieldForEdit;
+ return (
+
+ {({ processing, onSubmit, onCancel }) => renderFieldForEdit({ processing, onSubmit, onCancel })}
+
+ );
+ }
+
+ renderViewMode() {
+ return this.props.renderFieldForView({ onEditorOn: this.handleEditorOn });
+ }
+
+ handleEditorOn = action(() => {
+ this.editorOn = true;
+ });
+
+ handleFormSubmission = action(async form => {
+ try {
+ await this.notifyHandler(this.props.onSubmit, form);
+ runInAction(() => {
+ this.editorOn = false;
+ });
+ } catch (error) {
+ displayError(error);
+ form.clear();
+ runInAction(() => {
+ this.editorOn = false;
+ });
+ }
+ });
+
+ handleCancel = action(async () => {
+ this.editorOn = false;
+
+ // notify onCancel
+ await this.notifyHandler(this.props.onCancel);
+ });
+
+ handleFormError = action(async (form, errors) => {
+ await this.notifyHandler(this.props.onError, form, errors);
+ });
+
+ notifyHandler = async (handlerFn, ...args) => {
+ const handlerFnToNotify = handlerFn || _.noop;
+ await handlerFnToNotify(...args);
+ };
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(EditableField, {
+ editorOn: observable,
+});
+
+export default observer(EditableField);
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/ErrorPointer.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/ErrorPointer.js
new file mode 100644
index 0000000000..a3bcfd9270
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/ErrorPointer.js
@@ -0,0 +1,31 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import c from 'classnames';
+
+// expected props
+// - field (via props), this is the mobx form field object
+const Component = observer(({ field, className }) => {
+ const { error = '' } = field;
+ const hasError = !_.isEmpty(error); // IMPORTANT do NOT use field.hasError
+
+ if (!hasError) return null;
+ return {error}
;
+});
+
+export default Component;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Form.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Form.js
new file mode 100644
index 0000000000..9f60ce2fb6
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Form.js
@@ -0,0 +1,190 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { decorate, action, observable, runInAction } from 'mobx';
+import { Dimmer, Loader, Message } from 'semantic-ui-react';
+import c from 'classnames';
+
+// expected props
+// - form (via props) the mobx form instance
+// - onSuccess (via props) is called once mobx form calls on hooks.onSuccess(), receives (form)
+// - onError (via props) is called once mobx form calls on hooks.onError(), receives (form)
+// - onCancel (via props) receives (form)
+// - dimmer (via props) default to true, set to false if you don't want to use the dimmer (buttons will still be disabled during processing)
+// - className (via props)
+class Form extends React.Component {
+ constructor(props) {
+ super(props);
+ this.formHooks = {
+ onSuccess: this.handleFormSubmission,
+ onError: this.handleFormErrors,
+ };
+
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ }
+
+ getForm() {
+ return this.props.form;
+ }
+
+ getDimmer() {
+ const dimmer = this.props.dimmer;
+ return _.isUndefined(dimmer) ? true : !!dimmer;
+ }
+
+ getOnCancel() {
+ return this.props.onCancel || _.noop;
+ }
+
+ getOnSuccess() {
+ return this.props.onSuccess || _.noop;
+ }
+
+ getOnError() {
+ return this.props.onError || _.noop;
+ }
+
+ getFormErrors() {
+ const form = this.getForm();
+ const errorMap = form.errors() || {};
+ const errors = [];
+ const visit = obj => {
+ if (_.isNil(obj)) return;
+ if (_.isString(obj) && !_.isEmpty(obj)) {
+ errors.push(obj);
+ return;
+ }
+ if (_.isArray(obj) || _.isObject(obj)) {
+ _.forEach(obj, value => {
+ visit(value);
+ });
+ }
+ };
+
+ visit(errorMap);
+ return errors;
+ }
+
+ handleFormSubmission = async form => {
+ const onSuccess = this.getOnSuccess();
+ this.formProcessing = true;
+ try {
+ const result = await onSuccess(form);
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+
+ return result;
+ } catch (error) {
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+
+ throw error;
+ }
+ };
+
+ handleFormErrors = async form => {
+ const onError = this.getOnError();
+ this.formProcessing = false;
+ const errors = this.getFormErrors();
+
+ return onError(form, errors);
+ };
+
+ handleSubmit = event => {
+ const form = this.getForm();
+ event.preventDefault();
+ event.stopPropagation();
+ this.formProcessing = true;
+ try {
+ return form.onSubmit(event, this.formHooks);
+ } catch (error) {
+ this.formProcessing = false;
+ throw error;
+ }
+ };
+
+ handleCancel = event => {
+ const form = this.getForm();
+ const onCancel = this.getOnCancel();
+
+ event.preventDefault();
+ event.stopPropagation();
+ this.formProcessing = false;
+ form.reset();
+ onCancel(form);
+ };
+
+ renderErrorPanel() {
+ const errors = this.getFormErrors();
+ const size = errors.length;
+ if (size === 0) return null;
+ const title = `Please Correct The Following Error${size === 1 ? '' : 's'}`;
+ const toMessage = msg => (_.isObject(msg) ? JSON.stringify(msg) : `${msg}`);
+
+ return (
+
+ {title}
+
+ {_.map(errors, (msg, index) => (
+ {toMessage(msg)}
+ ))}
+
+
+ );
+ }
+
+ render() {
+ const processing = this.formProcessing;
+ const renderer = _.isFunction(this.props.children) ? this.props.children : _.noop;
+ const className = this.props.className;
+ const dimmer = this.getDimmer();
+ const errors = this.getFormErrors();
+
+ return (
+
+ {dimmer && (
+
+ Processing
+
+ )}
+ {this.renderErrorPanel()}
+ {renderer({
+ processing,
+ errors,
+ onSubmit: this.handleSubmit,
+ onCancel: this.handleCancel,
+ })}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(Form, {
+ formProcessing: observable,
+ handleSubmit: action,
+ handleFormSubmission: action,
+ handleFormErrors: action,
+ handleCancel: action,
+});
+
+export default observer(Form);
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Header.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Header.js
new file mode 100644
index 0000000000..d9b9646064
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Header.js
@@ -0,0 +1,36 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Header } from 'semantic-ui-react';
+import c from 'classnames';
+
+// expected props
+// - field (via props), this is the mobx form field object
+// - className (via props)
+const Component = observer(({ field, className = 'mt0 mb1' }) => {
+ const { id, label } = field;
+
+ return (
+
+ );
+});
+
+export default Component;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Input.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Input.js
new file mode 100644
index 0000000000..91c11a1895
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Input.js
@@ -0,0 +1,74 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Input } from 'semantic-ui-react';
+import c from 'classnames';
+
+import Header from './Header';
+import Description from './Description';
+import ErrorPointer from './ErrorPointer';
+
+// expected props
+// - field (via props), this is the mobx form field object
+// - className (via props)
+//
+// The following props are to support existing React Semantic UI props:
+// - fluid (via props), default to true
+// - disabled (via props), default to false
+// - autoFocus (via props), default to false
+// - icon (via props)
+// - iconPosition (via props)
+const Component = observer(
+ ({
+ field,
+ fluid = true,
+ disabled = false,
+ type = 'text',
+ className = 'mb4',
+ autoFocus = false,
+ icon,
+ iconPosition,
+ }) => {
+ const { error = '' } = field;
+ const hasError = !_.isEmpty(error); // IMPORTANT do NOT use field.hasError
+ const isDisabled = field.disabled || disabled;
+ const disabledClass = isDisabled ? 'disabled' : '';
+ const errorClass = hasError ? 'error' : '';
+ const attrs = {
+ fluid,
+ disabled: isDisabled,
+ error: hasError,
+ ..._.omit(field.bind(), ['label']),
+ autoFocus,
+ type,
+ icon,
+ iconPosition,
+ };
+
+ return (
+
+
+
+
+
+
+ );
+ },
+);
+
+export default Component;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/TextArea.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/TextArea.js
new file mode 100644
index 0000000000..3c1eb42d98
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/TextArea.js
@@ -0,0 +1,55 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { TextArea } from 'semantic-ui-react';
+import c from 'classnames';
+
+import Header from './Header';
+import Description from './Description';
+import ErrorPointer from './ErrorPointer';
+
+// expected props
+// - field (via props), this is the mobx form field object
+// - className (via props)
+//
+// The following props are to support existing React Semantic UI props:
+// - rows (via props), number of rows
+// - disabled (via props), default to false
+const Component = observer(({ field, disabled = false, className = 'mb4', rows = 5 }) => {
+ const { error = '' } = field;
+ const hasError = !_.isEmpty(error); // IMPORTANT do NOT use field.hasError
+ const isDisabled = field.disabled || disabled;
+ const disabledClass = isDisabled ? 'disabled' : '';
+ const errorClass = hasError ? 'error' : '';
+ const attrs = {
+ disabled: isDisabled,
+ rows,
+ ..._.omit(field.bind(), ['label']),
+ };
+
+ return (
+
+
+
+
+
+
+ );
+});
+
+export default Component;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Toggle.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Toggle.js
new file mode 100644
index 0000000000..4c920e23c1
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/Toggle.js
@@ -0,0 +1,105 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Icon } from 'semantic-ui-react';
+import c from 'classnames';
+
+import Header from './Header';
+import Description from './Description';
+import ErrorPointer from './ErrorPointer';
+
+// expected props
+// - form (via props)
+// - field (via props), this is the mobx form field object
+// - show (via props), can be 'headerOnly', 'toggleOnly', 'both' (default to 'both')
+// - className (via props)
+//
+// The following props are to support existing React Semantic UI props:
+// - disabled (via props), default to false
+// - size (via props), default to large
+const Component = observer(({ field, disabled = false, show = 'both', className = 'mb4', size = 'large' }) => {
+ const { id, value, sync, error = '', extra = {} } = field;
+ const { yesLabel = 'Yes', noLabel = 'No' } = extra || {};
+ const hasError = !_.isEmpty(error); // IMPORTANT do NOT use field.hasError
+ const isDisabled = field.disabled || disabled;
+ const disabledClass = isDisabled ? 'disabled' : '';
+ const errorClass = hasError ? 'error' : '';
+ const yesSelected = (_.isBoolean(value) && value === true) || value === 'true';
+ const cursor = isDisabled ? 'op-3' : 'cursor-pointer';
+ const handleClick = toAssign => event => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (isDisabled) return;
+ sync(toAssign);
+ field.validate({ showErrors: true });
+ };
+
+ const yesAttributes = {
+ name: 'toggle on',
+ color: hasError ? 'red' : 'blue',
+ size,
+ className: 'mr1',
+ disabled: isDisabled,
+ };
+ const noAttributes = {
+ name: 'toggle off',
+ color: hasError ? 'red' : 'grey',
+ size,
+ className: 'mr1',
+ disabled: isDisabled,
+ };
+
+ const headerOrHeaderAndToggle = show === 'both' || show === 'headerOnly';
+ const headerOnly = show === 'headerOnly';
+ const toggleOnly = show === 'toggleOnly';
+
+ const toggleButton = (
+
+ {yesSelected && (
+
+
+ {yesLabel}
+
+ )}
+ {!yesSelected && (
+
+
+ {noLabel}
+
+ )}
+
+ );
+
+ return (
+
+ {headerOrHeaderAndToggle && (
+ <>
+
+
+ {!headerOnly && toggleButton}
+
+
+
+ >
+ )}
+ {toggleOnly && toggleButton}
+
+ );
+});
+
+export default Component;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/YesNo.js b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/YesNo.js
new file mode 100644
index 0000000000..56d2758bed
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/helpers/fields/YesNo.js
@@ -0,0 +1,81 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Button } from 'semantic-ui-react';
+import c from 'classnames';
+
+import Header from './Header';
+import Description from './Description';
+import ErrorPointer from './ErrorPointer';
+
+// expected props
+// - form (via props)
+// - field (via props), this is the mobx form field object
+// - className (via props)
+//
+// The following props are to support existing React Semantic UI props:
+// - disabled (via props), default to false
+// - size (via props), default to small
+const Component = observer(({ field, disabled = false, size = 'small', className = 'mb4', onClick }) => {
+ const { id, value, sync, error = '', extra = {} } = field;
+ const { yesLabel = 'Yes', noLabel = 'No', yesValue = true, noValue = false, showHeader = true } = extra;
+ const hasError = !_.isEmpty(error); // IMPORTANT do NOT use field.hasError
+ const isDisabled = field.disabled || disabled;
+ const disabledClass = isDisabled ? 'disabled' : '';
+ const errorClass = hasError ? 'error' : '';
+ const yesSelected = value === yesValue;
+ const noSelected = value === noValue;
+ const handleClick = toAssign => event => {
+ event.preventDefault();
+ event.stopPropagation();
+ sync(toAssign);
+ field.validate({ showErrors: true });
+ if (onClick) onClick(toAssign, field);
+ };
+
+ const yesAttributes = {
+ onClick: handleClick(yesValue),
+ disabled: isDisabled,
+ };
+ const noAttributes = {
+ onClick: handleClick(noValue),
+ disabled: isDisabled,
+ };
+
+ if (yesSelected) yesAttributes.color = 'teal';
+ if (noSelected) noAttributes.color = 'teal';
+
+ return (
+
+
+ {showHeader &&
}
+
+
+ {yesLabel}
+
+ {noLabel}
+
+
+
+
+
+
+ );
+});
+
+export default Component;
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/users/AddUser.js b/addons/addon-base-ui/packages/base-ui/src/parts/users/AddUser.js
new file mode 100644
index 0000000000..75b062b067
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/users/AddUser.js
@@ -0,0 +1,256 @@
+/*
+ * 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 { inject, observer } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { decorate, observable, action, runInAction } from 'mobx';
+import { Button, Dimmer, Header, List, Loader, Radio, Segment } from 'semantic-ui-react';
+import _ from 'lodash';
+
+import { getAddUserForm, getAddUserFormFields } from '../../models/forms/AddUserForm';
+import { displayError } from '../../helpers/notification';
+import { createLink } from '../../helpers/routing';
+import validate from '../../models/forms/Validate';
+
+class AddUser extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.formProcessing = false;
+ this.validationErrors = new Map();
+ this.user = {};
+ });
+ this.form = getAddUserForm();
+ this.addUserFormFields = getAddUserFormFields();
+ }
+
+ render() {
+ return (
+
+
+
{this.renderAddUserForm()}
+
+ );
+ }
+
+ renderAddUserForm() {
+ const processing = this.formProcessing;
+ const fields = this.addUserFormFields;
+ const toEditableInput = (attributeName, type = 'text') => {
+ const handleChange = action(event => {
+ event.preventDefault();
+ this.user[attributeName] = event.target.value;
+ });
+ return (
+
+
+
+ );
+ };
+ const toRadioGroupInput = ({
+ attributeName,
+ radioOptions,
+ defaultSelected,
+ isBooleanInput = true,
+ trueValue = 'yes',
+ }) => {
+ const handleChange = () =>
+ action((event, { value }) => {
+ if (isBooleanInput) {
+ this.user[attributeName] = value === trueValue;
+ } else {
+ this.user[attributeName] = value;
+ }
+ event.stopPropagation();
+ });
+ let count = 0;
+ return (
+
+ {_.map(radioOptions, radioOption => {
+ return (
+
+ );
+ })}
+
+ );
+ };
+
+ return (
+
+
+ Checking
+
+
+ {this.renderField('username', toEditableInput('username'))}
+
+
+ {this.renderField('password', toEditableInput('password', 'password'))}
+
+
+ {this.renderField('email', toEditableInput('email', 'email'))}
+
+
+ {this.renderField('firstName', toEditableInput('firstName'))}
+
+
+ {this.renderField('lastName', toEditableInput('lastName'))}
+
+
+ {this.renderField(
+ 'isAdmin',
+ toRadioGroupInput({
+ attributeName: 'isAdmin',
+ defaultSelected: this.user.isAdmin ? 'yes' : 'no',
+ isBooleanInput: true,
+ radioOptions: [
+ { value: 'yes', label: 'Yes' },
+ { value: 'no', label: 'No' },
+ ],
+ }),
+ )}
+
+
+ {this.renderField(
+ 'status',
+ toRadioGroupInput({
+ attributeName: 'status',
+ defaultSelected: this.user.status || 'active',
+ isBooleanInput: false,
+ radioOptions: [
+ { value: 'active', label: 'Active' },
+ { value: 'inactive', label: 'Inactive' },
+ ],
+ }),
+ )}
+
+
+ {this.renderButtons()}
+
+ );
+ }
+
+ renderButtons() {
+ const processing = this.formProcessing;
+ return (
+
+
+ Add User
+
+
+ Cancel
+
+
+ );
+ }
+
+ renderField(name, component) {
+ const fields = this.addUserFormFields;
+ const explain = fields[name].explain;
+ const label = fields[name].label;
+ const hasExplain = !_.isEmpty(explain);
+ const fieldErrors = this.validationErrors.get(name);
+ const hasError = !_.isEmpty(fieldErrors);
+
+ return (
+
+
+ {hasExplain &&
{explain}
}
+
{component}
+ {hasError && (
+
+
+ {_.map(fieldErrors, fieldError => (
+
+ {fieldError}
+
+ ))}
+
+
+ )}
+
+ );
+ }
+
+ goto(pathname) {
+ const location = this.props.location;
+ const link = createLink({ location, pathname });
+ this.props.history.push(link);
+ }
+
+ handleCancel = action(event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.formProcessing = false;
+ this.goto('/users');
+ });
+
+ handleSubmit = action(async () => {
+ this.formProcessing = true;
+ try {
+ // Perform client side validations first
+ const validationResult = await validate(this.user, this.addUserFormFields);
+ // if there are any client side validation errors then do not attempt to make API call
+ if (validationResult.fails()) {
+ runInAction(() => {
+ this.validationErrors = validationResult.errors;
+ this.formProcessing = false;
+ });
+ } else {
+ // There are no client side validation errors so ask the store to add user (which will make API call to server to add the user)
+ await this.getStore().addUser(this.user);
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ this.goto('/users');
+ }
+ } catch (error) {
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ displayError(error);
+ }
+ });
+
+ getStore() {
+ return this.props.usersStore;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(AddUser, {
+ formProcessing: observable,
+ user: observable,
+ validationErrors: observable,
+});
+export default inject('userStore', 'usersStore')(withRouter(observer(AddUser)));
diff --git a/addons/addon-base-ui/packages/base-ui/src/parts/users/UsersList.js b/addons/addon-base-ui/packages/base-ui/src/parts/users/UsersList.js
new file mode 100644
index 0000000000..5abc853a97
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/parts/users/UsersList.js
@@ -0,0 +1,406 @@
+/*
+ * 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 { Button, Container, Header, Icon, Checkbox, Label, Dimmer, Loader, Segment, Radio } from 'semantic-ui-react';
+import { withRouter } from 'react-router-dom';
+import { decorate, action, observable, runInAction } from 'mobx';
+import { getSnapshot } from 'mobx-state-tree';
+import { inject, observer } from 'mobx-react';
+import ReactTable from 'react-table';
+
+import { isStoreError, isStoreLoading, isStoreReady } from '../../models/BaseStore';
+import ErrorBox from '../helpers/ErrorBox';
+import { createLink } from '../../helpers/routing';
+import { displayError } from '../../helpers/notification';
+import BasicProgressPlaceholder from '../helpers/BasicProgressPlaceholder';
+import { swallowError } from '../../helpers/utils';
+
+class UsersList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ // An object that keeps track of which user is being edited
+ // Each key in the object below has key as user's unique id (/)
+ // and value as flag indicating whether to show the editor for the user
+ this.mapOfUsersBeingEdited = {};
+ this.formProcessing = false;
+ });
+ }
+
+ getStore() {
+ return this.props.usersStore;
+ }
+
+ componentDidMount() {
+ const store = this.getStore();
+ swallowError(store.load());
+ }
+
+ handleEditorOn = user =>
+ action(event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ // Get the underlying plain JavaScript object from the "user"
+ // MobX State Tree object using "getSnapshot" function
+ this.mapOfUsersBeingEdited[user.id] = _.assign({ id: user.id }, getSnapshot(user));
+ // The this.mapOfUsersBeingEdited is observable, to make sure the render is triggered update the this.mapOfUsersBeingEdited
+ // reference by reassigning a new reference to this.mapOfUsersBeingEdited
+ this.mapOfUsersBeingEdited = _.clone(this.mapOfUsersBeingEdited);
+ });
+
+ goto(pathname) {
+ const { location, history } = this.props;
+ const link = createLink({ location, pathname });
+ history.push(link);
+ }
+
+ renderNoNonRootAdmins() {
+ return (
+
+
+
+ Brand new data lake
+
+ No admin users in the Data Lake. Please add users in the Data Lake or Configure Authentication Provider then
+ login as a regular non-root User.
+
+
+
+
+ Add Users
+
+
+ Configure Auth Provider
+
+
+
+ );
+ }
+
+ renderHeader() {
+ return (
+
+
+
+
+ Users
+ {this.renderTotal()}
+
+
+
+ Add User
+
+
+ );
+ }
+
+ renderTotal() {
+ const store = this.getStore();
+ if (isStoreError(store) || isStoreLoading(store)) return null;
+ const nonRootUsers = store.nonRootUsers;
+ const count = nonRootUsers.length;
+
+ return {count} ;
+ }
+
+ renderMain() {
+ return this.renderUsers();
+ }
+
+ renderUsers() {
+ // Read "this.mapOfUsersBeingEdited" in the "render" method here
+ // The usersBeingEditedMap is then used in the ReactTable
+ // If we directly use this.mapOfUsersBeingEdited in the ReactTable's cell method, MobX does not
+ // realize that it is being used in the outer component's "render" method's scope
+ // Due to this, MobX does not re-render the component when observable state changes.
+ // To make this work correctly, we need to access "this.mapOfUsersBeingEdited" out side of ReactTable once
+ const usersBeingEditedMap = this.mapOfUsersBeingEdited;
+ // console.log(_.keys(usersBeingEditedMap));
+
+ const store = this.getStore();
+ const nonRootUsers = store.nonRootUsers;
+ // const nonRootUsers = store.list;
+ const pageSize = Math.min(nonRootUsers.length, 50);
+ const showPagination = nonRootUsers.length > pageSize;
+
+ const displayEditableInput = attributeName => row => {
+ const user = row.original;
+ const userBeingEdited = usersBeingEditedMap[user.id];
+ const handleChange = action(event => {
+ event.preventDefault();
+ userBeingEdited[attributeName] = event.target.value;
+ });
+ return userBeingEdited ? (
+
+
+
+ ) : (
+ user[attributeName]
+ );
+ };
+
+ const handleCheckboxChange = (userBeingEdited, attributeName) =>
+ action((event, { checked }) => {
+ userBeingEdited[attributeName] = checked;
+ // update this.mapOfUsersBeingEdited reference to force re-render
+ this.mapOfUsersBeingEdited = _.clone(this.mapOfUsersBeingEdited);
+ event.stopPropagation();
+ });
+ const handleRadioChange = (userBeingEdited, attributeName) =>
+ action((event, { value }) => {
+ userBeingEdited[attributeName] = value;
+ // update this.mapOfUsersBeingEdited reference to force re-render
+ this.mapOfUsersBeingEdited = _.clone(this.mapOfUsersBeingEdited);
+ event.stopPropagation();
+ });
+
+ const booleanColumnValueFilter = (trueString = 'yes', falseString = 'no') => (filter, row) => {
+ const columnValueBoolean = row[filter.id];
+ const columnValueStr = columnValueBoolean ? trueString : falseString;
+ const filterValue = filter.value.toLowerCase();
+ // Allow filtering by typing "yes/no" or "true/false"
+ return (
+ columnValueStr.indexOf(filterValue) === 0 ||
+ String(columnValueBoolean)
+ .toLowerCase()
+ .indexOf(filterValue) === 0
+ );
+ };
+
+ const processing = this.formProcessing;
+
+ if (!store.hasNonRootUsers) {
+ return null;
+ }
+
+ return (
+ // TODO: add api token stats and active flag here in the table
+
+
+ Updating
+
+ {
+ const columnValue = String(row[filter.id]).toLowerCase();
+ const filterValue = filter.value.toLowerCase();
+ return columnValue.indexOf(filterValue) >= 0;
+ }}
+ columns={[
+ {
+ Header: 'User Name',
+ accessor: 'username',
+ },
+ {
+ Header: 'Email',
+ accessor: 'email',
+ Cell: displayEditableInput('email'),
+ },
+ {
+ Header: 'First Name',
+ accessor: 'firstName',
+ Cell: displayEditableInput('firstName'),
+ },
+ {
+ Header: 'Last Name',
+ accessor: 'lastName',
+ Cell: displayEditableInput('lastName'),
+ },
+ {
+ Header: 'Admin',
+ accessor: 'isAdmin',
+ filterMethod: booleanColumnValueFilter(),
+ Cell: row => {
+ const user = row.original;
+ const userBeingEdited = usersBeingEditedMap[user.id];
+ return userBeingEdited ? (
+
+
+
+ ) : user.isAdmin ? (
+
+
+ Yes
+
+ ) : (
+ No
+ );
+ },
+ },
+ {
+ Header: 'Status',
+ accessor: 'isActive',
+ filterMethod: booleanColumnValueFilter('active', 'inactive'),
+ minWidth: 125,
+ Cell: row => {
+ const user = row.original;
+ const userBeingEdited = usersBeingEditedMap[user.id];
+ const isActive = userBeingEdited ? userBeingEdited.status.toLowerCase() === 'active' : row.value;
+ return userBeingEdited ? (
+
+
+
+
+ ) : user.isActive ? (
+
+
+
+ Active
+
+
+ ) : (
+
+ Inactive
+
+ );
+ },
+ },
+ {
+ Header: '',
+ filterable: false,
+ Cell: cell => {
+ const user = cell.original;
+ const userBeingEdited = usersBeingEditedMap[user.id];
+ return userBeingEdited ? (
+
+
+
+
+ ) : (
+
+ );
+ },
+ },
+ ]}
+ />
+
+ );
+ }
+
+ render() {
+ const store = this.getStore();
+ let content;
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreReady(store) && !store.hasNonRootAdmins) {
+ content = this.renderNoNonRootAdmins();
+ } else if (isStoreReady(store) && store.hasNonRootAdmins) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderHeader()}
+ {content}
+
+ );
+ }
+
+ handleSave = user =>
+ action(async event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ this.formProcessing = true;
+
+ try {
+ await this.getStore().updateUser(user);
+ runInAction(() => {
+ this.mapOfUsersBeingEdited[user.id] = undefined;
+ // // The this.mapOfUsersBeingEdited is observable, to make sure the render is triggered update the this.mapOfUsersBeingEdited
+ // // reference by reassigning a new reference to this.mapOfUsersBeingEdited
+ // this.mapOfUsersBeingEdited = _.assign({}, this.mapOfUsersBeingEdited);
+ // this.mapOfUsersBeingEdited = {
+ // [user.id]: undefined,
+ // };
+ this.formProcessing = false;
+ });
+ } catch (err) {
+ runInAction(() => {
+ this.formProcessing = false;
+ });
+ displayError(err);
+ }
+ });
+
+ handleCancel = user =>
+ action(event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.mapOfUsersBeingEdited[user.id] = undefined;
+ // // The this.mapOfUsersBeingEdited is observable, to make sure the render is triggered update the this.mapOfUsersBeingEdited
+ // // reference by reassigning a new reference to this.mapOfUsersBeingEdited
+ this.mapOfUsersBeingEdited = _.clone(this.mapOfUsersBeingEdited);
+ // this.mapOfUsersBeingEdited = {
+ // [user.id]: undefined,
+ // };
+ });
+
+ handleAddUser = () => {
+ this.goto('/users/add');
+ };
+
+ handleAddAuthenticationProvider = () => {
+ this.goto('/authentication-providers');
+ };
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(UsersList, {
+ mapOfUsersBeingEdited: observable,
+ formProcessing: observable,
+});
+
+export default inject('userStore', 'usersStore')(withRouter(observer(UsersList)));
diff --git a/addons/addon-base-ui/packages/base-ui/src/plugins/app-component-plugin.js b/addons/addon-base-ui/packages/base-ui/src/plugins/app-component-plugin.js
new file mode 100644
index 0000000000..3284585f42
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/plugins/app-component-plugin.js
@@ -0,0 +1,34 @@
+/*
+ * 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 App from '../App';
+import AutoLogout from '../parts/AutoLogout';
+
+// eslint-disable-next-line no-unused-vars
+function getAppComponent({ location, appContext }) {
+ return App;
+}
+
+// eslint-disable-next-line no-unused-vars
+function getAutoLogoutComponent({ location, appContext }) {
+ return AutoLogout;
+}
+
+const plugin = {
+ getAppComponent,
+ getAutoLogoutComponent,
+};
+
+export default plugin;
diff --git a/addons/addon-base-ui/packages/base-ui/src/plugins/app-context-items-plugin.js b/addons/addon-base-ui/packages/base-ui/src/plugins/app-context-items-plugin.js
new file mode 100644
index 0000000000..e6ed0e1c98
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/plugins/app-context-items-plugin.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 * as appRunner from '../models/AppRunner';
+import * as appStore from '../models/App';
+import * as cleaner from '../models/Cleaner';
+import * as sessionStore from '../models/SessionStore';
+import * as showdown from '../models/Showdown';
+import * as userApiKeysStore from '../models/api-keys/UserApiKeysStore';
+import * as authentication from '../models/authentication/Authentication';
+import * as authenticationProviderConfigsStore from '../models/authentication/AuthenticationProviderConfigsStore';
+import * as authenticationProviderPublicConfigsStore from '../models/authentication/AuthenticationProviderPublicConfigsStore';
+import * as userDisplayName from '../models/users/UserDisplayName';
+import * as usersStore from '../models/users/UsersStore';
+import * as userStore from '../models/users/UserStore';
+import loginImage from '../../images/login-image.gif';
+
+/**
+ * Registers base stores to the appContext object
+ *
+ * @param appContext An application context object
+ */
+// eslint-disable-next-line no-unused-vars
+function registerAppContextItems(appContext) {
+ appRunner.registerContextItems(appContext);
+ appStore.registerContextItems(appContext);
+ cleaner.registerContextItems(appContext);
+ sessionStore.registerContextItems(appContext);
+ showdown.registerContextItems(appContext);
+ userApiKeysStore.registerContextItems(appContext);
+ authentication.registerContextItems(appContext);
+ authenticationProviderConfigsStore.registerContextItems(appContext);
+ authenticationProviderPublicConfigsStore.registerContextItems(appContext);
+ userDisplayName.registerContextItems(appContext);
+ usersStore.registerContextItems(appContext);
+ userStore.registerContextItems(appContext);
+ appContext.assets.images.loginImage = loginImage;
+}
+
+// eslint-disable-next-line no-unused-vars
+function postRegisterAppContextItems(appContext) {
+ // No impl at this level
+}
+
+const plugin = {
+ registerAppContextItems,
+ postRegisterAppContextItems,
+};
+export default plugin;
diff --git a/addons/addon-base-ui/packages/base-ui/src/plugins/authentication-plugin.js b/addons/addon-base-ui/packages/base-ui/src/plugins/authentication-plugin.js
new file mode 100644
index 0000000000..62cb986609
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/plugins/authentication-plugin.js
@@ -0,0 +1,103 @@
+/*
+ * 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 { logout } from '../helpers/api';
+
+/**
+ * Called when user attempts to login explicitly (i.e., when the user explicitly initiates the login process).
+ * Note that this method is ONLY invoked during explicit login initiation. For example, if the user has logged in before
+ * and during the application initialization, the application detects the login automatically (due to presence of the
+ * authorization token in local storage or url etc) this method is NOT called.
+ *
+ * Also, this method is called upon login attempt BEFORE the login process is actually complete.
+ *
+ * @returns {Promise}
+ */
+async function loginInitiated() {
+ // No-op at the moment
+}
+
+/**
+ * Called when the application detects that the user has logged in explicitly or implicitly.
+ * Note that this is called even for implicit login detection. For example, if the user has logged in before and during
+ * the application initialization, the application detects login automatically (due to presence of the authorization token
+ * in local storage or url etc) this method is still called.
+ *
+ * Also, this method is called AFTER login is detected i.e., after the login process is complete.
+ *
+ * @param explicitLogin A flag indicating whether the login was detected as a result of explicit login attempt.
+ * This flag will NOT be passed in situations where the application cannot determine with certainty if the detected login
+ * was a result of explicit login or implicit login.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function loginDetected({ explicitLogin } = {}) {
+ // No-op at the moment
+}
+
+/**
+ * Called when user attempts to logout explicitly (i.e., when the user explicitly initiates the logout process).
+ * Note that this method is only invoked during explicit logout initiation. For example, if the user has logged out before
+ * and during the application initialization, the application detects that the user has logged out (due to absence of the
+ * authorization token in local storage and url etc) this method is NOT called.
+ *
+ * @param autoLogout A flag indicating whether the logout was explicitly initiated due to an auto-logout action.
+ * For example, if the application explicitly initiates an auto-logout sequence after detecting user inactivity for
+ * certain period.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function logoutInitiated({ autoLogout } = {}) {
+ // Call the logout API
+ await logout();
+}
+
+/**
+ * Called when the application detects that the user has logged out explicitly or implicitly.
+ * Note that this is called even for implicit logout detection. For example, if the user has logged out before
+ * and during the application initialization, the application detects that the user has logged out (due to absence of the
+ * authorization token in local storage and url etc) this method is still called.
+ * (In this sense the "log out detection" is more of "log in NOT detected")
+ *
+ * Also, this method is called AFTER logout is detected i.e., after the logout process is complete
+ * (and the authorization token(s) have been cleared from memory or local storage).
+ *
+ * @param explicitLogout A flag indicating whether the logout was detected as a result of explicit logout attempt.
+ * This flag will NOT be passed in situations where the application cannot determine with certainty if the detected logout
+ * was a result of explicit logout or implicit logout.
+ * @param autoLogout A flag indicating whether the logout was explicitly initiated due to an auto-logout action.
+ * For example, if the application explicitly initiates an auto-logout sequence after detecting user inactivity for
+ * certain period.
+ * This flag will NOT be passed in situations where the application cannot determine with certainty if the detected logout
+ * was a result of explicit auto-logout or implicit logout.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function logoutDetected({ explicitLogout, autoLogout } = {}) {
+ // No-op at the moment
+}
+
+const plugin = {
+ loginInitiated,
+ loginDetected,
+
+ logoutInitiated,
+ logoutDetected,
+};
+
+export default plugin;
diff --git a/addons/addon-base-ui/packages/base-ui/src/plugins/initialization-plugin.js b/addons/addon-base-ui/packages/base-ui/src/plugins/initialization-plugin.js
new file mode 100644
index 0000000000..c52811576d
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/plugins/initialization-plugin.js
@@ -0,0 +1,93 @@
+/*
+ * 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 { setIdToken } from '../helpers/api';
+import { displayWarning } from '../helpers/notification';
+
+const AUTHN_EXTENSION_POINT = 'authentication';
+
+/**
+ * This is where we run the initialization logic that is common across any type of applications.
+ *
+ * @param payload A free form object. This function makes a property named 'tokenInfo' available on this payload object.
+ * @param appContext An application context object containing various Mobx Stores, Models etc.
+ *
+ * @returns {Promise}
+ */
+async function init(payload, appContext) {
+ const { authentication, authenticationProviderPublicConfigsStore, pluginRegistry } = appContext;
+
+ await authenticationProviderPublicConfigsStore.load();
+
+ const tokenInfo = await authentication.getIdTokenInfo();
+ payload.tokenInfo = { ...payload.tokenInfo, ...tokenInfo };
+
+ const { idToken, decodedIdToken } = tokenInfo;
+ if (tokenInfo.status === 'notExpired') {
+ setIdToken(idToken, decodedIdToken);
+ authentication.saveIdToken(idToken);
+ // Set selected authentication provider. This is used during logout
+ authentication.setSelectedAuthenticationProviderId(decodedIdToken.iss);
+
+ // Notify each authentication plugins's 'loginDetected' method since we detected that the user is logged in
+ // (i.e., we have active token).
+ // Note that we are not passing "explicitLogin: true" or "explicitLogin: false"
+ // because we can't determine for sure if this was an explicit login (i.e., the user logged in by clicking login button)
+ // or we have access to the token from memory or local store because the user had logged in the past
+ await pluginRegistry.runPlugins(AUTHN_EXTENSION_POINT, 'loginDetected');
+ } else {
+ // Treat all other cases such as
+ // - if the token was not found (i.e., tokenInfo.status === 'notFound') or
+ // - if the token was corrupted (i.e., tokenInfo.status === 'corrupted') or
+ // - if the token was expired (i.e., tokenInfo.status === 'expired') etc as NOT-logged in.
+ // Currently the application treats "not logged in detected" as same as "logout detected" so notify all
+ // authentication plugins of 'logoutDetected'
+ // Note that we are not passing "explicitLogout: true" or "explicitLogout: false"
+ // because we can't determine for sure if this was an explicit logout or implicit (i.e., we could not find active token)
+ // Same way we are not passing 'autoLogout' flag because we can't determine for sure if this was an explicit logout
+ // by user or application code due to as auto-logout (e.g., due to user inactivity)
+ await pluginRegistry.runPlugins(AUTHN_EXTENSION_POINT, 'logoutDetected');
+ }
+}
+
+/**
+ * This is where we run the post initialization logic that is common across any type of applications.
+ *
+ * @param payload A free form object. This function expects a property named 'tokenInfo' to be available on the payload object.
+ * @param appContext An application context object containing various Mobx Stores, Models etc.
+ *
+ * @returns {Promise}
+ */
+async function postInit(payload, appContext) {
+ const tokenNotExpired = _.get(payload, 'tokenInfo.status') === 'notExpired';
+ if (!tokenNotExpired) return; // Continue only if we have a token that is not expired
+
+ const userStore = appContext.userStore;
+ await userStore.load();
+
+ const isRootUser = userStore.user.isRootUser;
+ if (isRootUser) {
+ displayWarning('You have logged in as root user. Logging in as root user is discouraged.');
+ }
+}
+
+const plugin = {
+ init,
+ postInit,
+};
+
+export default plugin;
diff --git a/addons/addon-base-ui/packages/base-ui/src/plugins/menu-items-plugin.js b/addons/addon-base-ui/packages/base-ui/src/plugins/menu-items-plugin.js
new file mode 100644
index 0000000000..59e8a43277
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/plugins/menu-items-plugin.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 _ from 'lodash';
+/**
+ * Adds base navigation menu items to the given itemsMap.
+ *
+ * @param itemsMap A Map containing navigation items. This object is a Map that has route paths (urls) as
+ * keys and menu item object with the following shape
+ *
+ * {
+ * title: STRING, // Title for the navigation menu item
+ * icon: STRING, // semantic ui icon name fot the navigation menu item
+ * shouldShow: BOOLEAN || FUNCTION, // A flag or a function that returns a flag indicating whether to show the item or not (useful when showing menu items conditionally)
+ * render: OPTIONAL FUNCTION, // Optional function that returns rendered menu item component. Use this ONLY if you want to control full rendering of the menu item.
+ * }
+ *
+ * @param appContext An application context object containing various Mobx Stores, Models etc.
+ *
+ * @returns Map<*> Returns A Map containing navigation menu items with the same shape as "itemsMap"
+ */
+// eslint-disable-next-line no-unused-vars
+function registerMenuItems(itemsMap, { location, appContext }) {
+ const isAdmin = _.get(appContext, 'userStore.user.isAdmin');
+ const items = new Map([
+ ...itemsMap,
+ ['/dashboard', { title: 'Dashboard', icon: 'dashboard', shouldShow: true }],
+ ['/authentication-providers', { title: 'Auth', icon: 'user secret', shouldShow: isAdmin }],
+ ['/users', { title: 'Users', icon: 'users', shouldShow: isAdmin }],
+ ['/api-keys', { title: 'API Keys', icon: 'key', shouldShow: true }],
+ ]);
+
+ return items;
+}
+const plugin = {
+ registerMenuItems,
+};
+export default plugin;
diff --git a/addons/addon-base-ui/packages/base-ui/src/plugins/routes-plugin.js b/addons/addon-base-ui/packages/base-ui/src/plugins/routes-plugin.js
new file mode 100644
index 0000000000..a4866ec129
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/plugins/routes-plugin.js
@@ -0,0 +1,76 @@
+/*
+ * 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 Dashboard from '../parts/dashboard/Dashboard';
+import AddAuthenticationProvider from '../parts/authentication-providers/AddAuthenticationProvider';
+import EditAuthenticationProvider from '../parts/authentication-providers/EditAuthenticationProvider';
+import AuthenticationProvidersList from '../parts/authentication-providers/AuthenticationProvidersList';
+import ApiKeysList from '../parts/api-keys/ApiKeysList';
+import AddUser from '../parts/users/AddUser';
+import UsersList from '../parts/users/UsersList';
+import withAuth from '../withAuth';
+
+/**
+ * Adds base routes to the given routesMap.
+ * @param routesMap A Map containing routes. This object is a Map that has route paths as
+ * keys and React Component as value.
+ * @param appContext An application context object containing various Mobx Stores, Models etc.
+ *
+ * @returns {Promise<*>} Returns a Map with the mapping of base routes vs React Component
+ */
+// eslint-disable-next-line no-unused-vars
+function registerRoutes(routesMap, { location, appContext }) {
+ const routes = new Map([
+ ...routesMap,
+ ['/authentication-providers/add', withAuth(AddAuthenticationProvider)],
+ ['/authentication-providers/:authenticationProviderConfigId/edit', withAuth(EditAuthenticationProvider)],
+ ['/authentication-providers', withAuth(AuthenticationProvidersList)],
+ ['/api-keys', withAuth(ApiKeysList)],
+ ['/users/add', withAuth(AddUser)],
+ ['/users', withAuth(UsersList)],
+ ['/dashboard', withAuth(Dashboard)],
+ ]);
+
+ return routes;
+}
+
+/**
+ * Returns default route. By default this method returns the
+ * '/dashboard' route as the default route for all non-root users and returns
+ * '/users' route for root user.
+ * @returns {{search: *, state: *, hash: *, pathname: string}}
+ */
+function getDefaultRouteLocation({ location, appContext }) {
+ const userStore = appContext.userStore;
+ // See https://reacttraining.com/react-router/web/api/withRouter
+ const isRootUser = _.get(userStore, 'user.isRootUser');
+ const defaultLocation = {
+ pathname: isRootUser ? '/users' : '/dashboard',
+ search: location.search, // we want to keep any query parameters
+ hash: location.hash,
+ state: location.state,
+ };
+
+ return defaultLocation;
+}
+
+const plugin = {
+ registerRoutes,
+ getDefaultRouteLocation,
+};
+
+export default plugin;
diff --git a/addons/addon-base-ui/packages/base-ui/src/render-utils.js b/addons/addon-base-ui/packages/base-ui/src/render-utils.js
new file mode 100644
index 0000000000..abd2f10aca
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/render-utils.js
@@ -0,0 +1,70 @@
+/*
+ * 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 ReactDOM from 'react-dom';
+import { Provider } from 'mobx-react';
+import { BrowserRouter } from 'react-router-dom';
+import { Message, Icon, Container } from 'semantic-ui-react';
+
+// Render the AppContainer component which will then ask plugins to provide the App component
+function renderAppContainer(AppContainer, appContext) {
+ ReactDOM.render(
+
+
+
+
+ ,
+ document.getElementById('root'),
+ );
+}
+
+// Render a progress message
+function renderProgress(
+ progressContent = (
+
+ Just one second
+ Great things are now happening, please wait!
+
+ ),
+) {
+ ReactDOM.render(
+
+
+
+ {progressContent}
+
+ ,
+ document.getElementById('root'),
+ );
+}
+
+// Render an error message
+function renderError(err) {
+ const error = _.get(err, 'message', 'Unknown error');
+ ReactDOM.render(
+
+
+ We have a problem
+ {error}
+ See if refreshing the browser will resolve your issue
+
+ ,
+ document.getElementById('root'),
+ );
+}
+
+export { renderAppContainer, renderProgress, renderError };
diff --git a/addons/addon-base-ui/packages/base-ui/src/service-worker.js b/addons/addon-base-ui/packages/base-ui/src/service-worker.js
new file mode 100644
index 0000000000..57b7959a88
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/service-worker.js
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+// This optional code is used to register a service worker.
+// register() is not called by default.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on subsequent visits to a page, after all the
+// existing tabs open on the page have been closed, since previously cached
+// resources are updated in the background.
+
+// To learn more about the benefits of this model and instructions on how to
+// opt-in, read https://bit.ly/CRA-PWA
+
+const isLocalhost = Boolean(
+ window.location.hostname === 'localhost' ||
+ // [::1] is the IPv6 localhost address.
+ window.location.hostname === '[::1]' ||
+ // 127.0.0.1/8 is considered localhost for IPv4.
+ window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/),
+);
+
+export function register(config) {
+ if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+ // The URL constructor is available in all browsers that support SW.
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
+ if (publicUrl.origin !== window.location.origin) {
+ // Our service worker won't work if PUBLIC_URL is on a different origin
+ // from what our page is served on. This might happen if a CDN is used to
+ // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+ return;
+ }
+
+ window.addEventListener('load', () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+ if (isLocalhost) {
+ // This is running on localhost. Let's check if a service worker still exists or not.
+ checkValidServiceWorker(swUrl, config);
+
+ // Add some additional logging to localhost, pointing developers to the
+ // service worker/PWA documentation.
+ navigator.serviceWorker.ready.then(() => {
+ console.log(
+ 'This web app is being served cache-first by a service ' +
+ 'worker. To learn more, visit https://bit.ly/CRA-PWA',
+ );
+ });
+ } else {
+ // Is not localhost. Just register service worker
+ registerValidSW(swUrl, config);
+ }
+ });
+ }
+}
+
+function registerValidSW(swUrl, config) {
+ navigator.serviceWorker
+ .register(swUrl)
+ .then(registration => {
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing;
+ if (installingWorker == null) {
+ return;
+ }
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the updated precached content has been fetched,
+ // but the previous service worker will still serve the older
+ // content until all client tabs are closed.
+ console.log(
+ 'New content is available and will be used when all ' +
+ 'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
+ );
+
+ // Execute callback
+ if (config && config.onUpdate) {
+ config.onUpdate(registration);
+ }
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log('Content is cached for offline use.');
+
+ // Execute callback
+ if (config && config.onSuccess) {
+ config.onSuccess(registration);
+ }
+ }
+ }
+ };
+ };
+ })
+ .catch(error => {
+ console.error('Error during service worker registration:', error);
+ });
+}
+
+function checkValidServiceWorker(swUrl, config) {
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl)
+ .then(response => {
+ // Ensure service worker exists, and that we really are getting a JS file.
+ const contentType = response.headers.get('content-type');
+ if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then(registration => {
+ registration.unregister().then(() => {
+ window.location.reload();
+ });
+ });
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl, config);
+ }
+ })
+ .catch(() => {
+ console.log('No internet connection found. App is running in offline mode.');
+ });
+}
+
+export function unregister() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready.then(registration => {
+ registration.unregister();
+ });
+ }
+}
diff --git a/addons/addon-base-ui/packages/base-ui/src/withAuth.js b/addons/addon-base-ui/packages/base-ui/src/withAuth.js
new file mode 100644
index 0000000000..9a98418aeb
--- /dev/null
+++ b/addons/addon-base-ui/packages/base-ui/src/withAuth.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 React from 'react';
+import { inject, observer } from 'mobx-react';
+import DefaultLoginScreen from './parts/Login';
+
+class Wrapper extends React.Component {
+ renderLogin() {
+ return this.props.loginComp;
+ }
+
+ renderAuthenticated() {
+ const Comp = this.props.Comp;
+ const props = this.getWrappedCompProps({ authenticated: true });
+ return ;
+ }
+
+ render() {
+ const app = this.props.app;
+ let content = null;
+
+ if (app.userAuthenticated) {
+ content = this.renderAuthenticated();
+ } else {
+ content = this.renderLogin();
+ }
+
+ return content;
+ }
+
+ // private utility methods
+ getWrappedCompProps(additionalProps) {
+ const props = { ...this.props, ...additionalProps };
+ delete props.Comp;
+ delete props.loginComp;
+ return props;
+ }
+}
+
+const WrapperComp = inject('app', 'assets')(observer(Wrapper));
+
+function withAuth(Comp, { loginComp } = { loginComp: }) {
+ return function component(props) {
+ return ;
+ };
+}
+
+export default withAuth;
diff --git a/addons/addon-base-ui/packages/serverless-ui-tools/.eslintrc.json b/addons/addon-base-ui/packages/serverless-ui-tools/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/addons/addon-base-ui/packages/serverless-ui-tools/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-ui/packages/serverless-ui-tools/.gitignore b/addons/addon-base-ui/packages/serverless-ui-tools/.gitignore
new file mode 100644
index 0000000000..cd2b1adc7a
--- /dev/null
+++ b/addons/addon-base-ui/packages/serverless-ui-tools/.gitignore
@@ -0,0 +1,13 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/addons/addon-base-ui/packages/serverless-ui-tools/.prettierrc.json b/addons/addon-base-ui/packages/serverless-ui-tools/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-ui/packages/serverless-ui-tools/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-ui/packages/serverless-ui-tools/README.md b/addons/addon-base-ui/packages/serverless-ui-tools/README.md
new file mode 100644
index 0000000000..252b2b6584
--- /dev/null
+++ b/addons/addon-base-ui/packages/serverless-ui-tools/README.md
@@ -0,0 +1,72 @@
+## Prerequisites
+
+#### Tools
+
+- Node 12+
+- AWS CLI
+
+#### Project variables
+
+This plugin expects the following variables to exist in `${self:custom.settings}`:
+
+- `websiteBucketName` - The S3 bucket to deploy to
+- `websiteCloudFrontDistributionId` - (_Optional_) if you have a CloudFront distribution and wish to use the `--invalidate-cache` flag
+
+This plugin also expects any number of `${self:custom.envTemplate`} environment variable mappings to exist. Local-specific overrides should be nested inside a `LocalOverrides` key. For example:
+
+```yaml
+# ========================================================================
+# Variables shared between .env.local and .env.production
+# ========================================================================
+
+REACT_APP_LOCAL_DEV: false
+REACT_APP_AWS_REGION: ${self:custom.settings.awsRegion}
+
+# ========================================================================
+# Overrides for .env.local
+# ========================================================================
+
+localOverrides:
+ REACT_APP_LOCAL_DEV: true
+ REACT_APP_API_URL: 'http://localhost:3000'
+```
+
+## Usage
+
+When installed as a [Serverless plugin](https://serverless.com/framework/docs/providers/aws/guide/plugins/), this provides the following CLI commands:
+
+### `sls package-ui [--local]`
+
+Packages the UI, ready for deployment. For Create React App (the only supported UI provider at present), this also generates an `.env.local` and `.env.production` environment files (depending whether the `--local` flag is set).
+
+#### `--local=true`
+
+If enabled, `$ npm run build` is not executed.
+
+- Default: false
+
+---
+
+### `sls deploy-ui [--build-dir]`
+
+Deploys (via `aws s3 sync`) a target directory to the `WebsiteBucketName` bucket.
+
+#### `--build-dir`
+
+Specify the directory containing UI code to be deployed to S3.
+
+- Default: `build/`
+
+#### `--invalidate-cache=true`
+
+If enabled, invalidates the entire CloudFront distribution after deploying (only if the bucket was modified). Requires a `websiteCloudFrontId` setting to be specified.
+
+- Default: false
+
+---
+
+### `sls start-ui`
+
+Serves the UI over a local development server.
+
+---
diff --git a/addons/addon-base-ui/packages/serverless-ui-tools/index.js b/addons/addon-base-ui/packages/serverless-ui-tools/index.js
new file mode 100644
index 0000000000..2e69d96686
--- /dev/null
+++ b/addons/addon-base-ui/packages/serverless-ui-tools/index.js
@@ -0,0 +1,247 @@
+/*
+ * 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.
+ */
+
+const fs = require('fs');
+
+const _ = require('lodash');
+const aws = require('aws-sdk');
+const chalk = require('chalk');
+
+const { toLines } = require('./lib/utils/env.js');
+const { runCommand } = require('./lib/utils/command.js');
+
+class ServerlessUIToolsPlugin {
+ constructor(serverless, options) {
+ this.serverless = serverless;
+ this.options = options;
+
+ this.commands = {
+ 'deploy-ui': {
+ usage: 'Deploys (via "aws s3 sync") a target directory to the `websiteBucketName` bucket.',
+ lifecycleEvents: ['deploy', 'invalidate-cache'],
+ options: {
+ 'build-dir': {
+ usage: 'Specify the directory containing UI code to be deployed to S3.',
+ default: 'build/',
+ },
+ 'invalidate-cache': {
+ usage:
+ 'If enabled, invalidates the entire CloudFront distribution after deploying (only if the bucket was modified). Requires a `websiteCloudFrontId` setting to be specified.',
+ default: false,
+ },
+ },
+ },
+ 'package-ui': {
+ usage:
+ 'Packages the UI, ready for deployment. For Create React App (the default and only supported UI provider at present), this also generates ' +
+ 'an `.env.local` and `.env.production` environment file (depending whether the "--local" flag is set).',
+ lifecycleEvents: ['write-env', 'build'],
+ options: {
+ local: {
+ usage: 'If enabled, "$ pnpm run build" is not executed.',
+ default: false,
+ },
+ },
+ },
+ 'start-ui': {
+ usage: 'Serves the UI over a local development server',
+ lifecycleEvents: ['start'],
+ options: {},
+ },
+ };
+
+ this.hooks = {
+ 'deploy-ui:deploy': this.deploy.bind(this),
+ 'deploy-ui:invalidate-cache': this.invalidateCache.bind(this),
+ 'package-ui:write-env': this.writeEnv.bind(this),
+ 'package-ui:build': this.build.bind(this),
+ 'start-ui:start': this.startUI.bind(this),
+ };
+
+ this.cli = {
+ raw(message) {
+ serverless.cli.consoleLog(chalk.dim(message));
+ },
+ log(message) {
+ serverless.cli.consoleLog(`[serverless-ui-tools] ${chalk.yellowBright(message)}`);
+ },
+ };
+ }
+
+ getCloudFront() {
+ const profile = this.serverless.service.custom.settings.awsProfile;
+ const region = this.serverless.service.custom.settings.awsRegion;
+
+ aws.config.update({
+ maxRetries: 3,
+ region,
+ sslEnabled: true,
+ });
+
+ // if a an AWS SDK profile has been configured, use its credentials
+ if (profile) {
+ const credentials = new aws.SharedIniFileCredentials({ profile });
+ aws.config.update({ credentials });
+ }
+ return new aws.CloudFront();
+ }
+
+ async deploy() {
+ const bucketName = this.serverless.service.custom.settings.websiteBucketName;
+ let buildDir = this.options['build-dir'];
+ // Ensure trailing slash
+ if (buildDir.substr(-1) !== '/') {
+ buildDir += '/';
+ }
+
+ this.cli.log(`Deploying UI in ${buildDir} to ${bucketName} S3 bucket...`);
+
+ this.mutatedBucket = false;
+ try {
+ const awsProfile = this.serverless.service.custom.settings.awsProfile;
+ const args = [
+ 's3',
+ 'sync',
+ buildDir,
+ `s3://${bucketName}`,
+ '--delete',
+ '--region',
+ this.serverless.service.custom.settings.awsRegion,
+ ];
+ if (awsProfile) {
+ args.push('--profile', awsProfile);
+ }
+ await runCommand({
+ command: 'aws',
+ args,
+ stdout: {
+ log: this.cli.log,
+ raw: msg => {
+ this.mutatedBucket = true; // If external command outputs to stdout.raw then we probably mutated the bucket
+ this.cli.raw(msg);
+ },
+ },
+ });
+ } catch (err) {
+ throw new Error(`Error running "aws s3 sync": ${err}`);
+ }
+ }
+
+ writeEnv() {
+ this.cli.log('Reading from ${self:custom.envTemplate}...'); // eslint-disable-line
+
+ // ==== Load template
+ let env = { ...this.serverless.service.custom.envTemplate };
+ if (_.isEmpty(env)) {
+ throw new Error('custom.envTemplate must be defined');
+ }
+
+ // ==== Apply local-only overrides
+ const isLocal = this.options.local;
+ if (isLocal) {
+ this.cli.log('Applying local overrides:\n');
+ this.cli.raw(`${toLines(env.localOverrides)}\n`);
+ env = { ...env, ...env.localOverrides };
+ }
+
+ // ==== Write CRA environment file
+ const fileName = isLocal ? '.env.local' : '.env.production';
+ const text = toLines(env);
+ const comment = `# GENERATED BY the "deploy-ui" command. Please don't edit this file nor commit this file to git.\
+ \n# If you need to update its content, run the "deploy-ui" command again.\
+ \n# ${new Date()}`;
+ const content = `${comment}\n\n${text}`;
+
+ this.cli.log(`Writing Create React App environment file "${fileName}" with the following content:\n`);
+ this.cli.raw(`${content}\n`);
+
+ fs.writeFileSync(fileName, content);
+ }
+
+ async invalidateCache() {
+ const distributionId = this.serverless.service.custom.settings.websiteCloudFrontId;
+ const shouldInvalidate = this.options['invalidate-cache'];
+ if (shouldInvalidate && !distributionId) {
+ throw Error('You specified "--invalidate-cache", but `websiteCloudFrontId` setting was not found');
+ }
+
+ if (shouldInvalidate && this.mutatedBucket) {
+ this.cli.log('Invalidating CloudFront distribution cache...');
+ const sdk = this.getCloudFront();
+
+ try {
+ const invalidation = await sdk
+ .createInvalidation({
+ DistributionId: distributionId,
+ InvalidationBatch: {
+ CallerReference: Date.now().toString(),
+ Paths: {
+ Quantity: 1,
+ Items: ['/*'], // Invalidate all files
+ },
+ },
+ })
+ .promise();
+
+ this.cli.log(
+ `Created new CloudFront invalidation: id=${invalidation.Invalidation.Id}, status=${invalidation.Invalidation.Status}, paths="/*"`,
+ );
+ } catch (err) {
+ throw new Error(`Error invalidating CloudFront ${distributionId} cache: ${err}`);
+ }
+ }
+
+ if (this.mutatedBucket) {
+ this.cli.log('UI deployed successfully');
+ } else {
+ this.cli.log('Nothing new to deploy. Did you forget to call "package-ui"? Skipping deployment...');
+ }
+ }
+
+ async build() {
+ const isLocal = this.options.local;
+ if (!isLocal) {
+ this.cli.log('Running "pnpm build"...');
+
+ try {
+ await runCommand({
+ command: 'pnpm',
+ args: ['run', 'build'],
+ stdout: this.cli,
+ });
+ } catch (err) {
+ throw new Error(`Error running "pnpm build": ${err}`);
+ }
+ }
+
+ this.cli.log('UI packaged successfully');
+ }
+
+ async startUI() {
+ this.cli.log('Running "pnpm start"...');
+
+ try {
+ await runCommand({
+ command: 'pnpm',
+ args: ['run', 'start'],
+ stdout: this.cli,
+ });
+ } catch (err) {
+ throw new Error(`Error running "pnpm start": ${err}`);
+ }
+ }
+}
+
+module.exports = ServerlessUIToolsPlugin;
diff --git a/addons/addon-base-ui/packages/serverless-ui-tools/jest.config.js b/addons/addon-base-ui/packages/serverless-ui-tools/jest.config.js
new file mode 100644
index 0000000000..60c221fc1d
--- /dev/null
+++ b/addons/addon-base-ui/packages/serverless-ui-tools/jest.config.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-ui/packages/serverless-ui-tools/lib/utils/command.js b/addons/addon-base-ui/packages/serverless-ui-tools/lib/utils/command.js
new file mode 100644
index 0000000000..023bc5ed09
--- /dev/null
+++ b/addons/addon-base-ui/packages/serverless-ui-tools/lib/utils/command.js
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+/**
+ * Helpers for executing an external command.
+ * */
+const _ = require('lodash');
+const chalk = require('chalk');
+const spawn = require('cross-spawn');
+
+// to help avoid unleashing Zalgo see http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony
+const callLater = (callback, ...args) => {
+ setImmediate(() => {
+ callback(...args);
+ });
+};
+
+const runCommand = ({ command, args, successCodes = [0], cwd, stdout }) => {
+ const child = spawn(command, args, { stdio: 'pipe', cwd });
+ stdout.log(`${chalk.bgGreen('>>')} ${command} ${args.join(' ')}`);
+
+ return new Promise((resolve, reject) => {
+ // we are using _.once() because the error and exit events might be fired one after the other
+ // see https://nodejs.org/api/child_process.html#child_process_event_error
+ const rejectOnce = _.once(reject);
+ const resolveOnce = _.once(resolve);
+ const errors = [];
+
+ child.stdout.on('data', data => {
+ stdout.raw(data);
+ });
+
+ child.stderr.on('data', data => {
+ errors.push(data.toString().trim());
+ });
+
+ child.on('exit', code => {
+ if (successCodes.includes(code)) {
+ callLater(resolveOnce);
+ } else {
+ callLater(rejectOnce, new Error(`process exited with code ${code}: ${errors.join('\n')}`));
+ }
+ });
+ child.on('error', () => callLater(rejectOnce, new Error('Failed to start child process.')));
+ });
+};
+
+module.exports = { runCommand };
diff --git a/addons/addon-base-ui/packages/serverless-ui-tools/lib/utils/env.js b/addons/addon-base-ui/packages/serverless-ui-tools/lib/utils/env.js
new file mode 100644
index 0000000000..31109c143d
--- /dev/null
+++ b/addons/addon-base-ui/packages/serverless-ui-tools/lib/utils/env.js
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+// Convert from {'REACT_APP_FOO': 'bar'} to REACT_APP_FOO=bar
+const toLines = map => {
+ // Filter out nested objects
+ const flatMap = _.pickBy(map, v => !_.isObject(v));
+ // Convert to key-value pairs
+ const lines = _.map(flatMap, (value, key) => `${key}=${value}`);
+ // Separate by newlines
+ return lines.join('\n');
+};
+
+module.exports = { toLines };
diff --git a/addons/addon-base-ui/packages/serverless-ui-tools/package.json b/addons/addon-base-ui/packages/serverless-ui-tools/package.json
new file mode 100644
index 0000000000..590c178554
--- /dev/null
+++ b/addons/addon-base-ui/packages/serverless-ui-tools/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@aws-ee/base-serverless-ui-tools",
+ "version": "1.0.0",
+ "private": true,
+ "description": "A serverless framework plugin to help with packaging and deploying a UI",
+ "author": "aws-ee",
+ "main": "index.js",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "aws-sdk": "^2.647.0",
+ "chalk": "^2.4.2",
+ "cross-spawn": "^7.0.1",
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-workflow-api/README.md b/addons/addon-base-workflow-api/README.md
new file mode 100644
index 0000000000..df6ffe3c3d
--- /dev/null
+++ b/addons/addon-base-workflow-api/README.md
@@ -0,0 +1,25 @@
+# Base Workflow API Add-On
+
+This add-on introduces the rest API for workflow functionality.
+
+The following sections list the add-on contribution.
+
+## npm packages
+
+- @aws-ee/base-workflow-api
+
+## Runtime extension points
+- Used
+ - 'services'
+ - 'routes'
+
+## REST APIs
+- /api/step-templates
+- /api/workflow-templates
+- /api/workflows
+
+## Dependencies
+
+- base Add-on
+- Base Workflow Add-on
+
diff --git a/addons/addon-base-workflow-api/packages/base-worklfow-api/.eslintrc.json b/addons/addon-base-workflow-api/packages/base-worklfow-api/.eslintrc.json
new file mode 100644
index 0000000000..c700cf03a8
--- /dev/null
+++ b/addons/addon-base-workflow-api/packages/base-worklfow-api/.eslintrc.json
@@ -0,0 +1,26 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "import/no-unresolved": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-workflow-api/packages/base-worklfow-api/.gitignore b/addons/addon-base-workflow-api/packages/base-worklfow-api/.gitignore
new file mode 100644
index 0000000000..f2fb153198
--- /dev/null
+++ b/addons/addon-base-workflow-api/packages/base-worklfow-api/.gitignore
@@ -0,0 +1,19 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+/coverage/
+.build
diff --git a/addons/addon-base-workflow-api/packages/base-worklfow-api/.prettierrc.json b/addons/addon-base-workflow-api/packages/base-worklfow-api/.prettierrc.json
new file mode 100644
index 0000000000..d3846d96f3
--- /dev/null
+++ b/addons/addon-base-workflow-api/packages/base-worklfow-api/.prettierrc.json
@@ -0,0 +1,16 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all",
+ "overrides": [
+ {
+ "files": ["*.yml", "*.yaml"],
+ "options": {
+ "singleQuote": false
+ }
+ }
+ ]
+}
+
diff --git a/addons/addon-base-workflow-api/packages/base-worklfow-api/jest.config.js b/addons/addon-base-workflow-api/packages/base-worklfow-api/jest.config.js
new file mode 100644
index 0000000000..70544476d7
--- /dev/null
+++ b/addons/addon-base-workflow-api/packages/base-worklfow-api/jest.config.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+};
diff --git a/addons/addon-base-workflow-api/packages/base-worklfow-api/jsconfig.json b/addons/addon-base-workflow-api/packages/base-worklfow-api/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-workflow-api/packages/base-worklfow-api/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/controllers/step-template-controller.js b/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/controllers/step-template-controller.js
new file mode 100644
index 0000000000..5f0f10403f
--- /dev/null
+++ b/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/controllers/step-template-controller.js
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+
+ const stepTemplateService = await context.service('stepTemplateService');
+
+ // ===============================================================
+ // GET / (mounted to /api/step-templates)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const result = await stepTemplateService.listVersions();
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /latest (mounted to /api/step-templates)
+ // ===============================================================
+ router.get(
+ '/latest',
+ wrap(async (req, res) => {
+ const result = await stepTemplateService.list();
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id (mounted to /api/step-templates)
+ // ===============================================================
+ router.get(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+
+ const result = await stepTemplateService.listVersions({ id });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/latest (mounted to /api/step-templates)
+ // ===============================================================
+ router.get(
+ '/:id/latest',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+
+ const result = await stepTemplateService.mustFindVersion({ id });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/v/:v (mounted to /api/step-templates)
+ // ===============================================================
+ router.get(
+ '/:id/v/:v',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const v = req.params.v;
+
+ const result = await stepTemplateService.mustFindVersion({ id, v });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST /:id/v/:v/validate (mounted to /api/step-templates)
+ // ===============================================================
+ router.post(
+ '/:id/v/:v/validate',
+ wrap(async (req, res) => {
+ const {
+ params: { id, v },
+ body: config = {},
+ } = req;
+
+ const result = await stepTemplateService.mustValidateVersion({ id, v, config });
+ res.status(200).json(result);
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/controllers/workflow-controller.js b/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/controllers/workflow-controller.js
new file mode 100644
index 0000000000..810f5fad84
--- /dev/null
+++ b/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/controllers/workflow-controller.js
@@ -0,0 +1,264 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ const boom = context.boom;
+ const settings = context.settings;
+
+ const workflowService = await context.service('workflowService');
+ const workflowDraftService = await context.service('workflowDraftService');
+ const workflowInstanceService = await context.service('workflowInstanceService');
+ const workflowTriggerService = await context.service('workflowTriggerService');
+ const workflowAssignmentService = await context.service('workflowAssignmentService');
+
+ // ===============================================================
+ // POST /:id/v/:v/trigger (mounted to /api/workflows)
+ // ===============================================================
+ router.post(
+ '/:id/v/:v/trigger',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const vStr = req.params.v;
+ const input = _.get(req.body, 'input');
+ const meta = _.get(req.body, 'meta', {});
+ const requestContext = res.locals.requestContext;
+
+ meta.workflowId = id;
+ meta.workflowVer = parseInt(vStr, 10);
+ meta.smWorkflow = settings.get('smWorkflow');
+
+ const result = await workflowTriggerService.triggerWorkflow(requestContext, meta, input);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/v/:v/instances (mounted to /api/workflows)
+ // ===============================================================
+ router.get(
+ '/:id/v/:v/instances',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const v = req.params.v;
+
+ const result = await workflowInstanceService.list({ workflowId: id, workflowVer: v });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/v/:v/instances/:instanceId (mounted to /api/workflows)
+ // ===============================================================
+ router.get(
+ '/:id/v/:v/instances/:instanceId',
+ wrap(async (req, res) => {
+ const instanceId = req.params.instanceId;
+
+ const result = await workflowInstanceService.mustFindInstance({ id: instanceId });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST /instances/status (mounted to /api/workflows)
+ // ===============================================================
+ router.post(
+ '/instances/status/:status',
+ wrap(async (req, res) => {
+ const status = req.params.status;
+ const { startTime, endTime } = req.body;
+ const result = await workflowInstanceService.listByStatus({
+ status,
+ startTime,
+ endTime,
+ });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/assignments (mounted to /api/workflows)
+ // ===============================================================
+ router.get(
+ '/:id/assignments',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const id = req.params.id;
+
+ const result = await workflowAssignmentService.listByWorkflow(requestContext, { workflowId: id });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /drafts (mounted to /api/workflows)
+ // ===============================================================
+ router.get(
+ '/drafts',
+ wrap(async (_req, res) => {
+ const requestContext = res.locals.requestContext;
+ const principalIdentifier = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+
+ const result = await workflowDraftService.list({ principalIdentifier });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET / (mounted to /api/workflows)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (_req, res) => {
+ const result = await workflowService.listVersions();
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /latest (mounted to /api/workflows)
+ // ===============================================================
+ router.get(
+ '/latest',
+ wrap(async (_req, res) => {
+ const result = await workflowService.list();
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id (mounted to /api/workflows)
+ // ===============================================================
+ router.get(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+
+ const result = await workflowService.listVersions({ id });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/latest (mounted to /api/workflows)
+ // ===============================================================
+ router.get(
+ '/:id/latest',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+
+ const result = await workflowService.mustFindVersion({ id });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/v/:v (mounted to /api/workflows)
+ // ===============================================================
+ router.get(
+ '/:id/v/:v',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const v = req.params.v;
+
+ const result = await workflowService.mustFindVersion({ id, v });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST /:id/v/ (mounted to /api/workflows)
+ // ===============================================================
+ router.post(
+ '/:id/v/',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+ const manifest = req.body;
+
+ if (manifest.id !== id) throw boom.badRequest('The workflow ids do not match', true);
+
+ const result = await workflowService.createVersion(requestContext, manifest);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST /drafts (mounted to /api/workflows)
+ // ===============================================================
+ router.post(
+ '/drafts',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const possibleBody = req.body;
+ const result = await workflowDraftService.createDraft(requestContext, possibleBody);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST /drafts/publish (mounted to /api/workflows)
+ // ===============================================================
+ router.post(
+ '/drafts/publish',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const draft = req.body;
+ const result = await workflowDraftService.publishDraft(requestContext, draft);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // PUT /drafts/:id (mounted to /api/workflows)
+ // ===============================================================
+ router.put(
+ '/drafts/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+ const draft = req.body;
+
+ if (draft.id !== id) throw boom.badRequest('The workflow draft ids do not match', true);
+
+ const result = await workflowDraftService.updateDraft(requestContext, draft);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // DELETE /drafts/:id (mounted to /api/workflows)
+ // ===============================================================
+ router.delete(
+ '/drafts/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ await workflowDraftService.deleteDraft(requestContext, { id });
+ res.status(200).json({});
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/controllers/workflow-template-controller.js b/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/controllers/workflow-template-controller.js
new file mode 100644
index 0000000000..925df641ca
--- /dev/null
+++ b/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/controllers/workflow-template-controller.js
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+async function configure(context) {
+ const router = context.router();
+ const wrap = context.wrap;
+ const boom = context.boom;
+
+ const workflowTemplateService = await context.service('workflowTemplateService');
+ const workflowTemplateDraftService = await context.service('workflowTemplateDraftService');
+
+ // ===============================================================
+ // GET /drafts (mounted to /api/workflow-templates)
+ // ===============================================================
+ router.get(
+ '/drafts',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const principalIdentifier = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const result = await workflowTemplateDraftService.list({ principalIdentifier });
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET / (mounted to /api/workflow-templates)
+ // ===============================================================
+ router.get(
+ '/',
+ wrap(async (req, res) => {
+ const result = await workflowTemplateService.listVersions();
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /latest (mounted to /api/workflow-templates)
+ // ===============================================================
+ router.get(
+ '/latest',
+ wrap(async (req, res) => {
+ const result = await workflowTemplateService.list();
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id (mounted to /api/workflow-templates)
+ // ===============================================================
+ router.get(
+ '/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+
+ const result = await workflowTemplateService.listVersions({ id });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/latest (mounted to /api/workflow-templates)
+ // ===============================================================
+ router.get(
+ '/:id/latest',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+
+ const result = await workflowTemplateService.mustFindVersion({ id });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // GET /:id/v/:v (mounted to /api/workflow-templates)
+ // ===============================================================
+ router.get(
+ '/:id/v/:v',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const v = req.params.v;
+
+ const result = await workflowTemplateService.mustFindVersion({ id, v });
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST /:id/v/ (mounted to /api/workflow-templates)
+ // ===============================================================
+ router.post(
+ '/:id/v/',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+ const manifest = req.body;
+
+ if (manifest.id !== id) throw boom.badRequest('The workflow template ids do not match', true);
+
+ const result = await workflowTemplateService.createVersion(requestContext, manifest);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST /drafts (mounted to /api/workflow-templates)
+ // ===============================================================
+ router.post(
+ '/drafts',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const possibleBody = req.body;
+ const result = await workflowTemplateDraftService.createDraft(requestContext, possibleBody);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // POST /drafts/publish (mounted to /api/workflow-templates)
+ // ===============================================================
+ router.post(
+ '/drafts/publish',
+ wrap(async (req, res) => {
+ const requestContext = res.locals.requestContext;
+ const draft = req.body;
+ const result = await workflowTemplateDraftService.publishDraft(requestContext, draft);
+
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // PUT /drafts/:id (mounted to /api/workflow-templates)
+ // ===============================================================
+ router.put(
+ '/drafts/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+ const draft = req.body;
+
+ if (draft.id !== id) throw boom.badRequest('The workflow template draft ids do not match', true);
+
+ const result = await workflowTemplateDraftService.updateDraft(requestContext, draft);
+ res.status(200).json(result);
+ }),
+ );
+
+ // ===============================================================
+ // DELETE /drafts/:id (mounted to /api/workflow-templates)
+ // ===============================================================
+ router.delete(
+ '/drafts/:id',
+ wrap(async (req, res) => {
+ const id = req.params.id;
+ const requestContext = res.locals.requestContext;
+
+ await workflowTemplateDraftService.deleteDraft(requestContext, { id });
+ res.status(200).json({});
+ }),
+ );
+
+ return router;
+}
+
+module.exports = configure;
diff --git a/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/plugins/routes-plugin.js b/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/plugins/routes-plugin.js
new file mode 100644
index 0000000000..526973866c
--- /dev/null
+++ b/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/plugins/routes-plugin.js
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+const setupAuthContext = require('@aws-ee/base-controllers/lib/middlewares/setup-auth-context');
+const prepareContext = require('@aws-ee/base-controllers/lib/middlewares/prepare-context');
+const ensureActive = require('@aws-ee/base-controllers/lib/middlewares/ensure-active');
+
+const stepTemplateController = require('../controllers/step-template-controller');
+const workflowTemplateController = require('../controllers/workflow-template-controller');
+const workflowControllers = require('../controllers/workflow-controller');
+
+/**
+ * Adds the workflow api routes to the given routesMap.
+ *
+ * @param routesMap A Map containing routes. This object is a Map that has route paths as
+ * keys and an array of functions that configure the router as value. Each function in the
+ * array is expected have the following signature. The function accepts context and router
+ * arguments and returns a configured router.
+ *
+ * (context, router) => configured router
+ *
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} Returns a Map with the mapping of the routes vs their router configurer functions
+ */
+// eslint-disable-next-line no-unused-vars
+async function getRoutes(routesMap, pluginRegistry) {
+ const routes = new Map([
+ ...routesMap,
+ // PROTECTED APIS accessible only to logged in active users
+ ['/api/step-templates', [setupAuthContext, prepareContext, ensureActive, stepTemplateController]],
+ ['/api/workflow-templates', [setupAuthContext, prepareContext, ensureActive, workflowTemplateController]],
+ ['/api/workflows', [setupAuthContext, prepareContext, ensureActive, workflowControllers]],
+ ]);
+
+ return routes;
+}
+
+const plugin = {
+ getRoutes,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/plugins/services-plugin.js b/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/plugins/services-plugin.js
new file mode 100644
index 0000000000..fe004d0ab6
--- /dev/null
+++ b/addons/addon-base-workflow-api/packages/base-worklfow-api/lib/plugins/services-plugin.js
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+const StepTemplateService = require('@aws-ee/base-workflow-core/lib/workflow/step/step-template-service');
+const WorkflowTemplateDraftService = require('@aws-ee/base-workflow-core/lib/workflow/workflow-template-draft-service');
+const WorkflowTemplateService = require('@aws-ee/base-workflow-core/lib/workflow/workflow-template-service');
+const WorkflowService = require('@aws-ee/base-workflow-core/lib/workflow/workflow-service');
+const WorkflowDraftService = require('@aws-ee/base-workflow-core/lib/workflow/workflow-draft-service');
+const WorkflowAssignmentService = require('@aws-ee/base-workflow-core/lib/workflow/workflow-assignment-service');
+const WorkflowInstanceService = require('@aws-ee/base-workflow-core/lib/workflow/workflow-instance-service');
+const WorkflowTriggerService = require('@aws-ee/base-workflow-core/lib/workflow/workflow-trigger-service');
+
+const settingKeys = {
+ tablePrefix: 'dbTablePrefix',
+};
+
+/**
+ * Registers the services needed by the workflow to expose its API in the api-handler lambda function
+ * @param container An instance of ServicesContainer to register services to
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerServices(container, pluginRegistry) {
+ container.register('stepTemplateService', new StepTemplateService());
+ container.register('workflowTemplateDraftService', new WorkflowTemplateDraftService());
+ container.register('workflowTemplateService', new WorkflowTemplateService());
+ container.register('workflowService', new WorkflowService());
+ container.register('workflowDraftService', new WorkflowDraftService());
+ container.register('workflowAssignmentService', new WorkflowAssignmentService());
+ container.register('workflowInstanceService', new WorkflowInstanceService());
+ container.register('workflowTriggerService', new WorkflowTriggerService());
+}
+
+/**
+ * Registers static settings needed by the workflow to expose its API in the api-handler lambda function
+ * @param existingStaticSettings An existing static settings plain javascript object containing settings as key/value contributed by other plugins
+ * @param settings Default instance of settings service that resolves settings from environment variables
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point
+ *
+ * @returns {Promise<*>} A promise that resolves to static settings object
+ */
+// eslint-disable-next-line no-unused-vars
+function getStaticSettings(existingStaticSettings, settings, pluginRegistry) {
+ const staticSettings = {
+ ...existingStaticSettings,
+ };
+
+ // Register all dynamodb table names used by the base rest api addon
+ const tablePrefix = settings.get(settingKeys.tablePrefix);
+ const table = (key, suffix) => {
+ staticSettings[key] = `${tablePrefix}-${suffix}`;
+ };
+ table('dbTableStepTemplates', 'DbStepTemplates');
+ table('dbTableWorkflowTemplates', 'DbWorkflowTemplates');
+ table('dbTableWorkflowTemplateDrafts', 'DbWorkflowTemplateDrafts');
+ table('dbTableWorkflowDrafts', 'DbWorkflowDrafts');
+ table('dbTableWorkflows', 'DbWorkflows');
+ table('dbTableWorkflowInstances', 'DbWorkflowInstances');
+ table('dbTableWfAssignments', 'DbWfAssignments');
+
+ return staticSettings;
+}
+
+const plugin = {
+ getStaticSettings,
+ // getLoggingContext, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerSettingsService, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerLoggerService, // not implemented, the default behavior provided by addon-base is sufficient
+ registerServices,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-workflow-api/packages/base-worklfow-api/package.json b/addons/addon-base-workflow-api/packages/base-worklfow-api/package.json
new file mode 100644
index 0000000000..6054abfa08
--- /dev/null
+++ b/addons/addon-base-workflow-api/packages/base-worklfow-api/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@aws-ee/base-workflow-api",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A library containing the controllers and routes for the base workflow api",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-workflow-core": "workspace:*",
+ "@aws-ee/base-controllers": "workspace:*",
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.1.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-workflow-ui/README.md b/addons/addon-base-workflow-ui/README.md
new file mode 100644
index 0000000000..dc2623a81a
--- /dev/null
+++ b/addons/addon-base-workflow-ui/README.md
@@ -0,0 +1,20 @@
+# Base Workflow UI Add-On
+
+This add-on introduces the UI for base workflow functionality.
+
+The following sections list the add-on contribution.
+
+## npm packages
+
+- @aws-ee/base-workflow-ui
+
+## Runtime extension points
+- Used (ui)
+ - 'contextItems'
+ - 'menuItems'
+ - 'routes'
+
+## Dependencies
+
+- Base Workflow API Add-on
+- Base UI Add-on
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/.babelrc b/addons/addon-base-workflow-ui/packages/base-workflow-ui/.babelrc
new file mode 100644
index 0000000000..83064a1e60
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/.babelrc
@@ -0,0 +1,4 @@
+{
+ "plugins": [["@babel/plugin-proposal-class-properties", { "loose": true }]],
+ "presets": ["@babel/preset-react"]
+}
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/.eslintrc.json b/addons/addon-base-workflow-ui/packages/base-workflow-ui/.eslintrc.json
new file mode 100644
index 0000000000..90ac399dcf
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/.eslintrc.json
@@ -0,0 +1,42 @@
+{
+ "extends": ["airbnb", "prettier", "prettier/react", "plugin:jest/recommended", "eslint-config-prettier"],
+ "parser": "babel-eslint",
+ "plugins": ["jest", "prettier", "react"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "no-console": 0,
+ "no-plusplus": 0,
+ "no-nested-ternary": 0,
+ "import/no-named-as-default": 0,
+ "import/no-named-as-default-member": 0,
+ "react/jsx-props-no-spreading": 0,
+ "react/jsx-one-expression-per-line": 0,
+ "react/jsx-filename-extension": 0,
+ "react/destructuring-assignment": 0,
+ "react/sort-comp": 0,
+ "react/prop-types": 0,
+ "jsx-a11y/no-static-element-interactions": 0,
+ "jsx-a11y/click-events-have-key-events": 0
+ },
+ "env": {
+ "browser": true,
+ "node": true,
+ "es6": true,
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/.gitignore b/addons/addon-base-workflow-ui/packages/base-workflow-ui/.gitignore
new file mode 100644
index 0000000000..49cc63674e
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/.gitignore
@@ -0,0 +1,45 @@
+# See https://help.github.com/ignore-files/ for more about ignoring files.
+
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+**/npm-debug.log*
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# dependencies
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# transpiled code
+dist
+
+# misc
+.env.local
+.env.development
+.env.development.local
+.env.test.local
+.env.production
+.env.production.local
+
+yarn-debug.log*
+yarn-error.log*
+
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/.prettierrc.json b/addons/addon-base-workflow-ui/packages/base-workflow-ui/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/jest.config.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/jest.config.js
new file mode 100644
index 0000000000..60c221fc1d
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/jest.config.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/jsconfig.json b/addons/addon-base-workflow-ui/packages/base-workflow-ui/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/package.json b/addons/addon-base-workflow-ui/packages/base-workflow-ui/package.json
new file mode 100644
index 0000000000..3b22bc4e17
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/package.json
@@ -0,0 +1,88 @@
+{
+ "name": "@aws-ee/base-workflow-ui",
+ "version": "0.1.0",
+ "private": true,
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-ui": "workspace:*",
+ "aws-sdk": "^2.647.0",
+ "chart.js": "^2.9.3",
+ "classnames": "^2.2.6",
+ "jwt-decode": "^2.2.0",
+ "lodash": "^4.17.15",
+ "mobx": "^5.15.4",
+ "mobx-react": "^6.1.7",
+ "mobx-react-form": "^2.0.8",
+ "mobx-state-tree": "^3.15.0",
+ "numeral": "^2.0.6",
+ "react": "^16.12.0",
+ "react-avatar": "^3.9.0",
+ "react-beautiful-dnd": "^11.0.5",
+ "react-chartjs-2": "^2.9.0",
+ "react-dom": "^16.12.0",
+ "react-dotdotdot": "^1.3.1",
+ "react-responsive-carousel": "^3.1.51",
+ "react-router-dom": "^5.1.2",
+ "react-select": "^3.0.8",
+ "react-table": "^6.11.5",
+ "react-timeago": "^4.4.0",
+ "semantic-ui-react": "^0.88.2",
+ "showdown": "^1.9.1",
+ "toastr": "^2.1.4",
+ "typeface-lato": "0.0.75",
+ "validatorjs": "^3.18.1"
+ },
+ "devDependencies": {
+ "@babel/cli": "^7.8.4",
+ "@babel/core": "^7.8.6",
+ "@babel/plugin-proposal-class-properties": "^7.8.3",
+ "@babel/plugin-transform-react-jsx": "^7.8.3",
+ "@babel/preset-env": "^7.8.6",
+ "@babel/preset-react": "^7.8.3",
+ "babel-eslint": "^10.0.3",
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.0.1",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-jsx-a11y": "^6.2.3",
+ "eslint-plugin-prettier": "^3.1.2",
+ "eslint-plugin-react": "^7.18.3",
+ "eslint-plugin-react-hooks": "^1.7.0",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "prop-types": "^15.7.2",
+ "serverless": "^1.63.0",
+ "serverless-deployment-bucket": "^1.1.0",
+ "typescript": "^3.7.5",
+ "webpack": "4.41.2"
+ },
+ "scripts": {
+ "babel": "babel src/ --out-dir dist/ --source-maps",
+ "babel:watch": "babel src/ --out-dir dist/ --source-maps --watch",
+ "build": "pnpm run babel",
+ "build:watch": "pnpm run babel:watch",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "prepare": "pnpm run build"
+ },
+ "files": [
+ "README.md",
+ "dist/",
+ "src/"
+ ],
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/helpers/api.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/helpers/api.js
new file mode 100644
index 0000000000..9d4a807c43
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/helpers/api.js
@@ -0,0 +1,136 @@
+/*
+ * 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 { httpApiGet, httpApiPost, httpApiPut, httpApiDelete } from '@aws-ee/base-ui/dist/helpers/api';
+
+async function getWorkflowTemplates() {
+ return httpApiGet('api/workflow-templates');
+}
+
+async function getWorkflowTemplate(id) {
+ return httpApiGet(`api/workflow-templates/${encodeURIComponent(id)}`);
+}
+
+async function getWorkflowTemplateDrafts() {
+ return httpApiGet('api/workflow-templates/drafts');
+}
+
+async function createWorkflowTemplateDraft({ isNewTemplate, templateId, templateTitle }) {
+ return httpApiPost('api/workflow-templates/drafts', {
+ data: {
+ isNewTemplate,
+ templateId,
+ templateTitle,
+ },
+ });
+}
+
+async function updateWorkflowTemplateDraft(draft) {
+ return httpApiPut(`api/workflow-templates/drafts/${encodeURIComponent(draft.id)}`, { data: draft });
+}
+
+async function publishWorkflowTemplateDraft(draft) {
+ return httpApiPost('api/workflow-templates/drafts/publish', { data: draft });
+}
+
+async function deleteWorkflowTemplateDraft(draft) {
+ return httpApiDelete(`api/workflow-templates/drafts/${encodeURIComponent(draft.id)}`);
+}
+
+async function getStepTemplates() {
+ return httpApiGet('api/step-templates');
+}
+
+async function getWorkflows() {
+ return httpApiGet('api/workflows');
+}
+
+async function getWorkflowDrafts() {
+ return httpApiGet('api/workflows/drafts');
+}
+
+async function createWorkflowDraft({ isNewWorkflow, workflowId, templateId }) {
+ return httpApiPost('api/workflows/drafts', {
+ data: {
+ isNewWorkflow,
+ workflowId,
+ workflowVer: 0,
+ templateId,
+ templateVer: 0,
+ },
+ });
+}
+
+async function updateWorkflowDraft(draft) {
+ return httpApiPut(`api/workflows/drafts/${encodeURIComponent(draft.id)}`, { data: draft });
+}
+
+async function publishWorkflowDraft(draft) {
+ return httpApiPost('api/workflows/drafts/publish', { data: draft });
+}
+
+async function deleteWorkflowDraft(draft) {
+ return httpApiDelete(`api/workflows/drafts/${encodeURIComponent(draft.id)}`);
+}
+
+async function getWorkflow(id) {
+ return httpApiGet(`api/workflows/${encodeURIComponent(id)}`);
+}
+
+async function listWorkflowInstancesByStatus({ status, data }) {
+ return httpApiPost(`api/workflows/instances/status/${status}`, { data });
+}
+
+async function getWorkflowInstances(id, ver) {
+ return httpApiGet(`api/workflows/${encodeURIComponent(id)}/v/${ver}/instances`);
+}
+
+async function getWorkflowInstance(workflowId, workflowVer, instanceId) {
+ return httpApiGet(
+ `api/workflows/${encodeURIComponent(workflowId)}/v/${workflowVer}/instances/${encodeURIComponent(instanceId)}`,
+ );
+}
+
+async function triggerWorkflow(workflowId, workflowVer, data) {
+ return httpApiPost(`api/workflows/${encodeURIComponent(workflowId)}/v/${workflowVer}/trigger`, { data });
+}
+
+async function getWorkflowAssignments(id) {
+ return httpApiGet(`api/workflows/${encodeURIComponent(id)}/assignments`);
+}
+
+export {
+ getWorkflowTemplates,
+ getWorkflowTemplate,
+ getWorkflowTemplateDrafts,
+ createWorkflowTemplateDraft,
+ updateWorkflowTemplateDraft,
+ publishWorkflowTemplateDraft,
+ deleteWorkflowTemplateDraft,
+ getStepTemplates,
+ getWorkflows,
+ getWorkflowDrafts,
+ createWorkflowDraft,
+ updateWorkflowDraft,
+ publishWorkflowDraft,
+ deleteWorkflowDraft,
+ getWorkflow,
+ listWorkflowInstancesByStatus,
+ getWorkflowInstances,
+ getWorkflowInstance,
+ triggerWorkflow,
+ getWorkflowAssignments,
+};
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/images/white-gradient.png b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/images/white-gradient.png
new file mode 100644
index 0000000000..98dfa019c5
Binary files /dev/null and b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/images/white-gradient.png differ
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/configuration/ConfigurationEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/configuration/ConfigurationEditor.js
new file mode 100644
index 0000000000..7f0bfdcc81
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/configuration/ConfigurationEditor.js
@@ -0,0 +1,314 @@
+/*
+ * 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, getSnapshot, applySnapshot } from 'mobx-state-tree';
+
+import { createForm } from '@aws-ee/base-ui/dist/helpers/form';
+import { InputManifest, toMobxFormFields, isConditionTrue } from '@aws-ee/base-ui/dist/models/forms/InputManifest';
+
+// ==================================================================
+// ConfigurationEditor
+// ==================================================================
+const ConfigurationEditor = types
+ .model('ConfigurationEditor', {
+ currentSectionIndex: 0, // IMPORTANT section index start from 0 not 1
+ review: false,
+ inputManifest: types.maybe(InputManifest),
+ configuration: types.optional(
+ types.map(types.union(types.null, types.undefined, types.integer, types.number, types.boolean, types.string)),
+ {},
+ ),
+ mode: types.optional(types.enumeration('Mode', ['create', 'edit']), 'create'), // mode - either "create" or "edit"
+ })
+
+ .volatile(_self => ({
+ originalConfig: undefined,
+ originalSectionConfig: undefined, // the key/value object for the original section config after next()
+ }))
+
+ .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 => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ // If the value of a form field is an object, then make the value a json string instead
+ const normalizeForm = obj => {
+ return _.transform(
+ obj,
+ (result, value, key) => {
+ result[key] = _.isObject(value) ? JSON.stringify(value) : value;
+ },
+ {},
+ );
+ };
+
+ // Returns a key/value object for configuration keys that are part of the given input manifest section
+ const getSectionConfig = inputManifestSection => {
+ const config = {};
+ const section = inputManifestSection;
+ if (section === undefined) return config;
+ const flattened = self.inputManifest.getSectionFlattened(section) || [];
+ flattened.forEach(item => {
+ const key = item.name;
+ if (self.configuration.has(key)) config[key] = _.cloneDeep(self.configuration.get(key));
+ });
+
+ return config;
+ };
+
+ const resetOriginalSectionConfig = () => {
+ self.originalSectionConfig = getSectionConfig(self.inputManifestSection);
+ };
+
+ // Returns all config keys (if any) that belong to input manifest sections after the given index
+ const configKeysAfter = index => {
+ const sections = _.slice(_.get(self.inputManifest, 'sections', []), Math.max(index + 1, 0));
+ const keys = [];
+ _.forEach(sections, section => {
+ const config = getSectionConfig(section);
+ const configKeys = _.keys(config) || [];
+ if (!_.isEmpty(configKeys)) keys.push(...configKeys);
+ });
+
+ return keys;
+ };
+
+ return {
+ afterCreate() {
+ // We keep the original values of the configuration object so that when we do cancel, we simply restore the original copy
+ self.originalConfig = getSnapshot(self.configuration);
+ resetOriginalSectionConfig();
+ },
+
+ cleanup() {
+ superCleanup();
+ },
+
+ next(form) {
+ const configuration = self.configuration;
+ configuration.merge(normalizeForm(form.values()));
+
+ const changed = !_.isEqual(self.originalSectionConfig, getSectionConfig(self.inputManifestSection));
+ const keysAfter = configKeysAfter(self.currentSectionIndex);
+ const nextSectionIndex = self.nextSectionIndex;
+ const before = self.currentSectionIndex;
+
+ if (nextSectionIndex !== -1) self.currentSectionIndex = nextSectionIndex;
+ const after = self.currentSectionIndex;
+
+ resetOriginalSectionConfig();
+
+ // If the configuration keys changed, then it is time to clear all configuration keys (if any) after the current section
+ // In case of edit mode, do not clear any section (we need to pre-populate all sections with existing values)
+ if (!self.isEditMode && changed) {
+ _.forEach(keysAfter, key => {
+ self.configuration.delete(key);
+ });
+ }
+
+ // If the section index didn't move forward, it means that we don't have any more sections
+ // for input and it is time to show the review content
+ self.review = before === after;
+ },
+
+ previous(_form) {
+ if (self.review) {
+ self.review = false;
+ return;
+ }
+ // const configuration = self.configuration;
+ // configuration.merge(normalizeForm(form.values()));
+ const previousSectionIndex = self.previousSectionIndex;
+ if (previousSectionIndex !== -1) self.currentSectionIndex = previousSectionIndex;
+ resetOriginalSectionConfig();
+ },
+
+ clearConfigs() {
+ self.configuration.clear();
+ },
+
+ clearSectionConfigs() {
+ // We only clear configuration keys that belong to the current section
+ if (self.empty) {
+ self.configuration.clear();
+ return;
+ }
+
+ const section = self.inputManifestSection;
+ if (section === undefined) return;
+ const flattened = self.inputManifest.getSectionFlattened(section) || [];
+ flattened.forEach(item => {
+ self.configuration.delete(item.name);
+ });
+ },
+
+ applyChanges() {
+ self.originalConfig = getSnapshot(self.configuration);
+ },
+
+ cancel() {
+ self.review = false;
+ self.currentSectionIndex = 0;
+ if (self.originalConfig) {
+ applySnapshot(self.configuration, self.originalConfig);
+ }
+
+ resetOriginalSectionConfig();
+ },
+
+ restart() {
+ self.cancel();
+ },
+ };
+ })
+
+ .views(self => ({
+ get isEditMode() {
+ return self.mode === 'edit';
+ },
+
+ get inputManifestSection() {
+ if (self.inputManifest === undefined) return undefined;
+ const sections = self.inputManifest.sections;
+ const index = self.currentSectionIndex;
+ if (index > self.totalSections) return undefined;
+ if (index >= sections.length) return undefined;
+ return sections[index];
+ },
+
+ // A list of objects, where each object represents a configuration name/entry that is not undefined
+ // [ {name: 'xyz', title: '...', value: 'true', etc}, {name: 'abc', title: '...', value: 'something', etc}, ... ]
+ get definedConfigList() {
+ if (self.inputManifest === undefined) return [];
+ const inputEntries = self.inputManifest.flattened;
+ const configMap = self.configuration;
+ const list = [];
+ _.forEach(inputEntries, entry => {
+ let value = configMap.get(entry.name);
+ if (_.isUndefined(value)) value = entry.value;
+ if (!_.isUndefined(value)) list.push({ ...entry, value });
+ });
+
+ return list;
+ },
+
+ // A map of all names in inputManifest with their values from the configuration object if they exist
+ // or from the inputManifest if they exist, otherwise undefined is given as the value for the key
+ // An example of returned object shape: { 'configName': 'demo', 'doYouWantThis': undefined }
+ get merged() {
+ const inputEntries = self.inputManifest.flattened;
+ const map = {};
+ _.forEach(inputEntries, entry => {
+ map[entry.name] = entry.value;
+ });
+
+ /* eslint-disable no-restricted-syntax, no-unused-vars */
+ for (const [key, value] of self.configuration.entries()) {
+ map[key] = value;
+ }
+ /* eslint-enable no-restricted-syntax, no-unused-vars */
+
+ return map;
+ },
+
+ get formFields() {
+ const index = self.currentSectionIndex;
+ if (self.totalSections < index) return [];
+ const input = self.inputManifestSection;
+ if (_.isUndefined(input)) return [];
+
+ return toMobxFormFields(input.children, self.merged);
+ },
+
+ get form() {
+ return createForm(self.formFields);
+ },
+
+ get totalSections() {
+ if (self.inputManifest === undefined) return 0;
+ return self.inputManifest.sections.length;
+ },
+
+ get hasNext() {
+ return self.nextSectionIndex !== -1 && !self.review;
+ },
+
+ get hasPrevious() {
+ return self.previousSectionIndex !== -1 || self.review;
+ },
+
+ // Returns the next section index
+ // if the current section is the last section, return -1
+ // walk through the remaining sections and return the index of the first one
+ // that has condition === true, otherwise return -1
+ get nextSectionIndex() {
+ if (self.totalSections < self.currentSectionIndex) return -1;
+ if (self.inputManifest === undefined) return -1;
+ const sections = self.inputManifest.sections;
+ const merged = self.merged;
+ let found = false;
+ let index = self.currentSectionIndex + 1;
+
+ while (!found && index < self.totalSections) {
+ const entry = sections[index];
+ found = isConditionTrue(entry.condition, merged);
+ if (!found) index += 1;
+ }
+
+ return found ? index : -1;
+ },
+
+ // Returns the previous section index
+ // if the current section is 0, return -1
+ // walk through the previous sections and return the index of the first one
+ // that has condition === true, otherwise return -1
+ get previousSectionIndex() {
+ if (self.currentSectionIndex === 0) return -1;
+ const sections = self.inputManifest.sections;
+ const merged = self.merged;
+ let found = false;
+ let index = self.currentSectionIndex - 1;
+
+ while (!found && index >= 0) {
+ const entry = sections[index];
+ found = isConditionTrue(entry.condition, merged);
+ if (!found) index -= 1;
+ }
+
+ return found ? index : -1;
+ },
+
+ get sectionsTitles() {
+ const sections = self.inputManifest.sections;
+ return _.map(sections, index => index.title);
+ },
+
+ get empty() {
+ if (self.inputManifest === undefined) return true;
+ return self.inputManifest.empty;
+ },
+ }));
+
+// Note: Do NOT register ConfigurationEditor in the global context
+
+export default ConfigurationEditor;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/CreateWorkflowDraftForm.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/CreateWorkflowDraftForm.js
new file mode 100644
index 0000000000..5d2b82401b
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/CreateWorkflowDraftForm.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 { createForm } from '@aws-ee/base-ui/dist/helpers/form';
+
+const createWorkflowDraftFields = [
+ {
+ name: 'draftFor',
+ label: 'Draft For',
+ placeholder: 'Select one',
+ rules: 'required|in:newWorkflow,existingWorkflow',
+ },
+ {
+ name: 'templateId',
+ label: 'Workflow Template',
+ placeholder: 'Select a workflow template to start from',
+ extra: {
+ explain: `To create a workflow, you need to select an existing workflow template as a starting point.`,
+ },
+ rules: 'required|string|between:3,150|alpha_dash',
+ },
+ {
+ name: 'workflowId',
+ label: 'Workflow Id',
+ placeholder: 'Type a unique id for this workflow',
+ extra: {
+ explain: `This is a required field and the number of characters must be between 3 and 100 and no spaces. Only
+ alpha-numeric characters and dashes are allowed. Once a draft is created you can not change the workflow id.`,
+ },
+ rules: 'required|string|between:3,100|alpha_dash',
+ },
+];
+
+function getCreateDraftForm() {
+ return createForm(createWorkflowDraftFields);
+}
+
+export default getCreateDraftForm;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/CreateWorkflowTemplateDraftForm.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/CreateWorkflowTemplateDraftForm.js
new file mode 100644
index 0000000000..978dfb0f57
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/CreateWorkflowTemplateDraftForm.js
@@ -0,0 +1,55 @@
+/*
+ * 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 '@aws-ee/base-ui/dist/helpers/form';
+
+const createWorkflowTemplateDraftFields = {
+ templateId: {
+ label: 'Workflow Template Id',
+ placeholder: 'Type a unique id for this template',
+ extra: {
+ explain: `This is a required field and the number of characters must be between 3 and 50 and no spaces. Only
+ alpha-numeric characters and dashes are allowed. Once a draft is created you can not change the template id.`,
+ },
+ value: 'tempValue',
+ rules: 'required|string|between:3,50|alpha_dash',
+ },
+
+ templateTitle: {
+ label: 'Workflow Template Title',
+ placeholder: 'Type a title for the workflow template',
+ extra: {
+ explain: `This is a required field and the number of characters must be between 3 and 255.
+ The title is shown in many places in the UI.`,
+ },
+ value: 'tempValue',
+ rules: 'required|string|between:3,255',
+ },
+
+ draftFor: {
+ label: 'Draft For',
+ placeholder: 'Select one',
+ extra: {
+ explain: 'Decide if you want to create a new workflow template or edit an existing one.',
+ },
+ rules: 'required|string',
+ },
+};
+
+function getCreateDraftForm() {
+ return createForm(createWorkflowTemplateDraftFields);
+}
+
+export default getCreateDraftForm;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/EditWorkflowDraftMetaForm.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/EditWorkflowDraftMetaForm.js
new file mode 100644
index 0000000000..4a1e3544aa
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/EditWorkflowDraftMetaForm.js
@@ -0,0 +1,126 @@
+/*
+ * 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 '@aws-ee/base-ui/dist/helpers/form';
+
+const editWorkflowDraftMetaFields = version => {
+ const { title = '', desc = '', instanceTtl, runSpec = {} } = version;
+ const warnMessage = 'The workflow template used by this workflow does not allow you to modify this field';
+ const warnIfCanNotOverride = (prop, text = warnMessage) => (version.canOverrideProp(prop) ? undefined : text);
+ const canOverride = prop => version.canOverrideProp(prop);
+
+ const result = [
+ {
+ name: 'title',
+ label: 'Workflow Title',
+ placeholder: 'Type a title for the workflow',
+ extra: {
+ explain: `This is a required field and the number of characters must be between 3 and 255.
+ The title is shown in many places in the UI.`,
+ warn: warnIfCanNotOverride('title'),
+ },
+ value: title,
+ rules: 'required|string|between:3,255',
+ disabled: !canOverride('title'),
+ },
+
+ {
+ name: 'desc',
+ label: 'Workflow Description',
+ placeholder: 'Type a description for the workflow',
+ extra: {
+ explain: `The description can be written in markdown but must be between 3 and 4000 characters.`,
+ warn: warnIfCanNotOverride('desc'),
+ },
+ value: desc,
+ rules: 'required|string|between:3,4000',
+ disabled: !canOverride('desc'),
+ },
+
+ {
+ name: 'instanceTtl',
+ label: 'Time to Live (TTL) for instances of the workflow',
+ placeholder: 'Type the number of days',
+ extra: {
+ explain: `The number of days for which a record of a workflow instance is kept in the database.
+ Leave it empty or type -1 if you don't want to have a time limit on the record.`,
+ warn: warnIfCanNotOverride('instanceTtl'),
+ },
+ value: instanceTtl,
+ rules: 'integer',
+ disabled: !canOverride('instanceTtl'),
+ },
+
+ {
+ name: 'runSpecSize',
+ label: 'Runtime lambda size',
+ extra: {
+ warn: warnIfCanNotOverride('runSpecSize'),
+ options: [
+ {
+ value: 'small',
+ text: 'Small',
+ },
+ {
+ value: 'medium',
+ text: 'Medium',
+ },
+ {
+ value: 'large',
+ text: 'Large',
+ },
+ ],
+ },
+ value: runSpec.size || 'small',
+ rules: 'required|in:small,medium,large',
+ disabled: !canOverride('runSpecSize'),
+ },
+
+ {
+ name: 'runSpecTarget',
+ label: 'Runtime target',
+ extra: {
+ warn: warnIfCanNotOverride('runSpecTarget'),
+ options: [
+ {
+ value: 'stepFunctions',
+ text: 'Step Functions',
+ },
+ {
+ value: 'workerLambda',
+ text: 'Worker Lambda',
+ },
+ {
+ value: 'inPlace',
+ text: 'In Place',
+ },
+ ],
+ },
+ value: runSpec.target || 'stepFunctions',
+ rules: 'required|in:stepFunctions,workerLambda,inPlace',
+ disabled: !canOverride('runSpecTarget'),
+ },
+ ];
+
+ return [...result];
+};
+
+function getEditWorkflowDraftMetaForm(template) {
+ const fields = editWorkflowDraftMetaFields(template);
+
+ return createForm(fields);
+}
+
+export default getEditWorkflowDraftMetaForm;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/EditWorkflowTemplateDraftMetaForm.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/EditWorkflowTemplateDraftMetaForm.js
new file mode 100644
index 0000000000..390363c825
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/EditWorkflowTemplateDraftMetaForm.js
@@ -0,0 +1,120 @@
+/*
+ * 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 { createForm } from '@aws-ee/base-ui/dist/helpers/form';
+
+const editWorkflowTemplateDraftMetaFields = templateVersion => {
+ const { title = '', desc = '', instanceTtl, runSpec = {}, propertyOverrideSummaryRows = [] } = templateVersion;
+ const result = [
+ {
+ name: 'templateTitle',
+ label: 'Workflow Template Title',
+ placeholder: 'Type a title for the workflow template',
+ extra: {
+ explain: `This is a required field and the number of characters must be between 3 and 255.
+ The title is shown in many places in the UI.`,
+ },
+ value: title,
+ rules: 'required|string|between:3,255',
+ },
+
+ {
+ name: 'templateDesc',
+ label: 'Workflow Template Description',
+ placeholder: 'Type a description for the workflow template',
+ extra: {
+ explain: `The description can be written in markdown but must be between 3 and 4000 characters.`,
+ },
+ value: desc,
+ rules: 'required|string|between:3,4000',
+ },
+
+ {
+ name: 'instanceTtl',
+ label: 'Time to Live (TTL) for instances of the workflow',
+ placeholder: 'Type the number of days',
+ extra: {
+ explain: `The number of days for which a record of a workflow instance is kept in the database.
+ Leave it empty or type -1 if you don't want to have a time limit on the record.`,
+ },
+ value: instanceTtl,
+ rules: 'integer',
+ },
+
+ {
+ name: 'runSpecSize',
+ label: 'Runtime lambda size',
+ extra: {
+ options: [
+ {
+ value: 'small',
+ text: 'Small',
+ },
+ {
+ value: 'medium',
+ text: 'Medium',
+ },
+ {
+ value: 'large',
+ text: 'Large',
+ },
+ ],
+ },
+ value: runSpec.size || 'small',
+ rules: 'required|in:small,medium,large',
+ },
+
+ {
+ name: 'runSpecTarget',
+ label: 'Runtime target',
+ extra: {
+ options: [
+ {
+ value: 'stepFunctions',
+ text: 'Step Functions',
+ },
+ {
+ value: 'workerLambda',
+ text: 'Worker Lambda',
+ },
+ {
+ value: 'inPlace',
+ text: 'In Place',
+ },
+ ],
+ },
+ value: runSpec.target || 'stepFunctions',
+ rules: 'required|in:stepFunctions,workerLambda,inPlace',
+ },
+ ];
+
+ const propsOverrideFields = _.map(propertyOverrideSummaryRows, ({ name, title_, allowed = false }) => ({
+ name: `propsOverride_${name}`,
+ label: title_,
+ value: allowed,
+ default: allowed,
+ }));
+
+ return [...result, ...propsOverrideFields];
+};
+
+function getEditWorkflowTemplateDraftMetaForm(template) {
+ const fields = editWorkflowTemplateDraftMetaFields(template);
+
+ return createForm(fields);
+}
+
+export default getEditWorkflowTemplateDraftMetaForm;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/TriggerWorkflowForm.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/TriggerWorkflowForm.js
new file mode 100644
index 0000000000..b4fb250d4a
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/TriggerWorkflowForm.js
@@ -0,0 +1,34 @@
+/*
+ * 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 '@aws-ee/base-ui/dist/helpers/form';
+
+const triggerWorkflowFields = [
+ {
+ name: 'workflowInput',
+ label: 'Workflow Input',
+ placeholder: 'Provide a JSON input',
+ extra: {
+ explain: `This is an advance operation. You will need to provide an input in the form of a json object.`,
+ },
+ rules: 'string',
+ },
+];
+
+function getTriggerWorkflowForm() {
+ return createForm(triggerWorkflowFields);
+}
+
+export default getTriggerWorkflowForm;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/WorkflowStepConfigOverrideForm.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/WorkflowStepConfigOverrideForm.js
new file mode 100644
index 0000000000..38cb0dcdbc
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/WorkflowStepConfigOverrideForm.js
@@ -0,0 +1,34 @@
+/*
+ * 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 { createForm } from '@aws-ee/base-ui/dist/helpers/form';
+
+const workflowStepConfigOverrideFields = step => {
+ const rows = step.configOverrideSummaryRows || [];
+ return _.map(rows, ({ name, title, allowed = false }) => ({
+ name,
+ label: title,
+ value: allowed,
+ default: allowed,
+ }));
+};
+
+function getWorkflowStepConfigOverrideForm(step) {
+ const fields = workflowStepConfigOverrideFields(step);
+ return createForm(fields);
+}
+
+export default getWorkflowStepConfigOverrideForm;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/WorkflowStepDescForm.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/WorkflowStepDescForm.js
new file mode 100644
index 0000000000..06e88fd767
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/WorkflowStepDescForm.js
@@ -0,0 +1,58 @@
+/*
+ * 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 '@aws-ee/base-ui/dist/helpers/form';
+
+const workflowStepDescFields = (step, { isTemplate = true } = {}) => {
+ const { title = '', desc = '', derivedTitle = '', derivedDesc = '' } = step;
+ const propsOverrideOption = step.propsOverrideOption || { allowed: [] };
+ const warnMessage = 'The workflow template used by this workflow does not allow you to modify this field';
+ const canOverride = prop => isTemplate || propsOverrideOption.allowed.includes(prop);
+ const warnIfCanNotOverride = (prop, text = warnMessage) => (canOverride(prop) ? undefined : text);
+
+ return {
+ stepTitle: {
+ label: 'Title',
+ placeholder: 'Type a title for the step',
+ extra: {
+ explain: `This is a required field and the number of characters must be between 3 and 255.
+ The title is shown in many places in the UI.`,
+ warn: warnIfCanNotOverride('title'),
+ },
+ value: title || derivedTitle,
+ rules: 'required|string|between:3,255',
+ disabled: !canOverride('title'),
+ },
+
+ stepDesc: {
+ label: 'Description',
+ placeholder: 'Type a description for the step',
+ extra: {
+ explain: `The description can be written in markdown but must be between 3 and 4000 characters.`,
+ warn: warnIfCanNotOverride('desc'),
+ },
+ value: desc || derivedDesc,
+ rules: 'required|string|between:3,4000',
+ disabled: !canOverride('desc'),
+ },
+ };
+};
+
+function getWorkflowStepDescForm(step, options) {
+ const fields = workflowStepDescFields(step, options);
+ return createForm(fields);
+}
+
+export default getWorkflowStepDescForm;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/WorkflowStepPropsForm.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/WorkflowStepPropsForm.js
new file mode 100644
index 0000000000..d8112daa8f
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/WorkflowStepPropsForm.js
@@ -0,0 +1,45 @@
+/*
+ * 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 '@aws-ee/base-ui/dist/helpers/form';
+
+const workflowStepPropsFields = (step, { isTemplate = true } = {}) => {
+ const { skippable } = step;
+ const propsOverrideOption = step.propsOverrideOption || { allowed: [] };
+ const warnMessage = 'The workflow template used by this workflow does not allow you to modify this field';
+ const canOverride = prop => isTemplate || propsOverrideOption.allowed.includes(prop);
+ const warnIfCanNotOverride = (prop, text = warnMessage) => (canOverride(prop) ? undefined : text);
+
+ return {
+ skippable: {
+ label: 'Skip this step if pervious steps failed?',
+ extra: {
+ explain: 'If a previous step failed, should this step still run by the workflow engine?',
+ warn: warnIfCanNotOverride('skippable'),
+ },
+ value: skippable,
+ rules: 'required|boolean',
+ disabled: !canOverride('skippable'),
+ },
+ };
+};
+
+function getWorkflowStepPropsForm(step, options) {
+ const fields = workflowStepPropsFields(step, options);
+
+ return createForm(fields);
+}
+
+export default getWorkflowStepPropsForm;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/WorkflowStepPropsOverrideForm.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/WorkflowStepPropsOverrideForm.js
new file mode 100644
index 0000000000..50dc603d3b
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/forms/WorkflowStepPropsOverrideForm.js
@@ -0,0 +1,34 @@
+/*
+ * 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 { createForm } from '@aws-ee/base-ui/dist/helpers/form';
+
+const workflowStepPropsOverrideFields = step => {
+ const rows = step.propertyOverrideSummaryRows || [];
+ return _.map(rows, ({ name, title, allowed = false }) => ({
+ name,
+ label: title,
+ value: allowed,
+ default: allowed,
+ }));
+};
+
+function getWorkflowStepPropsOverrideForm(step) {
+ const fields = workflowStepPropsOverrideFields(step);
+ return createForm(fields);
+}
+
+export default getWorkflowStepPropsOverrideForm;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-step-templates/StepTemplate.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-step-templates/StepTemplate.js
new file mode 100644
index 0000000000..b5a9263434
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-step-templates/StepTemplate.js
@@ -0,0 +1,140 @@
+/*
+ * 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, applySnapshot, getEnv, getSnapshot } from 'mobx-state-tree';
+
+import { InputManifest, applyMarkdown, visit } from '@aws-ee/base-ui/dist/models/forms/InputManifest';
+
+// ==================================================================
+// Helpers
+// ==================================================================
+
+// Given an input manifest we want to derive an admin input manifest from it, this is done by doing the following:
+// - combining all the sections into one
+// - removing all conditions in all input entries
+// - removing 'required' from rules attributes
+function deriveAdminInputManifest(inputManifest = {}) {
+ const admin = _.cloneDeep(inputManifest);
+ const visitFn = item => {
+ if (_.isString(item.rules)) {
+ item.rules = item.rules.replace(/\|required\|/, '');
+ item.rules = item.rules.replace(/required\|/, '');
+ item.rules = item.rules.replace(/\|required/, '');
+ item.rules = item.rules.replace(/required/, '');
+ }
+ delete item.condition;
+ return item;
+ };
+ const sections = _.map(admin.sections, (section = {}) => visit(section.children, visitFn));
+
+ admin.sections = [{ children: _.flatten(sections) }];
+ return admin;
+}
+
+// ==================================================================
+// StepTemplateVersion
+// ==================================================================
+const StepTemplateVersion = types
+ .model('StepTemplateVersion', {
+ id: '',
+ v: types.maybeNull(types.number),
+ title: '',
+ desc: '',
+ skippable: types.maybe(types.boolean),
+ inputManifest: types.maybe(InputManifest),
+ adminInputManifest: types.maybe(InputManifest),
+ })
+ .actions(self => {
+ function transformManifest(manifest) {
+ // We now apply markdown
+ const showdown = getEnv(self).showdown;
+ const assets = {}; // TODO resolve the assets of the step template (assets is a map of the name
+ // of the asset and the url of the asset), an asset is just an image file.
+ if (manifest) {
+ return applyMarkdown({ inputManifest: manifest, showdown, assets });
+ }
+ return manifest;
+ }
+
+ return {
+ afterCreate() {
+ self.inputManifest = transformManifest(self.inputManifest);
+ if (self.adminInputManifest) {
+ self.adminInputManifest = transformManifest(self.adminInputManifest);
+ } else if (self.inputManifest) {
+ self.adminInputManifest = deriveAdminInputManifest(getSnapshot(self.inputManifest));
+ }
+ },
+
+ setStepTemplateVersion(template) {
+ applySnapshot(self, template);
+ self.afterCreate();
+ },
+ };
+ })
+
+ .views(_self => ({}));
+
+// ==================================================================
+// StepTemplate
+// ==================================================================
+const StepTemplate = types
+ .model('StepTemplate', {
+ id: types.identifier,
+ versions: types.optional(types.array(StepTemplateVersion), []),
+ })
+ .actions(self => ({
+ setStepTemplate(template) {
+ // we try to preserve any existing version objects and update their content instead
+ const mapOfExisting = _.keyBy(self.versions, version => version.v.toString());
+ const processed = [];
+
+ _.forEach(template.versions, templateVersion => {
+ const existing = mapOfExisting[templateVersion.v];
+ if (existing) {
+ existing.setStepTemplateVersion(templateVersion);
+ processed.push(existing);
+ } else {
+ processed.push(StepTemplateVersion.create(templateVersion));
+ }
+ });
+
+ self.versions.replace(processed);
+ },
+ }))
+
+ .views(self => ({
+ get latest() {
+ // we loop through all 'v' numbers and pick the template with the largest 'v' value
+ let largestVersion = self.versions[0];
+ _.forEach(self.versions, version => {
+ if (version.v > largestVersion.v) {
+ largestVersion = version;
+ }
+ });
+ return largestVersion;
+ },
+
+ getVersion(v) {
+ return _.find(self.versions, ['v', v]);
+ },
+
+ get versionNumbers() {
+ return _.map(self.versions, version => version.v);
+ },
+ }));
+
+export { StepTemplate, StepTemplateVersion };
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-step-templates/StepTemplatesStore.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-step-templates/StepTemplatesStore.js
new file mode 100644
index 0000000000..8eda5e2805
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-step-templates/StepTemplatesStore.js
@@ -0,0 +1,122 @@
+/*
+ * 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';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { getStepTemplates } from '../../helpers/api';
+import { StepTemplate } from './StepTemplate';
+
+// ==================================================================
+// StepTemplatesStore
+// ==================================================================
+const StepTemplatesStore = BaseStore.named('StepTemplatesStore')
+ .props({
+ templates: types.optional(types.map(StepTemplate), {}),
+ tickPeriod: 900 * 1000, // 15 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const versions = await getStepTemplates();
+ const templates = toTemplates(versions);
+
+ // we try to preserve existing template versions data and merge the new data instead
+ self.runInAction(() => {
+ const previousKeys = {};
+ self.templates.forEach((value, key) => {
+ previousKeys[key] = true;
+ });
+ templates.forEach(template => {
+ const id = template.id;
+ const hasPrevious = self.templates.has(id);
+
+ self.addTemplate(template);
+
+ if (hasPrevious) {
+ delete previousKeys[id];
+ }
+ });
+
+ _.forEach(previousKeys, (value, key) => {
+ self.templates.delete(key);
+ });
+ });
+ },
+
+ addTemplate(rawTemplate) {
+ const id = rawTemplate.id;
+ const previous = self.templates.get(id);
+
+ if (!previous) {
+ self.templates.put(rawTemplate);
+ } else {
+ previous.setStepTemplate(rawTemplate);
+ }
+ },
+
+ cleanup: () => {
+ self.templates.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.templates.size === 0;
+ },
+
+ get total() {
+ return self.templates.size;
+ },
+
+ get list() {
+ const result = [];
+ self.templates.forEach(template => result.push(template));
+
+ return _.sortBy(result, ['latest.title']);
+ },
+
+ getTemplate(id) {
+ return self.templates.get(id);
+ },
+ }));
+
+// Given an array of [ { id: tmp1, v: 0, ... }, { id: tmp1, v:1, ... } ]
+// return an array of the grouping of the template versions based on their ids
+// [ { id, versions: [ ... ] }, { id, versions: [ ... ] }, ...]
+function toTemplates(versions) {
+ const map = {};
+ _.forEach(versions, version => {
+ const id = version.id;
+ const entry = map[id] || { id, versions: [] };
+ entry.versions.push(version);
+ map[id] = entry;
+ });
+
+ return _.values(map);
+}
+
+function registerContextItems(appContext) {
+ appContext.stepTemplatesStore = StepTemplatesStore.create({}, appContext);
+}
+
+export { StepTemplatesStore, registerContextItems };
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/WorkflowTemplate.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/WorkflowTemplate.js
new file mode 100644
index 0000000000..35c6d90c58
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/WorkflowTemplate.js
@@ -0,0 +1,262 @@
+/*
+ * 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, applySnapshot, detach, clone } from 'mobx-state-tree';
+import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier';
+import { generateId } from '@aws-ee/base-ui/dist/helpers/utils';
+
+import { WorkflowTemplateStep } from './WorkflowTemplateStep';
+
+const titles = {
+ instanceTtl: 'Time to Live (TTL) for instances of the workflow',
+ runSpecSize: 'Runtime lambda size',
+ runSpecTarget: 'Runtime target',
+ title: 'Title',
+ desc: 'Description',
+ steps: 'Add, remove, and rearrange steps',
+};
+
+const supportedPropsOverrideKeys = ['runSpecSize', 'runSpecTarget', 'instanceTtl', 'title', 'desc', 'steps'];
+
+// ==================================================================
+// PropsOverrideOption
+// ==================================================================
+const PropsOverrideOption = types
+ .model('PropsOverrideOption', {
+ allowed: types.optional(types.array(types.string), []),
+ })
+ .views(self => ({
+ get overrideSummaryRows() {
+ const canOverride = prop => self.allowed.includes(prop);
+
+ const result = [
+ { title: titles.steps, allowed: canOverride('steps'), name: 'steps' },
+ { title: titles.instanceTtl, allowed: canOverride('instanceTtl'), name: 'instanceTtl' },
+ { title: titles.runSpecSize, allowed: canOverride('runSpecSize'), name: 'runSpecSize' },
+ { title: titles.runSpecTarget, allowed: canOverride('runSpecTarget'), name: 'runSpecTarget' },
+ { title: titles.title, allowed: canOverride('title'), name: 'title' },
+ { title: titles.desc, allowed: canOverride('desc'), name: 'desc' },
+ ];
+
+ _.forEach(self.allowed, prop => {
+ if (supportedPropsOverrideKeys.includes(prop)) return;
+ result.push({ title: prop, allowed: true });
+ });
+
+ return result;
+ },
+
+ canOverride(prop) {
+ return self.allowed.includes(prop);
+ },
+ }));
+
+// ==================================================================
+// RunSpec
+// ==================================================================
+const RunSpec = types
+ .model('RunSpec', {
+ size: '',
+ target: '',
+ })
+ .views(self => ({
+ get propertySummaryRows() {
+ return [
+ { title: titles.runSpecSize, value: self.size },
+ { title: titles.runSpecTarget, value: self.target },
+ ];
+ },
+ }));
+
+// ==================================================================
+// WorkflowTemplateVersion
+// ==================================================================
+const WorkflowTemplateVersion = types
+ .model('WorkflowTemplateVersion', {
+ id: '',
+ v: types.number,
+ rev: types.maybeNull(types.number),
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ title: '',
+ desc: '',
+ instanceTtl: types.maybeNull(types.number),
+ runSpec: RunSpec,
+ propsOverrideOption: types.maybe(PropsOverrideOption),
+ selectedSteps: types.optional(types.array(WorkflowTemplateStep), []),
+ })
+ .actions(self => ({
+ setWorkflowTemplateVersion(template) {
+ applySnapshot(self, template);
+ },
+
+ setTitle(title) {
+ self.title = title;
+ },
+
+ setDescription(desc) {
+ self.desc = desc;
+ },
+
+ setInstanceTtl(value) {
+ let answer = null;
+ if (_.isString(value)) {
+ const parsed = parseInt(value, 10);
+ if (_.isNaN(parsed)) answer = null;
+ else answer = parsed;
+ } else if (_.isNumber(value)) {
+ answer = value;
+ }
+
+ self.instanceTtl = answer;
+ },
+
+ setRunSpec(runSpec) {
+ applySnapshot(self.runSpec, runSpec);
+ },
+
+ setPropsOverrideOption(option) {
+ applySnapshot(self.propsOverrideOption, option);
+ },
+
+ reinsertStep(currentIndex, targetIndex) {
+ const current = self.selectedSteps[currentIndex];
+
+ detach(current);
+ self.selectedSteps.splice(targetIndex, 0, current);
+ },
+
+ removeStep(idOrStep) {
+ const step = _.isString(idOrStep) ? self.getStep(idOrStep) : idOrStep;
+ self.selectedSteps.remove(step);
+ },
+
+ addStep(step) {
+ const { id, v, skippable, title, desc } = step;
+ const workflowStep = WorkflowTemplateStep.create(
+ {
+ id: generateId('wt-step'),
+ stepTemplateId: id,
+ stepTemplateVer: v,
+ title,
+ desc,
+ skippable,
+ stepTemplate: clone(step),
+ },
+ getEnv(self),
+ );
+
+ self.selectedSteps.push(workflowStep);
+
+ return workflowStep;
+ },
+ }))
+
+ .views(self => ({
+ getStep(id) {
+ return _.find(self.selectedSteps, step => step.id === id);
+ },
+
+ get descHtml() {
+ const showdown = getEnv(self).showdown;
+ return showdown.convert(self.desc, self.assets); // TODO declare assets
+ },
+
+ get system() {
+ return self.createdBy === '_system_';
+ },
+
+ get propertySummaryRows() {
+ return [
+ {
+ title: titles.instanceTtl,
+ value: self.instanceTtl,
+ },
+ ...self.runSpec.propertySummaryRows,
+ ];
+ },
+
+ get propertyOverrideSummaryRows() {
+ if (_.isNil(self.propsOverrideOption)) return [];
+ return self.propsOverrideOption.overrideSummaryRows;
+ },
+
+ // A workflow template can always rearrange its steps, don't confuse this with 'canWorkflowRearrangeSteps'
+ get canRearrangeSteps() {
+ return true;
+ },
+
+ get canWorkflowRearrangeSteps() {
+ return self.canWorkflowOverrideProp('steps');
+ },
+
+ canWorkflowOverrideProp(prop) {
+ return self.propsOverrideOption.canOverride(prop);
+ },
+ }));
+
+// ==================================================================
+// WorkflowTemplate
+// ==================================================================
+const WorkflowTemplate = types
+ .model('WorkflowTemplate', {
+ id: types.identifier,
+ versions: types.optional(types.array(WorkflowTemplateVersion), []),
+ })
+ .actions(self => ({
+ setWorkflowTemplate(template) {
+ // we try to preserve any existing version objects and update their content instead
+ const mapOfExisting = _.keyBy(self.versions, version => version.v.toString());
+ const processed = [];
+
+ _.forEach(template.versions, templateVersion => {
+ const existing = mapOfExisting[templateVersion.v];
+ if (existing) {
+ existing.setWorkflowTemplateVersion(templateVersion);
+ processed.push(existing);
+ } else {
+ processed.push(WorkflowTemplateVersion.create(templateVersion));
+ }
+ });
+
+ self.versions.replace(processed);
+ },
+ }))
+
+ .views(self => ({
+ get latest() {
+ // we loop through all 'v' numbers and pick the template with the largest 'v' value
+ let largestVersion = self.versions[0];
+ _.forEach(self.versions, version => {
+ if (version.v > largestVersion.v) {
+ largestVersion = version;
+ }
+ });
+ return largestVersion;
+ },
+
+ getVersion(v) {
+ return _.find(self.versions, ['v', v]);
+ },
+
+ get versionNumbers() {
+ return _.map(self.versions, version => version.v);
+ },
+ }));
+
+export { WorkflowTemplate, WorkflowTemplateVersion, RunSpec };
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/WorkflowTemplateStep.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/WorkflowTemplateStep.js
new file mode 100644
index 0000000000..bcb903efe1
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/WorkflowTemplateStep.js
@@ -0,0 +1,215 @@
+/*
+ * 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, getParent, applySnapshot, getSnapshot } from 'mobx-state-tree';
+
+import { StepTemplateVersion } from '../workflow-step-templates/StepTemplate';
+
+const titles = {
+ title: 'Title',
+ desc: 'Description',
+ skippable: 'Skip this step if pervious steps failed',
+};
+
+const supportedPropsOverrideKeys = ['title', 'desc', 'skippable'];
+
+// ==================================================================
+// PropsOverrideOption
+// ==================================================================
+const PropsOverrideOption = types
+ .model('PropsOverrideOption', {
+ allowed: types.optional(types.array(types.string), []),
+ })
+ .actions(self => ({
+ setAllowed(allowed = []) {
+ self.allowed.replace(allowed);
+ },
+ }))
+ .views(self => ({
+ get overrideSummaryRows() {
+ const canOverride = prop => self.allowed.includes(prop);
+
+ const result = [
+ { title: titles.title, allowed: canOverride('title'), name: 'title' },
+ { title: titles.desc, allowed: canOverride('desc'), name: 'desc' },
+ { title: titles.skippable, allowed: canOverride('skippable'), name: 'skippable' },
+ ];
+
+ _.forEach(self.allowed, (prop, index) => {
+ if (supportedPropsOverrideKeys.includes(prop)) return;
+ result.push({ title: prop, allowed: true, name: `${index}-${prop}` });
+ });
+
+ return result;
+ },
+ }));
+
+// ==================================================================
+// ConfigOverrideOption
+// ==================================================================
+const ConfigOverrideOption = types
+ .model('ConfigOverrideOption', {
+ allowed: types.optional(types.array(types.string), []),
+ })
+ .actions(self => ({
+ setAllowed(allowed = []) {
+ self.allowed.replace(allowed);
+ },
+ }))
+ .views(self => ({
+ get overrideSummaryRows() {
+ const result = _.cloneDeep(getParent(self).configSummaryRows || []);
+ const map = _.keyBy(result, 'name');
+ self.allowed.forEach(key => {
+ const entry = map[key];
+ if (entry !== undefined) {
+ entry.allowed = true;
+ } else {
+ result.push({ name: key, allowed: true });
+ }
+ });
+
+ return result;
+ },
+ }));
+
+// ==================================================================
+// WorkflowTemplateStep
+// ==================================================================
+const WorkflowTemplateStep = types
+ .model('WorkflowTemplateStep', {
+ id: '',
+ stepTemplateId: '',
+ stepTemplateVer: types.maybeNull(types.number),
+ title: types.maybe(types.string),
+ desc: types.maybe(types.string),
+ propsOverrideOption: types.optional(PropsOverrideOption, {}),
+ configOverrideOption: types.optional(ConfigOverrideOption, {}),
+ stepTemplate: StepTemplateVersion,
+ skippable: types.maybe(types.boolean),
+ defaults: types.optional(
+ types.map(types.union(types.null, types.undefined, types.integer, types.number, types.boolean, types.string)),
+ {},
+ ),
+ })
+ .actions(self => ({
+ afterCreate() {
+ if (_.isEmpty(self.id))
+ console.warn(`There is no id provided for this workflow template step`, getSnapshot(self));
+ },
+
+ setDesc(desc) {
+ if (desc === self.stepTemplate.desc) self.desc = undefined;
+ else self.desc = desc;
+ },
+
+ setTitle(title) {
+ if (title === self.stepTemplate.title) self.title = undefined;
+ else self.title = title;
+ },
+
+ setDefaults(defaults = {}) {
+ self.defaults.replace(defaults);
+ },
+
+ setSkippable(skippable) {
+ self.skippable = skippable;
+ },
+
+ setConfigOverrideOption(option) {
+ applySnapshot(self.configOverrideOption, option);
+ },
+
+ setPropsOverrideOption(option) {
+ applySnapshot(self.propsOverrideOption, option);
+ },
+ }))
+
+ .views(self => ({
+ get templateId() {
+ return self.stepTemplateId;
+ },
+
+ get templateVer() {
+ return self.stepTemplateVer;
+ },
+
+ get derivedTitle() {
+ return self.title || self.stepTemplate.title;
+ },
+
+ get derivedDesc() {
+ return self.desc || self.stepTemplate.desc;
+ },
+
+ get descHtml() {
+ const showdown = getEnv(self).showdown;
+ return showdown.convert(self.derivedDesc, self.assets); // TODO declare assets
+ },
+
+ get propertyOverrideSummaryRows() {
+ if (_.isNil(self.propsOverrideOption)) return [];
+ return self.propsOverrideOption.overrideSummaryRows;
+ },
+
+ get propertySummaryRows() {
+ return [
+ {
+ title: titles.skippable,
+ value: self.skippable,
+ },
+ ];
+ },
+
+ get configSummaryRows() {
+ // First, we build a map of all the input manifest entries
+ // Then, for entries where we actually have a config value in the 'defaults', we
+ // populate the value attribute in the entry.
+ const inputManifest = self.stepTemplate.inputManifest;
+
+ // We use 'additional' to keep track of entries that was not part of the inputManifest but yet there is a key in 'configs' for it.
+ // The order of the rows might be useful, so we try to preserve it by keeping track of additional
+ const additional = [];
+ let flattened = [];
+ let map = {};
+
+ if (inputManifest) {
+ flattened = _.cloneDeep(inputManifest.flattened || []);
+ map = _.keyBy(flattened, 'name');
+ }
+
+ /* eslint-disable no-restricted-syntax, no-unused-vars */
+ for (const [k, v] of self.defaults) {
+ let entry = map[k];
+ if (entry === undefined) {
+ entry = { name: k };
+ additional.push(entry);
+ }
+ entry.value = v;
+ map[k] = entry;
+ }
+ /* eslint-enable no-restricted-syntax, no-unused-vars */
+
+ return [...flattened, ...additional];
+ },
+
+ get configOverrideSummaryRows() {
+ if (_.isNil(self.configOverrideOption)) return [];
+ return self.configOverrideOption.overrideSummaryRows;
+ },
+ }));
+
+export { WorkflowTemplateStep, PropsOverrideOption, ConfigOverrideOption, supportedPropsOverrideKeys };
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/WorkflowTemplatesStore.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/WorkflowTemplatesStore.js
new file mode 100644
index 0000000000..2fb10cfebe
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/WorkflowTemplatesStore.js
@@ -0,0 +1,131 @@
+/*
+ * 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';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { uiEventBus } from '@aws-ee/base-ui/dist/models/SessionStore';
+
+import { getWorkflowTemplates } from '../../helpers/api';
+import { WorkflowTemplate } from './WorkflowTemplate';
+
+// ==================================================================
+// WorkflowTemplatesStore
+// ==================================================================
+const WorkflowTemplatesStore = BaseStore.named('WorkflowTemplatesStore')
+ .props({
+ templates: types.optional(types.map(WorkflowTemplate), {}),
+ tickPeriod: 900 * 1000, // 15 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const versions = await getWorkflowTemplates();
+ const templates = toTemplates(versions);
+
+ // we try to preserve existing template versions data and merge the new data instead
+ self.runInAction(() => {
+ const previousKeys = {};
+ self.templates.forEach((_value, key) => {
+ previousKeys[key] = true;
+ });
+ templates.forEach(template => {
+ const id = template.id;
+ const hasPrevious = self.templates.has(id);
+
+ self.addTemplate(template);
+
+ if (hasPrevious) {
+ delete previousKeys[id];
+ }
+ });
+
+ _.forEach(previousKeys, (_value, key) => {
+ self.templates.delete(key);
+ });
+ });
+ },
+
+ addTemplate(rawTemplate) {
+ const id = rawTemplate.id;
+ const previous = self.templates.get(id);
+
+ if (!previous) {
+ self.templates.put(rawTemplate);
+ } else {
+ previous.setWorkflowTemplate(rawTemplate);
+ }
+ },
+
+ cleanup: () => {
+ self.templates.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.templates.size === 0;
+ },
+
+ get total() {
+ return self.templates.size;
+ },
+
+ get list() {
+ const result = [];
+ self.templates.forEach(template => result.push(template));
+
+ return _.reverse(_.sortBy(result, ['latest.createdAt', 'title']));
+ // return result;
+ },
+
+ getTemplate(id) {
+ return self.templates.get(id);
+ },
+ }));
+
+// Given an array of [ { id: tmp1, v: 0, ... }, { id: tmp1, v:1, ... } ]
+// return an array of the grouping of the template versions based on their ids
+// [ { id, versions: [ ... ] }, { id, versions: [ ... ] }, ...]
+function toTemplates(versions) {
+ const map = {};
+ _.forEach(versions, version => {
+ const id = version.id;
+ const entry = map[id] || { id, versions: [] };
+ entry.versions.push(version);
+ map[id] = entry;
+ });
+
+ return _.values(map);
+}
+
+function registerContextItems(appContext) {
+ appContext.workflowTemplatesStore = WorkflowTemplatesStore.create({}, appContext);
+
+ uiEventBus.listenTo('workflowTemplatePublished', {
+ id: 'WorkflowTemplatesStore',
+ listener: async _event => {
+ appContext.workflowTemplatesStore.cleanup();
+ },
+ });
+}
+
+export { WorkflowTemplatesStore, registerContextItems };
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/drafts/WorkflowTemplateDraft.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/drafts/WorkflowTemplateDraft.js
new file mode 100644
index 0000000000..b7770b0b29
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/drafts/WorkflowTemplateDraft.js
@@ -0,0 +1,48 @@
+/*
+ * 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, applySnapshot } from 'mobx-state-tree';
+import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier';
+
+import { WorkflowTemplateVersion } from '../WorkflowTemplate';
+
+// ==================================================================
+// WorkflowTemplateDraft
+// ==================================================================
+const WorkflowTemplateDraft = types
+ .model('WorkflowTemplateDraft', {
+ id: types.identifier,
+ rev: types.maybe(types.number),
+ username: '',
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ templateId: '',
+ template: WorkflowTemplateVersion,
+ })
+ .actions(self => ({
+ setWorkflowTemplateDraft(draft) {
+ applySnapshot(self, draft);
+ },
+
+ setRev(rev) {
+ self.rev = rev;
+ },
+ }))
+
+ .views(_self => ({}));
+
+export default WorkflowTemplateDraft;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/drafts/WorkflowTemplateDraftsStore.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/drafts/WorkflowTemplateDraftsStore.js
new file mode 100644
index 0000000000..9c296cc6df
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/drafts/WorkflowTemplateDraftsStore.js
@@ -0,0 +1,182 @@
+/*
+ * 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, getSnapshot, getEnv } from 'mobx-state-tree';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import {
+ getWorkflowTemplateDrafts,
+ createWorkflowTemplateDraft,
+ updateWorkflowTemplateDraft,
+ publishWorkflowTemplateDraft,
+ deleteWorkflowTemplateDraft,
+} from '../../../helpers/api';
+import WorkflowTemplateDraft from './WorkflowTemplateDraft';
+
+// ==================================================================
+// WorkflowTemplateDraftsStore
+// ==================================================================
+const WorkflowTemplateDraftsStore = BaseStore.named('WorkflowTemplateDraftsStore')
+ .props({
+ drafts: types.optional(types.map(WorkflowTemplateDraft), {}),
+ tickPeriod: 900 * 1000, // 15 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ // private
+ function normalizeForSubmission(draft) {
+ const normalizedDraft = _.cloneDeep(getSnapshot(draft));
+ _.forEach(normalizedDraft.template.selectedSteps, step => {
+ delete step.stepTemplate;
+ });
+
+ return normalizedDraft;
+ }
+
+ return {
+ async doLoad() {
+ const drafts = await getWorkflowTemplateDrafts();
+
+ // We try to preserve existing drafts data and merge the new data instead
+ // We could have used self.drafts.replace(), but it will do clear().merge()
+ self.runInAction(() => {
+ const previousKeys = {};
+ self.drafts.forEach((value, key) => {
+ previousKeys[key] = true;
+ });
+ drafts.forEach(draft => {
+ const id = draft.id;
+ const hasPrevious = self.drafts.has(id);
+
+ self.addDraft(draft);
+
+ if (hasPrevious) {
+ delete previousKeys[id];
+ }
+ });
+
+ _.forEach(previousKeys, (value, key) => {
+ self.drafts.delete(key);
+ });
+ });
+ },
+
+ addDraft(rawDraft) {
+ const id = rawDraft.id;
+ const previous = self.drafts.get(id);
+
+ if (!previous) {
+ self.drafts.put(rawDraft);
+ } else {
+ previous.setWorkflowTemplateDraft(rawDraft);
+ }
+ },
+
+ async updateDraft(draft) {
+ const id = draft.id;
+ const previous = self.drafts.get(id);
+ if (previous === undefined) throw new Error(`Workflow Template Draft "${id}" does not exist`);
+
+ const updated = await updateWorkflowTemplateDraft(normalizeForSubmission(draft));
+ previous.setWorkflowTemplateDraft(updated);
+
+ return previous;
+ },
+
+ async createDraft({ isNewTemplate, templateId, templateTitle }) {
+ const draft = await createWorkflowTemplateDraft({ isNewTemplate, templateId, templateTitle });
+ self.addDraft(draft);
+
+ return draft;
+ },
+
+ async publishDraft(draft) {
+ const id = draft.id;
+ const previous = self.drafts.get(id);
+ if (previous === undefined) throw new Error(`Workflow Template Draft "${id}" does not exist`);
+
+ const publishResult = await publishWorkflowTemplateDraft(normalizeForSubmission(draft));
+
+ self.runInAction(() => {
+ if (!publishResult.hasErrors) self.drafts.delete(id);
+ });
+
+ return publishResult;
+ },
+
+ async deleteDraft(draft) {
+ const uiEventBus = getEnv(self).uiEventBus;
+ await deleteWorkflowTemplateDraft(draft);
+ await uiEventBus.fireEvent('workflowTemplateDraftDeleted', draft);
+ self.runInAction(() => {
+ self.drafts.delete(draft.id);
+ });
+ },
+
+ cleanup: () => {
+ self.drafts.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.drafts.size === 0;
+ },
+
+ get total() {
+ return self.drafts.size;
+ },
+
+ get list() {
+ const result = [];
+ self.drafts.forEach(drafts => result.push(drafts));
+
+ return _.reverse(_.sortBy(result, ['createdAt', 'title']));
+ },
+
+ hasTemplate(templateId) {
+ let found = false;
+ /* eslint-disable no-restricted-syntax, no-unused-vars */
+ for (const draft of self.drafts.values()) {
+ if (draft.template.id === templateId) {
+ found = true;
+ break;
+ }
+ }
+ /* eslint-enable no-restricted-syntax, no-unused-vars */
+
+ return found;
+ },
+
+ hasDraft(draftId) {
+ return self.drafts.has(draftId);
+ },
+
+ getDraft(id) {
+ return self.drafts.get(id);
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.workflowTemplateDraftsStore = WorkflowTemplateDraftsStore.create({}, appContext);
+}
+
+export { WorkflowTemplateDraftsStore, registerContextItems };
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/drafts/edit/WorkflowTemplateDraftEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/drafts/edit/WorkflowTemplateDraftEditor.js
new file mode 100644
index 0000000000..8c0fed2825
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/drafts/edit/WorkflowTemplateDraftEditor.js
@@ -0,0 +1,195 @@
+/*
+ * 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, getEnv, clone } from 'mobx-state-tree';
+import { uiEventBus } from '@aws-ee/base-ui/dist/models/SessionStore';
+
+import getEditWorkflowTemplateDraftMetaForm from '../../../forms/EditWorkflowTemplateDraftMetaForm';
+import WorkflowTemplateStepEditor from './WorkflowTemplateStepEditor';
+
+let globals; // a reference to the appContext
+
+// ==================================================================
+// WorkflowTemplateDraftEditor
+// ==================================================================
+const WorkflowTemplateDraftEditor = types
+ .model('WorkflowTemplateDraftEditor', {
+ draftId: '',
+ currentPage: 0, // there are only two pages, one for meta editing and one for steps editing
+ numPages: 3,
+ stepEditors: types.optional(types.map(WorkflowTemplateStepEditor), {}),
+ })
+
+ .volatile(_self => ({
+ draftCopy: undefined,
+ draftMetaForm: undefined,
+ }))
+
+ .actions(self => {
+ // private
+ function makeDraftCopy() {
+ self.runInAction(() => {
+ const draft = self.originalDraft;
+ self.draftCopy = clone(draft);
+ self.draftMetaForm = getEditWorkflowTemplateDraftMetaForm(self.draftCopy.template);
+ });
+ }
+
+ return {
+ // 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();
+ },
+
+ afterCreate() {
+ makeDraftCopy();
+ },
+
+ nextPage() {
+ if (self.currentPage < self.numPages - 1) self.currentPage += 1;
+ else self.currentPage = self.numPages - 1;
+ return self.currentPage;
+ },
+
+ previousPage() {
+ if (self.currentPage > 0) self.currentPage -= 1;
+ else self.currentPage = 0;
+ return self.currentPage;
+ },
+
+ cancel() {
+ // We make a fresh copy in case the existing copy one was used
+ makeDraftCopy();
+ self.currentPage = 0;
+ },
+
+ getStepEditor(step) {
+ const stepId = step.id;
+ const entry = self.stepEditors.get(stepId) || WorkflowTemplateStepEditor.create({ stepId }, getEnv(self));
+
+ self.stepEditors.set(stepId, entry);
+ return entry;
+ },
+
+ removeStepEditor(stepId) {
+ self.stepEditors.delete(stepId);
+ },
+
+ addStep(step) {
+ const template = self.draft.template;
+ template.addStep(step);
+ },
+
+ async update(draft) {
+ const updatedDraft = await self.workflowTemplateDraftsStore.updateDraft(draft);
+ // The following code is not the greatest idea, but okay for this scenario, the correct approach would have
+ // been to call makeDraftCopy(), however, this will result in losing some of the ui states in the draft card
+ self.draft.setRev(updatedDraft.rev);
+ },
+
+ async publish(draft) {
+ const result = await self.workflowTemplateDraftsStore.publishDraft(draft);
+
+ // Remove the wizard from the session store, if there were no errors
+ if (!result.hasErrors) {
+ await uiEventBus.fireEvent('workflowTemplateDraftDeleted', draft);
+ }
+
+ return result;
+ },
+ };
+ })
+
+ .views(self => ({
+ get workflowTemplateDraftsStore() {
+ return getEnv(self).workflowTemplateDraftsStore;
+ },
+
+ get hasNextPage() {
+ return self.currentPage < self.numPages - 1;
+ },
+
+ get hasPreviousPage() {
+ return self.currentPage > 0;
+ },
+
+ get originalDraft() {
+ const store = self.workflowTemplateDraftsStore;
+ return store.getDraft(self.draftId);
+ },
+
+ get draft() {
+ return self.draftCopy;
+ },
+
+ // Returns a WorkflowTemplateVersion model object
+ get version() {
+ return self.draft.template;
+ },
+
+ get metaForm() {
+ return self.draftMetaForm;
+ },
+
+ // Returns true if at least one step editor is in edit mode
+ get stepEditorsEditing() {
+ let found = false;
+ /* eslint-disable no-restricted-syntax, no-unused-vars */
+ for (const editor of self.stepEditors.values()) {
+ if (editor.editing) {
+ found = true;
+ break;
+ }
+ }
+ /* eslint-enable no-restricted-syntax, no-unused-vars */
+ return found;
+ },
+ }));
+
+function getWorkflowTemplateDraftEditor(draftId) {
+ const sessionStore = globals.sessionStore;
+ const id = encodeId(draftId);
+ const entry = sessionStore.map.get(id) || WorkflowTemplateDraftEditor.create({ draftId }, globals);
+
+ sessionStore.map.set(id, entry);
+ return entry;
+}
+
+function encodeId(draftId) {
+ return `WorkflowTemplateDraftEditor-${draftId}`;
+}
+
+function removeWizard(draftId) {
+ const sessionStore = globals.sessionStore;
+ const id = encodeId(draftId);
+
+ sessionStore.removeStartsWith(id);
+}
+
+function registerContextItems(appContext) {
+ // we are not actually registering anything here, just getting a reference to the appContext
+ globals = appContext;
+
+ uiEventBus.listenTo('workflowTemplateDraftDeleted', {
+ id: 'WorkflowTemplateDraftEditor',
+ listener: async event => {
+ // event will be the draft object
+ removeWizard(event.id);
+ },
+ });
+}
+
+export { WorkflowTemplateDraftEditor, getWorkflowTemplateDraftEditor, registerContextItems };
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/drafts/edit/WorkflowTemplateStepEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/drafts/edit/WorkflowTemplateStepEditor.js
new file mode 100644
index 0000000000..5f1976c9eb
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflow-templates/drafts/edit/WorkflowTemplateStepEditor.js
@@ -0,0 +1,162 @@
+/*
+ * 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, getParent, getEnv, clone, getSnapshot } from 'mobx-state-tree';
+
+import getWorkflowStepDescForm from '../../../forms/WorkflowStepDescForm';
+import getWorkflowStepPropsForm from '../../../forms/WorkflowStepPropsForm';
+import getWorkflowStepConfigOverrideForm from '../../../forms/WorkflowStepConfigOverrideForm';
+import getWorkflowStepPropsOverrideForm from '../../../forms/WorkflowStepPropsOverrideForm';
+import ConfigurationEditor from '../../../configuration/ConfigurationEditor';
+
+// ==================================================================
+// WorkflowTemplateStepEditor
+// ==================================================================
+const WorkflowTemplateStepEditor = types
+ .model('WorkflowTemplateStepEditor', {
+ stepId: '', // The step Id for the workflow template step
+ contentExpanded: false,
+ configEdit: false, // If we are editing mode or not for the configuration section
+ configOverrideEdit: false, // If we are editing mode or not for the configuration override section
+ descEdit: false, // If we are editing mode or not for the description section
+ propsEdit: false, // If we are editing mode or not for the props section
+ propsOverrideEdit: false, // If we are editing mode or not for the props override section
+ })
+
+ .volatile(_self => ({
+ configurationEditor: undefined,
+ stepDescForm: undefined,
+ stepConfigOverrideForm: undefined,
+ stepPropsForm: undefined,
+ stepPropsOverrideForm: undefined,
+ }))
+
+ .actions(self => {
+ return {
+ // 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();
+ },
+
+ afterAttach() {
+ if (self.configurationEditor !== undefined) return;
+ const step = self.step;
+ const inputManifest = _.get(step, 'stepTemplate.adminInputManifest');
+ const defaults = getSnapshot(step.defaults);
+
+ self.configurationEditor = ConfigurationEditor.create(
+ {
+ inputManifest: _.isUndefined(inputManifest) ? undefined : clone(inputManifest),
+ configuration: defaults,
+ },
+ getEnv(self),
+ );
+
+ self.stepConfigOverrideForm = getWorkflowStepConfigOverrideForm(step);
+ self.stepDescForm = getWorkflowStepDescForm(step);
+ self.stepPropsForm = getWorkflowStepPropsForm(step);
+ self.stepPropsOverrideForm = getWorkflowStepPropsOverrideForm(step);
+ },
+
+ setContentExpanded(flag) {
+ self.contentExpanded = !!flag; // !! will simply turn any type to a boolean type
+ },
+
+ setConfigEdit(flag) {
+ self.configEdit = flag;
+ },
+
+ setConfigOverrideEdit(flag) {
+ self.configOverrideEdit = flag;
+ },
+
+ setDescEdit(flag) {
+ self.descEdit = flag;
+ },
+
+ setPropsEdit(flag) {
+ self.propsEdit = flag;
+ },
+
+ setPropsOverrideEdit(flag) {
+ self.propsOverrideEdit = flag;
+ },
+
+ applyDefaults(configs = {}) {
+ self.step.setDefaults(configs);
+ },
+
+ applyConfigOverrides(configOverrides = []) {
+ self.step.setConfigOverrideOption({
+ allowed: configOverrides,
+ });
+ self.stepConfigOverrideForm = getWorkflowStepConfigOverrideForm(self.step);
+ },
+
+ applyPropsOverrides(propsOverrides = []) {
+ self.step.setPropsOverrideOption({
+ allowed: propsOverrides,
+ });
+ self.stepPropsOverrideForm = getWorkflowStepPropsOverrideForm(self.step);
+ },
+
+ applyDescAndTitle(desc, title) {
+ self.step.setDesc(desc);
+ self.step.setTitle(title);
+ self.stepDescForm = getWorkflowStepDescForm(self.step);
+ },
+
+ applySkippable(skippable) {
+ self.step.setSkippable(skippable);
+ self.stepPropsForm = getWorkflowStepPropsForm(self.step);
+ },
+ };
+ })
+
+ .views(self => ({
+ get step() {
+ const version = self.version;
+ return version.getStep(self.stepId);
+ },
+
+ get version() {
+ const parentEditor = getParent(self, 2);
+ return parentEditor.version;
+ },
+
+ get configOverrideForm() {
+ return self.stepConfigOverrideForm;
+ },
+
+ get descForm() {
+ return self.stepDescForm;
+ },
+
+ get propsForm() {
+ return self.stepPropsForm;
+ },
+
+ get propsOverrideForm() {
+ return self.stepPropsOverrideForm;
+ },
+
+ get editing() {
+ return self.configEdit || self.descEdit || self.propsEdit || self.configOverrideEdit || self.propsOverrideEdit;
+ },
+ }));
+
+export default WorkflowTemplateStepEditor;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/Workflow.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/Workflow.js
new file mode 100644
index 0000000000..88d5f0b5d3
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/Workflow.js
@@ -0,0 +1,434 @@
+/*
+ * 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, applySnapshot, detach } from 'mobx-state-tree';
+import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier';
+import { generateId, consolidateToMap } from '@aws-ee/base-ui/dist/helpers/utils';
+
+import { RunSpec } from '../workflow-templates/WorkflowTemplate';
+import WorkflowStep from './WorkflowStep';
+
+const titles = {
+ instanceTtl: 'Time to Live (TTL) for instances of the workflow',
+ runSpecSize: 'Runtime lambda size',
+ runSpecTarget: 'Runtime target',
+ title: 'Title',
+ desc: 'Description',
+ steps: 'Add, remove, and rearrange steps',
+};
+
+const statusColorMap = {
+ // 'not_started': '', // to default to grey
+ in_progress: 'orange',
+ error: 'red',
+ done: 'green',
+};
+
+// ==================================================================
+// WorkflowAssignment
+// ==================================================================
+const WorkflowAssignment = types
+ .model('WorkflowAssignment', {
+ id: '',
+ wf: '',
+ rev: types.number,
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ triggerType: '',
+ triggerTypeData: '',
+ })
+ .actions(self => ({
+ setWorkflowAssignment(assignment) {
+ applySnapshot(self, assignment);
+ },
+ }))
+
+ .views(self => ({
+ get system() {
+ return self.createdBy.username === '_system_';
+ },
+ }));
+
+// ==================================================================
+// WorkflowInstance
+// ==================================================================
+const WorkflowInstance = types
+ .model('WorkflowInstance', {
+ id: types.identifier,
+ wfId: '',
+ wfVer: types.number,
+ ttl: types.maybeNull(types.number),
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ msg: '',
+ wfStatus: '',
+ stStatuses: types.optional(types.frozen(), []),
+ runSpec: RunSpec,
+ input: types.optional(types.frozen(), {}),
+ workflow: types.optional(types.frozen(), {}),
+ })
+ .actions(self => ({
+ setWorkflowInstance(instance) {
+ applySnapshot(self, instance);
+ },
+ }))
+
+ .views(self => ({
+ get system() {
+ return self.createdBy.username === '_system_';
+ },
+
+ // This is the workflow version
+ get version() {
+ const workflowsStore = getEnv(self).workflowsStore;
+ const workflow = workflowsStore.getWorkflow(self.wfId);
+ if (!workflow) return undefined;
+ return workflow.getVersion(self.wfVer);
+ },
+
+ get pending() {
+ return self.wfStatus === 'not_started' || self.wfStatus === 'in_progress';
+ },
+
+ get statusSummary() {
+ const stepSummary = status => {
+ const count = _.size(_.filter(self.stStatuses, item => item.status === status));
+ return {
+ count,
+ statusLabel: _.startCase(status),
+ statusColor: statusColorMap[status],
+ };
+ };
+ const is = value => self.wfStatus === value;
+ const spread = {
+ success: is('done'),
+ error: is('error'),
+ warning: is('in_progress'),
+ };
+
+ return {
+ statusMsg: self.msg,
+ statusLabel: _.startCase(self.wfStatus),
+ statusColor: statusColorMap[self.wfStatus],
+ stepsSummary: [
+ stepSummary('done'),
+ stepSummary('error'),
+ stepSummary('in_progress'),
+ stepSummary('skipped'),
+ stepSummary('not_started'),
+ ],
+ msgSpread: spread,
+ };
+ },
+
+ get steps() {
+ const selectedSteps = self.workflow.selectedSteps || [];
+ const getStep = index => _.nth(selectedSteps, index);
+ const strip = (pre, msg, color) => {
+ if (_.startsWith(msg, pre)) {
+ return {
+ match: true,
+ parsed: msg.substring(pre.length),
+ color,
+ };
+ }
+ return {
+ match: false,
+ parsed: msg,
+ };
+ };
+ const parse = msg => {
+ if (_.isEmpty(msg)) return {};
+ let item = strip('WARN|||', msg, 'orange');
+ if (!item.match) {
+ item = strip('ERR|||', msg, 'red');
+ if (!item.match) {
+ item = strip('INFO|||', msg, 'green');
+ }
+ }
+
+ return item;
+ };
+ const result = [];
+
+ _.forEach(self.stStatuses, (stepStatus, index) => {
+ const step = getStep(index) || {};
+ const msgObj = parse(stepStatus.msg);
+ result.push({
+ statusMsg: msgObj.parsed,
+ statusLabel: _.startCase(stepStatus.status),
+ statusColor: msgObj.color || statusColorMap[stepStatus.status],
+ stepTemplateId: step.stepTemplateId || 'unknown',
+ stepTemplateVer: step.stepTemplateVer,
+ title: step.title || 'Not available',
+ startTime: stepStatus.startTime,
+ endTime: stepStatus.endTime,
+ });
+ });
+
+ return result;
+ },
+ }));
+
+// ==================================================================
+// WorkflowVersion
+// ==================================================================
+const WorkflowVersion = types
+ .model('WorkflowVersion', {
+ id: '',
+ v: types.number,
+ rev: types.maybe(types.number),
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ title: '',
+ desc: '',
+ instanceTtl: types.maybeNull(types.number),
+ runSpec: RunSpec,
+ stepsOrderChanged: types.boolean,
+ selectedSteps: types.optional(types.array(WorkflowStep), []),
+ instancesMap: types.optional(types.map(WorkflowInstance), {}),
+ workflowTemplateId: '',
+ workflowTemplateVer: types.maybe(types.number),
+ })
+ .actions(self => ({
+ setWorkflowVersion(version) {
+ const instancesMap = detach(self.instancesMap); // preserve the instances value
+ applySnapshot(self, version);
+ self.instancesMap = instancesMap;
+ },
+
+ // important "instances" is expected to be an array
+ setInstances(instances = []) {
+ consolidateToMap(self.instancesMap, instances, (exiting, newItem) => {
+ exiting.setWorkflowInstance(newItem);
+ });
+ },
+
+ setInstance(instance) {
+ self.instancesMap.put(instance);
+ },
+
+ setTitle(title) {
+ self.title = title;
+ },
+
+ setDescription(desc) {
+ self.desc = desc;
+ },
+
+ setInstanceTtl(value) {
+ let answer = null;
+ if (_.isString(value)) {
+ const parsed = parseInt(value, 10);
+ if (_.isNaN(parsed)) answer = null;
+ else answer = parsed;
+ } else if (_.isNumber(value)) {
+ answer = value;
+ }
+
+ self.instanceTtl = answer;
+ },
+
+ setRunSpec(runSpec) {
+ applySnapshot(self.runSpec, runSpec);
+ },
+
+ reinsertStep(currentIndex, targetIndex) {
+ const current = self.selectedSteps[currentIndex];
+
+ detach(current);
+ self.selectedSteps.splice(targetIndex, 0, current); // this will reattach the step
+ },
+
+ removeStep(idOrStep) {
+ const step = _.isString(idOrStep) ? self.getStep(idOrStep) : idOrStep;
+ self.selectedSteps.remove(step);
+ },
+
+ addStep(step) {
+ const { id, v, skippable, title, desc } = step;
+ const workflowStep = WorkflowStep.create(
+ {
+ id: generateId('wf-step'),
+ stepTemplateId: id,
+ stepTemplateVer: v,
+ title,
+ desc,
+ skippable,
+ },
+ getEnv(self),
+ );
+
+ workflowStep.makeNew();
+ self.selectedSteps.push(workflowStep);
+
+ return workflowStep;
+ },
+ }))
+
+ .views(self => ({
+ getStep(stepId) {
+ return _.find(self.selectedSteps, step => step.id === stepId);
+ },
+
+ get instances() {
+ const result = [];
+ self.instancesMap.forEach(value => {
+ // remember instancesMap is a Map not a simple object
+ result.push(value);
+ });
+
+ return result;
+ },
+
+ getInstance(id) {
+ return self.instancesMap.get(id);
+ },
+
+ get descHtml() {
+ const showdown = getEnv(self).showdown;
+ return showdown.convert(self.desc, self.assets); // TODO declare assets
+ },
+
+ get system() {
+ return self.createdBy.username === '_system_';
+ },
+
+ get propertySummaryRows() {
+ return [
+ {
+ title: titles.instanceTtl,
+ value: self.instanceTtl,
+ },
+ ...self.runSpec.propertySummaryRows,
+ ];
+ },
+
+ // This is the workflow template version
+ get template() {
+ const templatesStore = getEnv(self).workflowTemplatesStore;
+ const template = templatesStore.getTemplate(self.workflowTemplateId);
+ if (!template) return undefined;
+ return template.getVersion(self.workflowTemplateVer);
+ },
+
+ get canRearrangeSteps() {
+ const template = self.template;
+ if (!template) return false;
+ return template.canWorkflowRearrangeSteps;
+ },
+
+ canOverrideProp(prop) {
+ const template = self.template;
+ if (!template) return false;
+ return template.canWorkflowOverrideProp(prop);
+ },
+
+ get hasPendingInstances() {
+ return _.some(self.instances, ['pending', true]);
+ },
+ }));
+
+// ==================================================================
+// Workflow
+// ==================================================================
+const Workflow = types
+ .model('Workflow', {
+ id: types.identifier,
+ versions: types.optional(types.array(WorkflowVersion), []),
+ assignments: types.optional(types.array(WorkflowAssignment), []),
+ })
+ .actions(self => ({
+ setWorkflow(workflow) {
+ // we try to preserve any existing version objects and update their content instead
+ const mapOfExisting = _.keyBy(self.versions, version => version.v.toString());
+ const processed = [];
+
+ _.forEach(workflow.versions, workflowVersion => {
+ const existing = mapOfExisting[workflowVersion.v];
+ if (existing) {
+ existing.setWorkflowVersion(workflowVersion);
+ processed.push(existing);
+ } else {
+ processed.push(WorkflowVersion.create(workflowVersion));
+ }
+ });
+
+ self.versions.replace(processed);
+ },
+
+ setAssignments(assignments) {
+ // we try to preserve any existing assignment objects and update their content instead
+ const mapOfExisting = _.keyBy(self.assignments, 'id');
+ const processed = [];
+
+ _.forEach(assignments, assignment => {
+ const existing = mapOfExisting[assignment.id];
+ if (existing) {
+ existing.setWorkflowAssignment(assignment);
+ processed.push(existing);
+ } else {
+ processed.push(WorkflowAssignment.create(assignment));
+ }
+ });
+
+ self.assignments.replace(processed);
+ },
+ }))
+
+ .views(self => ({
+ get latest() {
+ // we loop through all 'v' numbers and pick the workflow with the largest 'v' value
+ let largestVersion = self.versions[0];
+ _.forEach(self.versions, version => {
+ if (version.v > largestVersion.v) {
+ largestVersion = version;
+ }
+ });
+ return largestVersion;
+ },
+
+ getVersion(v) {
+ return _.find(self.versions, ['v', v]);
+ },
+
+ get versionNumbers() {
+ return _.map(self.versions, version => version.v);
+ },
+ }));
+
+// Given an array of [ { id, v: 0, ... }, { id, v:1, ... } ]
+// return an array of the grouping of the workflow versions based on their ids
+// [ { id, versions: [ ... ] }, { id, versions: [ ... ] }, ...]
+function toWorkflows(versions) {
+ const map = {};
+ _.forEach(versions, version => {
+ const id = version.id;
+ const entry = map[id] || { id, versions: [] };
+ entry.versions.push(version);
+ map[id] = entry;
+ });
+
+ return _.values(map);
+}
+
+export { Workflow, WorkflowVersion, WorkflowInstance, toWorkflows };
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowAssignmentsStore.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowAssignmentsStore.js
new file mode 100644
index 0000000000..c8a8ea184b
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowAssignmentsStore.js
@@ -0,0 +1,84 @@
+/*
+ * 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, isStoreReady } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { getWorkflowAssignments } from '../../helpers/api';
+
+// ==================================================================
+// WorkflowAssignmentsStore
+// ==================================================================
+const WorkflowAssignmentsStore = BaseStore.named('WorkflowAssignmentsStore')
+ .props({
+ workflowId: '',
+ tickPeriod: 300 * 1000, // 5 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const parent = getParent(self, 2);
+ if (!isStoreReady(parent)) {
+ await parent.load();
+ }
+ const assignments = await getWorkflowAssignments(self.workflowId);
+ self.runInAction(() => {
+ const workflow = self.workflow;
+ if (!workflow) throw new Error(`Workflow "${self.workflowId}" does not exist`);
+ workflow.setAssignments(assignments);
+ });
+ },
+
+ cleanup: () => {
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get assignments() {
+ const workflow = self.workflow;
+ if (!workflow) return [];
+ return workflow.assignments;
+ },
+
+ get workflow() {
+ const parent = getParent(self, 2);
+ return parent.workflow;
+ },
+
+ get empty() {
+ return self.assignments.length === 0;
+ },
+
+ get total() {
+ return self.assignments.length;
+ },
+
+ get list() {
+ const result = self.assignments.slice();
+
+ return _.reverse(_.sortBy(result, ['createdAt']));
+ },
+ }));
+
+// Note: Do NOT register this in the app context, if you want to gain access to an instance
+// use WorkflowStore.getWorkflowAssignmentsStore()
+export default WorkflowAssignmentsStore;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowInstanceStore.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowInstanceStore.js
new file mode 100644
index 0000000000..cbd55ba11a
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowInstanceStore.js
@@ -0,0 +1,81 @@
+/*
+ * 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, getParent } from 'mobx-state-tree';
+import { BaseStore, isStoreReady } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { getWorkflowInstance } from '../../helpers/api';
+
+// ==================================================================
+// WorkflowInstanceStore
+// ==================================================================
+const WorkflowInstanceStore = BaseStore.named('WorkflowInstanceStore')
+ .props({
+ workflowId: '',
+ workflowVer: types.number,
+ instanceId: '',
+ tickPeriod: 300 * 1000, // 5 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const parent = getParent(self, 2);
+ if (!isStoreReady(parent)) {
+ await parent.load();
+ }
+ const instance = await getWorkflowInstance(self.workflowId, self.workflowVer, self.instanceId);
+ self.runInAction(() => {
+ const version = self.version;
+ if (!version) throw new Error(`Workflow "${self.workflowId}" v${self.workflowVer} does not exist`);
+ version.setInstance(instance);
+ const instanceMst = version.getInstance(self.instanceId);
+ if (instanceMst && instanceMst.pending) {
+ self.setFastTickPeriod();
+ } else {
+ self.setSlowTickPeriod();
+ }
+ });
+ },
+
+ setSlowTickPeriod() {
+ self.changeTickPeriod(300 * 1000); // 5 minutes
+ },
+
+ setFastTickPeriod() {
+ self.changeTickPeriod(5 * 1000); // 5 seconds
+ },
+
+ cleanup: () => {
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get version() {
+ const parent = getParent(self, 2);
+ const workflow = parent.workflow;
+ if (!workflow) return undefined;
+ return workflow.getVersion(self.workflowVer);
+ },
+ }));
+
+// Note: Do NOT register this in the app context, if you want to gain access to an instance
+// use WorkflowStore.getWorkflowInstanceStore()
+export default WorkflowInstanceStore;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowInstancesStore.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowInstancesStore.js
new file mode 100644
index 0000000000..480147a54c
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowInstancesStore.js
@@ -0,0 +1,124 @@
+/*
+ * 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, getParent } from 'mobx-state-tree';
+import { BaseStore, isStoreReady } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { getWorkflowInstances, triggerWorkflow } from '../../helpers/api';
+
+// ==================================================================
+// WorkflowInstancesStore
+// ==================================================================
+const WorkflowInstancesStore = BaseStore.named('WorkflowInstancesStore')
+ .props({
+ workflowId: '',
+ workflowVer: types.number,
+ tickPeriod: 300 * 1000, // 5 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const parent = getParent(self, 2);
+ if (!isStoreReady(parent)) {
+ await parent.load();
+ }
+ const instances = await getWorkflowInstances(self.workflowId, self.workflowVer);
+ self.runInAction(() => {
+ const version = self.version;
+ if (!version) throw new Error(`Workflow "${self.workflowId}" v${self.workflowVer} does not exist`);
+ version.setInstances(instances);
+ if (version.hasPendingInstances) {
+ self.setFastTickPeriod();
+ } else {
+ self.setSlowTickPeriod();
+ }
+ });
+ },
+
+ async triggerWorkflow({ input }) {
+ const result = await triggerWorkflow(self.workflowId, self.workflowVer, { input });
+ self.runInAction(() => {
+ const version = self.version;
+ if (!version) throw new Error(`Workflow "${self.workflowId}" v${self.workflowVer} does not exist`);
+ version.setInstance(result.instance);
+ if (version.hasPendingInstances) {
+ self.setFastTickPeriod();
+ } else {
+ self.setSlowTickPeriod();
+ }
+ });
+
+ return result;
+ },
+
+ setSlowTickPeriod() {
+ self.changeTickPeriod(300 * 1000); // 5 minutes
+ },
+
+ setFastTickPeriod() {
+ self.changeTickPeriod(5 * 1000); // 5 seconds
+ },
+
+ cleanup: () => {
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get instances() {
+ const version = self.version;
+ if (!version) return [];
+ return version.instances;
+ },
+
+ get version() {
+ const parent = getParent(self, 2);
+ const workflow = parent.workflow;
+ if (!workflow) return undefined;
+ return workflow.getVersion(self.workflowVer);
+ },
+
+ get empty() {
+ return self.instances.length === 0;
+ },
+
+ get total() {
+ return self.instances.length;
+ },
+
+ get list() {
+ const result = self.instances.slice();
+
+ return _.reverse(_.sortBy(result, ['createdAt']));
+ },
+
+ getInstance(id) {
+ return self.instancesMap.get(id);
+ },
+
+ hasInstance(id) {
+ return self.instancesMap.has(id);
+ },
+ }));
+
+// Note: Do NOT register this in the app context, if you want to gain access to an instance
+// use WorkflowStore.getWorkflowInstancesStore()
+export default WorkflowInstancesStore;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowStep.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowStep.js
new file mode 100644
index 0000000000..b19ea66cdd
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowStep.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 { types, getEnv, getSnapshot } from 'mobx-state-tree';
+
+import {
+ PropsOverrideOption,
+ ConfigOverrideOption,
+ supportedPropsOverrideKeys,
+} from '../workflow-templates/WorkflowTemplateStep';
+
+const titles = {
+ title: 'Title',
+ desc: 'Description',
+ skippable: 'Skip this step if pervious steps failed',
+};
+
+// ==================================================================
+// WorkflowStep
+// ==================================================================
+const WorkflowStep = types
+ .model('WorkflowStep', {
+ id: '',
+ stepTemplateId: '',
+ stepTemplateVer: types.maybeNull(types.number),
+ title: types.maybe(types.string),
+ desc: types.maybe(types.string),
+ propsOverrideOption: types.optional(PropsOverrideOption, {}),
+ configOverrideOption: types.optional(ConfigOverrideOption, {}),
+ skippable: types.maybe(types.boolean),
+ configs: types.optional(
+ types.map(types.union(types.null, types.undefined, types.integer, types.number, types.boolean, types.string)),
+ {},
+ ),
+ })
+ .actions(self => ({
+ afterCreate() {
+ if (_.isEmpty(self.id)) console.warn(`There is no id provided for this workflow step`, getSnapshot(self));
+ },
+
+ setDesc(desc) {
+ self.desc = desc;
+ },
+
+ setTitle(title) {
+ self.title = title;
+ },
+
+ setConfigs(configs = {}) {
+ self.configs.replace(configs);
+ },
+
+ setSkippable(skippable) {
+ self.skippable = skippable;
+ },
+
+ // You should only use this method if the workflow step is added manually by the user (when allowed)
+ // do not make a WorkflowStep that came from the server as new.
+ makeNew() {
+ self.propsOverrideOption.setAllowed(_.slice(supportedPropsOverrideKeys));
+ const stepTemplate = self.stepTemplate;
+ if (!stepTemplate) return;
+ const inputManifest = stepTemplate.inputManifest;
+ if (!inputManifest) return;
+ const names = inputManifest.names;
+ self.configOverrideOption.setAllowed(names);
+ },
+ }))
+
+ .views(self => ({
+ get templateId() {
+ return self.stepTemplateId;
+ },
+
+ get templateVer() {
+ return self.stepTemplateVer;
+ },
+
+ get descHtml() {
+ const showdown = getEnv(self).showdown;
+ return showdown.convert(self.desc, self.assets); // TODO declare assets
+ },
+
+ get stepTemplate() {
+ const id = self.stepTemplateId;
+ const v = self.stepTemplateVer;
+ const stepTemplatesStore = getEnv(self).stepTemplatesStore;
+ if (!stepTemplatesStore) return undefined;
+
+ const stepTemplate = stepTemplatesStore.getTemplate(id);
+ if (!stepTemplate) return undefined;
+ return stepTemplate.getVersion(v);
+ },
+
+ get propertySummaryRows() {
+ return [
+ {
+ title: titles.skippable,
+ value: self.skippable,
+ },
+ ];
+ },
+
+ get configSummaryRows() {
+ // First, we build a map of all the input manifest entries
+ // Then, for entries where we actually have a config value in the 'configs', we
+ // populate the value attribute in the entry.
+ const stepTemplate = self.stepTemplate;
+ if (stepTemplate === undefined) return [];
+ const inputManifest = stepTemplate.inputManifest;
+
+ // We use 'additional' to keep track of entries that was not part of the inputManifest but yet there is a key in 'configs' for it.
+ // The order of the rows might be useful, so we try to preserve it by keeping track of additional
+ const additional = [];
+ let flattened = [];
+ let map = {};
+
+ if (inputManifest) {
+ flattened = _.cloneDeep(inputManifest.flattened || []);
+ map = _.keyBy(flattened, 'name');
+ }
+
+ /* eslint-disable no-restricted-syntax, no-unused-vars */
+ for (const [k, v] of self.configs) {
+ let entry = map[k];
+ if (entry === undefined) {
+ entry = { name: k };
+ additional.push(entry);
+ }
+ entry.value = v;
+ map[k] = entry;
+ }
+ /* eslint-enable no-restricted-syntax, no-unused-vars */
+
+ return [...flattened, ...additional];
+ },
+ }));
+
+export default WorkflowStep;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowStore.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowStore.js
new file mode 100644
index 0000000000..28653ff811
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowStore.js
@@ -0,0 +1,106 @@
+/*
+ * 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, getParent } from 'mobx-state-tree';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import { getWorkflow } from '../../helpers/api';
+import { toWorkflows } from './Workflow';
+import WorkflowInstancesStore from './WorkflowInstancesStore';
+import WorkflowInstanceStore from './WorkflowInstanceStore';
+import WorkflowAssignmentsStore from './WorkflowAssignmentsStore';
+
+// ==================================================================
+// WorkflowStore
+// ==================================================================
+const WorkflowStore = BaseStore.named('WorkflowStore')
+ .props({
+ workflowId: '',
+ instancesStores: types.optional(types.map(WorkflowInstancesStore), {}),
+ instanceStores: types.optional(types.map(WorkflowInstanceStore), {}),
+ assignmentsStore: types.optional(types.map(WorkflowAssignmentsStore), {}),
+ tickPeriod: 300 * 1000, // 5 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const parent = getParent(self, 2);
+ const workflowRaw = await getWorkflow(self.workflowId);
+ const workflow = _.first(toWorkflows(workflowRaw));
+ parent.addWorkflow(workflow);
+ },
+
+ getInstancesStore: (workflowId, workflowVer) => {
+ const encodedId = `${workflowId}__${workflowVer}`;
+ let entry = self.instancesStores.get(encodedId);
+ if (!entry) {
+ // Lazily create a WorkflowInstancesStore for each workflow version
+ self.instancesStores.set(encodedId, WorkflowInstancesStore.create({ workflowId, workflowVer }));
+ entry = self.instancesStores.get(encodedId);
+ }
+
+ return entry;
+ },
+
+ getInstanceStore: (workflowVer, instanceId) => {
+ const workflowId = self.workflowId;
+ const encodedId = `${workflowId}__${workflowVer}__${instanceId}`;
+ let entry = self.instanceStores.get(encodedId);
+ if (!entry) {
+ // Lazily create a WorkflowInstanceStore for each workflow version
+ self.instanceStores.set(encodedId, WorkflowInstanceStore.create({ workflowId, workflowVer, instanceId }));
+ entry = self.instanceStores.get(encodedId);
+ }
+
+ return entry;
+ },
+
+ getAssignmentsStore: () => {
+ const workflowId = self.workflowId;
+ let entry = self.assignmentsStore.get(workflowId);
+ if (!entry) {
+ // Lazily create a WorkflowAssignmentsStore for each workflow
+ self.assignmentsStore.set(workflowId, WorkflowAssignmentsStore.create({ workflowId }));
+ entry = self.assignmentsStore.get(workflowId);
+ }
+
+ return entry;
+ },
+
+ cleanup: () => {
+ self.instancesStores.clear();
+ self.instanceStores.clear();
+ self.assignmentsStore.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get workflow() {
+ const parent = getParent(self, 2);
+ const w = parent.getWorkflow(self.workflowId);
+ return w;
+ },
+ }));
+
+// Note: Do NOT register this in the global context, if you want to gain access to an instance
+// use WorkflowsStore.getWorkflowStore()
+export default WorkflowStore;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowsStore.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowsStore.js
new file mode 100644
index 0000000000..3290ebc35a
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/WorkflowsStore.js
@@ -0,0 +1,161 @@
+/*
+ * 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';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { uiEventBus } from '@aws-ee/base-ui/dist/models/SessionStore';
+
+import { getWorkflows } from '../../helpers/api';
+import { Workflow, toWorkflows } from './Workflow';
+import WorkflowStore from './WorkflowStore';
+
+// ==================================================================
+// WorkflowsStore
+// ==================================================================
+const WorkflowsStore = BaseStore.named('WorkflowsStore')
+ .props({
+ workflows: types.optional(types.map(Workflow), {}),
+ workflowStores: types.optional(types.map(WorkflowStore), {}),
+ tickPeriod: 900 * 1000, // 15 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ return {
+ async doLoad() {
+ const versions = await getWorkflows();
+ const workflows = toWorkflows(versions);
+
+ // we try to preserve existing workflow versions data and merge the new data instead
+ self.runInAction(() => {
+ const previousKeys = {};
+ self.workflows.forEach((_value, key) => {
+ previousKeys[key] = true;
+ });
+ workflows.forEach(workflow => {
+ const id = workflow.id;
+ const hasPrevious = self.workflows.has(id);
+
+ self.addWorkflow(workflow);
+
+ if (hasPrevious) {
+ delete previousKeys[id];
+ }
+ });
+
+ _.forEach(previousKeys, (_value, key) => {
+ self.workflows.delete(key);
+ });
+ });
+ },
+
+ addWorkflow(rawWorkflow) {
+ if (!rawWorkflow) return;
+ const id = rawWorkflow.id;
+ const previous = self.workflows.get(id);
+
+ if (!previous) {
+ self.workflows.put(rawWorkflow);
+ } else {
+ previous.setWorkflow(rawWorkflow);
+ }
+ },
+
+ getWorkflowStore: workflowId => {
+ let entry = self.workflowStores.get(workflowId);
+ if (!entry) {
+ // Lazily create the store
+ self.workflowStores.set(workflowId, WorkflowStore.create({ workflowId }));
+ entry = self.workflowStores.get(workflowId);
+ }
+
+ return entry;
+ },
+
+ cleanup: () => {
+ self.workflows.clear();
+ self.workflowStores.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.workflows.size === 0;
+ },
+
+ get total() {
+ return self.workflows.size;
+ },
+
+ get list() {
+ const result = [];
+ self.workflows.forEach(workflow => result.push(workflow));
+
+ return _.reverse(_.sortBy(result, ['latest.createdAt', 'title']));
+ },
+
+ getWorkflow(id) {
+ return self.workflows.get(id);
+ },
+
+ hasWorkflow(id) {
+ return self.workflows.has(id);
+ },
+
+ asDropDownOptions() {
+ const result = [];
+ self.workflows.forEach(wf => {
+ const latestWfVersion = wf.latest.v;
+ wf.versions.forEach(wfv => {
+ result.push({
+ key: wf.id,
+ value: JSON.stringify({ wid: wf.id, wrv: wfv.v }),
+ text: wfv.v === latestWfVersion ? `${wf.id} (latest)` : `${wf.id} (v${wfv.v})`,
+ // content:
+ // wfv.v === latestWfVersion ? (
+ //
+ // {wf.id}
+ // latest version
+ //
+ // ) : (
+ //
+ // {wf.id}
+ // v{wfv.v}
+ //
+ // ),
+ });
+ });
+ });
+ return result;
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.workflowsStore = WorkflowsStore.create({}, appContext);
+
+ uiEventBus.listenTo('workflowPublished', {
+ id: 'WorkflowsStore',
+ listener: async _event => {
+ appContext.workflowsStore.cleanup();
+ },
+ });
+}
+
+export { WorkflowsStore, registerContextItems };
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/drafts/WorkflowDraft.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/drafts/WorkflowDraft.js
new file mode 100644
index 0000000000..0f1f785945
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/drafts/WorkflowDraft.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 { types, applySnapshot } from 'mobx-state-tree';
+import UserIdentifier from '@aws-ee/base-ui/dist/models/users/UserIdentifier';
+
+import { WorkflowVersion } from '../Workflow';
+
+// ==================================================================
+// WorkflowDraft
+// ==================================================================
+const WorkflowDraft = types
+ .model('WorkflowDraft', {
+ id: types.identifier,
+ rev: types.maybe(types.number),
+ username: '',
+ createdAt: '',
+ createdBy: types.optional(UserIdentifier, {}),
+ updatedAt: '',
+ updatedBy: types.optional(UserIdentifier, {}),
+ workflowId: '',
+ workflowVer: types.maybe(types.number),
+ templateId: '',
+ templateVer: types.maybe(types.number),
+ workflow: WorkflowVersion,
+ })
+ .actions(self => ({
+ setWorkflowDraft(draft) {
+ applySnapshot(self, draft);
+ },
+
+ setRev(rev) {
+ self.rev = rev;
+ },
+ }))
+
+ .views(_self => ({}));
+
+export default WorkflowDraft;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/drafts/WorkflowDraftsStore.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/drafts/WorkflowDraftsStore.js
new file mode 100644
index 0000000000..ef5d65c17b
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/drafts/WorkflowDraftsStore.js
@@ -0,0 +1,185 @@
+/*
+ * 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, getSnapshot, getEnv } from 'mobx-state-tree';
+import { BaseStore } from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import {
+ getWorkflowDrafts,
+ createWorkflowDraft,
+ updateWorkflowDraft,
+ publishWorkflowDraft,
+ deleteWorkflowDraft,
+} from '../../../helpers/api';
+import WorkflowDraft from './WorkflowDraft';
+
+// ==================================================================
+// WorkflowDraftsStore
+// ==================================================================
+const WorkflowDraftsStore = BaseStore.named('WorkflowDraftsStore')
+ .props({
+ drafts: types.optional(types.map(WorkflowDraft), {}),
+ tickPeriod: 900 * 1000, // 15 minutes
+ })
+
+ .actions(self => {
+ // save the base implementation of cleanup
+ const superCleanup = self.cleanup;
+
+ // private
+ function normalizeForSubmission(draft) {
+ const normalizedDraft = _.cloneDeep(getSnapshot(draft));
+ _.forEach(normalizedDraft.workflow.selectedSteps, step => {
+ delete step.stepTemplate;
+ delete step.configOverrideOption;
+ delete step.propsOverrideOption;
+ });
+
+ delete normalizedDraft.workflow.stepsOrderChanged;
+ delete normalizedDraft.workflow.instancesMap;
+ return normalizedDraft;
+ }
+
+ return {
+ async doLoad() {
+ const drafts = await getWorkflowDrafts();
+
+ // We try to preserve existing drafts data and merge the new data instead
+ // We could have used self.drafts.replace(), but it will do clear() then merge()
+ self.runInAction(() => {
+ const previousKeys = {};
+ self.drafts.forEach((_value, key) => {
+ previousKeys[key] = true;
+ });
+ drafts.forEach(draft => {
+ const id = draft.id;
+ const hasPrevious = self.drafts.has(id);
+
+ self.addDraft(draft);
+
+ if (hasPrevious) {
+ delete previousKeys[id];
+ }
+ });
+
+ _.forEach(previousKeys, (_value, key) => {
+ self.drafts.delete(key);
+ });
+ });
+ },
+
+ addDraft(rawDraft) {
+ const id = rawDraft.id;
+ const previous = self.drafts.get(id);
+
+ if (!previous) {
+ self.drafts.put(rawDraft);
+ } else {
+ previous.setWorkflowDraft(rawDraft);
+ }
+ },
+
+ async updateDraft(draft) {
+ const id = draft.id;
+ const previous = self.drafts.get(id);
+ if (previous === undefined) throw new Error(`Workflow Draft "${id}" does not exist`);
+
+ const updated = await updateWorkflowDraft(normalizeForSubmission(draft));
+ previous.setWorkflowDraft(updated);
+
+ return previous;
+ },
+
+ async createDraft({ isNewWorkflow, workflowId, templateId }) {
+ const draft = await createWorkflowDraft({ isNewWorkflow, workflowId, templateId });
+ self.addDraft(draft);
+
+ return draft;
+ },
+
+ async publishDraft(draft) {
+ const id = draft.id;
+ const previous = self.drafts.get(id);
+ if (previous === undefined) throw new Error(`Workflow Draft "${id}" does not exist`);
+
+ const publishResult = await publishWorkflowDraft(normalizeForSubmission(draft));
+ self.runInAction(() => {
+ if (!publishResult.hasErrors) self.drafts.delete(id);
+ });
+
+ return publishResult;
+ },
+
+ async deleteDraft(draft) {
+ const uiEventBus = getEnv(self).uiEventBus;
+ await deleteWorkflowDraft(draft);
+ await uiEventBus.fireEvent('workflowDraftDeleted', draft);
+ self.runInAction(() => {
+ self.drafts.delete(draft.id);
+ });
+ },
+
+ cleanup: () => {
+ self.drafts.clear();
+ superCleanup();
+ },
+ };
+ })
+
+ .views(self => ({
+ get empty() {
+ return self.drafts.size === 0;
+ },
+
+ get total() {
+ return self.drafts.size;
+ },
+
+ get list() {
+ const result = [];
+ self.drafts.forEach(drafts => result.push(drafts));
+
+ return _.reverse(_.sortBy(result, ['createdAt', 'title']));
+ },
+
+ hasWorkflow(workflowId) {
+ let found = false;
+ /* eslint-disable no-restricted-syntax, no-unused-vars */
+ for (const draft of self.drafts.values()) {
+ if (draft.workflow.id === workflowId) {
+ found = true;
+ break;
+ }
+ }
+ /* eslint-enable no-restricted-syntax, no-unused-vars */
+
+ return found;
+ },
+
+ hasDraft(draftId) {
+ return self.drafts.has(draftId);
+ },
+
+ getDraft(id) {
+ return self.drafts.get(id);
+ },
+ }));
+
+function registerContextItems(appContext) {
+ appContext.workflowDraftsStore = WorkflowDraftsStore.create({}, appContext);
+}
+
+export { WorkflowDraftsStore, registerContextItems };
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/drafts/edit/WorkflowDraftEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/drafts/edit/WorkflowDraftEditor.js
new file mode 100644
index 0000000000..dd3a0ad472
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/drafts/edit/WorkflowDraftEditor.js
@@ -0,0 +1,195 @@
+/*
+ * 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, getEnv, clone } from 'mobx-state-tree';
+import { uiEventBus } from '@aws-ee/base-ui/dist/models/SessionStore';
+
+import getEditWorkflowDraftMetaForm from '../../../forms/EditWorkflowDraftMetaForm';
+import WorkflowStepEditor from './WorkflowStepEditor';
+
+let globals; // a reference to the globals
+
+// ==================================================================
+// WorkflowDraftEditor
+// ==================================================================
+const WorkflowDraftEditor = types
+ .model('WorkflowDraftEditor', {
+ draftId: '',
+ currentPage: 0, // there are only two pages, one for meta editing and one for steps editing
+ numPages: 3,
+ stepEditors: types.optional(types.map(WorkflowStepEditor), {}),
+ })
+
+ .volatile(_self => ({
+ draftCopy: undefined,
+ draftMetaForm: undefined,
+ }))
+
+ .actions(self => {
+ // private
+ function makeDraftCopy() {
+ self.runInAction(() => {
+ const draft = self.originalDraft;
+ self.draftCopy = clone(draft);
+ self.draftMetaForm = getEditWorkflowDraftMetaForm(self.draftCopy.workflow);
+ });
+ }
+
+ return {
+ // 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();
+ },
+
+ afterCreate() {
+ makeDraftCopy();
+ },
+
+ nextPage() {
+ if (self.currentPage < self.numPages - 1) self.currentPage += 1;
+ else self.currentPage = self.numPages - 1;
+ return self.currentPage;
+ },
+
+ previousPage() {
+ if (self.currentPage > 0) self.currentPage -= 1;
+ else self.currentPage = 0;
+ return self.currentPage;
+ },
+
+ cancel() {
+ // We make a fresh copy in case the existing copy one was used
+ makeDraftCopy();
+ self.currentPage = 0;
+ },
+
+ getStepEditor(step) {
+ const stepId = step.id;
+ const entry = self.stepEditors.get(stepId) || WorkflowStepEditor.create({ stepId }, getEnv(self));
+
+ self.stepEditors.set(stepId, entry);
+ return entry;
+ },
+
+ removeStepEditor(stepId) {
+ self.stepEditors.delete(stepId);
+ },
+
+ addStep(step) {
+ const version = self.version;
+ version.addStep(step);
+ },
+
+ async update(draft) {
+ const updatedDraft = await self.workflowDraftsStore.updateDraft(draft);
+ // The following code is not the greatest idea, but okay for this scenario, the correct approach would have
+ // been to call makeDraftCopy(), however, this will result in losing some of the ui states in the draft card
+ self.draft.setRev(updatedDraft.rev);
+ },
+
+ async publish(draft) {
+ const result = await self.workflowDraftsStore.publishDraft(draft);
+
+ // Remove the editor from the session store, if there were no errors
+ if (!result.hasErrors) {
+ await uiEventBus.fireEvent('workflowDraftDeleted', draft);
+ }
+
+ return result;
+ },
+ };
+ })
+
+ .views(self => ({
+ get workflowDraftsStore() {
+ return getEnv(self).workflowDraftsStore;
+ },
+
+ get hasNextPage() {
+ return self.currentPage < self.numPages - 1;
+ },
+
+ get hasPreviousPage() {
+ return self.currentPage > 0;
+ },
+
+ get originalDraft() {
+ const store = self.workflowDraftsStore;
+ return store.getDraft(self.draftId);
+ },
+
+ get draft() {
+ return self.draftCopy;
+ },
+
+ // Returns a WorkflowVersion model object
+ get version() {
+ return self.draft.workflow;
+ },
+
+ get metaForm() {
+ return self.draftMetaForm;
+ },
+
+ // Returns true if at least one step editor is in edit mode
+ get stepEditorsEditing() {
+ let found = false;
+ /* eslint-disable no-restricted-syntax, no-unused-vars */
+ for (const editor of self.stepEditors.values()) {
+ if (editor.editing) {
+ found = true;
+ break;
+ }
+ }
+ /* eslint-enable no-restricted-syntax, no-unused-vars */
+ return found;
+ },
+ }));
+
+function getWorkflowDraftEditor(draftId) {
+ const sessionStore = globals.sessionStore;
+ const id = encodeId(draftId);
+ const entry = sessionStore.map.get(id) || WorkflowDraftEditor.create({ draftId }, globals);
+
+ sessionStore.map.set(id, entry);
+ return entry;
+}
+
+function encodeId(draftId) {
+ return `WorkflowDraftEditor-${draftId}`;
+}
+
+function removeEditor(draftId) {
+ const sessionStore = globals.sessionStore;
+ const id = encodeId(draftId);
+
+ sessionStore.removeStartsWith(id);
+}
+
+function registerContextItems(appContext) {
+ // we are not actually registering anything here, just getting a reference to the appContext
+ globals = appContext;
+
+ uiEventBus.listenTo('workflowDraftDeleted', {
+ id: 'WorkflowDraftEditor',
+ listener: async event => {
+ // event will be the draft object
+ removeEditor(event.id);
+ },
+ });
+}
+
+export { WorkflowDraftEditor, getWorkflowDraftEditor, registerContextItems };
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/drafts/edit/WorkflowStepEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/drafts/edit/WorkflowStepEditor.js
new file mode 100644
index 0000000000..40809aa6ea
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/models/workflows/drafts/edit/WorkflowStepEditor.js
@@ -0,0 +1,174 @@
+/*
+ * 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, getParent, getEnv, getSnapshot } from 'mobx-state-tree';
+import { visit } from '@aws-ee/base-ui/dist/models/forms/InputManifest';
+
+import getWorkflowStepDescForm from '../../../forms/WorkflowStepDescForm';
+import getWorkflowStepPropsForm from '../../../forms/WorkflowStepPropsForm';
+import ConfigurationEditor from '../../../configuration/ConfigurationEditor';
+
+// ==================================================================
+// WorkflowStepEditor
+// ==================================================================
+const WorkflowStepEditor = types
+ .model('WorkflowStepEditor', {
+ stepId: '', // The step id for the workflow step
+ contentExpanded: false,
+ configEdit: false, // If we are editing mode or not for the configuration section
+ descEdit: false, // If we are editing mode or not for the description section
+ propsEdit: false, // If we are editing mode or not for the props section
+ })
+
+ .volatile(_self => ({
+ configurationEditor: undefined,
+ stepDescForm: undefined,
+ stepPropsForm: undefined,
+ }))
+
+ .actions(self => {
+ return {
+ // 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();
+ },
+
+ afterAttach() {
+ if (self.configurationEditor !== undefined) return;
+ const step = self.step;
+ const inputManifest = _.get(step, 'stepTemplate.inputManifest');
+ const configs = getSnapshot(step.configs);
+ const allowed = step.configOverrideOption.allowed;
+ const defaults = self.defaults;
+
+ self.configurationEditor = ConfigurationEditor.create(
+ {
+ inputManifest: _.isUndefined(inputManifest)
+ ? undefined
+ : prepareInputManifest(inputManifest, { allowed, defaults }),
+ configuration: configs,
+ },
+ getEnv(self),
+ );
+
+ self.stepDescForm = getWorkflowStepDescForm(step, { isTemplate: false });
+ self.stepPropsForm = getWorkflowStepPropsForm(step, { isTemplate: false });
+ },
+
+ setContentExpanded(flag) {
+ self.contentExpanded = !!flag; // !! will simply turn any type to a boolean type
+ },
+
+ setConfigEdit(flag) {
+ self.configEdit = flag;
+ },
+
+ setDescEdit(flag) {
+ self.descEdit = flag;
+ },
+
+ setPropsEdit(flag) {
+ self.propsEdit = flag;
+ },
+
+ applyConfigs(configs = {}) {
+ self.step.setConfigs(configs);
+ },
+
+ applyDescAndTitle(desc, title) {
+ self.step.setDesc(desc);
+ self.step.setTitle(title);
+ self.stepDescForm = getWorkflowStepDescForm(self.step, { isTemplate: false });
+ },
+
+ applySkippable(skippable) {
+ self.step.setSkippable(skippable);
+ self.stepPropsForm = getWorkflowStepPropsForm(self.step, { isTemplate: false });
+ },
+ };
+ })
+
+ .views(self => ({
+ get step() {
+ const version = self.version;
+ return version.getStep(self.stepId);
+ },
+
+ // WorkflowVersion
+ get version() {
+ const parentEditor = getParent(self, 2);
+ return parentEditor.version;
+ },
+
+ // The workflow template step defaults (if this workflow step has an associated workflow template step with it)
+ get defaults() {
+ const workflowVersion = self.version;
+ if (!workflowVersion) return undefined;
+ const workflowTemplateVersion = workflowVersion.template;
+ if (!workflowTemplateVersion) return undefined;
+ const workflowTemplateStep = workflowTemplateVersion.getStep(self.stepId);
+ if (!workflowTemplateStep) return undefined;
+
+ return workflowTemplateStep.defaults;
+ },
+
+ get descForm() {
+ return self.stepDescForm;
+ },
+
+ get propsForm() {
+ return self.stepPropsForm;
+ },
+
+ get editing() {
+ return self.configEdit || self.descEdit || self.propsEdit;
+ },
+ }));
+
+// Returns a copy of the input manifest (but as a json object), the copy has its entries updated as follows:
+// - If the entry name is not allowed to be overridden, then disabled is turned on with a warn message in the extra section
+// - If there is a default value for the given key, then it is set on the entry
+function prepareInputManifest(inputManifest, { allowed = [], defaults: rawDefaults }) {
+ if (_.isEmpty(inputManifest)) return inputManifest;
+ const names = inputManifest.names;
+ const copy = _.cloneDeep(getSnapshot(inputManifest));
+ const defaults = rawDefaults ? getSnapshot(rawDefaults) : {};
+
+ const visitFn = item => {
+ if (!item.name) return undefined;
+ const name = item.name;
+ if (!names.includes(name)) return undefined;
+
+ if (!allowed.includes(name)) {
+ item.disabled = true;
+ _.set(item, 'extra.warn', 'The workflow template used by this workflow does not allow you to modify this field');
+ }
+
+ if (_.has(defaults, name)) {
+ item.default = defaults[name];
+ }
+ return item;
+ };
+
+ _.forEach(copy.sections, section => {
+ visit(section.children, visitFn);
+ });
+
+ return copy;
+}
+
+export default WorkflowStepEditor;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/ProgressPlaceholder.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/ProgressPlaceholder.js
new file mode 100644
index 0000000000..231715e47d
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/ProgressPlaceholder.js
@@ -0,0 +1,47 @@
+/*
+ * 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 { Segment, Placeholder, Divider } from 'semantic-ui-react';
+
+// expected props
+// - segmentCount (via props)
+// - className (via props)
+const Component = ({ segmentCount = 1 }) => {
+ const segment = index => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ return _.map(_.times(segmentCount, String), index => segment(index));
+};
+
+export default Component;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/component-states/WorkflowCommonCardState.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/component-states/WorkflowCommonCardState.js
new file mode 100644
index 0000000000..23540a3d48
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/component-states/WorkflowCommonCardState.js
@@ -0,0 +1,55 @@
+/*
+ * 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 { sessionStore, uiEventBus } from '@aws-ee/base-ui/dist/models/SessionStore';
+
+const WorkflowCommonUIState = types
+ .model('WorkflowCommonUIState', {
+ versionNumber: -1,
+ mainTabIndex: 0,
+ })
+ .actions(self => ({
+ setVersionNumber(v) {
+ self.versionNumber = v;
+ },
+ setMainTabIndex(index) {
+ self.mainTabIndex = index;
+ },
+ }));
+
+function getUIState(idSuffix) {
+ const id = `WorkflowCommonUIState-${idSuffix}`;
+ const entry = sessionStore.map.get(id) || WorkflowCommonUIState.create();
+
+ sessionStore.map.set(id, entry);
+ return entry;
+}
+
+uiEventBus.listenTo('workflowTemplatePublished', {
+ id: 'WorkflowCommonUIState',
+ listener: async event => {
+ sessionStore.removeStartsWith(`WorkflowCommonUIState-${event.id}`);
+ },
+});
+
+uiEventBus.listenTo('workflowPublished', {
+ id: 'WorkflowCommonUIState',
+ listener: async event => {
+ sessionStore.removeStartsWith(`WorkflowCommonUIState-${event.id}`);
+ },
+});
+
+export default getUIState;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/WorkflowCommonDraftCard.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/WorkflowCommonDraftCard.js
new file mode 100644
index 0000000000..9bb3809932
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/WorkflowCommonDraftCard.js
@@ -0,0 +1,199 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, observable, runInAction } from 'mobx';
+import TimeAgo from 'react-timeago';
+import { Header, Label, Button, Icon, Modal } from 'semantic-ui-react';
+import c from 'classnames';
+import { displayError, displaySuccess } from '@aws-ee/base-ui/dist/helpers/notification';
+
+import getUIState from '../component-states/WorkflowCommonCardState';
+
+// expected props
+// - draft - a WorkflowTemplateDraft or WorkflowDraft model instance (via props)
+// - draftsStore (via props) (either workflowTemplateDraftsStore or workflowDraftsStore)
+// - onEdit (via props) called with (draft)
+// - userDisplayName (via injection)
+// - className (via props)
+class WorkflowCommonDraftCard extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.shouldShowDeleteDialog = false;
+ this.deletingInProgress = false;
+ });
+ }
+
+ getDraftsStore() {
+ return this.props.draftsStore;
+ }
+
+ getState() {
+ return getUIState(this.getDraft().id);
+ }
+
+ selectedMainTabIndex() {
+ return this.getState().mainTabIndex;
+ }
+
+ getDraft() {
+ return this.props.draft;
+ }
+
+ getVersion() {
+ const draft = this.getDraft();
+ return draft.template || draft.workflow;
+ }
+
+ getUserDisplayNameService() {
+ return this.props.userDisplayName;
+ }
+
+ handleOnTabChange = (_event, data) => {
+ this.getState().setMainTabIndex(data.activeIndex);
+ };
+
+ handleEditDraft = async event => {
+ event.preventDefault();
+ event.stopPropagation();
+ const draft = this.getDraft();
+ if (this.props.onEdit) return this.props.onEdit(draft);
+ return undefined;
+ };
+
+ showDeleteDialog = () => {
+ this.shouldShowDeleteDialog = true;
+ this.deletingInProgress = false;
+ };
+
+ hideDeleteDialog = () => {
+ if (this.deletingInProgress) return;
+ this.shouldShowDeleteDialog = false;
+ };
+
+ handleDeleteDraft = async () => {
+ const clean = () => {
+ runInAction(() => {
+ this.shouldShowDeleteDialog = false;
+ this.deletingInProgress = false;
+ });
+ };
+
+ const draft = this.getDraft();
+ const draftsStore = this.getDraftsStore();
+
+ try {
+ this.deletingInProgress = true;
+ await draftsStore.deleteDraft(draft);
+ clean();
+ displaySuccess('Draft deleted successfully');
+ } catch (error) {
+ clean();
+ displayError(error);
+ }
+ };
+
+ render() {
+ const className = this.props.className;
+ const draft = this.getDraft();
+ const version = this.getVersion();
+ const isTemplate = draft.template !== undefined;
+ const { id, title } = version;
+ const { createdAt, createdBy } = draft;
+ const displayNameService = this.getUserDisplayNameService();
+ const by = () => by {displayNameService.getDisplayName(createdBy)} ;
+
+ return (
+ <>
+ {isTemplate && 'Template '} Draft
+
+
+
+ {title}
+
+
+ {id}
+
+
+ created {by()}
+
+
+
+
{this.renderActionButtons()}
+
+ {this.renderMainTabs(version)}
+
+ {this.renderDeleteDialog(version)}
+ >
+ );
+ }
+
+ renderDeleteDialog(version) {
+ const shouldShowDeleteDialog = this.shouldShowDeleteDialog;
+ const { id } = version;
+ const progress = this.deletingInProgress;
+
+ return (
+
+
+
+ Are you sure you want to delete draft "{id}" ?
+
+
+
+ Cancel
+
+
+ Delete
+
+
+
+ );
+ }
+
+ renderActionButtons() {
+ return (
+
+
+
+
+ );
+ }
+
+ renderMainTabs(version) {
+ const renderer = _.isFunction(this.props.children) ? this.props.children : _.noop;
+ const uiState = this.getState();
+ return renderer({
+ uiState,
+ version,
+ });
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowCommonDraftCard, {
+ showDeleteDialog: action,
+ hideDeleteDialog: action,
+ handleDeleteDraft: action,
+ handleEditDraft: action,
+ shouldShowDeleteDialog: observable,
+ deletingInProgress: observable,
+});
+
+export default inject('userDisplayName')(observer(WorkflowCommonDraftCard));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonDraftStepsEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonDraftStepsEditor.js
new file mode 100644
index 0000000000..946aa51d7d
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonDraftStepsEditor.js
@@ -0,0 +1,323 @@
+/*
+ * 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 { observer, inject, Observer } from 'mobx-react';
+import { decorate, action, runInAction, observable } from 'mobx';
+import { Button, Header, Dimmer, Loader, Message } from 'semantic-ui-react';
+import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
+import c from 'classnames';
+import { displayError, displaySuccess } from '@aws-ee/base-ui/dist/helpers/notification';
+
+import AddStepDropDown from '../../../workflow-step-templates/AddStepDropDown';
+
+// expected props
+// - editor (via prop) an instance of the WorkflowTemplateDraftEditor model or WorkflowDraftEditor model
+// - stepEditor (vai props) an instance of the WorkflowTemplateStepEditor react component or WorkflowStepEditor reactComponent
+class WorkflowCommonDraftStepsEditor extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.processing = false;
+ this.clickedOnNext = false;
+ });
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ getEditor() {
+ return this.props.editor;
+ }
+
+ getStepEditorComponent() {
+ return this.props.stepEditor;
+ }
+
+ getVersion() {
+ return this.getEditor().version;
+ }
+
+ getSelectedSteps() {
+ return this.getVersion().selectedSteps;
+ }
+
+ handleAddStep = step => {
+ if (!step) return;
+ this.getEditor().addStep(step);
+ };
+
+ handleCancel = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.clickedOnNext = false;
+ this.processing = false;
+ const onCancel = this.props.onCancel || _.noop;
+ onCancel();
+ };
+
+ handleDelete = step => {
+ const editor = this.getEditor();
+ const version = this.getVersion();
+ const id = step.id;
+
+ version.removeStep(step);
+ setTimeout(() => {
+ editor.removeStepEditor(id);
+ }, 150);
+ };
+
+ handleNext = async event => {
+ this.clickedOnNext = true;
+ return this.handleSave(event);
+ };
+
+ handlePrevious = event => {
+ // we don't save the form in this case
+ event.preventDefault();
+ event.stopPropagation();
+ this.clickedOnNext = false;
+ this.getEditor().previousPage();
+ };
+
+ handleSave = async event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const editor = this.getEditor();
+ const { draft } = editor;
+
+ this.processing = true;
+
+ try {
+ await editor.update(draft);
+ runInAction(() => {
+ this.processing = false;
+ });
+ if (this.clickedOnNext) {
+ this.getEditor().nextPage();
+ return;
+ }
+ displaySuccess('Saved successfully');
+ } catch (error) {
+ runInAction(() => {
+ this.processing = false;
+ this.clickedOnNext = false;
+ });
+ displayError(error);
+ }
+ };
+
+ handleStepSave = async () => {
+ const editor = this.getEditor();
+ const { draft } = editor;
+
+ this.processing = true;
+
+ try {
+ await editor.update(draft);
+ runInAction(() => {
+ this.processing = false;
+ });
+ displaySuccess('Saved successfully');
+ } catch (error) {
+ runInAction(() => {
+ this.processing = false;
+ });
+ displayError(error);
+ }
+ };
+
+ onDragEnd = result => {
+ const version = this.getVersion();
+ if (!version.canRearrangeSteps) return;
+
+ // see https://egghead.io/lessons/react-persist-list-reordering-with-react-beautiful-dnd-using-the-ondragend-callback
+ const { draggableId, destination, source } = result;
+ const isSource = source.droppableId === 'selected-steps';
+ const isDestination = destination && destination.droppableId === 'selected-steps';
+ const isStep = !!version.getStep(draggableId);
+
+ if (!destination) {
+ // we don't support removal of a step by dragging it out of its container
+ return;
+ }
+
+ if (destination.droppableId === source.droppableId && destination.index === source.index) {
+ // we don't need to do anything here
+ return;
+ }
+
+ if (isSource && isDestination && isStep) {
+ // we are dealing with reordering of the steps
+ version.reinsertStep(source.index, destination.index);
+ }
+ };
+
+ render() {
+ const processing = this.processing;
+ const editor = this.getEditor();
+ const editing = editor.stepEditorsEditing;
+ const hasPrevious = editor.hasPreviousPage;
+ const version = this.getVersion();
+ const canRearrange = version.canRearrangeSteps;
+
+ return (
+ <>
+
+
+ Processing
+
+
+ {!canRearrange && (
+
+ Warning The workflow template used by this workflow does not allow for steps
+ to be deleted, added or rearranged
+
+ )}
+ {canRearrange && (
+
+
+ {(provided, snapshot) => (
+
+ {() => (
+
+ {this.renderSelectedSteps()}
+ {provided.placeholder}
+
+ )}
+
+ )}
+
+
+ )}
+ {!canRearrange && {this.renderSelectedSteps()}
}
+
+
+ {!editing && (
+
+
+ {hasPrevious && (
+
+ )}
+
+
+ Cancel
+
+
+ )}
+ >
+ );
+ }
+
+ renderSelectedSteps() {
+ const selectedSteps = this.getSelectedSteps();
+ const size = selectedSteps.length;
+
+ if (size === 0) {
+ return null;
+ }
+
+ const version = this.getVersion();
+ const canRearrange = version.canRearrangeSteps;
+ const editor = this.getEditor();
+ const getStepEditor = step => editor.getStepEditor(step);
+ const StepEditorComponent = this.getStepEditorComponent();
+
+ if (!canRearrange)
+ return _.map(selectedSteps, step => (
+
+
+ {() => (
+
+ )}
+
+
+ ));
+
+ return _.map(selectedSteps, (step, index) => (
+
+ {(provided, _snapshot) => (
+
+
+
+ )}
+
+ ));
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowCommonDraftStepsEditor, {
+ onDragEnd: action,
+ handleAddStep: action,
+ handleCancel: action,
+ handleDelete: action,
+ handleSave: action,
+ handleStepSave: action,
+ handleNext: action,
+ handlePrevious: action,
+ clickedOnNext: observable,
+ processing: observable,
+});
+
+export default inject()(observer(WorkflowCommonDraftStepsEditor));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonStepConfigEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonStepConfigEditor.js
new file mode 100644
index 0000000000..3371a34acd
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonStepConfigEditor.js
@@ -0,0 +1,143 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, computed } from 'mobx';
+import { Segment, Icon, Divider, Header } from 'semantic-ui-react';
+import ConfigTable from '@aws-ee/base-ui/dist/parts/configuration/ConfigTable';
+import ConfigurationEditor from '@aws-ee/base-ui/dist/parts/configuration/ConfigurationEditor';
+import ConfigurationReview from '@aws-ee/base-ui/dist/parts/configuration/ConfigurationReview';
+
+// expected props
+// - stepEditor - a WorkflowStepEditor or a WorkflowTemplateStepEditor model instance (via props)
+// - onSave - called when the configuration is saved (via props)
+// - className (via props)
+class WorkflowCommonStepConfigEditor extends React.Component {
+ getStepEditor() {
+ return this.props.stepEditor;
+ }
+
+ getStep() {
+ return this.getStepEditor().step;
+ }
+
+ get editing() {
+ return this.getStepEditor().configEdit;
+ }
+
+ handleEditOn = event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const stepEditorModel = this.getStepEditor();
+ stepEditorModel.setConfigEdit(true);
+ };
+
+ handleEditOff = () => {
+ const stepEditorModel = this.getStepEditor();
+ stepEditorModel.setConfigEdit(false);
+ };
+
+ handleSave = async configs => {
+ const onSave = this.props.onSave || _.noop;
+ const stepEditorModel = this.getStepEditor();
+
+ await onSave(configs);
+ stepEditorModel.setConfigEdit(false);
+ };
+
+ render() {
+ const editing = this.editing;
+ const step = this.getStep();
+ const configRows = step.configSummaryRows || [];
+ const hasConfigRows = configRows.length > 0;
+ const canEdit = !editing && hasConfigRows;
+
+ return (
+
+ {!editing && (
+
+
+
+ Configuration
+
+ {canEdit && (
+
+
+
+ )}
+
+ )}
+
+ {editing && this.renderConfigEditingContent()}
+ {!editing && this.renderConfigContent()}
+
+ );
+ }
+
+ renderConfigContent() {
+ const step = this.getStep();
+ const configRows = step.configSummaryRows || [];
+ const hasConfigRows = configRows.length > 0;
+
+ return (
+ <>
+ {hasConfigRows && (
+
+
+
+ )}
+ {!hasConfigRows && No configuration entries are available
}
+ >
+ );
+ }
+
+ renderConfigEditingContent() {
+ const model = this.getStepEditor().configurationEditor;
+ const review = model.review;
+
+ if (review) {
+ return (
+ <>
+
+ Review Configuration Changes
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowCommonStepConfigEditor, {
+ editing: computed,
+ handleEditOn: action,
+ handleEditOff: action,
+ handleSave: action,
+});
+
+export default inject()(observer(WorkflowCommonStepConfigEditor));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonStepDescEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonStepDescEditor.js
new file mode 100644
index 0000000000..7dcd621d33
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonStepDescEditor.js
@@ -0,0 +1,148 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, computed } from 'mobx';
+import { Icon, Divider, Header, Button } from 'semantic-ui-react';
+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';
+
+// expected props
+// - stepEditor - a WorkflowStepEditor or a WorkflowTemplateStepEditor model instance (via props)
+// - onSave - called when the desc/title are saved (via props)
+// - className (via props)
+class WorkflowCommonStepDescEditor extends React.Component {
+ getStepEditor() {
+ return this.props.stepEditor;
+ }
+
+ getStep() {
+ return this.getStepEditor().step;
+ }
+
+ getDescForm() {
+ return this.getStepEditor().descForm;
+ }
+
+ get editing() {
+ return this.getStepEditor().descEdit;
+ }
+
+ handleEditOn = event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const stepEditorModel = this.getStepEditor();
+ stepEditorModel.setDescEdit(true);
+ };
+
+ handleEditOff = () => {
+ const stepEditorModel = this.getStepEditor();
+ stepEditorModel.setDescEdit(false);
+ };
+
+ handleSave = async form => {
+ const onSave = this.props.onSave || _.noop;
+ const stepEditorModel = this.getStepEditor();
+ const { stepTitle, stepDesc } = form.values();
+
+ stepEditorModel.applyDescAndTitle(stepDesc, stepTitle);
+
+ await onSave();
+ stepEditorModel.setDescEdit(false);
+ };
+
+ render() {
+ const editing = this.editing;
+ const canEdit = !editing;
+
+ return (
+
+ {!editing && (
+
+
+
+ Description
+
+ {canEdit && (
+
+
+
+ )}
+
+ )}
+
+ {editing && this.renderEditingContent()}
+ {!editing && this.renderReadOnlyContent()}
+
+ );
+ }
+
+ renderReadOnlyContent() {
+ const step = this.getStep();
+
+ return
; // eslint-disable-line react/no-danger
+ }
+
+ renderEditingContent() {
+ const form = this.getDescForm();
+ const stepTitleField = form.$('stepTitle');
+ const stepDescField = form.$('stepDesc');
+
+ return (
+ <>
+
+ Change Title & Description
+
+
+ {({ processing, _onSubmit, onCancel }) => (
+ <>
+
+
+
+
+
+ Cancel
+
+
+ >
+ )}
+
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowCommonStepDescEditor, {
+ editing: computed,
+ handleEditOn: action,
+ handleEditOff: action,
+ handleSave: action,
+});
+
+export default inject()(observer(WorkflowCommonStepDescEditor));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonStepEditorCard.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonStepEditorCard.js
new file mode 100644
index 0000000000..b2f4882916
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonStepEditorCard.js
@@ -0,0 +1,122 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, computed } from 'mobx';
+import { Segment, Icon, Accordion } from 'semantic-ui-react';
+
+// expected props
+// - stepEditor - a WorkflowStepEditor model or a WorkflowTemplateStepEditor model (via props)
+// - onDelete - called when the step is to be deleted, passed (step) (via props)
+// - canDelete (via props) defaults to true
+// - canMove (via props) defaults to true
+// - className (via props)
+class WorkflowCommonStepEditorCard extends React.Component {
+ get contentExpanded() {
+ return this.getStepEditor().contentExpanded;
+ }
+
+ getStepEditor() {
+ return this.props.stepEditor;
+ }
+
+ getStep() {
+ return this.getStepEditor().step;
+ }
+
+ get canDelete() {
+ return this.props.canDelete === undefined ? true : this.props.canDelete;
+ }
+
+ get canMove() {
+ return this.props.canMove === undefined ? true : this.props.canMove;
+ }
+
+ handleExpandContent = event => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ const editor = this.getStepEditor();
+ editor.setContentExpanded(!this.contentExpanded);
+ };
+
+ handleDelete = event => {
+ event.stopPropagation(); // this was needed, otherwise, the handleClick was called after
+ // which resulted in mobx state tree warning about instance being accessed after being deleted
+ const onDelete = this.props.onDelete || _.noop;
+
+ onDelete(this.getStep());
+ };
+
+ render() {
+ const className = this.props.className || 'p0 pl1';
+ const step = this.getStep();
+
+ return (
+
+ {this.renderContent(step)}
+
+ );
+ }
+
+ renderContent(step) {
+ const opened = this.contentExpanded;
+ const canDelete = this.canDelete;
+ const canMove = this.canMove;
+
+ return (
+
+
+
+ {canMove && (
+
+
+
+ )}
+ {!canMove &&
}
+
+
+
{step.derivedTitle || step.title}
+
+ {step.templateId} v{step.templateVer}
+
+
+ {canDelete && (
+
+
+
+ )}
+
+
+
+ {this.props.children}
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowCommonStepEditorCard, {
+ contentExpanded: computed,
+ canDelete: computed,
+ canMove: computed,
+ handleDelete: action,
+ handleExpandContent: action,
+});
+
+export default inject()(observer(WorkflowCommonStepEditorCard));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonStepPropsEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonStepPropsEditor.js
new file mode 100644
index 0000000000..660df0c8c1
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-common/drafts/edit/WorkflowCommonStepPropsEditor.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 _ from 'lodash';
+import React from 'react';
+import { observer, inject } from 'mobx-react';
+import { decorate, action, computed } from 'mobx';
+import { Icon, Divider, Button, Segment, Header } from 'semantic-ui-react';
+
+import Form from '@aws-ee/base-ui/dist/parts/helpers/fields/Form';
+import Toggle from '@aws-ee/base-ui/dist/parts/helpers/fields/Toggle';
+import PropertyTable from '../../../workflow-templates/PropertyTable';
+
+// expected props
+// - stepEditor - a WorkflowStepEditor or aWorkflowTemplateStepEditor model instance (via props)
+// - onSave - called when the props are saved (via props)
+// - className (via props)
+class WorkflowCommonStepPropsEditor extends React.Component {
+ getStepEditor() {
+ return this.props.stepEditor;
+ }
+
+ getStep() {
+ return this.getStepEditor().step;
+ }
+
+ getPropsForm() {
+ return this.getStepEditor().propsForm;
+ }
+
+ get editing() {
+ return this.getStepEditor().propsEdit;
+ }
+
+ handleEditOn = event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const stepEditorModel = this.getStepEditor();
+ stepEditorModel.setPropsEdit(true);
+ };
+
+ handleEditOff = () => {
+ const stepEditorModel = this.getStepEditor();
+ stepEditorModel.setPropsEdit(false);
+ };
+
+ handleSave = async form => {
+ const onSave = this.props.onSave || _.noop;
+ const stepEditorModel = this.getStepEditor();
+ const { skippable } = form.values();
+
+ stepEditorModel.applySkippable(skippable);
+
+ await onSave();
+ stepEditorModel.setPropsEdit(false);
+ };
+
+ render() {
+ const editing = this.editing;
+ const canEdit = !editing;
+
+ return (
+
+ {!editing && (
+
+
+
+ Properties
+
+ {canEdit && (
+
+
+
+ )}
+
+ )}
+
+ {editing && this.renderEditingContent()}
+ {!editing && this.renderReadOnlyContent()}
+
+ );
+ }
+
+ renderReadOnlyContent() {
+ const step = this.getStep();
+ const propertyRows = step.propertySummaryRows || [];
+ return (
+
+
+
+ );
+ }
+
+ renderEditingContent() {
+ const form = this.getPropsForm();
+ const skippableField = form.$('skippable');
+
+ return (
+ <>
+
+
+ {({ processing, _onSubmit, onCancel }) => (
+
+ )}
+
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowCommonStepPropsEditor, {
+ editing: computed,
+ handleEditOn: action,
+ handleEditOff: action,
+ handleSave: action,
+});
+
+export default inject()(observer(WorkflowCommonStepPropsEditor));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-step-templates/AddStepDropDown.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-step-templates/AddStepDropDown.js
new file mode 100644
index 0000000000..955fa42fce
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-step-templates/AddStepDropDown.js
@@ -0,0 +1,115 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, runInAction, observable } from 'mobx';
+import { Dropdown } from 'semantic-ui-react';
+import c from 'classnames';
+
+// expected props
+// - stepTemplatesStore (via props)
+// - onSelected (via props) (optional), a function that receives (step)
+// - disabled (via props) (optional), default to false
+// - className (via props)
+class AddStepDropDown extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.selectedStep = undefined;
+ });
+ }
+
+ getStore() {
+ return this.props.stepTemplatesStore;
+ }
+
+ getSelectedStep() {
+ return this.selectedStep;
+ }
+
+ getStepDropDownOptions() {
+ const store = this.getStore();
+ const list = store.list;
+ const options = _.map(list, template => ({
+ text: template.latest.title,
+ value: template.id,
+ }));
+
+ return options;
+ }
+
+ handleChange = (_event, { value }) => {
+ if (_.isEmpty(value)) {
+ this.selectedStep = undefined;
+ return;
+ }
+
+ const store = this.getStore();
+ const step = store.getTemplate(value);
+ if (step === undefined) {
+ this.selectedStep = undefined;
+ return;
+ }
+ this.selectedStep = step.latest;
+ };
+
+ handleClose = (e, _d) => {
+ const onSelected = this.props.onSelected || _.noop;
+ const step = this.selectedStep;
+
+ this.selectedStep = undefined;
+ if (e === undefined) return; // this means the escape key was clicked
+ onSelected(step);
+ };
+
+ render() {
+ const disabled = this.props.disabled || false;
+ const className = this.props.className;
+ const step = this.getSelectedStep();
+ const options = this.getStepDropDownOptions();
+ const isEmpty = _.isEmpty(step);
+ const text = isEmpty ? 'Add Step' : step.title;
+ const value = isEmpty ? '' : step.id;
+
+ return (
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(AddStepDropDown, {
+ selectedStep: observable,
+ handleChange: action,
+ handleClose: action,
+});
+
+export default inject('stepTemplatesStore')(observer(AddStepDropDown));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/ConfigOverrideTable.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/ConfigOverrideTable.js
new file mode 100644
index 0000000000..52693f3bf2
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/ConfigOverrideTable.js
@@ -0,0 +1,85 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Table, Icon } from 'semantic-ui-react';
+import c from 'classnames';
+import Toggle from '@aws-ee/base-ui/dist/parts/helpers/fields/Toggle';
+
+// expected props
+// - rows (via props), an array of objects, [ { name, title, allowed }, { name, title, allowed }, ... ], if editable = false
+// otherwise the array is expected to be an array of mobx form fields instances
+// - editable (via props), is this a toggle table?
+// - className (via props)
+const Component = observer(({ rows = [], className = '', editable = false, processing = false }) => {
+ if (rows.length === 0) return null;
+ const getTitle = item => (editable ? item.label : item.title);
+
+ return (
+
+
+
+ Key
+ Can be changed?
+
+
+
+ {_.map(rows, (item, index) => (
+
+ {renderKey({ name: item.name, title: getTitle(item) })}
+
+ {!editable && renderValue(item)}
+ {editable && }
+
+
+ ))}
+
+
+ );
+});
+
+function renderValue({ allowed }) {
+ if (allowed)
+ return (
+
+
+ Yes
+
+ );
+ return (
+
+
+ No
+
+ );
+}
+
+function renderKey({ title = '', name }) {
+ const hasTitle = !_.isEmpty(title);
+
+ if (hasTitle) {
+ return (
+ <>
+ {title}
+ {name}
+ >
+ );
+ }
+ return {name}
;
+}
+
+export default Component;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/ConfigSection.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/ConfigSection.js
new file mode 100644
index 0000000000..4f6d6e35cd
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/ConfigSection.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 React from 'react';
+import { observer } from 'mobx-react';
+import { Segment } from 'semantic-ui-react';
+import ConfigTable from '@aws-ee/base-ui/dist/parts/configuration/ConfigTable';
+
+import ConfigOverrideTable from './ConfigOverrideTable';
+
+// expected props
+// - model (via props) with two properties: configSummaryRows and configOverrideSummaryRows
+// - message (via props), a message to display when both rows are empty
+// - className (via props)
+const Component = observer(({ model = {}, className = '', message = 'No configuration entries are available' }) => {
+ const configRows = model.configSummaryRows || [];
+ const hasConfigRows = configRows.length > 0;
+ const configOverrideRows = model.configOverrideSummaryRows || [];
+ const hasConfigOverrideRows = configOverrideRows.length > 0;
+ const empty = !hasConfigRows && !hasConfigOverrideRows;
+
+ return (
+ <>
+ {hasConfigRows && (
+
+
+
+ )}
+ {hasConfigOverrideRows && (
+
+
+
+ )}
+ {empty && {message}
}
+ >
+ );
+});
+
+export default Component;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/PropertyOverrideTable.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/PropertyOverrideTable.js
new file mode 100644
index 0000000000..37941145bb
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/PropertyOverrideTable.js
@@ -0,0 +1,71 @@
+/*
+ * 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 { observer } from 'mobx-react';
+import { Table, Icon } from 'semantic-ui-react';
+import c from 'classnames';
+import Toggle from '@aws-ee/base-ui/dist/parts/helpers/fields/Toggle';
+
+// expected props
+// - rows (via props), an array of objects, [ { name, title, allowed }, { name, title, allowed }, ... ], if editable = false
+// otherwise the array is expected to be an array of mobx form fields instances
+// - editable (via props), is this a toggle table?
+// - className (via props)
+const Component = observer(({ rows = [], className = '', editable = false, processing = false }) => {
+ if (rows.length === 0) return null;
+ const getTitle = item => (editable ? item.label : item.title);
+
+ return (
+
+
+
+ Property
+ Can be changed?
+
+
+
+ {_.map(rows, (item, index) => (
+
+ {getTitle(item)}
+
+ {!editable && convert(item.allowed)}
+ {editable && }
+
+
+ ))}
+
+
+ );
+});
+
+function convert(allowed) {
+ if (allowed)
+ return (
+
+
+ Yes
+
+ );
+ return (
+
+
+ No
+
+ );
+}
+
+export default Component;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/PropertySection.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/PropertySection.js
new file mode 100644
index 0000000000..df81f35952
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/PropertySection.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 React from 'react';
+import { observer } from 'mobx-react';
+import { Segment } from 'semantic-ui-react';
+
+import PropertyTable from './PropertyTable';
+import PropertyOverrideTable from './PropertyOverrideTable';
+
+// expected props
+// - model (via props) with two properties: propertySummaryRows and propertyOverrideSummaryRows
+// - message (via props), a message to display when both rows are empty
+// - className (via props)
+const Component = observer(({ model = {}, className = '', message = 'No properties are available' }) => {
+ const propertyRows = model.propertySummaryRows || [];
+ const hasPropertyRows = propertyRows.length > 0;
+ const propertyOverrideRows = model.propertyOverrideSummaryRows || [];
+ const hasPropertyOverrideRows = propertyOverrideRows.length > 0;
+ const empty = !hasPropertyRows && !hasPropertyOverrideRows;
+
+ return (
+ <>
+ {hasPropertyRows && (
+
+
+
+ )}
+ {hasPropertyOverrideRows && (
+
+
+
+ )}
+ {empty && {message}
}
+ >
+ );
+});
+
+export default Component;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/PropertyTable.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/PropertyTable.js
new file mode 100644
index 0000000000..1e6505283d
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/PropertyTable.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 _ from 'lodash';
+import React from 'react';
+import { observer } from 'mobx-react';
+import { Table } from 'semantic-ui-react';
+
+// expected props
+// - rows (via props), an array of objects, [ { title, value }, { title, value }, ... ]
+// - className (via props)
+const Component = observer(({ rows = [], className = '' }) => {
+ if (rows.length === 0) return null;
+
+ return (
+
+
+
+ Property
+ Value
+
+
+
+ {_.map(rows, (item, index) => (
+
+ {item.title}
+ {convert(item.value)}
+
+ ))}
+
+
+ );
+});
+
+function convert(value) {
+ return _.isNil(value) ? 'Not Provided' : value.toString();
+}
+
+export default Component;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/WorkflowTemplateCard.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/WorkflowTemplateCard.js
new file mode 100644
index 0000000000..1dc55ce427
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/WorkflowTemplateCard.js
@@ -0,0 +1,140 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, runInAction, action } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import TimeAgo from 'react-timeago';
+import { Header, Dropdown, Label } from 'semantic-ui-react';
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+
+import getUIState from '../workflow-common/component-states/WorkflowCommonCardState';
+import WorkflowTemplateCardTabs from './WorkflowTemplateCardTabs';
+
+// expected props
+// - template - a WorkflowTemplate model instance (via props)
+// - v - the selected version number, will default to latest or existing state (via props)
+// - userDisplayName (via injection)
+// - location (from react router)
+class WorkflowTemplateCard extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ const state = this.getState();
+ const versionSpecified = !_.isNil(this.props.v);
+ let v;
+
+ if (versionSpecified) {
+ v = this.props.v;
+ } else if (state.versionNumber === -1) {
+ v = this.props.template.latest.v;
+ } else {
+ v = state.versionNumber;
+ }
+ state.setVersionNumber(v);
+ });
+ }
+
+ getState() {
+ return getUIState(this.getTemplate().id);
+ }
+
+ selectedVersionNumber() {
+ return this.getState().versionNumber;
+ }
+
+ getTemplate() {
+ return this.props.template;
+ }
+
+ getTemplateVersion() {
+ const template = this.getTemplate();
+ const v = this.selectedVersionNumber();
+ const version = template.getVersion(v);
+ if (_.isEmpty(version)) {
+ // This is an error
+ displayError(`Version ${v} of this workflow template is not valid`);
+ return {};
+ }
+
+ return version;
+ }
+
+ getUserDisplayNameService() {
+ return this.props.userDisplayName;
+ }
+
+ handleOnVersionChange = ({ value = 1 }) => {
+ this.getState().setVersionNumber(value);
+ };
+
+ render() {
+ const templateVersion = this.getTemplateVersion();
+ const { id, v, title, createdAt, createdBy } = templateVersion;
+ const displayNameService = this.getUserDisplayNameService();
+ const isSystem = displayNameService.isSystem(createdBy);
+ const by = () => (isSystem ? '' : by {displayNameService.getDisplayName(createdBy)} );
+
+ return (
+ <>
+ Template
+
+
+ {title}
+
+ created {by()}
+
+
+
+ {id} {this.renderVersionDropdown(v)}
+
+
+ {this.renderMainTabs(templateVersion)}
+ >
+ );
+ }
+
+ renderVersionDropdown(currentVersion) {
+ const template = this.getTemplate();
+ const versions = template.versionNumbers;
+ const options = _.map(versions, version => ({ text: `v${version}`, value: version }));
+
+ if (versions.length === 1) return v{template.latest.v} ;
+
+ return (
+ this.handleOnVersionChange(data)}
+ />
+ );
+ }
+
+ renderMainTabs(template) {
+ const uiState = this.getState();
+ return ;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowTemplateCard, {
+ handleOnVersionChange: action,
+});
+
+export default inject('userDisplayName')(withRouter(observer(WorkflowTemplateCard)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/WorkflowTemplateCardTabs.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/WorkflowTemplateCardTabs.js
new file mode 100644
index 0000000000..d7e4f6201a
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/WorkflowTemplateCardTabs.js
@@ -0,0 +1,127 @@
+/*
+ * 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 { observer, inject, Observer } from 'mobx-react';
+import { decorate, action } from 'mobx';
+import { Tab, Grid, Label, Segment } from 'semantic-ui-react';
+
+import PropertySection from './PropertySection';
+
+import WorkflowTemplateStep from './WorkflowTemplateStep';
+
+// expected props
+// - template - either a WorkflowTemplateVersion model instance (via props)
+// - uiState - to keep track of the active tab (via props)
+// - className (via props)
+class WorkflowTemplateCardTabs extends React.Component {
+ getState() {
+ return this.props.uiState;
+ }
+
+ selectedMainTabIndex() {
+ return this.getState().mainTabIndex;
+ }
+
+ getTemplate() {
+ return this.props.template;
+ }
+
+ handleOnTabChange = (_event, data) => {
+ this.getState().setMainTabIndex(data.activeIndex);
+ };
+
+ render() {
+ const className = this.props.className || 'mt0';
+ const template = this.getTemplate();
+
+ const activeIndex = this.selectedMainTabIndex();
+ /* eslint-disable react/no-danger */
+ const panes = [
+ {
+ menuItem: 'Description',
+ render: () => (
+
+
+ {() =>
}
+
+
+ ),
+ },
+ {
+ menuItem: 'Steps',
+ render: () => (
+
+ {() => this.renderSteps(template)}
+
+ ),
+ },
+ {
+ menuItem: 'Properties',
+ render: () => (
+
+
+
+ ),
+ },
+ ];
+ /* eslint-enable react/no-danger */
+
+ return (
+
+ );
+ }
+
+ renderSteps(template) {
+ const steps = template.selectedSteps || [];
+
+ if (steps.length === 0) {
+ return No steps are provided ;
+ }
+
+ return (
+
+ {_.map(steps, (step, index) => (
+
+
+
+ {index + 1}
+
+
+
+
+
+
+
+
+ ))}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowTemplateCardTabs, {
+ handleOnTabChange: action,
+});
+
+export default inject()(observer(WorkflowTemplateCardTabs));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/WorkflowTemplateStep.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/WorkflowTemplateStep.js
new file mode 100644
index 0000000000..b8e04144ef
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/WorkflowTemplateStep.js
@@ -0,0 +1,113 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, runInAction, action, observable } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import { Accordion, Icon, Divider } from 'semantic-ui-react';
+
+import PropertySection from './PropertySection';
+import ConfigSection from './ConfigSection';
+
+// expected props
+// - step - an instance of WorkflowTemplateStep model (via props)
+// - location (from react router)
+class WorkflowTemplateStep extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.expanded = false;
+ });
+ }
+
+ getWorkflowTemplateStep() {
+ return this.props.step;
+ }
+
+ getStepTemplate() {
+ return this.getWorkflowTemplateStep().stepTemplate;
+ }
+
+ handleOnExpand = () => {
+ this.expanded = !this.expanded;
+ };
+
+ render() {
+ const step = this.getWorkflowTemplateStep();
+ const expanded = this.expanded;
+ return (
+
+
+
+
+
{step.derivedTitle || step.title}
+
+ {step.templateId} v{step.templateVer}
+
+
+
+
+ {this.renderDescriptionSection(step)}
+ {this.renderConfigurationSection(step)}
+ {this.renderPropertiesSection(step)}
+
+
+ );
+ }
+
+ renderDescriptionSection(step) {
+ /* eslint-disable react/no-danger */
+ return (
+ <>
+
+ Description
+
+
+ >
+ );
+ /* eslint-enable react/no-danger */
+ }
+
+ renderConfigurationSection(step) {
+ return (
+
+ );
+ }
+
+ renderPropertiesSection(step) {
+ return (
+ <>
+
+ Properties
+
+
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowTemplateStep, {
+ expanded: observable,
+ handleOnExpand: action,
+});
+
+export default inject()(withRouter(observer(WorkflowTemplateStep)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/WorkflowTemplatesList.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/WorkflowTemplatesList.js
new file mode 100644
index 0000000000..189ef4dad5
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/WorkflowTemplatesList.js
@@ -0,0 +1,41 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Container } from 'semantic-ui-react';
+
+import WorkflowPublishedTemplatesList from './published/WorkflowPublishedTemplatesList';
+import WorkflowTemplateDraftsList from './drafts/WorkflowTemplateDraftsList';
+
+// expected props
+// - location (from react router)
+class WorkflowTemplatesList extends React.Component {
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+export default inject()(withRouter(observer(WorkflowTemplatesList)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/CreateWorkflowTemplateDraft.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/CreateWorkflowTemplateDraft.js
new file mode 100644
index 0000000000..2d1fe72b77
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/CreateWorkflowTemplateDraft.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.
+ */
+
+import _ from 'lodash';
+import React from 'react';
+import { observer, inject } from 'mobx-react';
+import { decorate, action } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import { Label, Segment, Button } from 'semantic-ui-react';
+import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
+import { isStoreLoading, isStoreReady, isStoreError } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+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 getCreateDraftForm from '../../../models/forms/CreateWorkflowTemplateDraftForm';
+import ProgressPlaceHolder from '../../workflow-common/ProgressPlaceholder';
+
+// expected props
+// - onCancel (via prop) called on cancel
+// - workflowTemplateDraftsStore (via injection)
+// - workflowTemplatesStore (via injection)
+// - className (via props)
+// - location (from react router)
+class CreateWorkflowTemplateDraft extends React.Component {
+ constructor(props) {
+ super(props);
+ this.form = getCreateDraftForm();
+ }
+
+ getStore() {
+ return this.props.workflowTemplateDraftsStore;
+ }
+
+ getTemplatesStore() {
+ return this.props.workflowTemplatesStore;
+ }
+
+ getDropdownOptions() {
+ const store = this.getTemplatesStore();
+ const draftsStore = this.getStore();
+ const templates = store.list;
+ const options = [];
+
+ _.forEach(templates, template => {
+ if (!draftsStore.hasTemplate(template.id)) {
+ options.push({
+ text: template.latest.title || '',
+ value: template.id,
+ content: (
+
+
+ Existing
+ {' '}
+ {template.latest.title} {template.id}
+
+ ),
+ });
+ }
+ });
+
+ options.unshift({
+ text: 'New Workflow Template',
+ value: '-1',
+ content: (
+
+
+ New
+ {' '}
+ Workflow Template
+
+ ),
+ });
+
+ return options;
+ }
+
+ goto(pathname) {
+ const location = this.props.location;
+ const link = createLink({ location, pathname });
+
+ this.props.history.push(link);
+ }
+
+ handleSelectionChange = templateId => {
+ const form = this.form;
+ const templateIdField = form.$('templateId');
+ const templateTitleField = form.$('templateTitle');
+ const clear = () => {
+ templateIdField.clear();
+ templateTitleField.clear();
+ };
+ const set = template => {
+ templateIdField.set(template.id);
+ templateTitleField.set(template.latest.title);
+ };
+
+ if (templateId === '-1') {
+ clear();
+ } else {
+ const store = this.getTemplatesStore();
+ const template = store.getTemplate(templateId);
+ if (_.isNil(template)) {
+ displayError(`The template "${templateId}" is no longer available.`);
+ clear();
+ } else {
+ set(template);
+ }
+ }
+ };
+
+ handleCancel = () => {
+ const onCancel = this.props.onCancel || _.noop;
+ onCancel();
+ };
+
+ handleFormError = (_form, _errors) => {
+ // We don't need to do anything here
+ };
+
+ handleFormSubmission = async form => {
+ const values = form.values();
+ const isNewTemplate = values.draftFor === '-1';
+ const templateId = values.templateId;
+ const templateTitle = isNewTemplate ? values.templateTitle : undefined;
+ const store = this.getStore();
+
+ try {
+ const draft = await store.createDraft({ isNewTemplate, templateId, templateTitle });
+ form.clear();
+ this.goto(`/workflow-templates/drafts/edit/${encodeURIComponent(draft.id)}`);
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ render() {
+ const store = this.getStore();
+ 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 form = this.form;
+ const dropDownOptions = this.getDropdownOptions();
+ const dropDownField = form.$('draftFor');
+ const templateIdField = form.$('templateId');
+ const templateTitleField = form.$('templateTitle');
+ const draftForValue = dropDownField.value;
+ const isNew = draftForValue === '-1';
+
+ return (
+
+
+ {({ processing, _onSubmit, onCancel }) => (
+ <>
+
+ {isNew && (
+ <>
+
+
+ >
+ )}
+
+
+ Create Draft
+
+
+ Cancel
+
+
+ >
+ )}
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(CreateWorkflowTemplateDraft, {
+ handleCancel: action,
+ handleFormSubmission: action,
+ handleFormError: action,
+});
+
+export default inject(
+ 'workflowTemplateDraftsStore',
+ 'workflowTemplatesStore',
+)(withRouter(observer(CreateWorkflowTemplateDraft)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/WorkflowTemplateDraftsList.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/WorkflowTemplateDraftsList.js
new file mode 100644
index 0000000000..1532d4eb8b
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/WorkflowTemplateDraftsList.js
@@ -0,0 +1,169 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, runInAction, observable } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import { Header, Icon, Segment, Button } 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 ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import {
+ isStoreLoading,
+ isStoreReady,
+ isStoreEmpty,
+ isStoreNotEmpty,
+ isStoreError,
+} from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import WorkflowCommonDraftCard from '../../workflow-common/drafts/WorkflowCommonDraftCard';
+import ProgressPlaceHolder from '../../workflow-common/ProgressPlaceholder';
+import CreateDraftWizard from './CreateWorkflowTemplateDraft';
+import WorkflowTemplateCardTabs from '../WorkflowTemplateCardTabs';
+
+// expected props
+// - workflowTemplateDraftsStore (via injection)
+// - location (from react router)
+class WorkflowTemplateDraftsList extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.showCreateDraftWizard = false;
+ });
+ }
+
+ componentDidMount() {
+ const store = this.getStore();
+ swallowError(store.load());
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getStore();
+ store.stopHeartbeat();
+ }
+
+ getStore() {
+ return this.props.workflowTemplateDraftsStore;
+ }
+
+ handleCreateDraftClick() {
+ this.showCreateDraftWizard = true;
+ }
+
+ handleCreateDraftCancel() {
+ this.showCreateDraftWizard = false;
+ }
+
+ handleEditDraft = async draft => {
+ const goto = gotoFn(this);
+ goto(`/workflow-templates/drafts/edit/${encodeURIComponent(draft.id)}`);
+ };
+
+ render() {
+ const store = this.getStore();
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreReady(store) && isStoreEmpty(store)) {
+ content = this.renderEmpty();
+ } else if (isStoreReady(store) && isStoreNotEmpty(store)) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderTitle()}
+ {this.renderWizard()}
+ {content}
+
+ );
+ }
+
+ renderEmpty() {
+ const show = this.showCreateDraftWizard;
+ if (show) return null;
+ return (
+
+
+
+ No workflow template drafts
+
+
+ );
+ }
+
+ renderTitle() {
+ const disabled = this.showCreateDraftWizard;
+ return (
+
+
+
+ Workflow Template Drafts
+
+
+ this.handleCreateDraftClick()}>
+ Create Draft
+
+
+
+ );
+ }
+
+ renderWizard() {
+ const show = this.showCreateDraftWizard;
+ if (!show) return null;
+ return this.handleCreateDraftCancel()} />;
+ }
+
+ renderMain() {
+ const store = this.getStore();
+ const list = store.list;
+
+ return (
+
+ {_.map(list, draft => (
+
+
+ {({ uiState, version }) => }
+
+
+ ))}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowTemplateDraftsList, {
+ handleCreateDraftClick: action,
+ handleCreateDraftCancel: action,
+ handleEditDraft: action,
+ showCreateDraftWizard: observable,
+});
+
+export default inject('workflowTemplateDraftsStore')(withRouter(observer(WorkflowTemplateDraftsList)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateDraftEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateDraftEditor.js
new file mode 100644
index 0000000000..c705f4b6cd
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateDraftEditor.js
@@ -0,0 +1,223 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, observable, runInAction } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import TimeAgo from 'react-timeago';
+import { Header, Container, Breadcrumb, Label, Segment } from 'semantic-ui-react';
+import c from 'classnames';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import { isStoreReady, isStoreEmpty, isStoreNotEmpty } from '@aws-ee/base-ui/dist/models/BaseStore';
+import Stores from '@aws-ee/base-ui/dist/models/Stores';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+import ProgressPlaceHolder from '../../../workflow-common/ProgressPlaceholder';
+import { getWorkflowTemplateDraftEditor } from '../../../../models/workflow-templates/drafts/edit/WorkflowTemplateDraftEditor';
+import WorkflowTemplateDraftMetaEditor from './WorkflowTemplateDraftMetaEditor';
+import WorkflowCommonDraftStepsEditor from '../../../workflow-common/drafts/edit/WorkflowCommonDraftStepsEditor';
+import WorkflowTemplateStepEditor from './WorkflowTemplateStepEditor';
+import WorkflowTemplateDraftPublisher from './WorkflowTemplateDraftPublisher';
+
+// expected props
+// - workflowTemplateDraftsStore (via injection)
+// - stepTemplatesStore (via injection)
+// - draftId (via react router params)
+// - className (via props)
+// - location (from react router)
+class WorkflowTemplateDraftEditor extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.stores = new Stores([this.getStore(), this.props.stepTemplatesStore]);
+ });
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ this.stores.load();
+ }
+
+ getStore() {
+ return this.props.workflowTemplateDraftsStore;
+ }
+
+ getStores() {
+ return this.stores;
+ }
+
+ getDraftEditor() {
+ return getWorkflowTemplateDraftEditor(this.getDraft().id);
+ }
+
+ getDraftId() {
+ return decodeURIComponent((this.props.match.params || {}).draftId);
+ }
+
+ getDraft() {
+ const store = this.getStore();
+ if (!isStoreReady(store)) return {};
+ const draftId = this.getDraftId();
+
+ if (_.isNil(draftId)) return {};
+ return store.getDraft(draftId) || {};
+ }
+
+ getTemplateVersion() {
+ const draft = this.getDraft();
+ return draft.template || {};
+ }
+
+ getUserDisplayNameService() {
+ return this.props.userDisplayName;
+ }
+
+ hasDraft() {
+ const store = this.getStore();
+ const draft = this.getDraft();
+ return store.hasDraft(draft.id);
+ }
+
+ handleCancel = () => {
+ const editor = this.getDraftEditor();
+ editor.cancel();
+ const goto = gotoFn(this);
+ goto('/workflow-templates/published');
+ };
+
+ render() {
+ const stores = this.getStores();
+ const store = this.getStore();
+ let content = null;
+
+ if (stores.hasError) {
+ content = ;
+ } else if (stores.loading) {
+ content = ;
+ } else if (stores.ready && isStoreEmpty(store)) {
+ content = (
+
+ );
+ } else if (stores.ready && isStoreNotEmpty(store)) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderBreadcrumb()}
+ {content}
+
+ );
+ }
+
+ renderBreadcrumb() {
+ const draftId = this.getDraftId();
+ const goto = gotoFn(this);
+ return (
+
+ goto('/workflow-templates/published')}>
+ Workflow Template Drafts
+
+
+ {draftId}
+
+ );
+ }
+
+ renderMain() {
+ const hasDraft = this.hasDraft();
+ const draftId = this.getDraftId();
+ const className = this.props.className;
+ const draft = this.getDraft();
+ const templateVersion = this.getTemplateVersion();
+ const { id, title } = templateVersion;
+ const { createdAt, createdBy } = draft;
+ const displayNameService = this.getUserDisplayNameService();
+ const by = () => by {displayNameService.getDisplayName(createdBy)} ;
+
+ if (!hasDraft) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ Draft
+
+ {title}
+
+
+ {id}
+
+
+ created {by()}
+
+
+
+
+
+ {this.renderContent()}
+
+
+ );
+ }
+
+ renderContent() {
+ const editor = this.getDraftEditor();
+ const currentPage = editor.currentPage;
+
+ if (currentPage === 0) return this.renderMetaContent(editor);
+ if (currentPage === 1) return this.renderStepsContent(editor);
+ if (currentPage === 2) return this.renderPublishContent(editor);
+ return '';
+ }
+
+ renderMetaContent(editor) {
+ return ;
+ }
+
+ renderStepsContent(editor) {
+ return (
+
+ );
+ }
+
+ renderPublishContent(editor) {
+ return ;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowTemplateDraftEditor, {
+ handleCancel: action,
+ stores: observable,
+});
+
+export default inject(
+ 'userDisplayName',
+ 'workflowTemplateDraftsStore',
+ 'stepTemplatesStore',
+)(withRouter(observer(WorkflowTemplateDraftEditor)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateDraftMetaEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateDraftMetaEditor.js
new file mode 100644
index 0000000000..f5e347c833
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateDraftMetaEditor.js
@@ -0,0 +1,222 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, runInAction, observable } from 'mobx';
+import { Button, Header, Divider, Icon, Segment } from 'semantic-ui-react';
+
+import { displayError, displaySuccess } from '@aws-ee/base-ui/dist/helpers/notification';
+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 PropsOverrideTable from '../../PropertyOverrideTable';
+
+// expected props
+// - editor (via props) an instance of the WorkflowTemplateDraftEditor model
+// - onCancel (via props)
+class WorkflowTemplateDraftMetaEditor extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.clickedOnNext = false;
+ });
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ // WorkflowTemplateDraftEditor
+ getEditor() {
+ return this.props.editor;
+ }
+
+ getWorkflowTemplate() {
+ return this.getEditor().draft.template;
+ }
+
+ getMetaForm() {
+ return this.getEditor().metaForm;
+ }
+
+ resetFlags() {
+ // we use these flags to tell the difference between clicking on 'save' vs 'next' because
+ // 'next' will result in saving the form
+ this.clickedOnNext = false;
+ }
+
+ handleCancel = () => {
+ this.resetFlags();
+ const onCancel = this.props.onCancel || _.noop;
+ onCancel();
+ };
+
+ handlePrevious = event => {
+ // we don't save the form in this case
+ this.resetFlags();
+ event.preventDefault();
+ event.stopPropagation();
+ this.getEditor().previousPage();
+ };
+
+ handleOnSubmitNext(event, onSubmit) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.resetFlags();
+ this.clickedOnNext = true;
+
+ onSubmit(event); // this will eventually call handleFormSubmission()
+ }
+
+ handleFormSubmission = async form => {
+ const editor = this.getEditor();
+ const { templateTitle, templateDesc, instanceTtl, runSpecSize, runSpecTarget } = form.values();
+ const { draft } = editor;
+ const template = draft.template;
+ const allowed = [];
+ const toPropsOverride = (key, value) => {
+ const item = key.replace(/^propsOverride_/, '');
+ if (_.startsWith(key, 'propsOverride_') && !_.isEmpty(item) && value === true) allowed.push(item);
+ };
+
+ _.forEach(form.values(), (value, key) => toPropsOverride(key, value));
+
+ template.setTitle(templateTitle);
+ template.setDescription(templateDesc);
+ template.setInstanceTtl(instanceTtl);
+ template.setRunSpec({
+ size: runSpecSize,
+ target: runSpecTarget,
+ });
+ template.setPropsOverrideOption({ allowed });
+
+ try {
+ await editor.update(draft);
+ if (this.clickedOnNext) {
+ this.getEditor().nextPage();
+ return;
+ }
+ displaySuccess('The workflow template draft is saved successfully');
+ } catch (error) {
+ runInAction(() => {
+ this.resetFlags();
+ });
+ displayError(error);
+ }
+ };
+
+ handleFormErrors = () => {
+ window.scrollTo(0, 0);
+ };
+
+ render() {
+ const editor = this.getEditor();
+ const hasPrevious = editor.hasPreviousPage;
+ const form = this.getMetaForm();
+ const templateTitleField = form.$('templateTitle');
+ const templateDescField = form.$('templateDesc');
+ const instanceTtlField = form.$('instanceTtl');
+ const runSpecSizeField = form.$('runSpecSize');
+ const runSpecTargetField = form.$('runSpecTarget');
+ const rows = this.getWorkflowTemplate().propertyOverrideSummaryRows || [];
+ const fields = _.map(rows, item => form.$(`propsOverride_${item.name}`));
+
+ return (
+
+ {({ processing, onSubmit, onCancel }) => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ Properties Override
+
+
+
+
+
+
+ this.handleOnSubmitNext(e, onSubmit)}
+ />
+ {hasPrevious && (
+
+ )}
+
+
+ Cancel
+
+
+ >
+ )}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowTemplateDraftMetaEditor, {
+ handleCancel: action,
+ handleOnSubmitNext: action,
+ handleFormSubmission: action,
+ handleFormErrors: action,
+ handlePrevious: action,
+ resetFlags: action,
+ clickedOnNext: observable,
+});
+
+export default inject()(observer(WorkflowTemplateDraftMetaEditor));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateDraftPublisher.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateDraftPublisher.js
new file mode 100644
index 0000000000..de4dc20366
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateDraftPublisher.js
@@ -0,0 +1,161 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, runInAction, observable } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import { Button, Header, Dimmer, Loader } from 'semantic-ui-react';
+import { displayError, displaySuccess } from '@aws-ee/base-ui/dist/helpers/notification';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+
+import getUIState from '../../../workflow-common/component-states/WorkflowCommonCardState';
+import WorkflowTemplateCardTabs from '../../WorkflowTemplateCardTabs';
+
+// expected props
+// - editor (via prop) an instance of the WorkflowTemplateDraftEditor model
+// - uiEventBus (via props)
+// - location (from react router)
+class WorkflowTemplateDraftPublisher extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.processing = false;
+ });
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ getUiEventBus() {
+ return this.props.uiEventBus;
+ }
+
+ getState() {
+ const template = this.getWorkflowTemplate();
+ return getUIState(`${template.id}-draft-publish-page`);
+ }
+
+ // Return WorkflowTemplateDraftEditor
+ getEditor() {
+ return this.props.editor;
+ }
+
+ getWorkflowTemplate() {
+ return this.getEditor().draft.template;
+ }
+
+ handleCancel = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.processing = false;
+ const onCancel = this.props.onCancel || _.noop;
+ onCancel();
+ };
+
+ handlePrevious = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.processing = false;
+ this.getEditor().previousPage();
+ };
+
+ handlePublish = async event => {
+ event.preventDefault();
+ event.stopPropagation();
+ const editor = this.getEditor();
+ const { draft } = editor;
+ const goto = gotoFn(this);
+
+ this.processing = true;
+
+ try {
+ const publishResult = await editor.publish(draft);
+ runInAction(() => {
+ this.processing = false;
+ });
+ // TODO examine the publishResult and figure out if we have validation errors
+ if (publishResult.hasErrors) {
+ throw new Error('There were validation errors in your submission');
+ }
+ const eventBus = this.getUiEventBus();
+ await eventBus.fireEvent('workflowTemplatePublished', publishResult.template);
+ displaySuccess('The workflow template draft is published successfully');
+ goto('/workflow-templates/published');
+ return;
+ } catch (error) {
+ runInAction(() => {
+ this.processing = false;
+ });
+ displayError(error);
+ }
+ };
+
+ render() {
+ const processing = this.processing;
+ const template = this.getWorkflowTemplate();
+ const uiState = this.getState();
+
+ return (
+ <>
+
+
+ Processing
+
+
+ {/* TODO add an elaborate error/validation error panel, showing errors at all levels including workflow template level and step level */}
+
+
+
+
+
+
+ Cancel
+
+
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowTemplateDraftPublisher, {
+ handleCancel: action,
+ handlePublish: action,
+ handlePrevious: action,
+ processing: observable,
+});
+
+export default inject('uiEventBus')(withRouter(observer(WorkflowTemplateDraftPublisher)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateStepConfigOverrideEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateStepConfigOverrideEditor.js
new file mode 100644
index 0000000000..dee0757e33
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateStepConfigOverrideEditor.js
@@ -0,0 +1,165 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, computed } from 'mobx';
+import { Icon, Divider, Button, Segment, Header } from 'semantic-ui-react';
+import Form from '@aws-ee/base-ui/dist/parts/helpers/fields/Form';
+
+import ConfigOverrideTable from '../../ConfigOverrideTable';
+
+// expected props
+// - stepEditor - a WorkflowTemplateStepEditor model instance (via props)
+// - onSave - called when the props are saved (via props)
+// - className (via props)
+class WorkflowTemplateStepConfigOverrideEditor extends React.Component {
+ getStepEditor() {
+ return this.props.stepEditor;
+ }
+
+ getStep() {
+ return this.getStepEditor().step;
+ }
+
+ getConfigOverrideForm() {
+ return this.getStepEditor().configOverrideForm;
+ }
+
+ get editing() {
+ return this.getStepEditor().configOverrideEdit;
+ }
+
+ handleEditOn = event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const stepEditorModel = this.getStepEditor();
+ stepEditorModel.setConfigOverrideEdit(true);
+ };
+
+ handleEditOff = () => {
+ const stepEditorModel = this.getStepEditor();
+ stepEditorModel.setConfigOverrideEdit(false);
+ };
+
+ handleSave = async form => {
+ const onSave = this.props.onSave || _.noop;
+ const stepEditorModel = this.getStepEditor();
+ const values = form.values();
+ const allowed = _.filter(_.keys(values), key => values[key] === true);
+
+ stepEditorModel.applyConfigOverrides(allowed);
+
+ await onSave();
+ stepEditorModel.setConfigOverrideEdit(false);
+ };
+
+ render() {
+ const editing = this.editing;
+ const step = this.getStep();
+ const rows = step.configOverrideSummaryRows || [];
+ const hasRows = rows.length > 0;
+ const canEdit = !editing && hasRows;
+
+ return (
+
+ {!editing && (
+
+
+
+ Configuration Override
+
+ {canEdit && (
+
+
+
+ )}
+
+ )}
+
+ {editing && this.renderEditingContent()}
+ {!editing && this.renderReadOnlyContent()}
+
+ );
+ }
+
+ renderReadOnlyContent() {
+ const step = this.getStep();
+ const configOverrideRows = step.configOverrideSummaryRows || [];
+ const hasRows = configOverrideRows.length > 0;
+
+ return (
+ <>
+ {hasRows && (
+
+
+
+ )}
+ {!hasRows && No configuration entries are available
}
+ >
+ );
+ }
+
+ renderEditingContent() {
+ const form = this.getConfigOverrideForm();
+ const step = this.getStep();
+ const configOverrideRows = step.configOverrideSummaryRows || [];
+ const fields = _.map(configOverrideRows, item => form.$(item.name));
+
+ return (
+ <>
+
+ Change Configuration Override
+
+
+ {({ processing, _onSubmit, onCancel }) => (
+
+
+
+
+
+
+
+ Cancel
+
+
+
+ )}
+
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowTemplateStepConfigOverrideEditor, {
+ editing: computed,
+ handleEditOn: action,
+ handleEditOff: action,
+ handleSave: action,
+});
+
+export default inject()(observer(WorkflowTemplateStepConfigOverrideEditor));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateStepEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateStepEditor.js
new file mode 100644
index 0000000000..ccd079f93d
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateStepEditor.js
@@ -0,0 +1,135 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, computed } from 'mobx';
+
+import WorkflowCommonStepEditorCard from '../../../workflow-common/drafts/edit/WorkflowCommonStepEditorCard';
+import WorkflowCommonStepConfigEditor from '../../../workflow-common/drafts/edit/WorkflowCommonStepConfigEditor';
+import WorkflowCommonStepDescEditor from '../../../workflow-common/drafts/edit/WorkflowCommonStepDescEditor';
+import WorkflowCommonStepPropsEditor from '../../../workflow-common/drafts/edit/WorkflowCommonStepPropsEditor';
+import WorkflowTemplateStepConfigOverrideEditor from './WorkflowTemplateStepConfigOverrideEditor';
+import WorkflowTemplateStepPropsOverrideEditor from './WorkflowTemplateStepPropsOverrideEditor';
+
+// expected props
+// - stepEditor - a WorkflowTemplateStepEditor model instance (via props)
+// - onSave - called when the configuration is saved (via props)
+// - onDelete - called when the step is to be deleted, passed (step) (via props)
+// - canDelete (via props) defaults to true
+// - canMove (via props) defaults to true
+// - className (via props)
+class WorkflowTemplateStepEditor extends React.Component {
+ getStepEditor() {
+ return this.props.stepEditor;
+ }
+
+ getStep() {
+ return this.getStepEditor().step;
+ }
+
+ get canDelete() {
+ return this.props.canDelete === undefined ? true : this.props.canDelete;
+ }
+
+ get canMove() {
+ return this.props.canMove === undefined ? true : this.props.canMove;
+ }
+
+ handleDelete = step => {
+ const onDelete = this.props.onDelete || _.noop;
+ onDelete(step);
+ };
+
+ handleSave = async () => {
+ const onSave = this.props.onSave || _.noop;
+
+ return onSave();
+ };
+
+ handleConfigSave = async configs => {
+ const onSave = this.props.onSave || _.noop;
+ const stepEditorModel = this.getStepEditor();
+
+ stepEditorModel.applyDefaults(configs);
+
+ return onSave();
+ };
+
+ render() {
+ const className = this.props.className;
+ const stepEditor = this.getStepEditor();
+ const canDelete = this.canDelete;
+ const canMove = this.canMove;
+
+ return (
+
+ <>
+ {this.renderDescription()}
+ {this.renderConfiguration()}
+ {this.renderConfigOverride()}
+ {this.renderProps()}
+ {this.renderPropsOverride()}
+ >
+
+ );
+ }
+
+ renderDescription() {
+ const editorModel = this.getStepEditor();
+ return ;
+ }
+
+ renderConfiguration() {
+ const editorModel = this.getStepEditor();
+
+ return ;
+ }
+
+ renderConfigOverride() {
+ const editorModel = this.getStepEditor();
+ return (
+
+ );
+ }
+
+ renderProps() {
+ const editorModel = this.getStepEditor();
+ return ;
+ }
+
+ renderPropsOverride() {
+ const editorModel = this.getStepEditor();
+ return ;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowTemplateStepEditor, {
+ canDelete: computed,
+ canMove: computed,
+ handleDelete: action,
+ handleSave: action,
+ handleConfigSave: action,
+});
+
+export default inject()(observer(WorkflowTemplateStepEditor));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateStepPropsOverrideEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateStepPropsOverrideEditor.js
new file mode 100644
index 0000000000..f930c75977
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/drafts/edit/WorkflowTemplateStepPropsOverrideEditor.js
@@ -0,0 +1,162 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, computed } from 'mobx';
+import { Icon, Divider, Button, Segment, Header } from 'semantic-ui-react';
+import Form from '@aws-ee/base-ui/dist/parts/helpers/fields/Form';
+
+import PropsOverrideTable from '../../PropertyOverrideTable';
+
+// expected props
+// - stepEditor - a WorkflowTemplateStepEditor model instance (via props)
+// - onSave - called when the props are saved (via props)
+// - className (via props)
+class WorkflowTemplateStepPropsOverrideEditor extends React.Component {
+ getStepEditor() {
+ return this.props.stepEditor;
+ }
+
+ getStep() {
+ return this.getStepEditor().step;
+ }
+
+ getPropsOverrideForm() {
+ return this.getStepEditor().propsOverrideForm;
+ }
+
+ get editing() {
+ return this.getStepEditor().propsOverrideEdit;
+ }
+
+ handleEditOn = event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const stepEditorModel = this.getStepEditor();
+ stepEditorModel.setPropsOverrideEdit(true);
+ };
+
+ handleEditOff = () => {
+ const stepEditorModel = this.getStepEditor();
+ stepEditorModel.setPropsOverrideEdit(false);
+ };
+
+ handleSave = async form => {
+ const onSave = this.props.onSave || _.noop;
+ const stepEditorModel = this.getStepEditor();
+ const values = form.values();
+ const allowed = _.filter(_.keys(values), key => values[key] === true);
+
+ stepEditorModel.applyPropsOverrides(allowed);
+
+ await onSave();
+ stepEditorModel.setPropsOverrideEdit(false);
+ };
+
+ render() {
+ const editing = this.editing;
+ const canEdit = !editing;
+
+ return (
+
+ {!editing && (
+
+
+
+ Properties Override
+
+ {canEdit && (
+
+
+
+ )}
+
+ )}
+
+ {editing && this.renderEditingContent()}
+ {!editing && this.renderReadOnlyContent()}
+
+ );
+ }
+
+ renderReadOnlyContent() {
+ const step = this.getStep();
+ const rows = step.propertyOverrideSummaryRows || [];
+ const hasRows = rows.length > 0;
+
+ return (
+ <>
+ {hasRows && (
+
+
+
+ )}
+ {!hasRows && No properties are available to override
}
+ >
+ );
+ }
+
+ renderEditingContent() {
+ const form = this.getPropsOverrideForm();
+ const step = this.getStep();
+ const rows = step.propertyOverrideSummaryRows || [];
+ const fields = _.map(rows, item => form.$(item.name));
+
+ return (
+ <>
+
+ Change Properties Override
+
+
+ {({ processing, _onSubmit, onCancel }) => (
+
+
+
+
+
+
+
+ Cancel
+
+
+
+ )}
+
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowTemplateStepPropsOverrideEditor, {
+ editing: computed,
+ handleEditOn: action,
+ handleEditOff: action,
+ handleSave: action,
+});
+
+export default inject()(observer(WorkflowTemplateStepPropsOverrideEditor));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/published/WorkflowPublishedTemplatesList.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/published/WorkflowPublishedTemplatesList.js
new file mode 100644
index 0000000000..a904464a93
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflow-templates/published/WorkflowPublishedTemplatesList.js
@@ -0,0 +1,125 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Header, Icon, Segment } from 'semantic-ui-react';
+import { createLink } from '@aws-ee/base-ui/dist/helpers/routing';
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+import {
+ isStoreLoading,
+ isStoreReady,
+ isStoreEmpty,
+ isStoreNotEmpty,
+ isStoreError,
+} from '@aws-ee/base-ui/dist/models/BaseStore';
+
+import WorkflowTemplateCard from '../WorkflowTemplateCard';
+import ProgressPlaceHolder from '../../workflow-common/ProgressPlaceholder';
+
+// expected props
+// - workflowTemplatesStore (via injection)
+// - location (from react router)
+class WorkflowPublishedTemplatesList extends React.Component {
+ componentDidMount() {
+ const store = this.getStore();
+ swallowError(store.load());
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getStore();
+ store.stopHeartbeat();
+ }
+
+ getStore() {
+ return this.props.workflowTemplatesStore;
+ }
+
+ goto(pathname) {
+ const location = this.props.location;
+ const link = createLink({ location, pathname });
+
+ this.props.history.push(link);
+ }
+
+ render() {
+ const store = this.getStore();
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreReady(store) && isStoreEmpty(store)) {
+ content = this.renderEmpty();
+ } else if (isStoreReady(store) && isStoreNotEmpty(store)) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderTitle()}
+ {content}
+
+ );
+ }
+
+ renderEmpty() {
+ return (
+
+
+
+ No published workflow templates
+
+ To create a workflow template, start by creating a draft and then publish the draft.
+
+
+
+ );
+ }
+
+ renderTitle() {
+ return (
+
+
+
+ Published Workflow Templates
+
+
+ );
+ }
+
+ renderMain() {
+ const store = this.getStore();
+ const list = store.list;
+
+ return (
+
+ {list.map(template => (
+
+
+
+ ))}
+
+ );
+ }
+}
+
+export default inject('workflowTemplatesStore')(withRouter(observer(WorkflowPublishedTemplatesList)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/WorkflowsList.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/WorkflowsList.js
new file mode 100644
index 0000000000..20dd18fa03
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/WorkflowsList.js
@@ -0,0 +1,41 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { withRouter } from 'react-router-dom';
+import { Container } from 'semantic-ui-react';
+
+import WorkflowPublishedList from './published/WorkflowPublishedList';
+import WorkflowDraftsList from './drafts/WorkflowDraftsList';
+
+// expected props
+// - location (from react router)
+class WorkflowsList extends React.Component {
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ render() {
+ return (
+
+
+
+
+ );
+ }
+}
+
+export default inject()(withRouter(observer(WorkflowsList)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/CreateWorkflowDraft.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/CreateWorkflowDraft.js
new file mode 100644
index 0000000000..ab0e1a6416
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/CreateWorkflowDraft.js
@@ -0,0 +1,326 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, runInAction, computed } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import { Label, Segment, Button, Message } from 'semantic-ui-react';
+import { isStoreEmpty } from '@aws-ee/base-ui/dist/models/BaseStore';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import Stores from '@aws-ee/base-ui/dist/models/Stores';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+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 getCreateDraftForm from '../../../models/forms/CreateWorkflowDraftForm';
+import ProgressPlaceHolder from '../../workflow-common/ProgressPlaceholder';
+
+// expected props
+// - onCancel (via prop) called on cancel
+// - workflowsStore (via injection)
+// - workflowDraftsStore (via injection)
+// - workflowTemplatesStore (via injection)
+// - className (via props)
+// - location (from react router)
+class CreateWorkflowDraft extends React.Component {
+ constructor(props) {
+ super(props);
+ this.form = getCreateDraftForm();
+ runInAction(() => {
+ this.stores = new Stores([this.getWorkflowsStore(), this.getDraftsStore(), this.getTemplatesStore()]);
+ });
+ }
+
+ componentDidMount() {
+ this.getStores().load();
+ }
+
+ get emptyWorkflows() {
+ const store = this.getWorkflowsStore();
+ return isStoreEmpty(store);
+ }
+
+ get emptyWorkflowTemplates() {
+ const store = this.getTemplatesStore();
+ return isStoreEmpty(store);
+ }
+
+ getStores() {
+ return this.stores;
+ }
+
+ getWorkflowsStore() {
+ return this.props.workflowsStore;
+ }
+
+ getDraftsStore() {
+ return this.props.workflowDraftsStore;
+ }
+
+ getTemplatesStore() {
+ return this.props.workflowTemplatesStore;
+ }
+
+ getDraftForDropDownOptions() {
+ const hasWorkflows = !this.emptyWorkflows;
+ const hasTemplates = !this.emptyWorkflowTemplates;
+ const options = [];
+
+ if (hasWorkflows) {
+ options.push({
+ value: 'existingWorkflow',
+ text: 'An existing workflow',
+ content: (
+
+
+ Existing
+ {' '}
+ An existing workflow
+
+ ),
+ });
+ }
+
+ if (hasTemplates) {
+ options.push({
+ value: 'newWorkflow',
+ text: 'A new workflow',
+ content: (
+
+
+ New
+ {' '}
+ An new workflow
+
+ ),
+ });
+ }
+
+ return options;
+ }
+
+ getWorkflowDropDownOptions() {
+ const workflowsStore = this.getWorkflowsStore();
+ const draftsStore = this.getDraftsStore();
+ const workflows = workflowsStore.list;
+ const options = [];
+
+ // TODO the approach of looping through all the entries in the workflowsStore is not going to scale beyond 5000 workflows, we need an autocomplete approach
+ // for this
+ _.forEach(workflows, workflow => {
+ if (!draftsStore.hasWorkflow(workflow.id)) {
+ options.push({
+ text: workflow.latest.title || '',
+ value: workflow.id,
+ content: (
+
+
+ Existing
+ {' '}
+ {workflow.latest.title} {workflow.id}
+
+ ),
+ });
+ }
+ });
+
+ return options;
+ }
+
+ getWorkflowTemplatesDropDownOptions() {
+ const templatesStore = this.getTemplatesStore();
+ const templates = templatesStore.list;
+ const options = [];
+
+ _.forEach(templates, template => {
+ options.push({
+ text: template.latest.title || '',
+ value: template.id,
+ content: (
+
+
+ Template
+ {' '}
+ {template.latest.title} {template.id}
+
+ ),
+ });
+ });
+
+ return options;
+ }
+
+ handleDraftForSelectionChange = selection => {
+ const form = this.form;
+ const templateIdField = form.$('templateId');
+ const workflowIdField = form.$('workflowId');
+ const clear = () => {
+ templateIdField.clear();
+ workflowIdField.clear();
+ };
+
+ clear();
+
+ if (selection === 'existingWorkflow') {
+ templateIdField.set('__DO_NOT_USE__');
+ }
+ };
+
+ handleCancel = () => {
+ const onCancel = this.props.onCancel || _.noop;
+ onCancel();
+ };
+
+ handleFormError = (_form, _errors) => {
+ // We don't need to do anything here
+ };
+
+ handleFormSubmission = async form => {
+ const values = form.values();
+ const draftForValue = values.draftFor;
+ const isNewWorkflow = draftForValue === 'newWorkflow';
+
+ const templateId = isNewWorkflow ? values.templateId : undefined;
+ const workflowId = values.workflowId;
+ const draftsStore = this.getDraftsStore();
+ const goto = gotoFn(this);
+
+ try {
+ const draft = await draftsStore.createDraft({ isNewWorkflow, workflowId, templateId });
+ form.clear();
+ this.handleCancel();
+ goto(`/workflows/drafts/edit/${encodeURIComponent(draft.id)}`);
+ } catch (error) {
+ displayError(error);
+ }
+ };
+
+ render() {
+ const stores = this.getStores();
+ let content = null;
+
+ if (stores.hasError) {
+ content = ;
+ } else if (stores.loading) {
+ content = ;
+ } else if (stores.ready) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return content;
+ }
+
+ renderMain() {
+ const form = this.form;
+ const draftForDropDownOptions = this.getDraftForDropDownOptions();
+ const workflowDropDownOptions = this.getWorkflowDropDownOptions();
+ const templatesDropDownOptions = this.getWorkflowTemplatesDropDownOptions();
+ const dropDownField = form.$('draftFor');
+ const templateIdField = form.$('templateId');
+ const workflowIdField = form.$('workflowId');
+ const draftForValue = dropDownField.value;
+ const isNewWorkflow = draftForValue === 'newWorkflow';
+ const isExistingWorkflow = draftForValue === 'existingWorkflow';
+ const empty = this.emptyWorkflowTemplates && this.emptyWorkflows;
+
+ return (
+
+
+ {({ processing, _onSubmit, onCancel }) => (
+ <>
+ {empty && this.renderEmptyMessage()}
+
+ {!empty && isNewWorkflow && (
+ <>
+
+
+ >
+ )}
+ {!empty && isExistingWorkflow && (
+
+ )}
+
+
+ Create Draft
+
+
+ Cancel
+
+
+ >
+ )}
+
+
+ );
+ }
+
+ renderEmptyMessage() {
+ return (
+
+ Brand new system
+
+ This is a brand new installation of the data lake. There are no workflow templates or workflows to create a
+ draft from. At least one workflow template needs to be created before you can create a workflow draft.
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(CreateWorkflowDraft, {
+ emptyWorkflows: computed,
+ emptyWorkflowTemplates: computed,
+ handleDraftForSelectionChange: action,
+ handleCancel: action,
+ handleFormSubmission: action,
+ handleFormError: action,
+});
+
+export default inject(
+ 'workflowsStore',
+ 'workflowDraftsStore',
+ 'workflowTemplatesStore',
+)(withRouter(observer(CreateWorkflowDraft)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/WorkflowDraftsList.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/WorkflowDraftsList.js
new file mode 100644
index 0000000000..0dfdada21a
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/WorkflowDraftsList.js
@@ -0,0 +1,172 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, runInAction, observable } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import { Header, Icon, Segment, Button } 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 { isStoreEmpty, isStoreNotEmpty } from '@aws-ee/base-ui/dist/models/BaseStore';
+import Stores from '@aws-ee/base-ui/dist/models/Stores';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+import WorkflowCommonDraftCard from '../../workflow-common/drafts/WorkflowCommonDraftCard';
+import ProgressPlaceHolder from '../../workflow-common/ProgressPlaceholder';
+import CreateDraftWizard from './CreateWorkflowDraft';
+import WorkflowTemplateCardTabs from '../../workflow-templates/WorkflowTemplateCardTabs';
+
+// expected props
+// - workflowDraftsStore (via injection)
+// - stepTemplatesStore (via injection)
+// - location (from react router)
+class WorkflowDraftsList extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.showCreateDraftWizard = false;
+ this.stores = new Stores([this.getStore(), this.props.stepTemplatesStore]);
+ });
+ }
+
+ componentDidMount() {
+ this.getStores().load();
+ const store = this.getStore();
+ swallowError(store.load());
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getStore();
+ store.stopHeartbeat();
+ }
+
+ getStores() {
+ return this.stores;
+ }
+
+ getStore() {
+ return this.props.workflowDraftsStore;
+ }
+
+ handleCreateDraftClick() {
+ this.showCreateDraftWizard = true;
+ }
+
+ handleCreateDraftCancel() {
+ this.showCreateDraftWizard = false;
+ }
+
+ handleEditDraft = async draft => {
+ const goto = gotoFn(this);
+ goto(`/workflows/drafts/edit/${encodeURIComponent(draft.id)}`);
+ };
+
+ render() {
+ const stores = this.getStores();
+ const store = this.getStore();
+ let content = null;
+
+ if (stores.hasError) {
+ content = ;
+ } else if (stores.loading) {
+ content = ;
+ } else if (stores.ready && isStoreEmpty(store)) {
+ content = this.renderEmpty();
+ } else if (stores.ready && isStoreNotEmpty(store)) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderTitle()}
+ {this.renderWizard()}
+ {content}
+
+ );
+ }
+
+ renderEmpty() {
+ const show = this.showCreateDraftWizard;
+ if (show) return null;
+ return (
+
+
+
+ );
+ }
+
+ renderTitle() {
+ const disabled = this.showCreateDraftWizard;
+ return (
+
+
+
+ this.handleCreateDraftClick()}>
+ Create Draft
+
+
+
+ );
+ }
+
+ renderWizard() {
+ const show = this.showCreateDraftWizard;
+ if (!show) return null;
+ return this.handleCreateDraftCancel()} />;
+ }
+
+ renderMain() {
+ const store = this.getStore();
+ const list = store.list;
+
+ return (
+
+ {_.map(list, draft => (
+
+
+ {({ uiState, version }) => }
+
+
+ ))}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowDraftsList, {
+ handleCreateDraftClick: action,
+ handleCreateDraftCancel: action,
+ handleEditDraft: action,
+ showCreateDraftWizard: observable,
+});
+
+export default inject('workflowDraftsStore', 'stepTemplatesStore')(withRouter(observer(WorkflowDraftsList)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/edit/WorkflowDraftEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/edit/WorkflowDraftEditor.js
new file mode 100644
index 0000000000..93cede9644
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/edit/WorkflowDraftEditor.js
@@ -0,0 +1,223 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, observable, runInAction } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import TimeAgo from 'react-timeago';
+import { Header, Container, Breadcrumb, Label, Segment } from 'semantic-ui-react';
+import c from 'classnames';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import { isStoreReady, isStoreEmpty, isStoreNotEmpty } from '@aws-ee/base-ui/dist/models/BaseStore';
+import Stores from '@aws-ee/base-ui/dist/models/Stores';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+import ProgressPlaceHolder from '../../../workflow-common/ProgressPlaceholder';
+import { getWorkflowDraftEditor } from '../../../../models/workflows/drafts/edit/WorkflowDraftEditor';
+import WorkflowCommonDraftStepsEditor from '../../../workflow-common/drafts/edit/WorkflowCommonDraftStepsEditor';
+import WorkflowStepEditor from './WorkflowStepEditor';
+import WorkflowDraftMetaEditor from './WorkflowDraftMetaEditor';
+import WorkflowDraftPublisher from './WorkflowDraftPublisher';
+
+// expected props
+// - workflowDraftsStore (via injection)
+// - workflowTemplatesStore (via injection)
+// - stepTemplatesStore (via injection)
+// - userDisplayName (via injection)
+// - draftId (via react router params)
+// - className (via props)
+// - location (from react router)
+class WorkflowDraftEditor extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.stores = new Stores([this.getStore(), this.props.workflowTemplatesStore, this.props.stepTemplatesStore]);
+ });
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ this.stores.load();
+ }
+
+ getStore() {
+ return this.props.workflowDraftsStore;
+ }
+
+ getStores() {
+ return this.stores;
+ }
+
+ getUserDisplayNameService() {
+ return this.props.userDisplayName;
+ }
+
+ getWorkflowDraftEditor() {
+ return getWorkflowDraftEditor(this.getDraft().id);
+ }
+
+ getDraftId() {
+ return decodeURIComponent((this.props.match.params || {}).draftId);
+ }
+
+ getDraft() {
+ const store = this.getStore();
+ if (!isStoreReady(store)) return {};
+ const draftId = this.getDraftId();
+
+ if (_.isNil(draftId)) return {};
+ return store.getDraft(draftId) || {};
+ }
+
+ getVersion() {
+ const draftEditor = this.getWorkflowDraftEditor();
+ return draftEditor.version;
+ }
+
+ hasDraft() {
+ const store = this.getStore();
+ const draft = this.getDraft();
+ return store.hasDraft(draft.id);
+ }
+
+ handleCancel = () => {
+ const draftEditor = this.getWorkflowDraftEditor();
+ const goto = gotoFn(this);
+ draftEditor.cancel();
+ goto('/workflows/published');
+ };
+
+ render() {
+ const stores = this.getStores();
+ const store = this.getStore();
+ let content = null;
+
+ if (stores.hasError) {
+ content = ;
+ } else if (stores.loading) {
+ content = ;
+ } else if (stores.ready && isStoreEmpty(store)) {
+ content = ;
+ } else if (stores.ready && isStoreNotEmpty(store) && !this.hasDraft()) {
+ content = ;
+ } else if (stores.ready && isStoreNotEmpty(store) && this.hasDraft()) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderBreadcrumb()}
+ {content}
+
+ );
+ }
+
+ renderBreadcrumb() {
+ const draftId = this.getDraftId();
+ const goto = gotoFn(this);
+
+ return (
+
+ goto('/workflows/published')}>
+ Workflow Drafts
+
+
+ {draftId}
+
+ );
+ }
+
+ renderMain() {
+ const hasDraft = this.hasDraft();
+ const draftId = this.getDraftId();
+ const className = this.props.className;
+ const draft = this.getDraft();
+ const version = this.getVersion();
+ const { id, title } = version;
+ const { createdAt, createdBy } = draft;
+ const displayNameService = this.getUserDisplayNameService();
+ const by = () => by {displayNameService.getDisplayName(createdBy)} ;
+
+ if (!hasDraft) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ Draft
+
+ {title}
+
+
+ {id}
+
+
+ created {by()}
+
+
+
+
+
+ {this.renderContent()}
+
+
+ );
+ }
+
+ renderContent() {
+ const draftEditor = this.getWorkflowDraftEditor();
+ const currentPage = draftEditor.currentPage;
+
+ if (currentPage === 0) return this.renderMetaContent(draftEditor);
+ if (currentPage === 1) return this.renderStepsContent(draftEditor);
+ if (currentPage === 2) return this.renderPublishContent(draftEditor);
+ return '';
+ }
+
+ renderMetaContent(editor) {
+ return ;
+ }
+
+ renderStepsContent(editor) {
+ return (
+
+ );
+ }
+
+ renderPublishContent(editor) {
+ return ;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowDraftEditor, {
+ handleCancel: action,
+ stores: observable,
+});
+
+export default inject(
+ 'userDisplayName',
+ 'workflowDraftsStore',
+ 'workflowTemplatesStore',
+ 'stepTemplatesStore',
+)(withRouter(observer(WorkflowDraftEditor)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/edit/WorkflowDraftMetaEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/edit/WorkflowDraftMetaEditor.js
new file mode 100644
index 0000000000..87677d9e57
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/edit/WorkflowDraftMetaEditor.js
@@ -0,0 +1,202 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, runInAction, observable } from 'mobx';
+import { Button, Header, Divider, Icon } from 'semantic-ui-react';
+import { displayError, displaySuccess } from '@aws-ee/base-ui/dist/helpers/notification';
+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';
+
+// expected props
+// - editor (via props) an instance of the WorkflowDraftEditor model
+// - onCancel (via props)
+class WorkflowDraftMetaEditor extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.clickedOnNext = false;
+ });
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ // Returns WorkflowTemplateDraftEditor
+ getEditor() {
+ return this.props.editor;
+ }
+
+ // Returns WorkflowVersion
+ getVersion() {
+ return this.getEditor().version;
+ }
+
+ getMetaForm() {
+ return this.getEditor().metaForm;
+ }
+
+ resetFlags() {
+ // we use these flags to tell the difference between clicking on 'save' vs 'next' because
+ // 'next' will result in saving the form
+ this.clickedOnNext = false;
+ }
+
+ handleCancel = () => {
+ this.resetFlags();
+ const onCancel = this.props.onCancel || _.noop;
+ onCancel();
+ };
+
+ handlePrevious = event => {
+ // we don't save the form in this case
+ this.resetFlags();
+ event.preventDefault();
+ event.stopPropagation();
+ this.getEditor().previousPage();
+ };
+
+ handleOnSubmitNext(event, onSubmit) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.resetFlags();
+ this.clickedOnNext = true;
+
+ onSubmit(event); // this will eventually call handleFormSubmission()
+ }
+
+ handleFormSubmission = async form => {
+ const editor = this.getEditor();
+ const { title, desc, instanceTtl, runSpecSize, runSpecTarget } = form.values();
+ const { draft } = editor;
+ const version = editor.version;
+
+ version.setTitle(title);
+ version.setDescription(desc);
+ version.setInstanceTtl(instanceTtl);
+ version.setRunSpec({
+ size: runSpecSize,
+ target: runSpecTarget,
+ });
+
+ try {
+ await editor.update(draft);
+ if (this.clickedOnNext) {
+ this.getEditor().nextPage();
+ return;
+ }
+ displaySuccess('The workflow draft is saved successfully');
+ } catch (error) {
+ runInAction(() => {
+ this.resetFlags();
+ });
+ displayError(error);
+ }
+ };
+
+ handleFormErrors = () => {
+ window.scrollTo(0, 0);
+ };
+
+ render() {
+ const editor = this.getEditor();
+ const hasPrevious = editor.hasPreviousPage;
+ const form = this.getMetaForm();
+ const titleField = form.$('title');
+ const descField = form.$('desc');
+ const instanceTtlField = form.$('instanceTtl');
+ const runSpecSizeField = form.$('runSpecSize');
+ const runSpecTargetField = form.$('runSpecTarget');
+
+ return (
+
+ {({ processing, onSubmit, onCancel }) => (
+ <>
+
+
+
+
+
+
+
+
+
+ this.handleOnSubmitNext(e, onSubmit)}
+ />
+ {hasPrevious && (
+
+ )}
+
+
+ Cancel
+
+
+ >
+ )}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowDraftMetaEditor, {
+ handleCancel: action,
+ handleOnSubmitNext: action,
+ handleFormSubmission: action,
+ handleFormErrors: action,
+ handlePrevious: action,
+ resetFlags: action,
+ clickedOnNext: observable,
+});
+
+export default inject()(observer(WorkflowDraftMetaEditor));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/edit/WorkflowDraftPublisher.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/edit/WorkflowDraftPublisher.js
new file mode 100644
index 0000000000..57ef246517
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/edit/WorkflowDraftPublisher.js
@@ -0,0 +1,163 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, runInAction, observable } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import { Button, Header, Dimmer, Loader } from 'semantic-ui-react';
+import { displayError, displaySuccess } from '@aws-ee/base-ui/dist/helpers/notification';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+
+import getUIState from '../../../workflow-common/component-states/WorkflowCommonCardState';
+import WorkflowTemplateCardTabs from '../../../workflow-templates/WorkflowTemplateCardTabs';
+
+// expected props
+// - editor (via prop) an instance of the WorkflowDraftEditor model
+// - uiEventBus (via props)
+// - onCancel (via props)
+// - location (from react router)
+class WorkflowDraftPublisher extends React.Component {
+ constructor(props) {
+ super(props);
+
+ runInAction(() => {
+ this.processing = false;
+ });
+ }
+
+ componentDidMount() {
+ window.scrollTo(0, 0);
+ }
+
+ getUiEventBus() {
+ return this.props.uiEventBus;
+ }
+
+ getState() {
+ const version = this.getVersion();
+ return getUIState(`${version.id}-workflow-draft-publish-page`);
+ }
+
+ // Return WorkflowDraftEditor
+ getEditor() {
+ return this.props.editor;
+ }
+
+ // Returns WorkflowVersion
+ getVersion() {
+ return this.getEditor().version;
+ }
+
+ handleCancel = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.processing = false;
+ const onCancel = this.props.onCancel || _.noop;
+ onCancel();
+ };
+
+ handlePrevious = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.processing = false;
+ this.getEditor().previousPage();
+ };
+
+ handlePublish = async event => {
+ event.preventDefault();
+ event.stopPropagation();
+ const editor = this.getEditor();
+ const { draft } = editor;
+ const goto = gotoFn(this);
+
+ this.processing = true;
+
+ try {
+ const publishResult = await editor.publish(draft);
+ runInAction(() => {
+ this.processing = false;
+ });
+ // TODO examine the publishResult and figure out if we have validation errors
+ if (publishResult.hasErrors) {
+ throw new Error('There were validation errors in your submission');
+ }
+ const eventBus = this.getUiEventBus();
+ await eventBus.fireEvent('workflowPublished', publishResult.workflow);
+ displaySuccess('The workflow draft is published successfully');
+ goto('/workflows/published');
+ return;
+ } catch (error) {
+ runInAction(() => {
+ this.processing = false;
+ });
+ displayError(error);
+ }
+ };
+
+ render() {
+ const processing = this.processing;
+ const version = this.getVersion();
+ const uiState = this.getState();
+
+ return (
+ <>
+
+
+ Processing
+
+
+ {/* TODO add an elaborate error/validation error panel, showing errors at all levels including workflow level and step level */}
+
+
+
+
+
+
+ Cancel
+
+
+ >
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowDraftPublisher, {
+ handleCancel: action,
+ handlePublish: action,
+ handlePrevious: action,
+ processing: observable,
+});
+
+export default inject('uiEventBus')(withRouter(observer(WorkflowDraftPublisher)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/edit/WorkflowStepEditor.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/edit/WorkflowStepEditor.js
new file mode 100644
index 0000000000..a49fffacd7
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/drafts/edit/WorkflowStepEditor.js
@@ -0,0 +1,120 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, computed } from 'mobx';
+
+import WorkflowCommonStepEditorCard from '../../../workflow-common/drafts/edit/WorkflowCommonStepEditorCard';
+import WorkflowCommonStepDescEditor from '../../../workflow-common/drafts/edit/WorkflowCommonStepDescEditor';
+import WorkflowCommonStepPropsEditor from '../../../workflow-common/drafts/edit/WorkflowCommonStepPropsEditor';
+import WorkflowCommonStepConfigEditor from '../../../workflow-common/drafts/edit/WorkflowCommonStepConfigEditor';
+
+// expected props
+// - stepEditor - a WorkflowStepEditor model instance (via props)
+// - onSave - called when the configuration is saved (via props)
+// - onDelete - called when the step is to be deleted, passed (step) (via props)
+// - canDelete (via props) defaults to true
+// - canMove (via props) defaults to true
+// - className (via props)
+// - location (from react router)
+class WorkflowStepEditor extends React.Component {
+ getStepEditor() {
+ return this.props.stepEditor;
+ }
+
+ getStep() {
+ return this.getStepEditor().step;
+ }
+
+ get canDelete() {
+ return this.props.canDelete === undefined ? true : this.props.canDelete;
+ }
+
+ get canMove() {
+ return this.props.canMove === undefined ? true : this.props.canMove;
+ }
+
+ handleDelete = step => {
+ const onDelete = this.props.onDelete || _.noop;
+ onDelete(step);
+ };
+
+ handleSave = async () => {
+ const onSave = this.props.onSave || _.noop;
+
+ return onSave();
+ };
+
+ handleConfigSave = async configs => {
+ const onSave = this.props.onSave || _.noop;
+ const stepEditorModel = this.getStepEditor();
+
+ stepEditorModel.applyConfigs(configs);
+
+ return onSave();
+ };
+
+ render() {
+ const className = this.props.className;
+ const stepEditor = this.getStepEditor();
+ const canDelete = this.canDelete;
+ const canMove = this.canMove;
+
+ return (
+
+ <>
+ {this.renderDescription()}
+ {this.renderConfiguration()}
+ {this.renderProps()}
+ >
+
+ );
+ }
+
+ renderDescription() {
+ const editorModel = this.getStepEditor();
+ return ;
+ }
+
+ renderConfiguration() {
+ const editorModel = this.getStepEditor();
+
+ return ;
+ }
+
+ renderProps() {
+ const editorModel = this.getStepEditor();
+ return ;
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowStepEditor, {
+ canDelete: computed,
+ canMove: computed,
+ handleDelete: action,
+ handleSave: action,
+ handleConfigSave: action,
+});
+
+export default inject()(observer(WorkflowStepEditor));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowAssignmentsList.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowAssignmentsList.js
new file mode 100644
index 0000000000..191585725a
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowAssignmentsList.js
@@ -0,0 +1,144 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import TimeAgo from 'react-timeago';
+import { Header, Segment, Icon, Table } from 'semantic-ui-react';
+import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils';
+import { isStoreError, isStoreReady, isStoreLoading, isStoreEmpty } from '@aws-ee/base-ui/dist/models/BaseStore';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+import ProgressPlaceHolder from '../../workflow-common/ProgressPlaceholder';
+
+// expected props
+// - workflowVersion (via props)
+// - workflowsStore (via injection)
+// - userDisplayName (via injection)
+// - location (from react router)
+class WorkflowAssignmentsList extends React.Component {
+ componentDidMount() {
+ const store = this.getAssignmentsStore();
+ swallowError(store.load());
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getAssignmentsStore();
+ store.stopHeartbeat();
+ }
+
+ getWorkflowVersion() {
+ return this.props.workflowVersion;
+ }
+
+ getWorkflowStore() {
+ const version = this.getWorkflowVersion();
+ return this.props.workflowsStore.getWorkflowStore(version.id);
+ }
+
+ getAssignmentsStore() {
+ const workflowStore = this.getWorkflowStore();
+ return workflowStore.getAssignmentsStore();
+ }
+
+ getUserDisplayNameService() {
+ return this.props.userDisplayName;
+ }
+
+ render() {
+ const store = this.getAssignmentsStore();
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreReady(store) && isStoreEmpty(store)) {
+ content = this.renderEmptyAssignments();
+ } else if (isStoreReady(store) && !isStoreEmpty(store)) {
+ content = this.renderMain();
+ } else {
+ // We get here if the store is in the initial state
+ content = null;
+ }
+
+ return <>{content}>;
+ }
+
+ renderMain() {
+ const store = this.getAssignmentsStore();
+ const assignments = store.list;
+
+ return {this.renderAssignmentsTable(assignments)} ;
+ }
+
+ renderAssignmentsTable(assignments) {
+ return (
+
+
+
+ Id
+ Trigger
+ Configuration
+ Updated
+
+
+ {_.map(assignments, assignment => this.renderAssignmentRow(assignment))}
+
+ );
+ }
+
+ renderAssignmentRow(assignment) {
+ const { id, updatedAt, updatedBy, triggerType, triggerTypeData: config } = assignment;
+ const displayNameService = this.getUserDisplayNameService();
+ const isSystem = displayNameService.isSystem(updatedBy);
+ const by = () => (isSystem ? '' : by {displayNameService.getDisplayName(updatedBy)} );
+
+ return (
+
+ {id}
+ {triggerType}
+ {config}
+
+
+ {by()}
+
+
+ );
+ }
+
+ renderEmptyAssignments() {
+ return (
+
+
+
+ No assignments
+
+ Assignments allow you to configure the workflow to be triggered based on different criteria, try it out!
+
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowAssignmentsList, {});
+
+export default inject('workflowsStore', 'userDisplayName')(withRouter(observer(WorkflowAssignmentsList)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowDetailPage.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowDetailPage.js
new file mode 100644
index 0000000000..152239f6be
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowDetailPage.js
@@ -0,0 +1,187 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import TimeAgo from 'react-timeago';
+import { Header, Label, Breadcrumb, Container, Dropdown } 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 { isStoreError, isStoreReady, isStoreLoading } from '@aws-ee/base-ui/dist/models/BaseStore';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+import ProgressPlaceHolder from '../../workflow-common/ProgressPlaceholder';
+import getUIState from '../../workflow-common/component-states/WorkflowCommonCardState';
+import WorkflowDetailTabs from './WorkflowDetailTabs';
+
+// expected props
+// - workflowsStore (via injection)
+// - userDisplayName (via injection)
+// - location (from react router)
+class WorkflowDetailPage extends React.Component {
+ componentDidMount() {
+ const store = this.getStore();
+ swallowError(store.load());
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getStore();
+ store.stopHeartbeat();
+ }
+
+ getState() {
+ return getUIState(`wf-${this.getWorkflowId()}`);
+ }
+
+ getStore() {
+ const workflowId = this.getWorkflowId();
+ return this.props.workflowsStore.getWorkflowStore(workflowId);
+ }
+
+ getUserDisplayNameService() {
+ return this.props.userDisplayName;
+ }
+
+ getWorkflowId() {
+ return (this.props.match.params || {}).workflowId;
+ }
+
+ getVersionNumber() {
+ return parseInt((this.props.match.params || {}).version, 10);
+ }
+
+ getWorkflow() {
+ const store = this.getStore();
+ if (!isStoreReady(store)) return {};
+ return store.workflow;
+ }
+
+ getVersion() {
+ const workflow = this.getWorkflow();
+ const num = this.getVersionNumber();
+
+ return workflow.getVersion(num);
+ }
+
+ handleVersionChange = ({ value = '' }) => {
+ const goto = gotoFn(this);
+ const workflowId = this.getWorkflowId();
+ goto(`/workflows/published/id/${workflowId}/v/${value}`);
+ };
+
+ render() {
+ const store = this.getStore();
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreReady(store)) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderBreadcrumb()}
+ {content}
+
+ );
+ }
+
+ renderBreadcrumb() {
+ const workflowId = this.getWorkflowId();
+ const goto = gotoFn(this);
+
+ return (
+
+ goto('/workflows/published')}>
+ Workflows
+
+
+ {workflowId}
+
+ );
+ }
+
+ renderMain() {
+ const version = this.getVersion();
+ const { id, title, updatedAt, updatedBy, descHtml } = version;
+ const displayNameService = this.getUserDisplayNameService();
+ const isSystem = displayNameService.isSystem(updatedBy);
+ const by = () => (isSystem ? '' : by {displayNameService.getDisplayName(updatedBy)} );
+
+ const uiState = this.getState();
+
+ /* eslint-disable react/no-danger */
+ return (
+ <>
+
+
+
+ Workflow
+
+ {title}
+
+
+ updated {by()}
+
+
+
+
+ {id} {this.renderVersionDropdown()}
+
+
+
+
+ >
+ );
+ /* eslint-enable react/no-danger */
+ }
+
+ renderVersionDropdown() {
+ const workflow = this.getWorkflow();
+ const versions = workflow.versionNumbers;
+ const currentVersion = this.getVersionNumber();
+ const options = _.map(versions, version => ({ text: `v${version}`, value: version }));
+
+ if (versions.length === 1) return v{currentVersion} ;
+
+ return (
+ this.handleVersionChange(data)}
+ />
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowDetailPage, {
+ handleVersionChange: action,
+});
+
+export default inject('workflowsStore', 'userDisplayName')(withRouter(observer(WorkflowDetailPage)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowDetailTabs.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowDetailTabs.js
new file mode 100644
index 0000000000..f4b22ca9e4
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowDetailTabs.js
@@ -0,0 +1,134 @@
+/*
+ * 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 { observer, inject, Observer } from 'mobx-react';
+import { decorate, action } from 'mobx';
+import { Tab, Grid, Label, Segment } from 'semantic-ui-react';
+
+import PropertySection from '../../workflow-templates/PropertySection';
+import WorkflowTemplateStep from '../../workflow-templates/WorkflowTemplateStep';
+import WorkflowInstancesList from './WorkflowInstancesList';
+import WorkflowAssignmentsList from './WorkflowAssignmentsList';
+
+// expected props
+// - workflow - either a WorkflowVersion model instance (via props)
+// - uiState - to keep track of the active tab (via props)
+// - className (via props)
+class WorkflowDetailTabs extends React.Component {
+ getState() {
+ return this.props.uiState;
+ }
+
+ selectedMainTabIndex() {
+ return this.getState().mainTabIndex;
+ }
+
+ getVersion() {
+ return this.props.workflow;
+ }
+
+ handleTabChange = (event, data) => {
+ this.getState().setMainTabIndex(data.activeIndex);
+ };
+
+ render() {
+ const className = this.props.className || 'mt0';
+ const workflow = this.getVersion();
+ const id = workflow.id;
+ const v = workflow.v;
+
+ const activeIndex = this.selectedMainTabIndex();
+ const panes = [
+ {
+ menuItem: 'Instances',
+ render: () => (
+
+
+
+ ),
+ },
+ {
+ menuItem: 'Assignment',
+ render: () => (
+
+
+
+ ),
+ },
+ {
+ menuItem: 'Steps',
+ render: () => (
+
+ {() => this.renderSteps(workflow)}
+
+ ),
+ },
+ {
+ menuItem: 'Properties',
+ render: () => (
+
+
+
+ ),
+ },
+ ];
+
+ return (
+
+ );
+ }
+
+ renderSteps(workflow) {
+ const steps = workflow.selectedSteps || [];
+
+ if (steps.length === 0) {
+ return No steps are provided ;
+ }
+
+ return (
+
+ {_.map(steps, (step, index) => (
+
+
+
+ {index + 1}
+
+
+
+
+
+
+
+
+ ))}
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowDetailTabs, {
+ handleTabChange: action,
+});
+
+export default inject()(observer(WorkflowDetailTabs));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowInstanceDetailPage.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowInstanceDetailPage.js
new file mode 100644
index 0000000000..af570b852e
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowInstanceDetailPage.js
@@ -0,0 +1,223 @@
+/*
+ * 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 react/no-danger */
+import _ from 'lodash';
+import React from 'react';
+import { observer, inject } from 'mobx-react';
+import { decorate } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import TimeAgo from 'react-timeago';
+import { Header, Label, Breadcrumb, Container, Progress, Message, Table } 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 { isStoreError, isStoreReady, isStoreLoading } from '@aws-ee/base-ui/dist/models/BaseStore';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+import ProgressPlaceHolder from '../../workflow-common/ProgressPlaceholder';
+
+// expected props
+// - workflowsStore (via injection)
+// - userDisplayName (via injection)
+// - location (from react router)
+class WorkflowInstanceDetailPage extends React.Component {
+ componentDidMount() {
+ const store = this.getInstanceStore();
+ swallowError(store.load());
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getInstanceStore();
+ store.stopHeartbeat();
+ }
+
+ getInstanceStore() {
+ const workflowStore = this.getWorkflowStore();
+ const version = this.getVersionNumber();
+ const instanceId = this.getInstanceId();
+ return workflowStore.getInstanceStore(version, instanceId);
+ }
+
+ getWorkflowStore() {
+ const workflowId = this.getWorkflowId();
+ return this.props.workflowsStore.getWorkflowStore(workflowId);
+ }
+
+ getUserDisplayNameService() {
+ return this.props.userDisplayName;
+ }
+
+ getInstanceId() {
+ return (this.props.match.params || {}).instanceId;
+ }
+
+ getWorkflowId() {
+ return (this.props.match.params || {}).workflowId;
+ }
+
+ getVersionNumber() {
+ return parseInt((this.props.match.params || {}).version, 10);
+ }
+
+ getWorkflow() {
+ const store = this.getWorkflowStore();
+ if (!isStoreReady(store)) return {};
+ return store.workflow;
+ }
+
+ getVersion() {
+ const workflow = this.getWorkflow();
+ const num = this.getVersionNumber();
+
+ return workflow.getVersion(num);
+ }
+
+ getInstance() {
+ const instanceId = this.getInstanceId();
+ const workflowVersion = this.getVersion();
+ return workflowVersion.getInstance(instanceId);
+ }
+
+ render() {
+ const store = this.getInstanceStore();
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreReady(store)) {
+ content = this.renderMain();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderBreadcrumb()}
+ {content}
+
+ );
+ }
+
+ renderBreadcrumb() {
+ const workflowId = this.getWorkflowId();
+ const versionNumber = this.getVersionNumber();
+ const instanceId = this.getInstanceId();
+ const goto = gotoFn(this);
+
+ return (
+
+ goto('/workflows/published')}>
+ Workflows
+
+
+ goto(`/workflows/published/id/${workflowId}/v/${versionNumber}`)}>
+ {workflowId}
+
+
+ {instanceId}
+
+ );
+ }
+
+ renderMain() {
+ const version = this.getVersion();
+ const instance = this.getInstance();
+ const { id, title, descHtml } = version;
+ const { updatedAt, updatedBy } = instance;
+ const { statusMsg, statusLabel, statusColor, msgSpread } = instance.statusSummary;
+ const displayNameService = this.getUserDisplayNameService();
+ const isSystem = displayNameService.isSystem(updatedBy);
+ const by = () => (isSystem ? '' : by {displayNameService.getDisplayName(updatedBy)} );
+
+ return (
+ <>
+
+
+
+ {statusLabel}
+
+
+ Workflow Instance
+
+ {title} - {instance.id}
+
+
+ updated {by()}
+
+
+
+
+ {id}
+ {version.v}
+
+
+
+ {this.displayInstanceStatusMsg(statusMsg, msgSpread)}
+
+ {this.renderSteps(instance.steps)}
+ >
+ );
+ }
+
+ displayInstanceStatusMsg(msg, msgSpread) {
+ if (!msg) return null;
+ return {msg} ;
+ }
+
+ renderSteps(steps) {
+ if (_.isEmpty(steps)) return 'This workflow instance does not have any steps';
+
+ return (
+
+
+
+ Step
+ Status
+
+
+
+
+ {_.map(steps, (step, index) => (
+
+
+
+ {index + 1} {step.title}
+
+ {step.statusMsg && (
+ {step.statusMsg}
+ )}
+
+
+
+ {step.statusLabel}
+
+
+
+ ))}
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowInstanceDetailPage, {});
+
+export default inject('workflowsStore', 'userDisplayName')(withRouter(observer(WorkflowInstanceDetailPage)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowInstancesList.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowInstancesList.js
new file mode 100644
index 0000000000..18c3428e8f
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowInstancesList.js
@@ -0,0 +1,275 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, autorun, runInAction, observable } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import TimeAgo from 'react-timeago';
+import { Header, Segment, Icon, Statistic, Grid, Label, Button } from 'semantic-ui-react';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
+import { swallowError, niceNumber } from '@aws-ee/base-ui/dist/helpers/utils';
+import { isStoreError, isStoreReady, isStoreLoading, isStoreEmpty } from '@aws-ee/base-ui/dist/models/BaseStore';
+import Form from '@aws-ee/base-ui/dist/parts/helpers/fields/Form';
+import TextArea from '@aws-ee/base-ui/dist/parts/helpers/fields/TextArea';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+import getTriggerWorkflowForm from '../../../models/forms/TriggerWorkflowForm';
+import ProgressPlaceHolder from '../../workflow-common/ProgressPlaceholder';
+
+// expected props
+// - workflowVersion (via props)
+// - workflowsStore (via injection)
+// - userDisplayName (via injection)
+// - location (from react router)
+class WorkflowInstancesList extends React.Component {
+ constructor(props) {
+ super(props);
+ this.form = getTriggerWorkflowForm();
+ runInAction(() => {
+ this.triggerDialogShown = false;
+ });
+ }
+
+ componentDidMount() {
+ if (this.disposer) this.disposer();
+
+ this.disposer = autorun(() => {
+ const store = this.getInstancesStore();
+ if (!isStoreReady(store)) swallowError(store.load());
+ });
+
+ const store = this.getInstancesStore();
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getInstancesStore();
+ store.stopHeartbeat();
+ if (this.disposer) this.disposer();
+ }
+
+ getWorkflowVersion() {
+ return this.props.workflowVersion;
+ }
+
+ getWorkflowStore() {
+ const workflowVersion = this.getWorkflowVersion();
+ return this.props.workflowsStore.getWorkflowStore(workflowVersion.id);
+ }
+
+ getInstancesStore() {
+ const workflowStore = this.getWorkflowStore();
+ const workflowVersion = this.getWorkflowVersion();
+ return workflowStore.getInstancesStore(workflowVersion.id, workflowVersion.v);
+ }
+
+ getUserDisplayNameService() {
+ return this.props.userDisplayName;
+ }
+
+ cancelTriggerDialog = () => {
+ this.triggerDialogShown = false;
+ };
+
+ showTriggerDialog = () => {
+ this.triggerDialogShown = true;
+ };
+
+ handleFormSubmission = async form => {
+ const values = form.values();
+ const workflowInputStr = values.workflowInput;
+
+ try {
+ const store = this.getInstancesStore();
+
+ // Convert input JSON string to an input object
+ const input = JSON.parse(workflowInputStr);
+ await store.triggerWorkflow({ input });
+
+ form.clear();
+ this.cancelTriggerDialog();
+ } catch (error) {
+ if (error instanceof SyntaxError) {
+ displayError('Incorrect workflow input. Make sure the workflow input is a well-formed JSON.');
+ } else {
+ displayError(error);
+ }
+ }
+ };
+
+ handleInstanceClick = event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ // see https://reactjs.org/docs/events.html and https://github.com/facebook/react/issues/5733
+ const instanceId = event.currentTarget.dataset.instance;
+ const goto = gotoFn(this);
+ const { id, v } = this.getWorkflowVersion();
+
+ goto(`/workflows/published/id/${id}/v/${v}/instances/id/${instanceId}`);
+ };
+
+ render() {
+ const store = this.getInstancesStore();
+ let content = null;
+
+ if (isStoreError(store)) {
+ content = ;
+ } else if (isStoreLoading(store)) {
+ content = ;
+ } else if (isStoreReady(store) && isStoreEmpty(store)) {
+ content = this.renderEmptyInstances();
+ } else if (isStoreReady(store) && !isStoreEmpty(store)) {
+ content = this.renderMain();
+ } else {
+ // We get here if the store is in the initial state
+ content = null;
+ }
+
+ return (
+ <>
+ {this.renderTriggerDialog()}
+ {content}
+ >
+ );
+ }
+
+ renderMain() {
+ const store = this.getInstancesStore();
+ const list = store.list;
+
+ return _.map(list, instance => this.renderRow(instance));
+ }
+
+ renderRow(instance) {
+ const { id, createdAt, createdBy, statusSummary } = instance;
+ const displayNameService = this.getUserDisplayNameService();
+ const by = () => {displayNameService.getDisplayName(createdBy)} ;
+ const { statusLabel, statusColor, stepsSummary } = statusSummary;
+
+ return (
+
+
+
+
+
+ {statusLabel}
+
+
+ id {id}
+
+
+ {by()}
+
+
+ Steps
+
+ {_.map(stepsSummary, item => (
+
+ {niceNumber(item.count)}
+ {item.statusLabel}
+
+ ))}
+
+
+
+
+
+ );
+ }
+
+ renderEmptyInstances() {
+ return (
+
+
+
+ No instances
+
+ Once the workflow is triggered at least once, you will start seeing information about the instances in this
+ area.
+
+
+
+ );
+ }
+
+ renderTriggerDialog() {
+ const show = this.triggerDialogShown;
+
+ return (
+ <>
+ {!show && (
+
+
+ Trigger
+
+
+ )}
+ {show && this.renderTriggerDialogContent()}
+ >
+ );
+ }
+
+ renderTriggerDialogContent() {
+ const form = this.form;
+ const workflowInputField = form.$('workflowInput');
+
+ return (
+
+
+ {({ processing, _onSubmit, onCancel }) => (
+ <>
+
+
+
+ Trigger
+
+
+ Cancel
+
+
+ >
+ )}
+
+
+ );
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowInstancesList, {
+ triggerDialogShown: observable,
+ handleInstanceClick: action,
+ showTriggerDialog: action,
+ cancelTriggerDialog: action,
+ handleFormSubmission: action,
+});
+
+export default inject('workflowsStore', 'userDisplayName')(withRouter(observer(WorkflowInstancesList)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowPublishedList.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowPublishedList.js
new file mode 100644
index 0000000000..128b5752d2
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/parts/workflows/published/WorkflowPublishedList.js
@@ -0,0 +1,213 @@
+/*
+ * 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 { observer, inject } from 'mobx-react';
+import { decorate, action, runInAction } from 'mobx';
+import { withRouter } from 'react-router-dom';
+import TimeAgo from 'react-timeago';
+import { Header, Icon, Segment, Message, Table } from 'semantic-ui-react';
+import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing';
+import { isStoreEmpty, isStoreNotEmpty } from '@aws-ee/base-ui/dist/models/BaseStore';
+import Stores from '@aws-ee/base-ui/dist/models/Stores';
+import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox';
+
+// eslint-disable-next-line import/no-useless-path-segments
+import whiteGradient from '../../../../src/images/white-gradient.png'; // We need this because we are getting the image from src and not dist
+import ProgressPlaceHolder from '../../workflow-common/ProgressPlaceholder';
+
+// expected props
+// - workflowsStore (via injection)
+// - userDisplayName (via injection)
+// - location (from react router)
+class WorkflowPublishedList extends React.Component {
+ constructor(props) {
+ super(props);
+ runInAction(() => {
+ this.stores = new Stores([this.getStore()]);
+ });
+ }
+
+ componentDidMount() {
+ this.getStores().load({ forceLoad: true });
+ const store = this.getStore();
+ store.startHeartbeat();
+ }
+
+ componentWillUnmount() {
+ const store = this.getStore();
+ store.stopHeartbeat();
+ }
+
+ getStores() {
+ return this.stores;
+ }
+
+ getStore() {
+ return this.props.workflowsStore;
+ }
+
+ getUserDisplayNameService() {
+ return this.props.userDisplayName;
+ }
+
+ handleWorkflowClick = event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ // see https://reactjs.org/docs/events.html and https://github.com/facebook/react/issues/5733
+ const workflowId = event.currentTarget.dataset.workflow;
+ const version = event.currentTarget.dataset.version;
+ const goto = gotoFn(this);
+
+ goto(`/workflows/published/id/${workflowId}/v/${version}`);
+ };
+
+ render() {
+ const stores = this.getStores();
+ const store = this.getStore();
+ let content = null;
+
+ if (stores.hasError) {
+ content = ;
+ } else if (stores.loading) {
+ content = ;
+ } else if (stores.ready && isStoreEmpty(store)) {
+ content = this.renderEmpty();
+ } else if (stores.ready && isStoreNotEmpty(store)) {
+ content = this.renderTable();
+ } else {
+ content = null;
+ }
+
+ return (
+
+ {this.renderTitle()}
+ {content}
+
+ );
+ }
+
+ renderEmpty() {
+ const show = this.showCreateDraftWizard;
+ if (show) return null;
+ return (
+
+
+
+ No workflows
+
+ To create a workflow, start by creating a draft and then publish the draft.
+
+
+
+ );
+ }
+
+ renderTitle() {
+ return (
+
+
+
+ );
+ }
+
+ renderTable() {
+ const store = this.getStore();
+ const list = store.list;
+
+ return (
+
+
+
+ Workflow
+ Updated
+
+
+ {_.map(list, workflow => this.renderWorkflowRow(workflow))}
+
+ );
+ }
+
+ renderWorkflowRow(workflow) {
+ const latest = workflow.latest;
+ if (!latest)
+ return (
+
+
+
+ Workflow "{workflow.id}" does not have any version!
+
+
+
+ );
+
+ const { id, v, title, updatedAt, updatedBy } = latest;
+ const displayNameService = this.getUserDisplayNameService();
+ const isSystem = displayNameService.isSystem(updatedBy);
+ const by = () => (isSystem ? '' : {displayNameService.getDisplayName(updatedBy)} );
+
+ return (
+
+
+
+ {title}
+
+
+
+ {id} v{v}
+
+
+
+
+
+
+ {by()}
+
+
+
+ );
+ }
+
+ renderDescription(latest) {
+ /* eslint-disable react/no-danger */
+ return (
+ <>
+
+
+ >
+ );
+ /* eslint-enable react/no-danger */
+ }
+}
+
+// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
+decorate(WorkflowPublishedList, {
+ handleWorkflowClick: action,
+});
+
+export default inject('workflowsStore', 'userDisplayName')(withRouter(observer(WorkflowPublishedList)));
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/plugins/app-context-items-plugin.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/plugins/app-context-items-plugin.js
new file mode 100644
index 0000000000..cfc23e0d54
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/plugins/app-context-items-plugin.js
@@ -0,0 +1,45 @@
+/*
+ * 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 * as stepTemplatesStore from '../models/workflow-step-templates/StepTemplatesStore';
+import * as workflowTemplateDraftEditor from '../models/workflow-templates/drafts/edit/WorkflowTemplateDraftEditor';
+import * as workflowTemplateDraftsStore from '../models/workflow-templates/drafts/WorkflowTemplateDraftsStore';
+import * as workflowTemplatesStore from '../models/workflow-templates/WorkflowTemplatesStore';
+import * as workflowDraftEditor from '../models/workflows/drafts/edit/WorkflowDraftEditor';
+import * as workflowDraftsStore from '../models/workflows/drafts/WorkflowDraftsStore';
+import * as workflowsStore from '../models/workflows/WorkflowsStore';
+
+// eslint-disable-next-line no-unused-vars
+function registerAppContextItems(appContext) {
+ stepTemplatesStore.registerContextItems(appContext);
+ workflowTemplateDraftEditor.registerContextItems(appContext);
+ workflowTemplateDraftsStore.registerContextItems(appContext);
+ workflowTemplatesStore.registerContextItems(appContext);
+ workflowDraftEditor.registerContextItems(appContext);
+ workflowDraftsStore.registerContextItems(appContext);
+ workflowsStore.registerContextItems(appContext);
+}
+
+// eslint-disable-next-line no-unused-vars
+function postRegisterAppContextItems(appContext) {
+ // No impl at this level
+}
+
+const plugin = {
+ registerAppContextItems,
+ postRegisterAppContextItems,
+};
+
+export default plugin;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/plugins/menu-items-plugin.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/plugins/menu-items-plugin.js
new file mode 100644
index 0000000000..5208cd890a
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/plugins/menu-items-plugin.js
@@ -0,0 +1,47 @@
+/*
+ * 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';
+/**
+ * Adds workflow navigation menu items to the given itemsMap.
+ *
+ * @param itemsMap A Map containing navigation items. This object is a Map that has route paths (urls) as
+ * keys and menu item object with the following shape
+ *
+ * {
+ * title: STRING, // Title for the navigation menu item
+ * icon: STRING, // semantic ui icon name fot the navigation menu item
+ * shouldShow: BOOLEAN || FUNCTION, // A flag or a function that returns a flag indicating whether to show the item or not (useful when showing menu items conditionally)
+ * render: OPTIONAL FUNCTION, // Optional function that returns rendered menu item component. Use this ONLY if you want to control full rendering of the menu item.
+ * }
+ *
+ * @param context A context object containing all various stores
+ *
+ * @returns Map<*> Returns A Map containing navigation menu items with the same shape as "itemsMap"
+ */
+// eslint-disable-next-line no-unused-vars
+function registerMenuItems(itemsMap, { location, appContext }) {
+ const isAdmin = _.get(appContext, 'userStore.user.isAdmin');
+ const items = new Map([
+ ...itemsMap,
+ ['/workflows/published', { title: 'Workflows', icon: 'fork', shouldShow: isAdmin }],
+ ]);
+
+ return items;
+}
+const plugin = {
+ registerMenuItems,
+};
+export default plugin;
diff --git a/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/plugins/routes-plugin.js b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/plugins/routes-plugin.js
new file mode 100644
index 0000000000..dbdcbbc8f4
--- /dev/null
+++ b/addons/addon-base-workflow-ui/packages/base-workflow-ui/src/plugins/routes-plugin.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 withAuth from '@aws-ee/base-ui/dist/withAuth';
+
+import WorkflowTemplatesList from '../parts/workflow-templates/WorkflowTemplatesList';
+import WorkflowTemplateDraftEditor from '../parts/workflow-templates/drafts/edit/WorkflowTemplateDraftEditor';
+import WorkflowsList from '../parts/workflows/WorkflowsList';
+import WorkflowDraftEditor from '../parts/workflows/drafts/edit/WorkflowDraftEditor';
+import WorkflowDetailPage from '../parts/workflows/published/WorkflowDetailPage';
+import WorkflowInstanceDetailPage from '../parts/workflows/published/WorkflowInstanceDetailPage';
+
+/**
+ * Adds routes to the given routesMap.
+ * @param routesMap A Map containing routes. This object is a Map that has route paths as
+ * keys and React Component as value.
+ *
+ * @returns {Promise<*>} Returns a Map with the mapping of base routes vs React Component
+ */
+// eslint-disable-next-line no-unused-vars
+function registerRoutes(routesMap, { location, appContext }) {
+ const routes = new Map([
+ ...routesMap,
+ ['/workflow-templates/drafts/edit/:draftId', withAuth(WorkflowTemplateDraftEditor)],
+ ['/workflow-templates', withAuth(WorkflowTemplatesList)],
+ ['/workflows/drafts/edit/:draftId', withAuth(WorkflowDraftEditor)],
+ ['/workflows/published/id/:workflowId/v/:version/instances/id/:instanceId', withAuth(WorkflowInstanceDetailPage)],
+ ['/workflows/published/id/:workflowId/v/:version', withAuth(WorkflowDetailPage)],
+ ['/workflows', withAuth(WorkflowsList)],
+ ]);
+
+ return routes;
+}
+
+const plugin = {
+ registerRoutes,
+};
+
+export default plugin;
diff --git a/addons/addon-base-workflow/README.md b/addons/addon-base-workflow/README.md
new file mode 100644
index 0000000000..6f619170c2
--- /dev/null
+++ b/addons/addon-base-workflow/README.md
@@ -0,0 +1,87 @@
+# Base Workflow Add-On
+
+This add-on introduces the core workflow functionality. This includes:
+- Workflow steps, workflow templates and workflows
+
+The following sections list the add-on contribution.
+
+## npm packages
+
+- @aws-ee/workflow-engine
+- @aws-ee/base-workflow-core
+- @aws-ee/base-workflow-steps
+- @aws-ee/base-workflow-templates
+
+## Database tables
+
+- DbStepTemplates
+- DbWorkflowTemplates
+- DbWorkflowTemplateDrafts
+- DbWorkflowDrafts
+- DbWorkflows
+- DbWorkflowInstances
+- DbWfAssignments
+
+## Settings
+- New
+ - workflowsEnabled
+ - workflowStateMachineName
+ - workflowStateMachineArn
+ - (static) these settings are computed in code:
+ - dbTableStepTemplates
+ - dbTableWorkflowTemplates
+ - dbTableWorkflows
+ - dbTableWorkflowTemplateDrafts
+ - dbTableWorkflowDrafts
+ - dbTableWorkflowInstances
+ - dbTableWfAssignments
+
+- Used
+ - dbTablePrefix
+
+## Runtime extension points
+- New
+ - 'workflow-steps': { registerWorkflowSteps(stepRegistry) }
+ - 'workflow-templates': { registerWorkflowTemplates(templateRegistry) }
+ - 'workflows': { registerWorkflows(workflowRegistry) }
+ - 'workflow-assignments': { registerWorkflowAssignments(assignmentRegistry) }
+
+- Availability
+ - backend SDC
+ - backend/src/lambdas/workflow-loop-runner
+ - post-deployment SDC
+ - post-deployment/src/lambdas/post-deployment
+
+- Used
+ - 'service'
+ - 'postDeploymentStep'
+
+## New services
+- stepRegistryService
+- stepTemplateService
+- workflowAssignmentRegistryService
+- workflowAssignmentService
+- workflowDraftService
+- workflowInstanceService
+- workflowRegistryService
+- workflowService
+- workflowTemplateDraftService
+- workflowTemplateRegistryService
+- workflowTemplateService
+- workflowTriggerService
+
+## New post deployment steps
+- AddStepTemplates
+- AddWorkflowAssignments
+- AddWorkflowTemplates
+- AddWorkflows
+
+## CloudFormation resources
+- Workflow Loop Runner Lambda
+- Step Functions
+- Database tables
+- A few IAM roles
+
+## Dependencies
+- base Add-on
+
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/.eslintrc.json b/addons/addon-base-workflow/packages/base-workflow-core/.eslintrc.json
new file mode 100644
index 0000000000..c700cf03a8
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/.eslintrc.json
@@ -0,0 +1,26 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "import/no-unresolved": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/.gitignore b/addons/addon-base-workflow/packages/base-workflow-core/.gitignore
new file mode 100644
index 0000000000..f2fb153198
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/.gitignore
@@ -0,0 +1,19 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+/coverage/
+.build
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/.prettierrc.json b/addons/addon-base-workflow/packages/base-workflow-core/.prettierrc.json
new file mode 100644
index 0000000000..4ee7b34147
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/.prettierrc.json
@@ -0,0 +1,8 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
+
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/jest.config.js b/addons/addon-base-workflow/packages/base-workflow-core/jest.config.js
new file mode 100644
index 0000000000..70544476d7
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/jest.config.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+};
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/jsconfig.json b/addons/addon-base-workflow/packages/base-workflow-core/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/plugins/steps-plugin.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/plugins/steps-plugin.js
new file mode 100644
index 0000000000..2c4a4a5c4d
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/plugins/steps-plugin.js
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+const AddStepTemplates = require('../steps/add-step-templates');
+const AddWorkflowTemplates = require('../steps/add-workflow-templates');
+const AddWorkflows = require('../steps/add-workflows');
+const AddWorkflowAssignments = require('../steps/add-workflow-assignments');
+
+/**
+ * Returns a map of post deployment steps
+ *
+ * @param existingStepsMap Map of existing post deployment steps
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>}
+ */
+// eslint-disable-next-line no-unused-vars
+async function getSteps(existingStepsMap, pluginRegistry) {
+ const stepsMap = new Map([
+ ...existingStepsMap,
+ ['addStepTemplates', new AddStepTemplates()],
+ ['addWorkflowTemplates', new AddWorkflowTemplates()],
+ ['addWorkflows', new AddWorkflows()],
+ ['addWorkflowAssignments', new AddWorkflowAssignments()],
+ ]);
+
+ return stepsMap;
+}
+
+const plugin = {
+ getSteps,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/steps/add-step-templates.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/steps/add-step-templates.js
new file mode 100644
index 0000000000..339805ed76
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/steps/add-step-templates.js
@@ -0,0 +1,79 @@
+/*
+ * 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 */
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context');
+
+class AddStepTemplates extends Service {
+ constructor() {
+ super();
+ this.dependency(['deploymentStoreService', 'stepTemplateService', 'stepRegistryService']);
+ }
+
+ async init() {
+ await super.init();
+ }
+
+ async execute() {
+ const [registryService] = await this.service(['stepRegistryService']);
+
+ // steps = [ { id, v, yaml, implClass }]
+ const steps = await registryService.listSteps();
+
+ /* eslint-disable no-restricted-syntax */
+ for (const step of steps) {
+ const { id, v, yaml } = step;
+ const encodedId = `${id}-${v}`;
+ const yamlStr = JSON.stringify(yaml);
+ const existingItem = await this.findDeploymentItem({ id: encodedId });
+
+ if (existingItem && yamlStr === existingItem.value) {
+ this.log.info(`Skip step template [${id}] v${v} "${step.yaml.title}"`);
+ } else {
+ this.log.info(`Add/Update step template [${id}] v${v} "${step.yaml.title}"`);
+ await this.createVersion(yaml);
+ await this.createDeploymentItem({ encodedId, yamlStr });
+ }
+ }
+ /* eslint-enable no-restricted-syntax */
+ }
+
+ async findDeploymentItem({ id }) {
+ const [deploymentStore] = await this.service(['deploymentStoreService']);
+ return deploymentStore.find({ type: 'step-template', id });
+ }
+
+ async createDeploymentItem({ encodedId, yamlStr }) {
+ const [deploymentStore] = await this.service(['deploymentStoreService']);
+
+ return deploymentStore.createOrUpdate({ type: 'step-template', id: encodedId, value: yamlStr });
+ }
+
+ async createVersion(yaml) {
+ const [stepTemplateService] = await this.service(['stepTemplateService']);
+ const { id, v } = yaml;
+ const requestContext = getSystemRequestContext();
+ const existing = await stepTemplateService.findVersion({ id, v, fields: [] });
+
+ if (existing) {
+ const data = { ...yaml, rev: existing.rev };
+ return stepTemplateService.updateVersion(requestContext, data);
+ }
+ return stepTemplateService.createVersion(requestContext, yaml);
+ }
+}
+
+module.exports = AddStepTemplates;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/steps/add-workflow-assignments.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/steps/add-workflow-assignments.js
new file mode 100644
index 0000000000..36743a2e4c
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/steps/add-workflow-assignments.js
@@ -0,0 +1,84 @@
+/*
+ * 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 */
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context');
+
+class AddWorkflowAssignments extends Service {
+ constructor() {
+ // eslint-disable-line no-useless-constructor
+ super();
+ this.dependency(['deploymentStoreService', 'workflowAssignmentRegistryService', 'workflowAssignmentService']);
+ }
+
+ async init() {
+ await super.init();
+ }
+
+ async execute() {
+ const [registryService] = await this.service(['workflowAssignmentRegistryService']);
+
+ const assignments = await registryService.listAssignments();
+
+ /* eslint-disable no-restricted-syntax */
+ for (const assignment of assignments) {
+ const { id } = assignment;
+ const assignmentStr = JSON.stringify(assignment);
+ const existingItem = await this.findDeploymentItem({ id });
+
+ if (existingItem && assignmentStr === existingItem.value) {
+ this.log.info(
+ `Skip workflow assignment id "${id}" triggerType "${assignment.triggerType}" triggerTypeData "${assignment.triggerTypeData}"`,
+ );
+ } else {
+ this.log.info(
+ `Add/Update workflow assignment id "${id}" triggerType "${assignment.triggerType}" triggerTypeData "${assignment.triggerTypeData}"`,
+ );
+ await this.createAssignment(assignment);
+ await this.createDeploymentItem(assignment);
+ }
+ }
+ /* eslint-enable no-restricted-syntax */
+ }
+
+ async findDeploymentItem({ id }) {
+ const [deploymentStore] = await this.service(['deploymentStoreService']);
+ return deploymentStore.find({ type: 'workflow-assignment', id });
+ }
+
+ async createDeploymentItem(assignment) {
+ const [deploymentStore] = await this.service(['deploymentStoreService']);
+ const { id } = assignment;
+ const assignmentStr = JSON.stringify(assignment);
+
+ return deploymentStore.createOrUpdate({ type: 'workflow-assignment', id, value: assignmentStr });
+ }
+
+ async createAssignment(assignment) {
+ const [assignmentService] = await this.service(['workflowAssignmentService']);
+ const { id } = assignment;
+ const requestContext = getSystemRequestContext();
+ const existing = await assignmentService.find(requestContext, { id });
+
+ if (existing) {
+ const data = { ...assignment, rev: existing.rev };
+ return assignmentService.update(requestContext, data);
+ }
+ return assignmentService.create(requestContext, assignment);
+ }
+}
+
+module.exports = AddWorkflowAssignments;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/steps/add-workflow-templates.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/steps/add-workflow-templates.js
new file mode 100644
index 0000000000..9833463665
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/steps/add-workflow-templates.js
@@ -0,0 +1,80 @@
+/*
+ * 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 */
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context');
+
+class AddWorkflowTemplates extends Service {
+ constructor() {
+ // eslint-disable-line no-useless-constructor
+ super();
+ this.dependency(['deploymentStoreService', 'workflowTemplateService', 'workflowTemplateRegistryService']);
+ }
+
+ async init() {
+ await super.init();
+ }
+
+ async execute() {
+ const [registryService] = await this.service(['workflowTemplateRegistryService']);
+
+ // workflowTemplates = [ { id, v, yaml }]
+ const workflowTemplates = await registryService.listWorkflowTemplates();
+
+ /* eslint-disable no-restricted-syntax */
+ for (const template of workflowTemplates) {
+ const { id, v, yaml } = template;
+ const encodedId = `${id}-${v}`;
+ const yamlStr = JSON.stringify(yaml);
+ const existingItem = await this.findDeploymentItem({ id: encodedId });
+
+ if (existingItem && yamlStr === existingItem.value) {
+ this.log.info(`Skip template [${id}] v${v} "${template.yaml.title}"`);
+ } else {
+ this.log.info(`Add/Update template [${id}] v${v} "${template.yaml.title}"`);
+ await this.createVersion(yaml);
+ await this.createDeploymentItem({ encodedId, yamlStr });
+ }
+ }
+ /* eslint-enable no-restricted-syntax */
+ }
+
+ async findDeploymentItem({ id }) {
+ const [deploymentStore] = await this.service(['deploymentStoreService']);
+ return deploymentStore.find({ type: 'workflow-template', id });
+ }
+
+ async createDeploymentItem({ encodedId, yamlStr }) {
+ const [deploymentStore] = await this.service(['deploymentStoreService']);
+
+ return deploymentStore.createOrUpdate({ type: 'workflow-template', id: encodedId, value: yamlStr });
+ }
+
+ async createVersion(yaml) {
+ const [workflowTemplateService] = await this.service(['workflowTemplateService']);
+ const { id, v } = yaml;
+ const requestContext = getSystemRequestContext();
+ const existing = await workflowTemplateService.findVersion({ id, v, fields: [] });
+
+ if (existing) {
+ const data = { ...yaml, rev: existing.rev };
+ return workflowTemplateService.updateVersion(requestContext, data);
+ }
+ return workflowTemplateService.createVersion(requestContext, yaml);
+ }
+}
+
+module.exports = AddWorkflowTemplates;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/steps/add-workflows.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/steps/add-workflows.js
new file mode 100644
index 0000000000..fa3aeff4ec
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/post-deployment/steps/add-workflows.js
@@ -0,0 +1,80 @@
+/*
+ * 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 */
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context');
+
+class AddWorkflows extends Service {
+ constructor() {
+ // eslint-disable-line no-useless-constructor
+ super();
+ this.dependency(['deploymentStoreService', 'workflowService', 'workflowRegistryService']);
+ }
+
+ async init() {
+ await super.init();
+ }
+
+ async execute() {
+ const [registryService] = await this.service(['workflowRegistryService']);
+
+ // workflows = [ { id, v, yaml }]
+ const workflows = await registryService.listWorkflows();
+
+ /* eslint-disable no-restricted-syntax */
+ for (const workflow of workflows) {
+ const { id, v, yaml } = workflow;
+ const encodedId = `${id}-${v}`;
+ const yamlStr = JSON.stringify(yaml);
+ const existingItem = await this.findDeploymentItem({ id: encodedId });
+
+ if (existingItem && yamlStr === existingItem.value) {
+ this.log.info(`Skip workflow [${id}] v${v} "${workflow.yaml.title}"`);
+ } else {
+ this.log.info(`Add/Update workflow [${id}] v${v} "${workflow.yaml.title}"`);
+ await this.createVersion(yaml);
+ await this.createDeploymentItem({ encodedId, yamlStr });
+ }
+ }
+ /* eslint-enable no-restricted-syntax */
+ }
+
+ async findDeploymentItem({ id }) {
+ const [deploymentStore] = await this.service(['deploymentStoreService']);
+ return deploymentStore.find({ type: 'workflow', id });
+ }
+
+ async createDeploymentItem({ encodedId, yamlStr }) {
+ const [deploymentStore] = await this.service(['deploymentStoreService']);
+
+ return deploymentStore.createOrUpdate({ type: 'workflow', id: encodedId, value: yamlStr });
+ }
+
+ async createVersion(yaml) {
+ const [workflowService] = await this.service(['workflowService']);
+ const { id, v } = yaml;
+ const requestContext = getSystemRequestContext();
+ const existing = await workflowService.findVersion({ id, v, fields: [] });
+
+ if (existing) {
+ const data = { ...yaml, rev: existing.rev };
+ return workflowService.updateVersion(requestContext, data);
+ }
+ return workflowService.createVersion(requestContext, yaml);
+ }
+}
+
+module.exports = AddWorkflows;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/runner/handler.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/runner/handler.js
new file mode 100644
index 0000000000..11478c976f
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/runner/handler.js
@@ -0,0 +1,334 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const ServicesContainer = require('@aws-ee/base-services-container/lib/services-container');
+const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context');
+const WorkflowLoop = require('@aws-ee/workflow-engine/lib/workflow-loop');
+const StepStateProvider = require('@aws-ee/workflow-engine/lib/step/step-state-provider');
+const WorkflowPayload = require('@aws-ee/workflow-engine/lib/workflow-payload');
+const StepLoopProvider = require('@aws-ee/workflow-engine/lib/step/step-loop-provider');
+const WorkflowInstance = require('@aws-ee/workflow-engine/lib/workflow-instance');
+const WorkflowInput = require('@aws-ee/workflow-engine/lib/workflow-input');
+const { catchIfErrorAsync } = require('@aws-ee/workflow-engine/lib/helpers/utils');
+
+const WorkflowReporter = require('../workflow/helpers/workflow-reporter');
+
+const settingKeys = {
+ workflowsEnabled: 'workflowsEnabled',
+};
+
+// The event shape is:
+// NOTE: AWS StepFunctions limit the event size to 32KB, this is way we shorten many of the property names here.
+// {
+// "input": {
+// ... // the main input to the workflow
+// },
+// "meta": {
+// "wid": string, // "wid" = workflow id
+// "wrv": string // "wrv" = workflow revision (will then be parsed as int)
+// "sid": string, // "sid" = workflow instance id
+// // (if this is not present in here, nor in the loop, then a new workflow instance is created)
+// ...
+// },
+// "loop": {
+// "shouldWait": 0/1, // this is here to communicate with the AWS Step Functions (state machine)
+// "shouldLoop": 0/1, // this is here to communicate with the AWS Step Functions (state machine)
+// "shouldPass": 0/1, // this is here to communicate with the AWS Step Functions (state machine)
+// "shouldFail": 0/1, // this is here to communicate with the AWS Step Functions (state machine)
+// "wait": , // this is here to communicate with the AWS Step Functions (state machine)
+// "memento": {...}, // the workflowLoop memento
+// "slp": {...}, // "slp" = stepLoopProvider memento
+// "wp": {...}, // "wp" = workflowPayload memento
+// "ssp": { }, // step state provider mementos extremely limited in size
+// // should not be used for large string values. Remember the limit
+// // for the whole event object is 32KB.
+// "error": { msg: string, stack: string (trimmed), ... } // this is for the case where the
+// // workflowLoop catches the error
+// },
+//
+// "error": { // for the unhandled exceptions caught by AWS StepFunctions
+// "Error": "Error",
+// "Cause": {
+// "errorMessage": string, "errorType": "Error", "stackTrace": [ string ]
+// }
+// }
+// }
+
+function toNumber(input) {
+ if (_.isUndefined(input)) return undefined;
+ if (_.isNumber(input)) return input;
+
+ return parseInt(input, 10);
+}
+
+async function handler({ input = {}, meta = {}, loop = {} } = {}, _context, registerServices) {
+ // eslint-disable-line no-unused-vars
+ const { memento = {}, wp = {}, ssp = {}, slp = {}, sid: sidFromLoop } = loop;
+
+ // Register services
+ const container = new ServicesContainer(['settings', 'log']);
+ await registerServices(container);
+ await container.initServices();
+
+ // Check circular dependencies
+ container.validate();
+
+ const log = await container.find('log');
+ const settings = await container.find('settings');
+ const workflowsEnabled = settings.optionalBoolean(settingKeys.workflowsEnabled, true);
+
+ if (!workflowsEnabled) {
+ log.info('Skipping the processing of the workflows because the setting "workflowsEnabled" is false');
+ return { shouldEnd: 1 };
+ }
+
+ const { wid, wrv, sid: sidFromMeta } = meta;
+ let wrvParsed;
+ let sid = sidFromMeta || sidFromLoop;
+ const workflowInstanceService = await container.find('workflowInstanceService');
+ let instance;
+
+ // Wrap the raw input with WorkflowInput
+ const wfInput = new WorkflowInput({ input });
+
+ if (!sid) {
+ wrvParsed = toNumber(wrv);
+ if (_.isEmpty(wid) || _.isUndefined(wrvParsed)) {
+ throw new Error('The "meta" part of the input must contain "wid" and "wrv"');
+ }
+ instance = await workflowInstanceService.createInstance(
+ getSystemRequestContext(),
+ {
+ workflowId: wid,
+ workflowVer: wrvParsed,
+ status: 'in_progress',
+ },
+ wfInput,
+ );
+ sid = instance.id;
+ } else {
+ instance = await workflowInstanceService.mustFindInstance({ id: sid });
+ }
+
+ // TODO: Check if runSpec target is supported
+ const workflowInstance = new WorkflowInstance({ workflowInstance: instance });
+
+ // A convenient function to allow us to wrap a fn with catchIfErrorAsync
+ const safeCall = fn => async (...params) => catchIfErrorAsync(async () => fn(...params));
+
+ // Get the steps registry and register the steps and construct the classResolver
+ const stepRegistry = await container.find('stepRegistryService');
+ const classResolver = async ({ stepTemplateId, stepTemplateVer }) => {
+ const entry = await stepRegistry.findStep({
+ id: stepTemplateId,
+ v: stepTemplateVer,
+ });
+ return entry ? entry.implClass : undefined;
+ };
+
+ // ----
+ // Create and restore all the workflowLoop dependencies/helpers
+ // ----
+
+ // Create stepStateProvider
+ const stepStateProvider = new StepStateProvider();
+ stepStateProvider.setMemento(ssp);
+
+ // Create workflowReporter
+ const workflowReporter = new WorkflowReporter({
+ workflowInstance,
+ log,
+ workflowInstanceService,
+ });
+
+ // Create and restore workflowPayload
+ const workflowPayload = new WorkflowPayload({
+ workflowInstance,
+ meta,
+ input: wfInput,
+ });
+ wp.m = !wp.m || _.isEmpty(wp.m) ? workflowPayload.meta : wp.m;
+ workflowPayload.setMemento(wp);
+
+ // Create stepClassProvider
+ const stepClassProvider = {
+ getClass: async ({ step, workflowStatus }) => {
+ const Class = await classResolver(step);
+ if (_.isNil(Class)) return undefined;
+ const stepReporter = workflowReporter.getStepReporter({ step });
+ const stepState = await stepStateProvider.getStepState({ step });
+ const impl = new Class({
+ input: wfInput,
+ workflowInstance,
+ container,
+ workflowPayload,
+ step,
+ stepReporter,
+ stepState,
+ workflowStatus,
+ });
+ return impl;
+ },
+ };
+
+ // Create and restore stepLoopProvider
+ const stepLoopProvider = new StepLoopProvider({
+ workflowInstance,
+ stepClassProvider,
+ });
+ stepLoopProvider.setMemento(slp);
+
+ // Register with the step loop provider event and the step loop events
+ stepLoopProvider.on(
+ 'stepLoopCreated',
+ safeCall(async stepLoop => {
+ const step = stepLoop.step;
+ const reporter = workflowReporter.getStepReporter({ step });
+ stepLoop
+ .on(
+ 'stepLoopSkipped',
+ safeCall(async () => reporter.stepSkipped()),
+ )
+ .on(
+ 'stepLoopStarted',
+ safeCall(async () => reporter.stepStarted()),
+ )
+ .on(
+ 'stepLoopMethodCall',
+ safeCall(async name => reporter.print(`StepLoop - calling ${name}()`)),
+ )
+ .on(
+ 'stepLoopQueueAdd',
+ safeCall(async msg => reporter.print(msg)),
+ )
+ .on(
+ 'stepLoopStepPausing',
+ safeCall(async reasonForPause => reporter.stepPaused(reasonForPause)),
+ )
+ .on(
+ 'stepLoopStepResuming',
+ safeCall(async reasonForResume => reporter.stepResumed(reasonForResume)),
+ )
+ .on(
+ 'stepLoopStepMaxPauseReached',
+ safeCall(async () => reporter.stepMaxPauseReached()),
+ )
+ .on(
+ 'stepLoopRequestingGoTo',
+ safeCall(async () => {
+ // The step requested WF to execute from other step so the currently executing step is treated as passed
+ // (or "done" - as it has done it's job of requesting to executing from other step)
+ return reporter.stepPassed();
+ }),
+ )
+ .on(
+ 'stepLoopPassed',
+ safeCall(async () => reporter.stepPassed()),
+ )
+ .on(
+ 'stepLoopFailed',
+ safeCall(async (...params) => reporter.stepFailed(...params)),
+ )
+ .on('beforeStepLoopTick', async () => {
+ const stepState = await stepStateProvider.getStepState({ step });
+ await stepState.load();
+ await workflowPayload.load();
+ })
+ .on('afterStepLoopTick', async () => {
+ const stepState = await stepStateProvider.getStepState({ step });
+ await stepState.save();
+ await workflowPayload.save();
+ });
+ }),
+ );
+
+ // Create and restore the workflowLoop
+ const workflowLoop = new WorkflowLoop({ workflowInstance, stepLoopProvider });
+ workflowLoop.setMemento(memento);
+
+ // Register with the workflow loop events
+ workflowLoop
+ .on(
+ 'workflowStarted',
+ safeCall(async () => workflowReporter.workflowStarted()),
+ )
+ .on(
+ 'workflowPaused',
+ safeCall(async () => workflowReporter.workflowPaused()),
+ )
+ .on(
+ 'workflowResuming',
+ safeCall(async () => workflowReporter.workflowResuming()),
+ )
+ .on(
+ 'workflowPassed',
+ safeCall(async () => workflowReporter.workflowPassed()),
+ )
+ .on(
+ 'workflowFailed',
+ safeCall(async (...params) => workflowReporter.workflowFailed(...params)),
+ );
+
+ // Run one iteration of the workflowLoop
+ const decision = await workflowLoop.tick();
+
+ // Deal with the output
+ if (_.isEmpty(decision)) {
+ throw new Error("The workflow loop tick() method didn't return a decision object");
+ }
+ const output = {
+ shouldWait: 0,
+ shouldLoop: 0,
+ shouldPass: 0,
+ shouldFail: 0,
+ memento: workflowLoop.getMemento(),
+ slp: stepLoopProvider.getMemento(),
+ wp: workflowPayload.getMemento(),
+ ssp: stepStateProvider.getMemento(),
+ };
+
+ if (!sidFromMeta) output.sid = sid;
+
+ switch (decision.type) {
+ case 'loop':
+ output.shouldLoop = 1;
+ break;
+ case 'wait':
+ case 'pause':
+ output.shouldWait = 1;
+ output.wait = decision.wait;
+ break;
+ case 'pass':
+ output.shouldPass = 1;
+ break;
+ case 'fail':
+ output.shouldFail = 1;
+ output.error = _.omit(decision, ['type']);
+ break;
+ default:
+ throw new Error(`The workflow loop tick() method returned unsupported decision type of "${decision.type}"`);
+ }
+
+ return output;
+}
+
+function handlerFactory({ registerServices }) {
+ return async (event, context) => {
+ return handler(event, context, registerServices);
+ };
+}
+
+module.exports = handlerFactory;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/runner/plugins/services-plugin.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/runner/plugins/services-plugin.js
new file mode 100644
index 0000000000..7f2c01e47c
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/runner/plugins/services-plugin.js
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+const AwsService = require('@aws-ee/base-services/lib/aws/aws-service');
+const DbService = require('@aws-ee/base-services/lib/db-service');
+const JsonSchemaValidationService = require('@aws-ee/base-services/lib/json-schema-validation-service');
+const InputManifestValidationService = require('@aws-ee/base-services/lib/input-manifest/input-manifest-validation-service');
+const S3Service = require('@aws-ee/base-services/lib/s3-service');
+const LockService = require('@aws-ee/base-services/lib/lock/lock-service');
+const PluginRegistryService = require('@aws-ee/base-services/lib/plugin-registry/plugin-registry-service');
+const AuditWriterService = require('@aws-ee/base-services/lib/audit/audit-writer-service');
+const AuthorizationService = require('@aws-ee/base-services/lib/authorization/authorization-service');
+const UserAuthzService = require('@aws-ee/base-services/lib/user/user-authz-service');
+const UserService = require('@aws-ee/base-services/lib/user/user-service');
+const DbPasswordService = require('@aws-ee/base-services/lib/db-password/db-password-service');
+const StepRegistryService = require('../../workflow/step/step-registry-service');
+const StepTemplateService = require('../../workflow/step/step-template-service');
+const WorkflowTemplateRegistryService = require('../../workflow/workflow-template-registry-service');
+const WorkflowTemplateService = require('../../workflow/workflow-template-service');
+const WorkflowService = require('../../workflow/workflow-service');
+const WorkflowRegistryService = require('../../workflow/workflow-registry-service');
+const WorkflowAssignmentRegistryService = require('../../workflow/workflow-assignment-registry-service');
+const WorkflowAssignmentService = require('../../workflow/workflow-assignment-service');
+const WorkflowInstanceService = require('../../workflow/workflow-instance-service');
+const WorkflowTriggerService = require('../../workflow/workflow-trigger-service');
+
+const settingKeys = {
+ tablePrefix: 'dbTablePrefix',
+};
+
+/**
+ * Registers the services needed by the workflow loop runner lambda function
+ * @param container An instance of ServicesContainer to register services to
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+async function registerServices(container, pluginRegistry) {
+ container.register('aws', new AwsService());
+ container.register('dbService', new DbService(), { lazy: false });
+ container.register('jsonSchemaValidationService', new JsonSchemaValidationService());
+ container.register('inputManifestValidationService', new InputManifestValidationService());
+ container.register('s3Service', new S3Service());
+ container.register('auditWriterService', new AuditWriterService());
+ container.register('pluginRegistryService', new PluginRegistryService(pluginRegistry), { lazy: false });
+ container.register('lockService', new LockService());
+ container.register('userService', new UserService());
+ container.register('dbPasswordService', new DbPasswordService());
+ container.register('stepRegistryService', new StepRegistryService());
+ container.register('stepTemplateService', new StepTemplateService());
+ container.register('workflowTemplateService', new WorkflowTemplateService());
+ container.register('workflowTemplateRegistryService', new WorkflowTemplateRegistryService());
+ container.register('workflowService', new WorkflowService());
+ container.register('workflowRegistryService', new WorkflowRegistryService());
+ container.register('workflowAssignmentRegistryService', new WorkflowAssignmentRegistryService());
+ container.register('workflowAssignmentService', new WorkflowAssignmentService());
+ container.register('workflowInstanceService', new WorkflowInstanceService());
+ container.register('workflowTriggerService', new WorkflowTriggerService());
+
+ // Authorization Services from base addon
+ container.register('authorizationService', new AuthorizationService());
+ container.register('userAuthzService', new UserAuthzService());
+}
+
+/**
+ * Registers static settings required by the workflow loop runner lambda function
+ * @param existingStaticSettings An existing static settings plain javascript object containing settings as key/value contributed by other plugins
+ * @param settings Default instance of settings service that resolves settings from environment variables
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point
+ *
+ * @returns {Promise<*>} A promise that resolves to static settings object
+ */
+// eslint-disable-next-line no-unused-vars
+function getStaticSettings(existingStaticSettings, settings, pluginRegistry) {
+ const staticSettings = {
+ ...existingStaticSettings,
+ };
+
+ // Register all dynamodb table names used by the base rest api addon
+ const tablePrefix = settings.get(settingKeys.tablePrefix);
+ const table = (key, suffix) => {
+ staticSettings[key] = `${tablePrefix}-${suffix}`;
+ };
+ table('dbTableUsers', 'DbUsers');
+ table('dbTableLocks', 'DbLocks');
+ table('dbTableStepTemplates', 'DbStepTemplates');
+ table('dbTableWorkflowTemplates', 'DbWorkflowTemplates');
+ table('dbTableWorkflowTemplateDrafts', 'DbWorkflowTemplateDrafts');
+ table('dbTableWorkflowDrafts', 'DbWorkflowDrafts');
+ table('dbTableWorkflows', 'DbWorkflows');
+ table('dbTableWorkflowInstances', 'DbWorkflowInstances');
+ table('dbTableWfAssignments', 'DbWfAssignments');
+
+ return staticSettings;
+}
+
+const plugin = {
+ getStaticSettings,
+ // getLoggingContext, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerSettingsService, // not implemented, the default behavior provided by addon-base is sufficient
+ // registerLoggerService, // not implemented, the default behavior provided by addon-base is sufficient
+ registerServices,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/change-step-status.json b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/change-step-status.json
new file mode 100644
index 0000000000..391a02fcb5
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/change-step-status.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://basedl/root.json",
+ "type": "object",
+ "required": [
+ "instanceId",
+ "stepIndex"
+ ],
+ "properties": {
+ "instanceId": { "type": "string" },
+ "stepIndex": { "type": "integer" },
+ "status": { "type": "string", "enum": ["not_started", "in_progress", "paused", "error", "done", "skipped"] },
+ "clearMessage": { "type": "boolean" },
+ "message": { "type": "string" }
+ }
+}
\ No newline at end of file
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/change-workflow-status.json b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/change-workflow-status.json
new file mode 100644
index 0000000000..4a097a5512
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/change-workflow-status.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://basedl/root.json",
+ "type": "object",
+ "required": [
+ "instanceId",
+ "status"
+ ],
+ "properties": {
+ "instanceId": { "type": "string" },
+ "status": { "type": "string", "enum": ["not_started", "in_progress", "paused", "error", "done"] },
+ "clearMessage": { "type": "boolean" },
+ "message": { "type": "string" }
+ }
+}
\ No newline at end of file
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/create-wf-assignment.json b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/create-wf-assignment.json
new file mode 100644
index 0000000000..6b8af06aa1
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/create-wf-assignment.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "pattern": "^[A-Za-z0-9-_ ]+$"
+ },
+ "triggerType": {
+ "type": "string"
+ },
+ "triggerTypeData": {
+ "type": "string"
+ },
+ "wf": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "triggerType", "triggerTypeData", "wf"]
+}
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/create-workflow-instance.json b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/create-workflow-instance.json
new file mode 100644
index 0000000000..21f72a393d
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/create-workflow-instance.json
@@ -0,0 +1,26 @@
+{
+ "definitions": {
+ "runSpec": {
+ "type": "object",
+ "properties": {
+ "target": { "type": "string", "enum": ["stepFunctions", "workerLambda", "inPlace"] },
+ "size": { "type": "string", "enum": ["small", "medium", "large"] }
+ }
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://basedl/root.json",
+ "type": "object",
+ "required": [
+ "workflowId"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "workflowId": { "type": "string" },
+ "workflowVer": { "type": "integer", "minimum": 1 },
+ "runSpec": { "$ref": "#/definitions/runSpec" },
+ "status": { "type": "string", "enum": ["not_started", "in_progress", "paused", "error", "done"] },
+ "assignmentId": { "type": "string" },
+ "smWorkflow": { "type": "string" }
+ }
+}
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/save-step-attributes.json b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/save-step-attributes.json
new file mode 100644
index 0000000000..a128c0497e
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/save-step-attributes.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://basedl/root.json",
+ "type": "object",
+ "required": ["instanceId", "stepIndex"],
+ "properties": {
+ "instanceId": { "type": "string" },
+ "stepIndex": { "type": "integer" },
+ "attribs": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+}
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/step-template.json b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/step-template.json
new file mode 100644
index 0000000000..72e792db0e
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/step-template.json
@@ -0,0 +1,169 @@
+{
+ "definitions": {
+ "markdown": {
+ "type": "string"
+ },
+ "description": {
+ "$ref": "#/definitions/markdown"
+ },
+ "manifestCondition": {
+ "oneOf": [ { "type": "null" }, { "type": "string", "default": ""} ]
+ },
+ "manifestEntryInput": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "type": {
+ "type": "string",
+ "enum": [
+ "yesNoInput",
+ "stringInput",
+ "dropDownInput",
+ "textAreaInput",
+ "userSelectionInput",
+ "workflowSelectionInput"
+ ]
+ },
+ "condition": { "$ref": "#/definitions/manifestCondition" },
+ "title": { "type": "string" },
+ "desc": { "$ref": "#/definitions/description" },
+ "rules": { "type": "string" },
+ "nonInteractive": { "type": "boolean", "default": true},
+ "sensitive": { "type": "boolean", "default": false },
+ "divider": {
+ "type": "object",
+ "properties": {
+ "title": { "type": "string" },
+ "icon": { "type": "string" }
+ },
+ "additionalProperties": false
+ },
+ "yesLabel": { "type": "string" },
+ "noLabel": { "type": "string" },
+ "options": {
+ "typ": "array"
+ },
+ "extra": {
+ "type": "object"
+ }
+ },
+ "required": [
+ "name",
+ "type",
+ "title"
+ ]
+ },
+ "manifestEntrySegment": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "type": { "type": "string", "enum": [ "segment"] },
+ "condition": { "$ref": "#/definitions/manifestCondition" },
+ "raised": { "type": "boolean", "default": false },
+ "basic": { "type": "boolean", "default": false },
+ "ribbon": {
+ "type": "object",
+ "properties": {
+ "title": { "type": "string" },
+ "color": { "type": "string" }
+ },
+ "additionalProperties": false
+ },
+ "children": {
+ "type": "array",
+ "items": { "$ref": "#/definitions/inputEntryManifest" },
+ "default": []
+ }
+ },
+ "required": [
+ "type",
+ "children"
+ ]
+ },
+ "inputEntryManifest": {
+ "type": "object",
+ "oneOf": [ { "$ref" : "#/definitions/manifestEntrySegment" }, { "$ref": "#/definitions/manifestEntryInput" } ]
+ },
+ "inputSectionManifest": {
+ "type": "object",
+ "properties": {
+ "title": { "type": "string" },
+ "condition": { "$ref": "#/definitions/manifestCondition" },
+ "children": {
+ "type": "array",
+ "items": { "$ref": "#/definitions/inputEntryManifest" },
+ "default": []
+ }
+ },
+ "required": [
+ "children"
+ ]
+ },
+ "inputManifest": {
+ "type": "object",
+ "properties": {
+ "sections": { "typ": "array", "items": { "$ref": "#/definitions/inputSectionManifest" }, "default": [] }
+ }
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://basedl/root.json",
+ "type": "object",
+ "required": [
+ "id",
+ "v",
+ "title",
+ "desc",
+ "skippable",
+ "hidden"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "$id": "#/properties/id",
+ "type": "string",
+ "pattern": "^(.*)$"
+ },
+ "v": {
+ "$id": "#/properties/v",
+ "type": "integer",
+ "minimum": 0
+ },
+ "title": {
+ "$id": "#/properties/title",
+ "type": "string",
+ "default": "",
+ "pattern": "^(.*)$"
+ },
+ "desc": {
+ "$ref": "#/definitions/description"
+ },
+ "skippable": {
+ "$id": "#/properties/skippable",
+ "type": "boolean",
+ "default": false
+ },
+ "src": {
+ "$id": "#/properties/src",
+ "type": "object",
+ "required": [
+ "lambdaArn",
+ "pluginId"
+ ],
+ "properties": {
+ "lambdaArn": {
+ "$id": "#/properties/src/properties/lambdaArn",
+ "type": "string"
+ },
+ "pluginId": {
+ "$id": "#/properties/src/properties/pluginId",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "adminInputManifest": { "$ref": "#/definitions/inputManifest" },
+ "inputManifest": { "$ref": "#/definitions/inputManifest" },
+ "hidden": { "type": "boolean", "default": false }
+ }
+}
\ No newline at end of file
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/trigger-workflow.json b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/trigger-workflow.json
new file mode 100644
index 0000000000..21f72a393d
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/trigger-workflow.json
@@ -0,0 +1,26 @@
+{
+ "definitions": {
+ "runSpec": {
+ "type": "object",
+ "properties": {
+ "target": { "type": "string", "enum": ["stepFunctions", "workerLambda", "inPlace"] },
+ "size": { "type": "string", "enum": ["small", "medium", "large"] }
+ }
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://basedl/root.json",
+ "type": "object",
+ "required": [
+ "workflowId"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "workflowId": { "type": "string" },
+ "workflowVer": { "type": "integer", "minimum": 1 },
+ "runSpec": { "$ref": "#/definitions/runSpec" },
+ "status": { "type": "string", "enum": ["not_started", "in_progress", "paused", "error", "done"] },
+ "assignmentId": { "type": "string" },
+ "smWorkflow": { "type": "string" }
+ }
+}
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/update-wf-assignment.json b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/update-wf-assignment.json
new file mode 100644
index 0000000000..3c84590af3
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/update-wf-assignment.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "pattern": "^[A-Za-z0-9-_ ]+$"
+ },
+ "rev": {
+ "type": "number"
+ },
+ "triggerType": {
+ "type": "string"
+ },
+ "triggerTypeData": {
+ "type": "string"
+ },
+ "wf": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "rev", "triggerType", "triggerTypeData", "wf"]
+}
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/workflow-template.json b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/workflow-template.json
new file mode 100644
index 0000000000..c2808a7bec
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/workflow-template.json
@@ -0,0 +1,81 @@
+{
+ "definitions": {
+ "markdown": {
+ "type": "string"
+ },
+ "description": {
+ "$ref": "#/definitions/markdown"
+ },
+ "overrideOption": {
+ "type": "object",
+ "properties": {
+ "allowed": { "type": "array", "items": { "type": "string" }, "default": [] }
+ }
+ },
+ "runSpec": {
+ "type": "object",
+ "properties": {
+ "target": { "type": "string", "enum": ["stepFunctions", "workerLambda", "inPlace"] },
+ "size": { "type": "string", "enum": ["small", "medium", "large"] }
+ }
+ },
+ "selectedStep": {
+ "type": "object",
+ "properties": {
+ "stepTemplateId": { "type": "string", "pattern": "^(.*)$" },
+ "stepTemplateVer": { "type": "integer", "minimum": 0 },
+ "propsOverrideOption": { "$ref": "#/definitions/overrideOption" },
+ "configOverrideOption": { "$ref": "#/definitions/overrideOption" },
+ "title": { "type": "string" },
+ "desc": { "$ref": "#/definitions/description" },
+ "skippable": { "type": "boolean" },
+ "defaults": { "type": "object" },
+ "id": { "type": "string" }
+ },
+ "additionalProperties": false,
+ "required": [
+ "stepTemplateId",
+ "stepTemplateVer",
+ "id"
+ ]
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://basedl/root.json",
+ "type": "object",
+ "required": [
+ "id",
+ "v",
+ "title",
+ "selectedSteps",
+ "propsOverrideOption"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "$id": "#/properties/id",
+ "type": "string",
+ "pattern": "^(.*)$"
+ },
+ "v": {
+ "$id": "#/properties/v",
+ "type": "integer",
+ "minimum": 0
+ },
+ "title": {
+ "$id": "#/properties/title",
+ "type": "string",
+ "default": "",
+ "pattern": "^(.*)$"
+ },
+ "desc": {
+ "$ref": "#/definitions/description"
+ },
+ "hidden": { "type": "boolean", "default": false },
+ "builtin": { "type": "boolean", "default": false },
+ "selectedSteps": { "type": "array", "items": { "$ref": "#/definitions/selectedStep" }, "default": [] },
+ "propsOverrideOption": { "$ref": "#/definitions/overrideOption" },
+ "instanceTtl": { "oneOf": [ { "type": "null" }, { "type": "number", "default": -1} ] },
+ "runSpec": { "$ref": "#/definitions/runSpec" }
+ }
+}
\ No newline at end of file
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/workflow.json b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/workflow.json
new file mode 100644
index 0000000000..c703c51b8d
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/schema/workflow.json
@@ -0,0 +1,74 @@
+{
+ "definitions": {
+ "markdown": {
+ "type": "string"
+ },
+ "description": {
+ "$ref": "#/definitions/markdown"
+ },
+ "runSpec": {
+ "type": "object",
+ "properties": {
+ "target": { "type": "string", "enum": ["stepFunctions", "workerLambda", "inPlace"] },
+ "size": { "type": "string", "enum": ["small", "medium", "large"] }
+ }
+ },
+ "selectedStep": {
+ "type": "object",
+ "properties": {
+ "stepTemplateId": { "type": "string", "pattern": "^(.*)$" },
+ "stepTemplateVer": { "type": "integer", "minimum": 0 },
+ "title": { "type": "string" },
+ "desc": { "$ref": "#/definitions/description" },
+ "skippable": { "type": "boolean" },
+ "configs": { "type": "object" },
+ "id": { "type": "string" }
+ },
+ "additionalProperties": false,
+ "required": [
+ "stepTemplateId",
+ "stepTemplateVer",
+ "id"
+ ]
+ }
+ },
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://basedl/root.json",
+ "type": "object",
+ "required": [
+ "id",
+ "v",
+ "workflowTemplateId",
+ "workflowTemplateVer",
+ "selectedSteps"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "$id": "#/properties/id",
+ "type": "string",
+ "pattern": "^(.*)$"
+ },
+ "v": {
+ "$id": "#/properties/v",
+ "type": "integer",
+ "minimum": 0
+ },
+ "workflowTemplateId": { "type": "string" },
+ "workflowTemplateVer": { "type": "integer", "minimum": 1 },
+ "title": {
+ "$id": "#/properties/title",
+ "type": "string",
+ "default": "",
+ "pattern": "^(.*)$"
+ },
+ "desc": {
+ "$ref": "#/definitions/description"
+ },
+ "hidden": { "type": "boolean", "default": false },
+ "builtin": { "type": "boolean", "default": false },
+ "selectedSteps": { "type": "array", "items": { "$ref": "#/definitions/selectedStep" }, "default": [] },
+ "instanceTtl": { "oneOf": [ { "type": "null" }, { "type": "number", "default": -1} ] },
+ "runSpec": { "$ref": "#/definitions/runSpec" }
+ }
+}
\ No newline at end of file
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/config-override-option.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/config-override-option.js
new file mode 100644
index 0000000000..1911a2002a
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/config-override-option.js
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+class ConfigOverrideOption {
+ constructor(overrideOption = {}) {
+ this.overrideOption = overrideOption;
+ this.configs = (overrideOption.allowed || []).slice();
+ }
+
+ // Returns an array of the names of all the violated configs because they are being overridden
+ violatedConfigs(overridingConfig = {}, srcConfig = {}) {
+ const result = [];
+
+ const keys = Object.keys(overridingConfig);
+
+ _.forEach(keys, key => {
+ if (this.configs.includes(key)) return;
+ if (!_.isEqual(overridingConfig[key], srcConfig[key])) result.push(key);
+ });
+
+ return result;
+ }
+}
+
+module.exports = ConfigOverrideOption;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/props-override-option.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/props-override-option.js
new file mode 100644
index 0000000000..204ce6edba
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/props-override-option.js
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+class PropsOverrideOption {
+ constructor(overrideOption = {}, supportedKeys = [], transformer = key => key) {
+ this.overrideOption = overrideOption;
+ this.supportedKeys = supportedKeys;
+ this.transformer = transformer;
+ const props = (overrideOption.allowed || []).slice();
+ this.props = props;
+ // Special case for the 'steps' prop. If it is not present, it means that the workflow can NOT choose to rearrange the steps or even
+ // add different steps or remove existing steps.
+ if (props.includes('steps')) {
+ this.allowStepsOrderChange = true;
+ delete props.steps;
+ } else {
+ this.allowStepsOrderChange = false;
+ }
+ }
+
+ // Returns an array of the names of all the violated props because they are being overridden
+ violatedProps(overridingObj = {}, srcObj = {}) {
+ const result = [];
+
+ const keys = this.supportedKeys;
+
+ _.forEach(keys, key => {
+ if (this.props.includes(key)) return;
+ const transformedKey = this.transformer(key);
+ const sideA = _.get(overridingObj, transformedKey);
+ const sideB = _.get(srcObj, transformedKey);
+ if (!_.isEqual(sideA, sideB)) result.push(key);
+ });
+
+ return result;
+ }
+}
+
+module.exports = PropsOverrideOption;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/step-base.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/step-base.js
new file mode 100644
index 0000000000..7ebede88e0
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/step-base.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.
+ */
+
+const _ = require('lodash');
+const StepBaseFromWorkflowEngine = require('@aws-ee/workflow-engine/lib/step/step-base');
+
+class StepBase extends StepBaseFromWorkflowEngine {
+ constructor({ input, workflowInstance, workflowPayload, stepState, container, step, stepReporter, workflowStatus }) {
+ super({ input, workflowInstance, workflowPayload, stepState, step, stepReporter, workflowStatus });
+ this.container = container;
+ }
+
+ // Do NOT override this method, instead override 'init' if you need to do any initialization of the step
+ async initStep() {
+ this.settings = await this.mustFindServices('settings');
+ return this.init();
+ }
+
+ async init() {
+ // override this method if needed
+ return this;
+ }
+
+ // Looks up one or more services by name, if any of them are not found and exception is thrown
+ async mustFindServices(oneOrMany) {
+ const result = [];
+ /* eslint-disable no-restricted-syntax */
+ for (const name of _.concat(oneOrMany)) {
+ // eslint-disable-line no-restricted-syntax
+ const service = await this.container.find(name); // eslint-disable-line no-await-in-loop
+ if (!service)
+ throw new Error(
+ `The step tried to access the "${name}" service, but the "${name}" service was not registered.`,
+ );
+ result.push(service);
+ }
+ /* eslint-enable no-restricted-syntax */
+
+ if (!_.isArray(oneOrMany)) return _.head(result);
+ return result;
+ }
+
+ // Looks up one or more services by name, undefined will be returned for services the are not found
+ async optionallyFindServices(oneOrMany) {
+ const result = [];
+ /* eslint-disable no-restricted-syntax */
+ for (const name of _.concat(oneOrMany)) {
+ // eslint-disable-line no-restricted-syntax
+ const service = await this.container.find(name); // eslint-disable-line no-await-in-loop
+ result.push(service);
+ }
+ /* eslint-enable no-restricted-syntax */
+
+ if (!_.isArray(oneOrMany)) return _.head(result);
+ return result;
+ }
+
+ // Returns the list of input keys
+ // Used by step-reporter
+ // returns Object, key - key name, value - key type
+ async inputKeys() {
+ // TODO: uncomment it when implemented in all steps
+ // throw new Error('Input keys method must be implemented');
+ }
+
+ // Returns the list of output keys
+ // Used by step-reporter
+ // returns Object, key - key name, value - key type
+ async outputKeys() {
+ // TODO: uncomment it when implemented in all steps
+ // throw new Error('Input keys method must be implemented');
+ }
+
+ // Returns inputs for the step
+ async getStepInput() {
+ return this.getValues(await this.inputKeys());
+ }
+
+ // Returns outputs for the step
+ async getStepOutput() {
+ return this.getValues(await this.outputKeys());
+ }
+
+ // Returns object with values from payloadOrConfig
+ // argument - object, key - key name, value - type (string, number, object)
+ async getValues(keysMap) {
+ const result = {};
+ const keys = _.keys(keysMap);
+ // eslint-disable-next-line no-restricted-syntax
+ for (const key of keys) {
+ let value;
+ try {
+ const type = keysMap[key];
+ // eslint-disable-next-line no-await-in-loop
+ value = await this.payloadOrConfig[type](key);
+ } catch (error) {
+ // don't fail here because
+ // this is used for logging
+ // and value might not be computed yet
+ }
+ result[key] = value;
+ }
+ return result;
+ }
+}
+
+module.exports = StepBase;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/step-reporter.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/step-reporter.js
new file mode 100644
index 0000000000..b92d16b468
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/step-reporter.js
@@ -0,0 +1,140 @@
+/*
+ * 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.
+ */
+
+const StepReporterBase = require('@aws-ee/workflow-engine/lib/step/step-reporter');
+const { normalizeError } = require('@aws-ee/workflow-engine/lib/helpers/utils');
+
+// --------------------------------------------------
+// StepReporter
+// --------------------------------------------------
+class StepReporter extends StepReporterBase {
+ constructor({ workflowReporter, step, workflowInstanceService }) {
+ super({ workflowReporter, step });
+ this.instanceId = workflowReporter.wfInstance.id;
+ this.stepIndex = step.index;
+ this.instanceService = workflowInstanceService;
+ }
+
+ async stepStarted() {
+ await super.stepStarted();
+ const { instanceId, stepIndex, step } = this;
+ const wfInstance = this.workflowReporter.wfInstance;
+ const startTime = new Date().toISOString();
+ return this.instanceService.changeStepStatus({
+ instanceId,
+ stepIndex,
+ step,
+ wfInstance,
+ status: 'in_progress',
+ startTime,
+ });
+ }
+
+ async stepSkipped() {
+ await super.stepSkipped();
+ const { instanceId, stepIndex, step } = this;
+ const wfInstance = this.workflowReporter.wfInstance;
+ return this.instanceService.changeStepStatus({
+ instanceId,
+ stepIndex,
+ step,
+ wfInstance,
+ status: 'skipped',
+ });
+ }
+
+ async stepPaused(reasonForPause) {
+ await super.stepPaused(reasonForPause);
+ const { instanceId, stepIndex, step } = this;
+ const wfInstance = this.workflowReporter.wfInstance;
+ const endTime = new Date().toISOString();
+ return this.instanceService.changeStepStatus({
+ instanceId,
+ stepIndex,
+ step,
+ wfInstance,
+ status: 'paused',
+ endTime,
+ });
+ }
+
+ async stepResumed(reasonForResume) {
+ await super.stepResumed(reasonForResume);
+ const { instanceId, stepIndex, step } = this;
+ const wfInstance = this.workflowReporter.wfInstance;
+ const endTime = new Date().toISOString();
+ return this.instanceService.changeStepStatus({
+ instanceId,
+ stepIndex,
+ step,
+ wfInstance,
+ status: 'in_progress',
+ endTime,
+ });
+ }
+
+ async stepMaxPauseReached() {
+ return this.stepResumed('max pause time exhausted');
+ }
+
+ async stepPassed() {
+ await super.stepPassed();
+ const { instanceId, stepIndex, step } = this;
+ const wfInstance = this.workflowReporter.wfInstance;
+ const endTime = new Date().toISOString();
+ return this.instanceService.changeStepStatus({ instanceId, stepIndex, step, wfInstance, status: 'done', endTime });
+ }
+
+ // error is just an object (not necessarily an instance of Error) with the following two properties:
+ // - message & stack
+ async stepFailed(error) {
+ await super.stepFailed(error);
+ const { msg } = normalizeError(error);
+ const { instanceId, stepIndex, step } = this;
+ const wfInstance = this.workflowReporter.wfInstance;
+ const endTime = new Date().toISOString();
+ return this.instanceService.changeStepStatus({
+ instanceId,
+ stepIndex,
+ step,
+ wfInstance,
+ status: 'error',
+ message: msg,
+ endTime,
+ });
+ }
+
+ async statusMessage(message) {
+ await super.statusMessage(message);
+ const { instanceId, stepIndex, step } = this;
+ const wfInstance = this.workflowReporter.wfInstance;
+ return this.instanceService.changeStepStatus({ instanceId, stepIndex, step, wfInstance, message });
+ }
+
+ async clearStatusMessage() {
+ await super.clearStatusMessage();
+ const { instanceId, stepIndex, step } = this;
+ const wfInstance = this.workflowReporter.wfInstance;
+ return this.instanceService.changeStepStatus({
+ instanceId,
+ stepIndex,
+ step,
+ wfInstance,
+ clearMessage: true,
+ });
+ }
+}
+
+module.exports = StepReporter;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/supported-override.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/supported-override.js
new file mode 100644
index 0000000000..67d17ab46a
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/supported-override.js
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+const workflowPropsSupportedOverrideKeys = ['title', 'desc', 'instanceTtl', 'runSpecSize', 'runSpecTarget', 'steps'];
+const stepPropsSupportedOverrideKeys = ['title', 'desc', 'skippable'];
+
+// Some keys need to be transformed before they are used to lookup a property value
+const workflowPropsSupportedOverrideKeysTransformer = key => {
+ if (key === 'runSpecSize') return 'runSpec.size';
+ if (key === 'runSpecTarget') return 'runSpec.target';
+ return key;
+};
+
+// Some keys need to be transformed before they are used to lookup a property value
+const stepPropsSupportedOverrideKeysTransformer = key => key;
+
+module.exports = {
+ workflowPropsSupportedOverrideKeys,
+ workflowPropsSupportedOverrideKeysTransformer,
+ stepPropsSupportedOverrideKeys,
+ stepPropsSupportedOverrideKeysTransformer,
+};
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/workflow-reporter.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/workflow-reporter.js
new file mode 100644
index 0000000000..62419e9d93
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/helpers/workflow-reporter.js
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+const WorkflowReporterBase = require('@aws-ee/workflow-engine/lib/workflow-reporter');
+const { normalizeError } = require('@aws-ee/workflow-engine/lib/helpers/utils');
+
+const StepReporter = require('./step-reporter');
+
+// --------------------------------------------------
+// WorkflowReporter
+// --------------------------------------------------
+class WorkflowReporter extends WorkflowReporterBase {
+ constructor({ workflowInstance = {}, log, workflowInstanceService }) {
+ super({ workflowInstance, log });
+ this.instanceService = workflowInstanceService;
+ }
+
+ async workflowStarted() {
+ await super.workflowStarted();
+ return this.instanceService.changeWorkflowStatus({
+ workflowId: this.wfInstance.wf.id,
+ instanceId: this.wfInstance.id,
+ status: 'in_progress',
+ });
+ }
+
+ async workflowPaused() {
+ await super.workflowPaused();
+ return this.instanceService.changeWorkflowStatus({
+ workflowId: this.wfInstance.wf.id,
+ instanceId: this.wfInstance.id,
+ status: 'paused',
+ });
+ }
+
+ async workflowResuming() {
+ await super.workflowResuming();
+ return this.instanceService.changeWorkflowStatus({
+ workflowId: this.wfInstance.wf.id,
+ instanceId: this.wfInstance.id,
+ status: 'in_progress',
+ });
+ }
+
+ async workflowPassed() {
+ await super.workflowPassed();
+ return this.instanceService.changeWorkflowStatus({
+ workflowId: this.wfInstance.wf.id,
+ instanceId: this.wfInstance.id,
+ status: 'done',
+ });
+ }
+
+ // error is just an object (not necessarily an instance of Error) with the following two properties:
+ // - message & stack
+ async workflowFailed(error) {
+ await super.workflowFailed(error);
+ const { msg } = normalizeError(error);
+ return this.instanceService.changeWorkflowStatus({
+ workflowId: this.wfInstance.wf.id,
+ instanceId: this.wfInstance.id,
+ status: 'error',
+ message: msg,
+ });
+ }
+
+ getStepReporter({ step }) {
+ return new StepReporter({ workflowReporter: this, step, workflowInstanceService: this.instanceService });
+ }
+}
+
+module.exports = WorkflowReporter;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/step/step-registry-service.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/step/step-registry-service.js
new file mode 100644
index 0000000000..dd13d8ba32
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/step/step-registry-service.js
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const inputSchema = require('../../schema/step-template');
+
+class StepRegistryService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'pluginRegistryService']);
+ }
+
+ async init() {
+ await super.init();
+ this.store = []; // an array of objects of this shape: { key: , value: { yaml, implClass } }
+
+ const registry = await this.service('pluginRegistryService');
+ // We loop through each step plugin and ask it to register its steps
+ const plugins = await registry.getPlugins('workflow-steps');
+ // eslint-disable-next-line no-restricted-syntax
+ for (const plugin of plugins) {
+ // eslint-disable-next-line no-await-in-loop
+ await plugin.registerWorkflowSteps(this);
+ }
+ }
+
+ async add({ yaml, implClass }) {
+ const [jsonSchemaValidationService] = await this.service(['jsonSchemaValidationService']);
+ const { id, v } = yaml;
+ const existing = await this.findStep({ id, v });
+
+ if (existing)
+ throw this.boom.badRequest(
+ `You tried to register a step, but a step with the same template id "${id}" and version "${v}" already exists`,
+ true,
+ );
+ await jsonSchemaValidationService.ensureValid(yaml, inputSchema);
+
+ const key = this.encodeId({ id, v });
+ this.store.push({ key, value: { yaml, implClass } });
+ }
+
+ async findStep({ id, v }) {
+ const key = this.encodeId({ id, v });
+ const entry = _.find(this.store, ['key', key]);
+ return entry ? entry.value : undefined;
+ }
+
+ async mustFindStep({ id, v }) {
+ const step = await this.findStep({ id, v });
+ if (!step) {
+ throw this.boom.notFound(`The step template "${id}" ver "${v}" is not found`, true);
+ }
+ return step;
+ }
+
+ // Returns a list of all steps in an array of this shape: [{ id, v, yaml, implClass }, ...]
+ async listSteps() {
+ return _.map(this.store, item => {
+ const { yaml, implClass } = item.value;
+ const { id, v } = yaml;
+ return { id, v, yaml, implClass };
+ });
+ }
+
+ // private
+ encodeId({ id, v }) {
+ return `${id}_${v}`;
+ }
+}
+
+module.exports = StepRegistryService;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/step/step-template-service.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/step/step-template-service.js
new file mode 100644
index 0000000000..1b1173de72
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/step/step-template-service.js
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { ensureAdmin } = require('@aws-ee/base-services/lib/authorization/assertions');
+
+const { toVersionString, parseVersionString, runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+const inputSchema = require('../../schema/step-template');
+
+const settingKeys = {
+ tableName: 'dbTableStepTemplates',
+};
+
+class StepTemplateService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'dbService', 'inputManifestValidationService']);
+ }
+
+ async init() {
+ await super.init();
+ this.tableName = this.settings.get(settingKeys.tableName);
+ }
+
+ async createVersion(requestContext, manifest = {}, { isLatest = true, tableName } = {}) {
+ const [jsonSchemaValidationService] = await this.service(['jsonSchemaValidationService']);
+
+ await ensureAdmin(requestContext);
+
+ // TODO - validation does not check for additional props that are not supported, we need to fix it
+ // unfortunately, it is not a matter of adding 'additionalProperties' false, because this does
+ // not work with 'oneOf' option in json schema (possible bug in json schema)
+ // Validate input
+ await jsonSchemaValidationService.ensureValid(manifest, inputSchema);
+
+ const dbService = await this.service('dbService');
+ const table = tableName || this.tableName;
+ const { id, v } = manifest;
+ const logPrefix = `The step template "${id}" with ver "${v}" and rev "0"`;
+ const dbObject = toDbObject(manifest);
+
+ // TODO - we need to wrap the creation of the version and the update of the latest record in a transaction
+ const result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_not_exists(ver)') // yes we need this
+ .key({ id, ver: toVersionString(v) })
+ .item({ ...dbObject, rev: 0 })
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`${logPrefix} already exist`, true);
+ },
+ );
+
+ if (isLatest) {
+ // Note that this is not the typical versioning technique. This is because in this case the caller of this
+ // method already wants to update a specific version which might not be the latest version
+ await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .updatedAt(result.updatedAt)
+ .disableCreatedAt()
+ .key({ id, ver: toVersionString(0) })
+ .condition('(attribute_exists(id) and #latest <= :latest) or attribute_not_exists(id)')
+ .item({ ...result, latest: v })
+ .names({ '#latest': 'latest' })
+ .values({ ':latest': v })
+ .update();
+ },
+ async () => {
+ // we ignore the ConditionalCheckFailedException exception because it simply means that the created version is not the
+ // latest version anymore and there is no need to bother the caller of this fact
+ },
+ );
+ }
+ return toDataObject(result);
+ }
+
+ async updateVersion(requestContext, manifest = {}, { isLatest = true, tableName } = {}) {
+ const [jsonSchemaValidationService] = await this.service(['jsonSchemaValidationService']);
+
+ await ensureAdmin(requestContext);
+
+ // Validate input
+
+ // we need to remove 'rev' here because the schema does not allow it, we should have a schema
+ // that allows 'rev' but for now, we don't do that.
+ await jsonSchemaValidationService.ensureValid(_.omit(manifest, ['rev']), inputSchema);
+ // now we need to check that rev is supplied
+ if (_.isNil(manifest.rev))
+ throw this.boom.badRequest('The supplied step template does not have the "rev" property', true);
+
+ const dbService = await this.service('dbService');
+ const table = tableName || this.tableName;
+ const { id, v, rev } = manifest;
+ const logPrefix = `The step template "${id}" with ver "${v}" and rev "${rev}"`;
+ const dbObject = toDbObject(manifest);
+
+ // lets keep track of what we need to remove
+ const remove = [];
+ if (manifest.inputManifest === undefined) remove.push('inputManifest');
+ if (manifest.adminInputManifest === undefined) remove.push('adminInputManifest');
+
+ // TODO - we need to wrap the creation of the version and the update of the latest record in a transaction
+ const result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_exists(ver)') // yes we need this
+ .key({ id, ver: toVersionString(v) })
+ .rev(rev)
+ .remove(remove)
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ // There are two scenarios here:
+ // 1 - The "v" entry does not exist
+ // 2 - The "rev" does not match
+ // We will display the appropriate error message accordingly
+ const existing = await this.findVersion({ id, v, fields: ['id', 'v'] });
+ if (existing) {
+ throw this.boom.badRequest(
+ `${logPrefix} information changed just before your request is processed, please try again`,
+ true,
+ );
+ }
+ throw this.boom.badRequest(`${logPrefix} does not exist`, true);
+ },
+ );
+
+ if (isLatest) {
+ // Note that this is not the typical versioning technique. This is because in this case the caller of this
+ // method already wants to update a specific version which might not be the latest version
+ await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .updatedAt(result.updatedAt)
+ .key({ id, ver: toVersionString(0) })
+ .condition('#latest = :latest')
+ .item(result)
+ .remove(remove)
+ .names({ '#latest': 'latest' })
+ .values({ ':latest': v })
+ .update();
+ },
+ async () => {
+ // we ignore the ConditionalCheckFailedException exception because it simply means that the updated version is not the
+ // latest version anymore and there is no need to bother the caller of this fact
+ },
+ );
+ }
+
+ return toDataObject(result);
+ }
+
+ // List all versions for all step templates or for a specific step template if the step template id was provided
+ async listVersions({ id, fields = [] } = {}) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ if (_.isNil(id)) {
+ // The scanner route
+ const result = await dbService.helper
+ .scanner()
+ .table(table)
+ .filter('attribute_not_exists(latest)') // we don't want to return the v0000_ one
+ .limit(2000)
+ .projection(fields)
+ .scan();
+ return _.map(result, item => toDataObject(item));
+ }
+
+ const result = await dbService.helper
+ .query()
+ .table(table)
+ .key('id', id)
+ .forward(false)
+ .filter('attribute_not_exists(latest)') // we don't want to return the v0000_ one
+ .limit(2000)
+ .projection(fields)
+ .query();
+ return _.map(result, item => toDataObject(item));
+ }
+
+ // List latest versions of all the step templates
+ async list({ fields = [] } = {}) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ // The scanner route
+ const result = await dbService.helper
+ .scanner()
+ .table(table)
+ .filter('attribute_exists(latest)')
+ .limit(2000)
+ .projection(fields)
+ .scan();
+ return _.map(result, item => toDataObject(item));
+ }
+
+ async findVersion({ id, v = 0, fields = [] }, { tableName } = {}) {
+ const dbService = await this.service('dbService');
+ // This function can accept a different tableName to use for the lookup, this is useful in places
+ // such as post deployment
+ const table = tableName || this.tableName;
+
+ const result = await dbService.helper
+ .getter()
+ .table(table)
+ .key({ id, ver: toVersionString(v) })
+ .projection(fields)
+ .get();
+
+ return toDataObject(result);
+ }
+
+ async mustFindVersion({ id, v = 0, fields }) {
+ const step = await this.findVersion({ id, v, fields });
+ if (!step) throw this.boom.notFound(`The step template "${id}" ver "${v}" is not found`, true);
+ return step;
+ }
+
+ async mustValidateVersion({ id, v = 0, config = {} }) {
+ const [inputManifestValidationService] = await this.service(['inputManifestValidationService']);
+ const step = await this.mustFindVersion({ id, v });
+ const { inputManifest = {} } = step;
+ const validationErrors = await inputManifestValidationService.getValidationErrors(inputManifest, config);
+ return { validationErrors };
+ }
+}
+
+// Do some properties renaming to prepare the object to be saved in the database
+function toDbObject(dataObject) {
+ const result = { ...dataObject };
+
+ delete result.ver;
+ delete result.createdAt;
+ delete result.updatedAt;
+ delete result.rev;
+
+ return result;
+}
+
+// Do some properties renaming to restore the object that was saved in the database
+function toDataObject(dbObject) {
+ if (_.isNil(dbObject)) return dbObject;
+ if (!_.isObject(dbObject)) return dbObject;
+
+ const result = { ...dbObject };
+ result.v = result.latest ? result.latest : parseVersionString(dbObject.ver);
+
+ delete result.ver;
+ delete result.latest;
+
+ return result;
+}
+
+module.exports = StepTemplateService;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-assignment-registry-service.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-assignment-registry-service.js
new file mode 100644
index 0000000000..e146f01315
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-assignment-registry-service.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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const inputSchema = require('../schema/create-wf-assignment');
+
+class WorkflowAssignmentRegistryService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'pluginRegistryService']);
+ }
+
+ async init() {
+ await super.init();
+ this.store = {}; // a map { : { id, triggerType, triggerData, wf } }
+
+ const registry = await this.service('pluginRegistryService');
+ // We loop through each plugin and ask it to register its assignments
+ const plugins = await registry.getPlugins('workflow-assignments');
+ // eslint-disable-next-line no-restricted-syntax
+ for (const plugin of plugins) {
+ // eslint-disable-next-line no-await-in-loop
+ await plugin.registerWorkflowAssignments(this);
+ }
+ }
+
+ async add(rawData) {
+ const [jsonSchemaValidationService] = await this.service(['jsonSchemaValidationService']);
+ const { id } = rawData;
+
+ await jsonSchemaValidationService.ensureValid(rawData, inputSchema);
+
+ // We allow assignments with the same ids to be overwritten
+ this.store[id] = rawData;
+ }
+
+ async findAssignment(id) {
+ return this.store[id];
+ }
+
+ // Returns a list of all assignment in array of this shape: [{ id, triggerType, triggerData, wf }, ...]
+ async listAssignments() {
+ return _.values(this.store);
+ }
+}
+
+module.exports = WorkflowAssignmentRegistryService;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-assignment-service.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-assignment-service.js
new file mode 100644
index 0000000000..a97b8cdeb7
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-assignment-service.js
@@ -0,0 +1,215 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const createSchema = require('../schema/create-wf-assignment');
+const updateSchema = require('../schema/update-wf-assignment');
+
+const settingKeys = {
+ tableName: 'dbTableWfAssignments',
+};
+const typeIndexName = 'TypeIndex';
+const workflowIndexName = 'WorkflowIndex';
+
+class WorkflowAssignmentService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'dbService', 'auditWriterService']);
+ }
+
+ async init() {
+ await super.init();
+ const [dbService] = await this.service(['dbService']);
+ const table = this.settings.get(settingKeys.tableName);
+
+ this._getter = () => dbService.helper.getter().table(table);
+ this._updater = () => dbService.helper.updater().table(table);
+ this._query = () => dbService.helper.query().table(table);
+ this._deleter = () => dbService.helper.deleter().table(table);
+ }
+
+ async find(requestContext, { id, fields = [] }) {
+ const result = await this._getter()
+ .key({ id })
+ .projection(fields)
+ .get();
+
+ return this._fromDbToDataObject(result);
+ }
+
+ async mustFind(requestContext, { id, fields = [] }) {
+ const result = await this.find(requestContext, { id, fields });
+ if (!result) throw this.boom.notFound(`workflow assignment with id "${id}" does not exist`, true);
+ return result;
+ }
+
+ async listByTriggerType(requestContext, { triggerType, beginsWith, fields = [] }) {
+ // beginsWith is optional
+ let op = this._query()
+ .index(typeIndexName)
+ .key('triggerType', triggerType);
+
+ if (!_.isEmpty(beginsWith)) op = op.sortKey('triggerTypeData').begins(beginsWith);
+
+ const result = await op
+ .limit(2000)
+ .projection(fields)
+ .query();
+
+ return _.map(result, item => this._fromDbToDataObject(item));
+ }
+
+ async listByWorkflow(requestContext, { workflowId, fields = [] }) {
+ if (!_.isString(workflowId) || _.isEmpty(workflowId)) throw this.boom.badRequest('workflow id is missing');
+
+ const result = await this._query()
+ .index(workflowIndexName)
+ .key('wf', workflowId)
+ .limit(2000)
+ .projection(fields)
+ .query();
+
+ return _.map(result, item => this._fromDbToDataObject(item));
+ }
+
+ async create(requestContext, rawData) {
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+
+ // Validate input
+ await validationService.ensureValid(rawData, createSchema);
+
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const { id } = rawData;
+
+ // Prepare the db object
+ const dbObject = this._fromRawToDbObject(rawData, { rev: 0, createdBy: by, updatedBy: by });
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_not_exists(id)') // yes we need this
+ .key({ id })
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`workflow assignment with id "${id}" already exists`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-workflow-assignment', body: result });
+
+ return result;
+ }
+
+ async update(requestContext, rawData) {
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+
+ // Validate input
+ await validationService.ensureValid(rawData, updateSchema);
+
+ // For now, we assume that 'updatedBy' is always a user and not a group
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const { id, rev } = rawData;
+
+ // Prepare the db object
+ const dbObject = _.omit(this._fromRawToDbObject(rawData, { updatedBy: by }), ['rev']);
+
+ // Time to save the the db object
+ const result = await runAndCatch(
+ async () => {
+ return this._updater()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .rev(rev)
+ .item(dbObject)
+ .update();
+ },
+ async () => {
+ // There are two scenarios here:
+ // 1 - The wf-assignment does not exist
+ // 2 - The "rev" does not match
+ // We will display the appropriate error message accordingly
+ const existing = await this.find(requestContext, { id, fields: ['id', 'updatedBy'] });
+ if (existing) {
+ throw this.boom.badRequest(
+ `workflow assignment information changed by "${
+ (existing.updatedBy || {}).username
+ }" just before your request is processed, please try again`,
+ true,
+ );
+ }
+ throw this.boom.notFound(`workflow assignment with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'update-workflow-assignment', body: result });
+
+ return result;
+ }
+
+ async delete(requestContext, { id }) {
+ // Lets now remove the item from the database
+ const result = await runAndCatch(
+ async () => {
+ return this._deleter()
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id })
+ .delete();
+ },
+ async () => {
+ throw this.boom.notFound(`workflow assignment with id "${id}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'delete-workflow-assignment', body: { id } });
+
+ return result;
+ }
+
+ // Do some properties renaming to prepare the object to be saved in the database
+ _fromRawToDbObject(rawObject, overridingProps = {}) {
+ const dbObject = { ...rawObject, ...overridingProps };
+ return dbObject;
+ }
+
+ // Do some properties renaming to restore the object that was saved in the database
+ _fromDbToDataObject(rawDb, overridingProps = {}) {
+ if (_.isNil(rawDb)) return rawDb; // important, leave this if statement here, otherwise, your update methods won't work correctly
+ if (!_.isObject(rawDb)) return rawDb;
+
+ const dataObject = { ...rawDb, ...overridingProps };
+ return dataObject;
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+module.exports = WorkflowAssignmentService;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-draft-service.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-draft-service.js
new file mode 100644
index 0000000000..0e9cef75a5
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-draft-service.js
@@ -0,0 +1,348 @@
+/*
+ * 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 */
+const _ = require('lodash');
+const slugify = require('slugify');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+
+// const { ensureAdmin } = require('../authorization-assertions/assertions');
+const inputSchema = require('../schema/workflow');
+
+const settingKeys = {
+ tableName: 'dbTableWorkflowDrafts',
+};
+const usernameIndexName = 'UsernameIndex';
+
+class WorkflowDraftService extends Service {
+ constructor() {
+ super();
+ this.dependency([
+ 'jsonSchemaValidationService',
+ 'workflowTemplateService',
+ 'workflowService',
+ 'stepTemplateService',
+ 'auditWriterService',
+ 'dbService',
+ ]);
+ }
+
+ async init() {
+ await super.init();
+ this.tableName = this.settings.get(settingKeys.tableName);
+ }
+
+ async createDraft(
+ requestContext,
+ { isNewWorkflow = true, workflowId: workflowIdRaw, workflowVer = 0, templateId, templateVer = 0 } = {},
+ ) {
+ const [workflowService, workflowTemplateService] = await this.service([
+ 'workflowService',
+ 'workflowTemplateService',
+ ]);
+
+ // await ensureAdmin(requestContext);
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const username = encodePrincipalIdentifier(by);
+
+ const now = new Date().toISOString();
+ const workflowId = slugify(_.kebabCase(_.startsWith(workflowIdRaw, 'wf-') ? workflowIdRaw : `wf-${workflowIdRaw}`));
+ const draftId = `${username}_${workflowId}_${workflowVer}`;
+ const draft = {
+ id: draftId,
+ workflowId,
+ workflowVer,
+ username,
+ };
+
+ if (isNewWorkflow) {
+ if (_.isEmpty(_.trim(workflowIdRaw))) throw this.boom.badRequest('A workflow id must be provided.', true);
+ const existingWorkflow = await workflowService.findVersion({ id: workflowId });
+ if (existingWorkflow) throw this.boom.badRequest('A workflow with the same workflow id exists.', true);
+ if (_.isEmpty(templateId)) throw this.boom.badRequest('A template id must be provided.', true);
+ const template = await workflowTemplateService.mustFindVersion({ id: templateId, v: templateVer });
+ const selectedSteps = _.map(template.selectedSteps, step => ({
+ stepTemplateId: step.stepTemplateId,
+ stepTemplateVer: step.stepTemplateVer,
+ id: step.id,
+ title: step.title,
+ desc: step.desc,
+ skippable: step.skippable,
+ configs: _.cloneDeep(step.defaults || {}),
+ propsOverrideOption: _.cloneDeep(step.propsOverrideOption || {}),
+ configOverrideOption: _.cloneDeep(step.configOverrideOption || {}),
+ }));
+
+ draft.templateId = template.id;
+ draft.templateVer = template.v;
+ draft.workflow = {
+ id: workflowId,
+ title: template.title,
+ desc: template.desc,
+ v: 1,
+ rev: 0,
+ runSpec: _.cloneDeep(template.runSpec),
+ hidden: false, // template.hidden, TODO - figure out a way to handle this
+ builtin: false, // template.builtin, TODO - figure out a way to handle this
+ instanceTtl: _.isNumber(template.instanceTtl) ? template.instanceTtl : null,
+ createdBy: by,
+ updatedBy: by,
+ updatedAt: now,
+ createdAt: now,
+ selectedSteps,
+ stepsOrderChanged: false,
+ workflowTemplateId: template.id,
+ workflowTemplateVer: template.v,
+ };
+
+ draft.workflow = await workflowService.prepareWorkflow(draft.workflow);
+ } else {
+ // if it is not a new workflow, then we do not use the template id
+ if (!_.isEmpty(templateId))
+ throw this.boom.badRequest('You can not change the template id of an existing workflow', true);
+ const workflow = await workflowService.mustFindVersion({ id: workflowId, v: workflowVer });
+ draft.workflow = workflow;
+ // TODO: we need to check if the draft is using a different version of the template where the existing workflow is not.
+ // When that is the case, we need to correctly deal with config overrides
+ draft.templateId = workflow.workflowTemplateId;
+ draft.templateVer = workflow.workflowTemplateVer;
+ }
+
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ const result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_not_exists(id)') // yes we need this
+ .key('id', draft.id)
+ .item({ ...draft, rev: 0, createdBy: by, updatedBy: by })
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(
+ `A draft for the same workflow "${workflowId}" already exists, you can not create two drafts for the same workflow`,
+ true,
+ );
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-workflow-draft', body: result });
+
+ return result;
+ }
+
+ async updateDraft(requestContext, draft = {}) {
+ const [jsonSchemaValidationService, workflowService] = await this.service([
+ 'jsonSchemaValidationService',
+ 'workflowService',
+ ]);
+
+ // await ensureAdmin(requestContext);
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const username = encodePrincipalIdentifier(by);
+
+ const workflow = draft.workflow;
+ // Validate the workflow
+ await jsonSchemaValidationService.ensureValid(
+ _.omit(workflow, ['rev', 'updatedBy', 'updatedAt', 'createdBy', 'createdAt']),
+ inputSchema,
+ );
+
+ const originalDraft = await this.mustFindDraft({ id: draft.id });
+
+ // Check if the owner of this draft is the same entity that is trying to update the draft
+ if (originalDraft.username !== username)
+ throw this.boom.forbidden('You are not authorized to perform this operation', true);
+
+ const originalWorkflow = originalDraft.workflow;
+ if (workflow.id !== originalWorkflow.id || workflow.v !== originalWorkflow.v) {
+ throw this.boom.badRequest('You can not change the workflow id of an existing draft', true);
+ }
+
+ const mergedWorkflow = { ...originalWorkflow, ...workflow };
+ if (_.isEmpty(_.trim(mergedWorkflow.desc))) delete mergedWorkflow.desc;
+
+ // Prepare the workflow
+ const preparedWorkflow = await workflowService.prepareWorkflow(mergedWorkflow);
+
+ const mergedDraft = _.omit({ ...originalDraft, ...draft }, ['updatedAt', 'updatedBy']);
+ mergedDraft.workflow = preparedWorkflow;
+
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ const result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_exists(id)') // yes we need this
+ .key('id', mergedDraft.id)
+ .rev(mergedDraft.rev)
+ .item({ ...mergedDraft, updatedBy: by })
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(
+ 'A change was made to the draft just before your update, your update is now out of sync, please try again',
+ true,
+ );
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'update-workflow-draft', body: result });
+
+ return result;
+ }
+
+ async publishDraft(requestContext, draft = {}) {
+ const [workflowService] = await this.service(['workflowService']);
+ const publishResult = {};
+
+ // First we simply update the draft, this ensures that certain constraints are checked
+ const updatedDraft = await this.updateDraft(requestContext, draft);
+
+ // TODO - loop through each step and ensure that the 'configs' are validated against the step inputManifest
+
+ const workflow = updatedDraft.workflow;
+ // we also need to remove certain fields
+ delete workflow.rev;
+ delete workflow.updatedAt;
+ delete workflow.updatedBy;
+ delete workflow.createdAt;
+ delete workflow.createdBy;
+ delete workflow.stepsOrderChanged;
+ _.forEach(workflow.selectedSteps, step => {
+ delete step.propsOverrideOption;
+ delete step.configOverrideOption;
+ });
+
+ // We need to determine the version we want to create for the workflow. The solution below
+ // is not perfect as it has a slight chance of failing if someone managed to start publishing another draft for the same workflow
+ // at the same time.
+
+ // We have two cases:
+ // - The draft is trying to publish a workflow that never existed
+ // - The draft is trying to publish a workflow that exists
+
+ const existing = await workflowService.findVersion({ id: workflow.id });
+ let newVersion = 1;
+
+ if (existing) {
+ newVersion = existing.v + 1;
+ }
+
+ workflow.v = newVersion;
+ const publishedWorkflow = await workflowService.createVersion(requestContext, workflow);
+
+ publishResult.workflow = publishedWorkflow;
+ publishResult.hasErrors = false;
+
+ await this.deleteDraft(requestContext, { id: draft.id });
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'publish-workflow-draft', body: publishResult });
+
+ return publishResult;
+ }
+
+ async deleteDraft(requestContext, { id }) {
+ // await ensureAdmin(requestContext);
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const username = encodePrincipalIdentifier(by);
+ const originalDraft = await this.mustFindDraft({ id });
+
+ // Check if the owner of this draft is the same entity that is trying to delete the draft
+ if (originalDraft.username !== username)
+ throw this.boom.forbidden('You are not authorized to perform this operation', true);
+
+ // Lets now remove the draft from the database
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+ await dbService.helper
+ .deleter()
+ .table(table)
+ .condition('attribute_exists(id)') // yes we need this
+ .key('id', id)
+ .delete();
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'delete-workflow-draft', body: { id } });
+ }
+
+ // List all drafts for a username
+ async list({ principalIdentifier, fields = [] } = {}) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+ const username = encodePrincipalIdentifier(principalIdentifier);
+
+ // The query route
+ const result = await dbService.helper
+ .query()
+ .table(table)
+ .index(usernameIndexName)
+ .key('username', username)
+ .limit(2000)
+ .projection(fields)
+ .query();
+
+ return result;
+ }
+
+ async findDraft({ id, fields = [] }) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ const result = await dbService.helper
+ .getter()
+ .table(table)
+ .key('id', id)
+ .projection(fields)
+ .get();
+
+ return result;
+ }
+
+ async mustFindDraft({ id, fields }) {
+ const draft = await this.findDraft({ id, fields });
+ if (!draft) throw this.boom.notFound(`The workflow draft "${id}" is not found`, true);
+ return draft;
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+function encodePrincipalIdentifier(principalIdentifier = {}) {
+ const { username = 'unknown', ns = 'unknown' } = principalIdentifier;
+ return `ns=${ns},us=${username}`;
+}
+
+module.exports = WorkflowDraftService;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-instance-service.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-instance-service.js
new file mode 100644
index 0000000000..8064f93e7c
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-instance-service.js
@@ -0,0 +1,388 @@
+/*
+ * 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 */
+
+const _ = require('lodash');
+const shortid = require('shortid');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { ensureAdmin } = require('@aws-ee/base-services/lib/authorization/assertions');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const inputSchema = require('../schema/create-workflow-instance');
+const changeWorkflowStatusSchema = require('../schema/change-workflow-status');
+const changeStepStatusSchema = require('../schema/change-step-status');
+const saveStepAttributesSchema = require('../schema/save-step-attributes');
+
+const settingKeys = {
+ tableName: 'dbTableWorkflowInstances',
+};
+
+const workflowIndexName = 'WorkflowIndex';
+const workflowStatusIndexName = 'InstanceStatusCreatedIndex';
+
+class WorkflowInstanceService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'workflowService', 'dbService', 'auditWriterService']);
+ }
+
+ async init() {
+ await super.init();
+ this.tableName = this.settings.get(settingKeys.tableName);
+ }
+
+ async createInstance(requestContext, meta, input) {
+ const [jsonSchemaValidationService, workflowService] = await this.service([
+ 'jsonSchemaValidationService',
+ 'workflowService',
+ ]);
+
+ // Validate input
+ await jsonSchemaValidationService.ensureValid(meta, inputSchema);
+
+ const { workflowId, workflowVer, runSpec, status, assignmentId } = meta;
+ const workflow = await workflowService.mustFindVersion({ id: workflowId, v: workflowVer });
+ const [dbService] = await this.service(['dbService']);
+ const table = this.tableName;
+
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+
+ const instance = prepareNewInstance(workflow, { runSpec, status, assignmentId, input });
+
+ const result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_not_exists(id)') // yes we need this because we are using updater
+ .key({ id: instance.id })
+ .item({ ...instance, createdBy: by, updatedBy: by })
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest('Workflow instance already exist', true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-workflow-instance', body: result });
+
+ return result;
+ }
+
+ async changeWorkflowStatus(input) {
+ const [jsonSchemaValidationService] = await this.service(['jsonSchemaValidationService']);
+ // Validate input
+ await jsonSchemaValidationService.ensureValid(input, changeWorkflowStatusSchema);
+ const { instanceId, status, clearMessage = false, message } = input;
+
+ const [dbService] = await this.service(['dbService']);
+ const table = this.tableName;
+
+ const result = await runAndCatch(
+ async () => {
+ const item = { wfStatus: status };
+ let op = dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id: instanceId });
+
+ if (clearMessage) op = op.remove('msg');
+ else if (!_.isUndefined(message)) item.msg = message;
+
+ return op.item(item).update();
+ },
+ async () => {
+ throw this.boom.badRequest(`Workflow instance "${instanceId}" does not exist`, true);
+ },
+ );
+
+ return result;
+ }
+
+ /**
+ * A method to save additional step attributes in form of key/value pairs to the specified step in the
+ * specified workflow execution instance.
+ *
+ * @param input
+ * @returns {Promise}
+ */
+ async saveStepAttribs(requestContext, input) {
+ // TODO: Workflow Permissions Management is not implemented yet
+ // For now, only admins are allowed to save additional attributes against steps in a running workflow
+ // Once Workflow Permissions Management is implemented, modify this to honor those permissions.
+ //
+ // Since manual pause and play of a workflow uses this feature of saving a flag against a step to mark it
+ // paused/resumed this also means that only admins can resume a workflow manually as of now
+ await ensureAdmin(requestContext);
+
+ const [jsonSchemaValidationService, workflowService] = await this.service([
+ 'jsonSchemaValidationService',
+ 'workflowService',
+ ]);
+
+ await jsonSchemaValidationService.ensureValid(input, saveStepAttributesSchema);
+ const { instanceId, stepIndex, attribs } = input;
+ if (stepIndex < 0) {
+ throw this.boom.badRequest(
+ 'Invalid stepIndex specified. It must be non-zero index corresponding to ' +
+ 'the step in the workflow for which you want to save attributes',
+ true,
+ );
+ }
+
+ // update state in DynamoDB
+ const [dbService] = await this.service(['dbService']);
+ const table = this.tableName;
+
+ const workflowInstance = await this.findInstance({ id: instanceId });
+ if (!workflowInstance) {
+ throw this.boom.badRequest(`Workflow instance "${instanceId}" does not exist`, true);
+ }
+ const { wfId, wfVer, stAttribs: existingStepAttribs } = workflowInstance;
+ const workflow = await workflowService.mustFindVersion({ id: wfId, v: wfVer });
+ if (stepIndex > workflow.selectedSteps.length) {
+ throw this.boom.badRequest(
+ 'Invalid stepIndex specified. It must be non-zero index corresponding to ' +
+ 'the step in the workflow for which you want to save attributes. ' +
+ 'There is no step in the workflow at the specified step index',
+ true,
+ );
+ }
+ const stepAttribsToSet = existingStepAttribs || [];
+
+ // The "stepAttribsToSet" array contains additional step attributes for each step
+ if (stepAttribsToSet.length - 1 < stepIndex) {
+ // This is the first time some additional step attributes are being set for this step
+ // The array may not have been expanded yet to accommodate attribs for this step yet
+ // Fill array with empty objects as step attributes up to the step for which we are saving additional
+ // step attributes. This approach allows for lazily fitting the step attributes into the stAttribs array
+ // instead of populating them at item creation time in db
+ for (let i = 0; i < stepIndex; i += 1) {
+ if (_.isNil(stepAttribsToSet[i])) {
+ // There are no additional attributes stored against the step at index = i so initialize it with empty object
+ stepAttribsToSet[i] = {};
+ }
+ }
+ }
+ stepAttribsToSet[stepIndex] = attribs;
+
+ const result = await runAndCatch(
+ async () => {
+ let op = dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id: instanceId });
+
+ if (!_.isUndefined(attribs)) {
+ op = op
+ .set(`#stAttribs = :stAttribs`)
+ .names({ '#stAttribs': 'stAttribs' })
+ .values({ ':stAttribs': stepAttribsToSet });
+ }
+ return op.update();
+ },
+ async () => {
+ throw this.boom.badRequest(`Workflow instance "${instanceId}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'save-workflow-instance-step-attributes', body: result });
+
+ return result;
+ }
+
+ async changeStepStatus(input) {
+ const [jsonSchemaValidationService] = await this.service(['jsonSchemaValidationService']);
+
+ await jsonSchemaValidationService.ensureValid(input, changeStepStatusSchema);
+ const { instanceId, stepIndex, status, clearMessage = false, message, startTime, endTime } = input;
+
+ // update state in DynamoDB
+ const [dbService] = await this.service(['dbService']);
+ const table = this.tableName;
+
+ const result = await runAndCatch(
+ async () => {
+ let op = dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_exists(id)') // yes we need this
+ .key({ id: instanceId });
+
+ if (clearMessage) {
+ op = op.remove(`stStatuses[${stepIndex}].msg`);
+ } else if (!_.isUndefined(message)) {
+ op = op
+ .set(`#stStatuses[${stepIndex}].msg = :stepMsg`)
+ .names({ '#stStatuses': 'stStatuses' })
+ .values({ ':stepMsg': message });
+ }
+
+ if (!_.isUndefined(status)) {
+ op = op
+ .set(`#stStatuses[${stepIndex}].#status = :stepStatus`)
+ .names({ '#stStatuses': 'stStatuses', '#status': 'status' })
+ .values({ ':stepStatus': status });
+ }
+
+ if (!_.isUndefined(startTime)) {
+ op = op
+ .set(`#stStatuses[${stepIndex}].#startTime = :stepStartTime`)
+ .names({ '#stStatuses': 'stStatuses', '#startTime': 'startTime' })
+ .values({ ':stepStartTime': startTime });
+ }
+
+ if (!_.isUndefined(endTime)) {
+ op = op
+ .set(`#stStatuses[${stepIndex}].#endTime = :stepEndTime`)
+ .names({ '#stStatuses': 'stStatuses', '#endTime': 'endTime' })
+ .values({ ':stepEndTime': endTime });
+ }
+
+ return op.update();
+ },
+ async () => {
+ throw this.boom.badRequest(`Workflow instance "${instanceId}" does not exist`, true);
+ },
+ );
+
+ return result;
+ }
+
+ // List the all workflow instances by status for a specified time period
+ // startTime and endTime must be in ISO format
+ async listByStatus({ startTime, endTime, status, fields = [] } = {}) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ const result = await dbService.helper
+ .query()
+ .table(table)
+ .index(workflowStatusIndexName)
+ .key('wfStatus', status)
+ .sortKey('createdAt')
+ .between(startTime, endTime)
+ .projection(fields)
+ .query();
+
+ return result;
+ }
+
+ // List the first 1000 instances sorted by createdAt (up to a year ago)
+ async list({ workflowId, workflowVer, fields = [] } = {}) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+ const encodedId = encode(workflowId, workflowVer);
+ const past = new Date(Date.now() - 12 * 30 * 24 * 60 * 60 * 1000).toISOString(); // this is not accurate, just approximation for 12 months ago
+
+ const result = await dbService.helper
+ .query()
+ .table(table)
+ .index(workflowIndexName)
+ .key('wf', encodedId)
+ .sortKey('createdAt')
+ .gt(past)
+ .forward(false)
+ .limit(1000)
+ .projection(fields)
+ .query();
+
+ return result;
+ }
+
+ async findInstance({ id, fields = [] }) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ const result = await dbService.helper
+ .getter()
+ .table(table)
+ .key({ id })
+ .projection(fields)
+ .get();
+
+ return result;
+ }
+
+ async mustFindInstance({ id, fields }) {
+ const instance = await this.findInstance({ id, fields });
+ if (!instance) throw this.boom.notFound(`The workflow instance "${id}" is not found`, true);
+ return instance;
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+// This captures the logic of populating the workflow instance object given a workflow object.
+// Note: the provided workflow object will be mutated in process of creating the workflow instance.
+function prepareNewInstance(workflow, { runSpec = {}, status = 'not_started', assignmentId, input } = {}) {
+ const id = shortid.generate();
+ const wfId = workflow.id;
+ const wfVer = workflow.v;
+ const wf = encode(wfId, wfVer);
+ const stStatuses = [];
+
+ _.forEach(workflow.selectedSteps, step => {
+ delete step.desc;
+ delete step.propsOverrideOption;
+ delete step.configOverrideOption;
+ stStatuses.push({
+ status: 'not_started',
+ });
+ });
+
+ // We delete a few props from the workflow object to save space and bandwidth
+ delete workflow.desc;
+ delete workflow.createdBy;
+ delete workflow.createdAt;
+ delete workflow.updatedBy;
+ delete workflow.updatedAt;
+ delete workflow.rev;
+
+ const instance = {
+ id,
+ workflow,
+ wfId,
+ wfVer,
+ wf,
+ wfStatus: status,
+ stStatuses,
+ runSpec: { ...workflow.runSpec, ...runSpec },
+ assignmentId,
+ input,
+ };
+
+ if (workflow.instanceTtl > 0) instance.ttl = workflow.instanceTtl * 24 * 60 * 60 + Math.floor(Date.now() / 1000);
+ return instance;
+}
+
+function encode(id, v) {
+ return `${id}_${v}`;
+}
+
+module.exports = WorkflowInstanceService;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-registry-service.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-registry-service.js
new file mode 100644
index 0000000000..6ada73f58c
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-registry-service.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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const inputSchema = require('../schema/workflow');
+
+class WorkflowRegistryService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'pluginRegistryService']);
+ }
+
+ async init() {
+ await super.init();
+ this.store = []; // an array of objects of this shape: { key: , value: { yaml } }
+ const registry = await this.service('pluginRegistryService');
+ // We loop through each plugin and ask it to register its workflows
+ const plugins = await registry.getPlugins('workflows');
+ // eslint-disable-next-line no-restricted-syntax
+ for (const plugin of plugins) {
+ // eslint-disable-next-line no-await-in-loop
+ await plugin.registerWorkflows(this);
+ }
+ }
+
+ async add({ yaml }) {
+ const [jsonSchemaValidationService] = await this.service(['jsonSchemaValidationService']);
+ const { id, v } = yaml;
+ const existing = await this.findWorkflow({ id, v });
+
+ if (existing)
+ throw this.boom.badRequest(
+ `You tried to register a workflow, but a workflow with the same id "${id}" and version "${v}" already exists`,
+ true,
+ );
+ await jsonSchemaValidationService.ensureValid(yaml, inputSchema);
+
+ const key = this.encodeId({ id, v });
+ this.store.push({ key, value: { yaml } });
+ }
+
+ async findWorkflow({ id, v }) {
+ const key = this.encodeId({ id, v });
+ const entry = _.find(this.store, ['key', key]);
+ return entry ? entry.value : undefined;
+ }
+
+ // Returns a list of all workflow in array of this shape: [{ id, v, yaml }, ...]
+ async listWorkflows() {
+ return _.map(this.store, item => {
+ const { yaml } = item.value;
+ const { id, v } = yaml;
+ return { id, v, yaml };
+ });
+ }
+
+ // private
+ encodeId({ id, v }) {
+ return `${id}_${v}`;
+ }
+}
+
+module.exports = WorkflowRegistryService;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-service.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-service.js
new file mode 100644
index 0000000000..0dada0816a
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-service.js
@@ -0,0 +1,532 @@
+/*
+ * 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 */
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { ensureAdmin } = require('@aws-ee/base-services/lib/authorization/assertions');
+const { toVersionString, parseVersionString, runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const PropsOverrideOption = require('./helpers/props-override-option');
+const ConfigOverrideOption = require('./helpers/config-override-option');
+const inputSchema = require('../schema/workflow');
+const {
+ workflowPropsSupportedOverrideKeys,
+ workflowPropsSupportedOverrideKeysTransformer,
+ stepPropsSupportedOverrideKeys,
+ stepPropsSupportedOverrideKeysTransformer,
+} = require('./helpers/supported-override');
+
+const settingKeys = {
+ tableName: 'dbTableWorkflows',
+};
+
+class WorkflowService extends Service {
+ constructor() {
+ super();
+ this.dependency([
+ 'jsonSchemaValidationService',
+ 'stepTemplateService',
+ 'workflowTemplateService',
+ 'dbService',
+ 'auditWriterService',
+ ]);
+ }
+
+ async init() {
+ await super.init();
+ this.tableName = this.settings.get(settingKeys.tableName);
+ this.internals = {
+ findSteps: findSteps.bind(this),
+ applyOverrideConstraints: applyOverrideConstraints.bind(this),
+ };
+ }
+
+ async createVersion(requestContext, manifest = {}, { isLatest = true, tableName } = {}) {
+ const [jsonSchemaValidationService] = await this.service(['jsonSchemaValidationService']);
+
+ await ensureAdmin(requestContext);
+ // Validate input
+ await jsonSchemaValidationService.ensureValid(manifest, inputSchema);
+
+ const [dbService] = await this.service(['dbService']);
+ const table = tableName || this.tableName;
+ const { id, v } = manifest;
+ const logPrefix = `The workflow "${id}" with ver "${v}" and rev "0"`;
+ const preparedWorkflow = await this.prepareWorkflow(_.cloneDeep(manifest));
+
+ const dbObject = toDbObject(preparedWorkflow);
+
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+
+ // TODO: we need to wrap the creation of the version and the update of the latest record in a transaction
+ const result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_not_exists(ver)') // yes we need this
+ .key({ id, ver: toVersionString(v) })
+ .item({ ...dbObject, rev: 0, createdBy: by, updatedBy: by })
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`${logPrefix} already exist`, true);
+ },
+ );
+
+ if (isLatest) {
+ await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .updatedAt(result.updatedAt)
+ .disableCreatedAt()
+ .key({ id, ver: toVersionString(0) })
+ .condition('(attribute_exists(id) and #latest <= :latest) or attribute_not_exists(id)')
+ .item({ ...result, latest: v })
+ .names({ '#latest': 'latest' })
+ .values({ ':latest': v })
+ .update();
+ },
+ async () => {
+ // we ignore the ConditionalCheckFailedException exception because it simply means that the created version is not the
+ // latest version anymore and there is no need to inform the caller of this fact
+ },
+ );
+ }
+ const dataObjectResult = toDataObject(result);
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-workflow-version', body: dataObjectResult });
+
+ return dataObjectResult;
+ }
+
+ // Use this method if you have a workflow object and you want to enrich it with the necessary default values
+ // (such as step title, desc) also this method enforces any constraints specified in the workflow templates, such as
+ // if the workflow can override the title of the workflow, or the configuration value of a step. This method mutates the provided
+ // workflow object.
+ async prepareWorkflow(workflow) {
+ const { workflowTemplateId, workflowTemplateVer } = workflow;
+ const [workflowTemplateService] = await this.service(['workflowTemplateService']);
+
+ const workflowTemplate = await workflowTemplateService.mustFindVersion({
+ id: workflowTemplateId,
+ v: workflowTemplateVer,
+ });
+ const stepsOrderChanged = didStepsOrderChange(workflow, workflowTemplate);
+ const stepsMap = await this.internals.findSteps(workflow, workflowTemplate);
+
+ workflow.stepsOrderChanged = stepsOrderChanged;
+
+ workflow = applyDefaults(workflow, workflowTemplate, stepsMap);
+
+ this.internals.applyOverrideConstraints(workflow, workflowTemplate, stepsMap, stepsOrderChanged);
+
+ return workflow;
+ }
+
+ // NOTE: if a workflow is to be updated, a draft should be created and published, don't use this method to accomplish this.
+ // This method is here to help with scenarios where (internally) we want to update an existing version without creating a new one.
+ async updateVersion(requestContext, manifest = {}, { isLatest = true, tableName } = {}) {
+ const [jsonSchemaValidationService] = await this.service(['jsonSchemaValidationService']);
+
+ // we need to remove 'rev' here because the schema does not allow it, we should have a schema
+ // that allows 'rev' but for now, we don't do that.
+ await jsonSchemaValidationService.ensureValid(_.omit(manifest, ['rev']), inputSchema);
+ // now we need to check that rev is supplied
+ if (_.isNil(manifest.rev))
+ throw this.boom.badRequest('The supplied workflow does not have the "rev" property', true);
+
+ const [dbService] = await this.service(['dbService']);
+ const table = tableName || this.tableName;
+ const { id, v, rev } = manifest;
+ const logPrefix = `The workflow "${id}" with ver "${v}" and rev "${rev}"`;
+
+ // TODO: Validate configuration
+
+ const preparedWorkflow = await this.prepareWorkflow(_.cloneDeep(manifest));
+ const dbObject = toDbObject(preparedWorkflow);
+
+ // For now, we assume that updatedBy' is always a user and not a group
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+
+ // TODO: we need to wrap the creation of the version and the update of the latest record in a transaction
+ const result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_exists(ver)') // yes we need this
+ .key({ id, ver: toVersionString(v) })
+ .rev(rev)
+ .item({ ...dbObject, updatedBy: by })
+ .update();
+ },
+ async () => {
+ // There are two scenarios here:
+ // 1 - The "v" entry does not exist
+ // 2 - The "rev" does not match
+ const existing = await this.findVersion({ id, v, fields: ['id', 'v', 'updatedBy'] });
+ if (existing) {
+ throw this.boom.badRequest(
+ `${logPrefix} information changed by "${existing.updatedBy}" just before your request is processed, please try again`,
+ true,
+ );
+ }
+ throw this.boom.badRequest(`${logPrefix} does not exist`, true);
+ },
+ );
+
+ if (isLatest) {
+ await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .updatedAt(result.updatedAt)
+ .key({ id, ver: toVersionString(0) })
+ .condition('#latest = :latest')
+ .item(result)
+ .names({ '#latest': 'latest' })
+ .values({ ':latest': v })
+ .update();
+ },
+ async () => {
+ // we ignore the ConditionalCheckFailedException exception because it simply means that the updated version is not the
+ // latest version anymore and there is no need to inform the caller of this fact
+ },
+ );
+ }
+ return toDataObject(result);
+ }
+
+ // List all versions for all workflows or for a specific workflow if the workflow id was provided
+ async listVersions({ id, fields = [] } = {}) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ if (_.isNil(id)) {
+ // The scanner route
+ const result = await dbService.helper
+ .scanner()
+ .table(table)
+ .filter('attribute_not_exists(latest)')
+ .limit(2000)
+ .projection(fields)
+ .scan();
+ return _.map(result, item => toDataObject(item));
+ }
+
+ const result = await dbService.helper
+ .query()
+ .table(table)
+ .key('id', id)
+ .forward(false)
+ .filter('attribute_not_exists(latest)')
+ .limit(2000)
+ .projection(fields)
+ .query();
+ return _.map(result, item => toDataObject(item));
+ }
+
+ // List latest versions of all the workflows
+ async list({ fields = [] } = {}) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ // The scanner route
+ const result = await dbService.helper
+ .scanner()
+ .table(table)
+ .filter('attribute_exists(latest)')
+ .limit(2000)
+ .projection(fields)
+ .scan();
+ return _.map(result, item => toDataObject(item));
+ }
+
+ async findVersion({ id, v = 0, fields = [] }, { tableName } = {}) {
+ const dbService = await this.service('dbService');
+ // This function can accept a different tableName to use for the lookup, this is useful in places
+ // such as post deployment
+ const table = tableName || this.tableName;
+
+ const result = await dbService.helper
+ .getter()
+ .table(table)
+ .key({ id, ver: toVersionString(v) })
+ .projection(fields)
+ .get();
+
+ return toDataObject(result);
+ }
+
+ async mustFindVersion({ id, v = 0, fields }) {
+ const workflow = await this.findVersion({ id, v, fields });
+ if (!workflow) throw this.boom.notFound(`The workflow "${id}" ver "${v}" is not found`, true);
+ return workflow;
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+// Lookup all the step templates that are referenced in the manifest, they might be
+// in the workflowTemplate or they might not be (if the workflow decided to change the order and include
+// new steps or remove steps).
+// The output shape is a map:
+// { '': { templateSelectedStep, selectedStep, stepTemplate }, ... }
+// You get templateSelectedStep in the map if the step id belongs to a selected step from the workflow template,
+// otherwise you get just the selectedStep (when the workflow uses a step that is not part of the workflow template)
+// and the stepTemplate.
+async function findSteps(manifest, workflowTemplate) {
+ const [stepTemplateService] = await this.service(['stepTemplateService']);
+ const templateMap = {};
+ const map = {};
+
+ _.forEach(workflowTemplate.selectedSteps, step => {
+ templateMap[step.id] = { templateSelectedStep: step, stepTemplate: step.stepTemplate };
+ });
+
+ /* eslint-disable no-restricted-syntax */
+ for (const step of manifest.selectedSteps) {
+ const { stepTemplateId, stepTemplateVer } = step;
+ const id = step.id;
+ const entry = templateMap[id];
+
+ if (entry) {
+ map[id] = { selectedStep: step, ...entry };
+ } else {
+ const stepTemplate = await stepTemplateService.mustFindVersion({ id: stepTemplateId, v: stepTemplateVer });
+ map[id] = { selectedStep: step, stepTemplate };
+ }
+ }
+ /* eslint-enable no-restricted-syntax */
+
+ return map;
+}
+
+function applyDefaults(manifest, workflowTemplate, stepsMap) {
+ const ifNil = (value, defaultValue) => {
+ return _.isNil(value) ? defaultValue : value;
+ };
+
+ // First, we apply title, desc, builtin, hidden, runSpec from the workflow template
+ const result = {
+ ...createObj(workflowTemplate, ['title', 'desc', 'instanceTtl', 'builtin', 'hidden', 'runSpec']),
+ ...manifest,
+ };
+
+ // If instanceTtl is not provided then use the workflowTemplate, remember that -1 means indefinite
+ result.instanceTtl = ifNil(manifest.instanceTtl, workflowTemplate.instanceTtl);
+
+ result.selectedSteps = [];
+ _.forEach(manifest.selectedSteps, (step, index) => {
+ const mapEntry = stepsMap[step.id];
+ const { templateSelectedStep = {}, selectedStep, stepTemplate } = mapEntry;
+
+ // We start with title, desc, skippable and src
+ const stepResult = {
+ ...createObj(stepTemplate, ['title', 'desc', 'skippable', 'src']),
+ ...createObj(templateSelectedStep, ['title', 'desc', 'skippable']), // if templateSelectedStep exists
+ ...createObj(selectedStep, ['title', 'desc', 'skippable']),
+ ..._.omit(selectedStep, ['title', 'desc', 'skippable', 'src']),
+ };
+
+ // We now deal with the config and the defaults
+ const configs = { ...(templateSelectedStep.defaults || {}), ...selectedStep.configs };
+ stepResult.configs = removeEmptyStrings(configs);
+
+ // We attach the workflow template step override options to the workflow step for easy access
+ if (_.isEmpty(templateSelectedStep)) {
+ // Since we don't have a template selected step, we allow all possible overrides
+ stepResult.propsOverrideOption = { allowed: ['title', 'desc', 'skippable'] };
+ stepResult.configOverrideOption = {
+ allowed: _.flatten(
+ _.map(_.get(stepTemplate, 'inputManifest.sections', []), section => findConfigNames(section)),
+ ),
+ };
+ } else {
+ stepResult.configOverrideOption = templateSelectedStep.configOverrideOption;
+ stepResult.propsOverrideOption = templateSelectedStep.propsOverrideOption;
+ }
+
+ // Assign the result back to the selected step
+ result.selectedSteps[index] = stepResult;
+ });
+
+ return result;
+}
+
+function applyOverrideConstraints(manifest, workflowTemplate, stepsMap, stepsOrderChanged) {
+ // First we check the workflowTemplate props override constraints, this includes:
+ // title, desc, instanceTtl, runSpec
+ const workflowPropsOverrideOption = new PropsOverrideOption(
+ workflowTemplate.propsOverrideOption,
+ workflowPropsSupportedOverrideKeys,
+ workflowPropsSupportedOverrideKeysTransformer,
+ );
+ const workflowPropsViolation = workflowPropsOverrideOption.violatedProps(manifest, workflowTemplate);
+ const errors = [];
+
+ if (workflowPropsViolation.length > 0) {
+ errors.push(`The workflow can not override the following properties [${workflowPropsViolation}]`);
+ }
+
+ // Now, we loop through each step and collect all the violations
+ _.forEach(manifest.selectedSteps, step => {
+ const { stepTemplateId, stepTemplateVer } = step;
+ const mapEntry = stepsMap[step.id];
+ const { templateSelectedStep = {}, stepTemplate } = mapEntry;
+ if (_.isEmpty(templateSelectedStep)) return;
+
+ const srcStep = {
+ ...createObj(stepTemplate, ['title', 'desc', 'skippable', 'src']),
+ ...createObj(templateSelectedStep, ['title', 'desc', 'skippable']),
+ };
+
+ let overrideOption = new PropsOverrideOption(
+ templateSelectedStep.propsOverrideOption,
+ stepPropsSupportedOverrideKeys,
+ stepPropsSupportedOverrideKeysTransformer,
+ );
+ const propsViolation = overrideOption.violatedProps(step, srcStep);
+ if (propsViolation.length > 0) {
+ errors.push(
+ `The step "${stepTemplateId}" v${stepTemplateVer} can not override the following properties [${propsViolation}]`,
+ );
+ }
+
+ overrideOption = new ConfigOverrideOption(templateSelectedStep.configOverrideOption);
+ const configsViolation = overrideOption.violatedConfigs(step.configs, templateSelectedStep.defaults);
+ if (configsViolation.length > 0) {
+ errors.push(
+ `The step "${stepTemplateId}" v${stepTemplateVer} can not override the following configuration keys [${configsViolation}]`,
+ );
+ }
+ });
+
+ if (stepsOrderChanged && !workflowPropsOverrideOption.allowStepsOrderChange) {
+ errors.push('The workflow can not change the order of the steps');
+ }
+
+ if (errors.length > 0) {
+ throw this.boom.badRequest(`${errors.join('. ')}`, true);
+ }
+}
+
+// Did the order of the steps changed, were there additional steps or steps that are removed, or steps that are reordered
+function didStepsOrderChange(manifest, workflowTemplate = {}) {
+ const steps = manifest.selectedSteps || [];
+ const stepsSize = steps.length;
+ const templateSteps = workflowTemplate.selectedSteps || [];
+ const templateStepsSize = templateSteps.length;
+
+ if (stepsSize !== templateStepsSize) return true;
+ let changed = false;
+
+ _.forEach(steps, (step, index) => {
+ const templateStep = templateSteps[index];
+ if (templateStep.stepTemplateId !== step.stepTemplateId || templateStep.stepTemplateVer !== step.stepTemplateVer) {
+ changed = true;
+ return false;
+ }
+ if (templateStep.id !== step.id) {
+ changed = true;
+ return false;
+ }
+ return undefined;
+ });
+
+ return changed;
+}
+
+// Creates an object using the provided obj but only if the provided props are not nil
+function createObj(obj, props) {
+ const result = {};
+ _.forEach(props, prop => {
+ if (!_.isNil(obj[prop])) result[prop] = obj[prop];
+ });
+
+ return result;
+}
+
+// Do some properties renaming to prepare the object to be saved in the database
+function toDbObject(dataObject) {
+ const result = { ...dataObject };
+
+ delete result.ver;
+ delete result.createdAt;
+ delete result.createdBy;
+ delete result.updatedAt;
+ delete result.updatedBy;
+ delete result.rev;
+
+ return result;
+}
+
+// Do some properties renaming to restore the object that was saved in the database
+function toDataObject(dbObject) {
+ if (_.isNil(dbObject)) return dbObject;
+ if (!_.isObject(dbObject)) return dbObject;
+
+ const result = { ...dbObject };
+ result.v = result.latest ? result.latest : parseVersionString(dbObject.ver);
+
+ delete result.ver;
+ delete result.latest;
+
+ return result;
+}
+
+function findConfigNames(entry) {
+ if (entry === undefined) return [];
+
+ const out = [];
+ const { name, children = [] } = entry;
+ if (!entry.nonInteractive) {
+ out.push(name);
+ children.forEach(child => {
+ out.push(...findConfigNames(child));
+ });
+ }
+
+ return out;
+}
+
+// Go through the object own props and if they are empty strings, remove the props
+function removeEmptyStrings(srcObject) {
+ const result = {};
+
+ Object.keys(srcObject).forEach(key => {
+ const value = srcObject[key];
+ if (_.isString(value) && _.isEmpty(value)) return;
+ result[key] = value;
+ });
+
+ return result;
+}
+
+module.exports = WorkflowService;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-template-draft-service.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-template-draft-service.js
new file mode 100644
index 0000000000..c39248a472
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-template-draft-service.js
@@ -0,0 +1,327 @@
+/*
+ * 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 */
+const _ = require('lodash');
+const slugify = require('slugify');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { ensureAdmin } = require('@aws-ee/base-services/lib/authorization/assertions');
+const { runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const inputSchema = require('../schema/workflow-template');
+
+const settingKeys = {
+ tableName: 'dbTableWorkflowTemplateDrafts',
+};
+const usernameIndexName = 'UsernameIndex';
+
+function encodePrincipalIdentifier(principalIdentifier = {}) {
+ // principalIdentifier shape is { username, ns: user.ns }
+ const { username = 'unknown', ns = 'unknown' } = principalIdentifier;
+ return `ns=${ns},us=${username}`;
+}
+
+class WorkflowTemplateDraftService extends Service {
+ constructor() {
+ super();
+ this.dependency([
+ 'jsonSchemaValidationService',
+ 'workflowTemplateService',
+ 'stepTemplateService',
+ 'dbService',
+ 'auditWriterService',
+ ]);
+ }
+
+ async init() {
+ await super.init();
+ this.tableName = this.settings.get(settingKeys.tableName);
+ }
+
+ async createDraft(
+ requestContext,
+ { isNewTemplate = true, templateId: templateIdRaw, templateTitle, templateVer = 0 } = {},
+ ) {
+ const [workflowTemplateService] = await this.service(['workflowTemplateService']);
+
+ await ensureAdmin(requestContext);
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const username = encodePrincipalIdentifier(by);
+
+ const now = new Date().toISOString();
+ const templateId = slugify(_.kebabCase(_.startsWith(templateIdRaw, 'wt-') ? templateIdRaw : `wt-${templateIdRaw}`));
+ const draftId = `${username}_${templateId}_${templateVer}`;
+ const draft = {
+ id: draftId,
+ templateVer,
+ templateId,
+ username,
+ };
+
+ if (isNewTemplate) {
+ const existing = await workflowTemplateService.findVersion({ id: templateId });
+ if (existing) throw this.boom.badRequest('A workflow template with the same template id exists.', true);
+
+ draft.template = {
+ id: templateId,
+ title: templateTitle || 'Untitled',
+ v: 1,
+ rev: 0,
+ runSpec: {
+ target: 'stepFunctions',
+ size: 'small',
+ },
+ propsOverrideOption: {
+ allowed: [],
+ },
+ hidden: false,
+ builtin: false,
+ createdBy: by,
+ updatedBy: by,
+ updatedAt: now,
+ createdAt: now,
+ };
+ } else {
+ // if it is not a new template, then we do not use the title
+ if (!_.isEmpty(templateTitle))
+ throw this.boom.badRequest(
+ 'The title can not be changed at the time of create a draft of an existing template',
+ true,
+ );
+ draft.template = await workflowTemplateService.mustFindVersion({ id: templateId, v: templateVer });
+ }
+
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ const result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_not_exists(id)') // yes we need this
+ .key('id', draft.id)
+ .item({ ...draft, rev: 0, createdBy: by, updatedBy: by })
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(
+ `A draft for the same workflow template "${templateId}" already exists, you can not create two drafts for the same workflow template`,
+ true,
+ );
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-workflow-template-draft', body: result });
+
+ return result;
+ }
+
+ async updateDraft(requestContext, draft = {}) {
+ const [jsonSchemaValidationService, workflowTemplateService] = await this.service([
+ 'jsonSchemaValidationService',
+ 'workflowTemplateService',
+ ]);
+
+ await ensureAdmin(requestContext);
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const username = encodePrincipalIdentifier(by);
+
+ const template = draft.template;
+ const originalDraft = await this.mustFindDraft({ id: draft.id });
+
+ // Check if the owner of this draft is the same entity that is trying to update the draft
+ if (originalDraft.username !== username)
+ throw this.boom.forbidden('You are not authorized to perform this operation', true);
+
+ let originalTemplate = originalDraft.template;
+ if (template.id !== originalTemplate.id || template.v !== originalTemplate.v) {
+ originalTemplate = await workflowTemplateService.mustFindVersion({ id: template.id, v: template.v });
+ }
+
+ const mergedTemplate = { ...originalTemplate, ...template };
+ if (_.isEmpty(_.trim(mergedTemplate.desc))) delete mergedTemplate.desc;
+
+ // Validate the template manifest
+ await jsonSchemaValidationService.ensureValid(
+ _.omit(template, ['rev', 'updatedBy', 'updatedAt', 'createdBy', 'createdAt']),
+ inputSchema,
+ );
+
+ // Populate step template prop in the selected steps and the step ids if needed
+ await workflowTemplateService.populateSteps(mergedTemplate);
+
+ const mergedDraft = _.omit({ ...originalDraft, ...draft }, ['updatedAt', 'updatedBy']);
+ mergedDraft.template = mergedTemplate;
+ mergedDraft.templateId = mergedTemplate.id;
+ mergedDraft.templateVer = mergedTemplate.v;
+
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ const result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_exists(id)') // yes we need this
+ .key('id', mergedDraft.id)
+ .rev(mergedDraft.rev)
+ .item({ ...mergedDraft, updatedBy: by })
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(
+ 'A change was made to the draft just before your update, your update is now out of sync, please try again',
+ true,
+ );
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'update-workflow-template-draft', body: result });
+
+ return result;
+ }
+
+ async publishDraft(requestContext, draft = {}) {
+ const [workflowTemplateService] = await this.service(['workflowTemplateService']);
+ const publishResult = {};
+
+ // First we simply update the draft, this ensures that certain constraints are checked
+ const updatedDraft = await this.updateDraft(requestContext, draft);
+
+ // TODO: loop through each step and ensure that the 'defaults' are validated against the step adminInputManifest (or inputManifest, if adminInputManifest was not provided in the yaml file)
+
+ // We need to loop through all the steps and remove the step template, otherwise the createVersion won't work
+ const template = updatedDraft.template;
+ _.forEach(template.selectedSteps, step => {
+ delete step.stepTemplate;
+ delete step.isNew;
+ });
+
+ // we also need to remove certain fields
+ delete template.rev;
+ delete template.updatedAt;
+ delete template.updatedBy;
+ delete template.createdAt;
+ delete template.createdBy;
+
+ // Now we need to determine the version we want to create for the template. This is a bit tricky and the solution here
+ // is not perfect as it has a slight chance of failing if someone managed to start publishing another draft for the same template
+ // at the same time.
+ // Anyway, we have two cases:
+ // - The draft is trying to publish a template that never existed
+ // - The draft is trying to publish a template that exists
+
+ const existing = await workflowTemplateService.findVersion({ id: template.id });
+ let newVersion = 1;
+
+ if (existing) {
+ newVersion = existing.v + 1;
+ }
+
+ template.v = newVersion;
+ const publishedTemplate = await workflowTemplateService.createVersion(requestContext, template);
+
+ publishResult.template = publishedTemplate;
+ publishResult.hasErrors = false;
+
+ await this.deleteDraft(requestContext, { id: draft.id });
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'publish-workflow-template-draft', body: publishResult });
+
+ return publishResult;
+ }
+
+ async deleteDraft(requestContext, { id }) {
+ await ensureAdmin(requestContext);
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+ const username = encodePrincipalIdentifier(by);
+ const originalDraft = await this.mustFindDraft({ id });
+
+ // Check if the owner of this draft is the same entity that is trying to delete the draft
+ if (originalDraft.username !== username)
+ throw this.boom.forbidden('You are not authorized to perform this operation', true);
+
+ // Lets now remove the draft from the database
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+ await dbService.helper
+ .deleter()
+ .table(table)
+ .condition('attribute_exists(id)') // yes we need this
+ .key('id', id)
+ .delete();
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'delete-workflow-template-draft', body: { id } });
+ }
+
+ // List all drafts for a username
+ async list({ principalIdentifier, fields = [] } = {}) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+ const username = encodePrincipalIdentifier(principalIdentifier);
+
+ // The query route
+ const result = await dbService.helper
+ .query()
+ .table(table)
+ .index(usernameIndexName)
+ .key('username', username)
+ .limit(2000)
+ .projection(fields)
+ .query();
+
+ return result;
+ }
+
+ async findDraft({ id, fields = [] }) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ const result = await dbService.helper
+ .getter()
+ .table(table)
+ .key('id', id)
+ .projection(fields)
+ .get();
+
+ return result;
+ }
+
+ async mustFindDraft({ id, fields }) {
+ const draft = await this.findDraft({ id, fields });
+ if (!draft) throw this.boom.notFound(`The workflow template draft "${id}" is not found`, true);
+ return draft;
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+module.exports = WorkflowTemplateDraftService;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-template-registry-service.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-template-registry-service.js
new file mode 100644
index 0000000000..ff02a3e91e
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-template-registry-service.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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const inputSchema = require('../schema/workflow-template');
+
+class WorkflowTemplateRegistryService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'pluginRegistryService']);
+ }
+
+ async init() {
+ await super.init();
+ this.store = []; // an array of objects of this shape: { key: , value: { yaml } }
+ const registry = await this.service('pluginRegistryService');
+ // We loop through each plugin and ask it to register its templates
+ const plugins = await registry.getPlugins('workflow-templates');
+ // eslint-disable-next-line no-restricted-syntax
+ for (const plugin of plugins) {
+ // eslint-disable-next-line no-await-in-loop
+ await plugin.registerWorkflowTemplates(this);
+ }
+ }
+
+ async add({ yaml }) {
+ const [jsonSchemaValidationService] = await this.service(['jsonSchemaValidationService']);
+ const { id, v } = yaml;
+ const existing = await this.findWorkflowTemplate({ id, v });
+
+ if (existing)
+ throw this.boom.badRequest(
+ `You tried to register a workflow template, but a workflow template with the same id "${id}" and version "${v}" already exists`,
+ true,
+ );
+ await jsonSchemaValidationService.ensureValid(yaml, inputSchema);
+
+ const key = this.encodeId({ id, v });
+ this.store.push({ key, value: { yaml } });
+ }
+
+ async findWorkflowTemplate({ id, v }) {
+ const key = this.encodeId({ id, v });
+ const entry = _.find(this.store, ['key', key]);
+ return entry ? entry.value : undefined;
+ }
+
+ // Returns a list of all workflow templates in array of this shape: [{ id, v, yaml }, ...]
+ async listWorkflowTemplates() {
+ return _.map(this.store, item => {
+ const { yaml } = item.value;
+ const { id, v } = yaml;
+ return { id, v, yaml };
+ });
+ }
+
+ // private
+ encodeId({ id, v }) {
+ return `${id}_${v}`;
+ }
+}
+
+module.exports = WorkflowTemplateRegistryService;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-template-service.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-template-service.js
new file mode 100644
index 0000000000..3816d6ef6b
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-template-service.js
@@ -0,0 +1,351 @@
+/*
+ * 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 */
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { ensureAdmin } = require('@aws-ee/base-services/lib/authorization/assertions');
+const { toVersionString, parseVersionString, runAndCatch } = require('@aws-ee/base-services/lib/helpers/utils');
+
+const inputSchema = require('../schema/workflow-template');
+
+const settingKeys = {
+ tableName: 'dbTableWorkflowTemplates',
+};
+
+// Do some properties renaming to prepare the object to be saved in the database
+function toDbObject(dataObject) {
+ const result = { ...dataObject };
+
+ delete result.ver;
+ delete result.createdAt;
+ delete result.createdBy;
+ delete result.updatedAt;
+ delete result.updatedBy;
+ delete result.rev;
+
+ return result;
+}
+
+// Do some properties renaming to restore the object that was saved in the database
+function toDataObject(dbObject) {
+ if (_.isNil(dbObject)) return dbObject;
+ if (!_.isObject(dbObject)) return dbObject;
+
+ const result = { ...dbObject };
+ result.v = result.latest ? result.latest : parseVersionString(dbObject.ver);
+
+ delete result.ver;
+ delete result.latest;
+
+ return result;
+}
+
+// Go through the object own props and if they are empty strings, remove the props
+function removeEmptyStrings(srcObject) {
+ const result = {};
+
+ Object.keys(srcObject).forEach(key => {
+ const value = srcObject[key];
+ if (_.isString(value) && _.isEmpty(value)) return;
+ result[key] = value;
+ });
+
+ return result;
+}
+
+class WorkflowTemplateService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'stepTemplateService', 'dbService', 'auditWriterService']);
+ }
+
+ async init() {
+ await super.init();
+ this.tableName = this.settings.get(settingKeys.tableName);
+ }
+
+ async createVersion(requestContext, manifest = {}, { isLatest = true, tableName } = {}) {
+ const [jsonSchemaValidationService] = await this.service(['jsonSchemaValidationService']);
+
+ await ensureAdmin(requestContext);
+ manifest = this.applyDefaults(manifest);
+ // Validate input
+ await jsonSchemaValidationService.ensureValid(manifest, inputSchema);
+
+ const dbService = await this.service('dbService');
+ const table = tableName || this.tableName;
+ const { id, v } = manifest;
+ const logPrefix = `The workflow template "${id}" with ver "${v}" and rev "0"`;
+
+ manifest = await this.populateSteps(manifest);
+ const dbObject = toDbObject(manifest);
+
+ // For now, we assume that 'createdBy' and 'updatedBy' are always users and not groups
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+
+ // TODO - we need to wrap the creation of the version and the update of the latest record in a transaction
+ const result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_not_exists(ver)')
+ .key({ id, ver: toVersionString(v) })
+ .item({ ...dbObject, rev: 0, createdBy: by, updatedBy: by })
+ .update();
+ },
+ async () => {
+ throw this.boom.badRequest(`${logPrefix} already exist`, true);
+ },
+ );
+
+ if (isLatest) {
+ // Note that this is not the typical versioning technique. This is because in this case the caller of this
+ // method already wants to update a specific version which might not be the latest version
+ await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .updatedAt(result.updatedAt)
+ .disableCreatedAt()
+ .key({ id, ver: toVersionString(0) })
+ .condition('(attribute_exists(id) and #latest <= :latest) or attribute_not_exists(id)')
+ .item({ ...result, latest: v })
+ .names({ '#latest': 'latest' })
+ .values({ ':latest': v })
+ .update();
+ },
+ async () => {
+ // we ignore the ConditionalCheckFailedException exception because it simply means that the created version is not the
+ // latest version anymore and there is no need to bother the caller of this fact
+ },
+ );
+ }
+ const dataResult = toDataObject(result);
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-workflow-template-version', body: dataResult });
+
+ return dataResult;
+ }
+
+ async updateVersion(requestContext, manifest = {}, { isLatest = true, tableName } = {}) {
+ const [jsonSchemaValidationService] = await this.service(['jsonSchemaValidationService']);
+
+ await ensureAdmin(requestContext);
+ manifest = this.applyDefaults(manifest);
+
+ // Validate input
+ // we need to remove 'rev' here because the schema does not allow it, we should have a schema
+ // that allows 'rev' but for now, we don't do that.
+ await jsonSchemaValidationService.ensureValid(_.omit(manifest, ['rev']), inputSchema);
+ // now we need to check that rev is supplied
+ if (_.isNil(manifest.rev))
+ throw this.boom.badRequest('The supplied workflow template does not have the "rev" property', true);
+
+ const dbService = await this.service('dbService');
+ const table = tableName || this.tableName;
+ const { id, v, rev } = manifest;
+ const logPrefix = `The workflow template "${id}" with ver "${v}" and rev "${rev}"`;
+
+ manifest = await this.populateSteps(manifest);
+ const dbObject = toDbObject(manifest);
+
+ // For now, we assume that updatedBy' is always a user and not a group
+ const by = _.get(requestContext, 'principalIdentifier'); // principalIdentifier shape is { username, ns: user.ns }
+
+ // TODO: we need to wrap the creation of the version and the update of the latest record in a transaction
+ const result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .condition('attribute_exists(ver)')
+ .key({ id, ver: toVersionString(v) })
+ .rev(rev)
+ .item({ ...dbObject, updatedBy: by })
+ .update();
+ },
+ async () => {
+ // There are two scenarios here:
+ // 1 - The "v" entry does not exist
+ // 2 - The "rev" does not match
+ const existing = await this.findVersion({ id, v, fields: ['id', 'v', 'updatedBy'] });
+ if (existing) {
+ throw this.boom.badRequest(
+ `${logPrefix} information changed by "${existing.updatedBy}" just before your request is processed, please try again`,
+ true,
+ );
+ }
+ throw this.boom.badRequest(`${logPrefix} does not exist`, true);
+ },
+ );
+
+ if (isLatest) {
+ // Note that this is not the typical versioning technique. This is because in this case the caller of this
+ // method already wants to update a specific version which might not be the latest version
+ await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .updatedAt(result.updatedAt)
+ .key({ id, ver: toVersionString(0) })
+ .condition('#latest = :latest')
+ .item(result)
+ .names({ '#latest': 'latest' })
+ .values({ ':latest': v })
+ .update();
+ },
+ async () => {
+ // we ignore the ConditionalCheckFailedException exception because it simply means that the updated version is not the
+ // latest version anymore and there is no need to bother the caller of this fact
+ },
+ );
+ }
+
+ const dataResult = toDataObject(result);
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'update-workflow-template-version', body: dataResult });
+
+ return dataResult;
+ }
+
+ // List all versions for all workflow templates or for a specific workflow template if the workflow template id was provided
+ async listVersions({ id, fields = [] } = {}) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ if (_.isNil(id)) {
+ // The scanner route
+ const result = await dbService.helper
+ .scanner()
+ .table(table)
+ .filter('attribute_not_exists(latest)') // we don't want to return the v0000_ one
+ .limit(2000)
+ .projection(fields)
+ .scan();
+ return _.map(result, item => toDataObject(item));
+ }
+
+ const result = await dbService.helper
+ .query()
+ .table(table)
+ .key('id', id)
+ .forward(false)
+ .filter('attribute_not_exists(latest)') // we don't want to return the v0000_ one
+ .limit(2000)
+ .projection(fields)
+ .query();
+ const versions = _.map(result, item => toDataObject(item));
+ if (versions.length === 0) throw this.boom.notFound(`The workflow template "${id}" is not found`, true);
+
+ return versions;
+ }
+
+ // List latest versions of all the workflow templates
+ async list({ fields = [] } = {}) {
+ const dbService = await this.service('dbService');
+ const table = this.tableName;
+
+ // The scanner route
+ const result = await dbService.helper
+ .scanner()
+ .table(table)
+ .filter('attribute_exists(latest)')
+ .limit(2000)
+ .projection(fields)
+ .scan();
+ return _.map(result, item => toDataObject(item));
+ }
+
+ async findVersion({ id, v = 0, fields = [] }, { tableName } = {}) {
+ const dbService = await this.service('dbService');
+ // This function can accept a different tableName to use for the lookup, this is useful in places
+ // such as post deployment
+ const table = tableName || this.tableName;
+
+ const result = await dbService.helper
+ .getter()
+ .table(table)
+ .key({ id, ver: toVersionString(v) })
+ .projection(fields)
+ .get();
+
+ return toDataObject(result);
+ }
+
+ async mustFindVersion({ id, v = 0, fields }) {
+ const workflowTemplate = await this.findVersion({ id, v, fields });
+ if (!workflowTemplate) throw this.boom.notFound(`The workflow template "${id}" ver "${v}" is not found`, true);
+ return workflowTemplate;
+ }
+
+ applyDefaults(manifest) {
+ return {
+ runSpec: {
+ target: 'stepFunctions',
+ size: 'small',
+ },
+ ...manifest,
+ };
+ }
+
+ // This method mutates the manifest selected steps by populating the stepTemplate prop for each step
+ async populateSteps(manifest) {
+ const [stepTemplateService] = await this.service(['stepTemplateService']);
+ const idMap = {};
+ let index = 0;
+
+ // Get all the step templates
+ /* eslint-disable no-restricted-syntax */
+ for (const step of manifest.selectedSteps) {
+ const { stepTemplateId, stepTemplateVer } = step;
+ const stepTemplate = await stepTemplateService.mustFindVersion({ id: stepTemplateId, v: stepTemplateVer });
+
+ step.stepTemplate = stepTemplate;
+
+ if (idMap[step.id]) {
+ throw this.boom.badRequest(
+ `Step at index [${index}] has the same id "${step.id}" as a previous step. Step ids must be unique within the workflow`,
+ true,
+ );
+ }
+ /* eslint-enable no-restricted-syntax */
+
+ idMap[step.id] = true;
+ index += 1;
+ if (!_.isUndefined(step.defaults)) {
+ step.defaults = removeEmptyStrings(step.defaults); // This is because you won't be able to update an existing value in the database with a new value that is an empty string
+ }
+ }
+
+ return manifest;
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+module.exports = WorkflowTemplateService;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-trigger-service.js b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-trigger-service.js
new file mode 100644
index 0000000000..0776f22dac
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/lib/workflow/workflow-trigger-service.js
@@ -0,0 +1,97 @@
+/*
+ * 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 */
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const metaSchema = require('../schema/trigger-workflow');
+
+const settingKeys = {
+ stateMachineArn: 'smWorkflow',
+};
+
+class WorkflowTriggerService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'workflowService', 'workflowInstanceService', 'aws']);
+ }
+
+ async init() {
+ await super.init();
+ this.internals = {
+ triggerStepFunctions: triggerStepFunctions.bind(this),
+ };
+ }
+
+ async triggerWorkflow(requestContext, meta, input) {
+ const [jsonSchemaValidationService, workflowService, workflowInstanceService] = await this.service([
+ 'jsonSchemaValidationService',
+ 'workflowService',
+ 'workflowInstanceService',
+ ]);
+
+ // Get the latest version, if not provided
+ if (meta && !meta.workflowVer) {
+ const wfLatestVersion = workflowService.findVersion({ id: meta.workflowId });
+ meta.workflowVer = wfLatestVersion.ver;
+ }
+
+ // Validate input
+ await jsonSchemaValidationService.ensureValid(meta, metaSchema);
+
+ const instance = await workflowInstanceService.createInstance(requestContext, meta, input);
+ const target = instance.runSpec.target;
+
+ let result;
+ switch (target) {
+ case 'stepFunctions':
+ result = this.internals.triggerStepFunctions({ instance, meta, input });
+ break;
+ default:
+ throw this.boom.badRequest(`The run target "${target}" is not supported yet`);
+ }
+
+ return result;
+ }
+}
+
+async function triggerStepFunctions({ instance, meta, input }) {
+ const aws = await this.service('aws');
+ const { wfId: workflowId, wfVer: workflowVer, id: instanceId } = instance;
+ const stateMachineArn = meta.smWorkflow || this.settings.get(settingKeys.stateMachineArn);
+ const name = `${workflowId}_${workflowVer}_${instance.id}`;
+
+ const sf = new aws.sdk.StepFunctions();
+ const params = {
+ stateMachineArn,
+ input: JSON.stringify({
+ meta: _.assign({}, meta, { wid: workflowId, sid: instanceId, wrv: workflowVer, smWorkflow: stateMachineArn }),
+ input,
+ }),
+ name,
+ };
+
+ const data = await sf.startExecution(params).promise();
+
+ return {
+ status: instance.status,
+ instance,
+ runSpec: instance.runSpec,
+ executionArn: data.executionArn,
+ };
+}
+
+module.exports = WorkflowTriggerService;
diff --git a/addons/addon-base-workflow/packages/base-workflow-core/package.json b/addons/addon-base-workflow/packages/base-workflow-core/package.json
new file mode 100644
index 0000000000..0cb172d4b6
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-core/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@aws-ee/base-workflow-core",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A library containing base set of workflow related services and utilities",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-services": "workspace:*",
+ "@aws-ee/base-services-container": "workspace:*",
+ "@aws-ee/workflow-engine": "workspace:*",
+ "lodash": "^4.17.15",
+ "shortid": "^2.2.15",
+ "slugify": "^1.4.0"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.1.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-workflow/packages/base-workflow-templates/.eslintrc.json b/addons/addon-base-workflow/packages/base-workflow-templates/.eslintrc.json
new file mode 100644
index 0000000000..c700cf03a8
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-templates/.eslintrc.json
@@ -0,0 +1,26 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "import/no-unresolved": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-workflow/packages/base-workflow-templates/.gitignore b/addons/addon-base-workflow/packages/base-workflow-templates/.gitignore
new file mode 100644
index 0000000000..f2fb153198
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-templates/.gitignore
@@ -0,0 +1,19 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+/coverage/
+.build
diff --git a/addons/addon-base-workflow/packages/base-workflow-templates/.prettierrc.json b/addons/addon-base-workflow/packages/base-workflow-templates/.prettierrc.json
new file mode 100644
index 0000000000..4ee7b34147
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-templates/.prettierrc.json
@@ -0,0 +1,8 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
+
diff --git a/addons/addon-base-workflow/packages/base-workflow-templates/jest.config.js b/addons/addon-base-workflow/packages/base-workflow-templates/jest.config.js
new file mode 100644
index 0000000000..70544476d7
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-templates/jest.config.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+};
diff --git a/addons/addon-base-workflow/packages/base-workflow-templates/jsconfig.json b/addons/addon-base-workflow/packages/base-workflow-templates/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-templates/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-workflow/packages/base-workflow-templates/package.json b/addons/addon-base-workflow/packages/base-workflow-templates/package.json
new file mode 100644
index 0000000000..1fd6cf99fa
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-templates/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@aws-ee/base-workflow-templates",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A collection of base workflow templates",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-workflow-core": "workspace:*",
+ "lodash": "^4.17.15",
+ "shortid": "^2.2.15",
+ "slugify": "^1.4.0"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.1.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-workflow/packages/base-workflow-templates/templates/empty-workflow.yml b/addons/addon-base-workflow/packages/base-workflow-templates/templates/empty-workflow.yml
new file mode 100644
index 0000000000..b0db531f5b
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-templates/templates/empty-workflow.yml
@@ -0,0 +1,17 @@
+id: wt-empty
+v: 1
+title: Empty Workflow
+desc: |
+ An empty workflow so that you have full control of the workflow.
+hidden: false
+builtin: true
+propsOverrideOption: # these are for the workflow template itself and not for the step templates
+ allowed:
+ - title
+ - desc
+ - instanceTtl
+ - steps
+ - runSpecSize
+ - runSpecTarget
+instanceTtl: # Empty value means that it is indefinite
+selectedSteps: []
diff --git a/addons/addon-base-workflow/packages/base-workflow-templates/templates/workflow-templates-plugin.js b/addons/addon-base-workflow/packages/base-workflow-templates/templates/workflow-templates-plugin.js
new file mode 100644
index 0000000000..e924c391cd
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-workflow-templates/templates/workflow-templates-plugin.js
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+const emptyWorkflowYaml = require('./empty-workflow.yml');
+
+const add = yaml => ({ yaml });
+
+// The order is important, add your templates here
+const templates = [add(emptyWorkflowYaml)];
+
+async function registerWorkflowTemplates(registry) {
+ // eslint-disable-next-line no-restricted-syntax
+ for (const template of templates) {
+ await registry.add(template); // eslint-disable-line no-await-in-loop
+ }
+}
+
+module.exports = { registerWorkflowTemplates };
diff --git a/addons/addon-base-workflow/packages/base-worklfow-steps/.eslintrc.json b/addons/addon-base-workflow/packages/base-worklfow-steps/.eslintrc.json
new file mode 100644
index 0000000000..c700cf03a8
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-worklfow-steps/.eslintrc.json
@@ -0,0 +1,26 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "import/no-unresolved": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-workflow/packages/base-worklfow-steps/.gitignore b/addons/addon-base-workflow/packages/base-worklfow-steps/.gitignore
new file mode 100644
index 0000000000..f2fb153198
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-worklfow-steps/.gitignore
@@ -0,0 +1,19 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+/coverage/
+.build
diff --git a/addons/addon-base-workflow/packages/base-worklfow-steps/.prettierrc.json b/addons/addon-base-workflow/packages/base-worklfow-steps/.prettierrc.json
new file mode 100644
index 0000000000..4ee7b34147
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-worklfow-steps/.prettierrc.json
@@ -0,0 +1,8 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
+
diff --git a/addons/addon-base-workflow/packages/base-worklfow-steps/jest.config.js b/addons/addon-base-workflow/packages/base-worklfow-steps/jest.config.js
new file mode 100644
index 0000000000..70544476d7
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-worklfow-steps/jest.config.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+};
diff --git a/addons/addon-base-workflow/packages/base-worklfow-steps/jsconfig.json b/addons/addon-base-workflow/packages/base-worklfow-steps/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-worklfow-steps/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-workflow/packages/base-worklfow-steps/package.json b/addons/addon-base-workflow/packages/base-worklfow-steps/package.json
new file mode 100644
index 0000000000..86ea160388
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-worklfow-steps/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@aws-ee/base-workflow-steps",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A collection of base workflow steps",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-workflow-core": "workspace:*",
+ "lodash": "^4.17.15",
+ "shortid": "^2.2.15",
+ "slugify": "^1.4.0"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.1.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base-workflow/packages/base-worklfow-steps/steps/obtain-write-lock/obtain-write-lock.js b/addons/addon-base-workflow/packages/base-worklfow-steps/steps/obtain-write-lock/obtain-write-lock.js
new file mode 100644
index 0000000000..036be94afa
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-worklfow-steps/steps/obtain-write-lock/obtain-write-lock.js
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const StepBase = require('@aws-ee/base-workflow-core/lib/workflow/helpers/step-base');
+
+class ObtainLock extends StepBase {
+ async start() {
+ const attemptsCount = await this.config.number('attemptsCount');
+ const waitPeriod = await this.config.number('waitPeriod');
+
+ const obtained = await this.obtainToken();
+ if (obtained) return undefined;
+
+ return this.wait(waitPeriod)
+ .maxAttempts(attemptsCount)
+ .until('obtainToken');
+ }
+
+ async obtainToken() {
+ const lockIdKeyName = await this.config.string('lockIdKeyName');
+ const tokenKeyName = await this.config.string('writeTokenKeyName');
+ const expiresIn = await this.config.number('expiresIn');
+ const lockId = await this.payload.string(lockIdKeyName);
+ const lockService = await this.mustFindServices('lockService');
+
+ this.print(`Attempting to obtain a write lock for "${lockId}" with expiry value of "${expiresIn}"`);
+ const writeToken = await lockService.obtainWriteLock({ id: lockId, expiresIn });
+ if (_.isUndefined(writeToken)) return false;
+
+ this.print(
+ `successfully obtained a write lock for "${lockId}" with expiry value of "${expiresIn}" and writeToken "${writeToken}"`,
+ );
+ this.payload.setKey(tokenKeyName, writeToken);
+ await this.statusMessage(`Obtained write token "${writeToken}"`);
+ return true;
+ }
+
+ async inputKeys() {
+ const keys = {
+ lockIdKeyName: 'string',
+ writeTokenKeyName: 'string',
+ expiresIn: 'number',
+ };
+ const lockIdKeyName = await this.config.string('lockIdKeyName');
+ keys[lockIdKeyName] = 'string';
+ return keys;
+ }
+
+ async outputKeys() {
+ const keys = {};
+ const tokenKeyName = await this.config.string('writeTokenKeyName');
+ keys[tokenKeyName] = 'string';
+ return keys;
+ }
+}
+
+module.exports = ObtainLock;
diff --git a/addons/addon-base-workflow/packages/base-worklfow-steps/steps/obtain-write-lock/obtain-write-lock.yml b/addons/addon-base-workflow/packages/base-worklfow-steps/steps/obtain-write-lock/obtain-write-lock.yml
new file mode 100644
index 0000000000..fa0d0e1b3c
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-worklfow-steps/steps/obtain-write-lock/obtain-write-lock.yml
@@ -0,0 +1,55 @@
+id: st-obtain-write-lock
+v: 1
+title: Obtain Write Lock
+desc: |
+ This step attempts to obtain a lock given a lock id. You can configure its behavior via its config keys, such
+ as how many attempts should be tried and the time between each attempt.
+
+skippable: true # this means that if there is an error in a previous step, then this step will be skipped
+hidden: false
+
+inputManifest:
+ sections:
+ - title: Configuration
+ children:
+
+ - name: lockIdKeyName
+ type: stringInput
+ title: Payload key name
+ rules: required
+ desc: |
+ This is the key name to use when looking up the lock id from the payload
+
+ - name: attemptsCount
+ type: stringInput
+ title: Attempts count
+ rules: required|integer
+ default: 10
+ desc: |
+ How many times should this step attempt to obtain the lock? (if the previous attempt did not obtain the lock)
+
+ - name: waitPeriod
+ type: stringInput
+ title: Wait time
+ rules: required|integer
+ default: 1
+ desc: |
+ The time (in seconds) to wait before attempting again to obtain the lock (if the previous attempt did not obtain the lock)
+
+ - name: expiresIn
+ type: stringInput
+ title: Expires after
+ rules: required|integer
+ default: 7200
+ desc: |
+ The time (in seconds) before a write lock expires. Make sure you provide a value with enough buffer for your operation.
+ For example, if you are expecting your steps to take 1 second to finish, then set the lock expiry value to 7200 (2 hours) or larger.
+ Remember that this expiry mechanism is meant to address edge cases, where your "Release Lock" step fails for some reason.
+
+ - name: writeTokenKeyName
+ type: stringInput
+ title: Payload key name for the write token value
+ rules: required
+ default: writeLockToken
+ desc: |
+ This is the key name to use when populating the payload with the obtained write lock token.
diff --git a/addons/addon-base-workflow/packages/base-worklfow-steps/steps/release-write-lock/release-write-lock.js b/addons/addon-base-workflow/packages/base-worklfow-steps/steps/release-write-lock/release-write-lock.js
new file mode 100644
index 0000000000..e583cb74b8
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-worklfow-steps/steps/release-write-lock/release-write-lock.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.
+ */
+
+const StepBase = require('@aws-ee/base-workflow-core/lib/workflow/helpers/step-base');
+
+class ReleaseWriteLock extends StepBase {
+ async start() {
+ const tokenKeyName = await this.config.string('writeTokenKeyName');
+ const writeToken = await this.payload.optionalString(tokenKeyName);
+ const lockService = await this.mustFindServices('lockService');
+
+ if (writeToken) {
+ await lockService.releaseWriteLock({ writeToken });
+ await this.payload.removeKey(tokenKeyName);
+ await this.statusMessage(`Released write token "${writeToken}"`);
+ } else {
+ this.statusMessage('WARN|||No write token is found in the payload, therefore, no write lock to release');
+ }
+ }
+
+ async inputKeys() {
+ const keys = {
+ writeTokenKeyName: 'string',
+ };
+ const tokenKeyName = await this.config.string('writeTokenKeyName');
+ keys[tokenKeyName] = 'optionalString';
+ return keys;
+ }
+}
+
+module.exports = ReleaseWriteLock;
diff --git a/addons/addon-base-workflow/packages/base-worklfow-steps/steps/release-write-lock/release-write-lock.yml b/addons/addon-base-workflow/packages/base-worklfow-steps/steps/release-write-lock/release-write-lock.yml
new file mode 100644
index 0000000000..8ee48ed53a
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-worklfow-steps/steps/release-write-lock/release-write-lock.yml
@@ -0,0 +1,20 @@
+id: st-release-write-lock
+v: 1
+title: Release Write Lock
+desc: |
+ This step releases a write lock given the write lock token.
+
+skippable: false # this is important, leave it as false so that this step is called even if pervious steps had errors
+hidden: false
+
+inputManifest:
+ sections:
+ - title: Configuration
+ children:
+ - name: writeTokenKeyName
+ type: stringInput
+ title: Payload key name for the write token value
+ rules: required
+ default: writeLockToken
+ desc: |
+ This is the key name to use obtain the value of the write lock token from the payload.
diff --git a/addons/addon-base-workflow/packages/base-worklfow-steps/steps/workflow-steps-plugin.js b/addons/addon-base-workflow/packages/base-worklfow-steps/steps/workflow-steps-plugin.js
new file mode 100644
index 0000000000..e6b7817ce2
--- /dev/null
+++ b/addons/addon-base-workflow/packages/base-worklfow-steps/steps/workflow-steps-plugin.js
@@ -0,0 +1,35 @@
+/*
+ * 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 global-require */
+const obtainWriteLock = require('./obtain-write-lock/obtain-write-lock');
+const obtainWriteLockYaml = require('./obtain-write-lock/obtain-write-lock.yml');
+const releaseWriteLock = require('./release-write-lock/release-write-lock');
+const releaseWriteLockYaml = require('./release-write-lock/release-write-lock.yml');
+
+const add = (implClass, yaml) => ({ implClass, yaml });
+
+// The order is important, add your steps here
+const steps = [add(obtainWriteLock, obtainWriteLockYaml), add(releaseWriteLock, releaseWriteLockYaml)];
+
+async function registerWorkflowSteps(registry) {
+ // eslint-disable-next-line no-restricted-syntax
+ for (const step of steps) {
+ const { implClass, yaml } = step;
+ await registry.add({ implClass, yaml }); // eslint-disable-line no-await-in-loop
+ }
+}
+
+module.exports = { registerWorkflowSteps };
diff --git a/addons/addon-base-workflow/packages/workflow-engine/.eslintrc.json b/addons/addon-base-workflow/packages/workflow-engine/.eslintrc.json
new file mode 100644
index 0000000000..c700cf03a8
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/.eslintrc.json
@@ -0,0 +1,26 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "import/no-unresolved": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base-workflow/packages/workflow-engine/.gitignore b/addons/addon-base-workflow/packages/workflow-engine/.gitignore
new file mode 100644
index 0000000000..f2fb153198
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/.gitignore
@@ -0,0 +1,19 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+/coverage/
+.build
diff --git a/addons/addon-base-workflow/packages/workflow-engine/.prettierrc.json b/addons/addon-base-workflow/packages/workflow-engine/.prettierrc.json
new file mode 100644
index 0000000000..1059949f12
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/.prettierrc.json
@@ -0,0 +1,8 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "quoteProps": "consistent"
+}
+
diff --git a/addons/addon-base-workflow/packages/workflow-engine/jest.config.js b/addons/addon-base-workflow/packages/workflow-engine/jest.config.js
new file mode 100644
index 0000000000..70544476d7
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/jest.config.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+};
diff --git a/addons/addon-base-workflow/packages/workflow-engine/jsconfig.json b/addons/addon-base-workflow/packages/workflow-engine/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/helpers/event-delegate.js b/addons/addon-base-workflow/packages/workflow-engine/lib/helpers/event-delegate.js
new file mode 100644
index 0000000000..60f1ee035d
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/helpers/event-delegate.js
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+class EventDelegate {
+ constructor({ supportedEvents = [], sponsorName = '' } = {}) {
+ this.supportedEvents = supportedEvents;
+ this.sponsorName = sponsorName;
+ this.listenersMap = {}; // key is the event name, value is a list of all listeners
+ }
+
+ on(name, fn) {
+ const events = this.supportedEvents;
+ if (_.indexOf(events, name) === -1) throw new Error(`Event "${name}" is not supported by ${this.sponsorName}.`);
+ const entries = this.listenersMap[name] || [];
+ entries.push(fn);
+ this.listenersMap[name] = entries;
+
+ return this;
+ }
+
+ async fireEvent(name, ...params) {
+ const listeners = this.listenersMap[name];
+ if (_.isEmpty(listeners)) return;
+ /* eslint-disable no-restricted-syntax */
+ for (const listener of listeners) {
+ await listener(...params); // eslint-disable-line no-await-in-loop
+ }
+ /* eslint-enable no-restricted-syntax */
+ }
+}
+
+module.exports = EventDelegate;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/helpers/key-getter-delegate.js b/addons/addon-base-workflow/packages/workflow-engine/lib/helpers/key-getter-delegate.js
new file mode 100644
index 0000000000..f2de8a30a5
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/helpers/key-getter-delegate.js
@@ -0,0 +1,136 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+class KeyGetterDelegate {
+ constructor(findFnAsync, { loadFn, storeTitle = '' } = {}) {
+ this.findFnAsync = findFnAsync;
+ this.storeTitle = storeTitle;
+ this.loadFn = loadFn;
+ }
+
+ async string(key) {
+ const value = await this.mustFind(key);
+ if (!_.isString(value) || _.isEmpty(value))
+ throw new Error(`${this.storeTitle} key "${key}" is not a string or is null or is empty, value = [${value}].`);
+
+ return value;
+ }
+
+ async number(key) {
+ const value = await this.mustFind(key);
+ if (!_.isNumber(value)) throw new Error(`${this.storeTitle} key "${key}" is not a number, value = [${value}].`);
+
+ return value;
+ }
+
+ async boolean(key) {
+ const value = await this.mustFind(key);
+ if (!_.isBoolean(value)) throw new Error(`${this.storeTitle} key "${key}" is not a boolean, value = [${value}].`);
+
+ return value;
+ }
+
+ async object(key) {
+ const value = await this.mustFind(key);
+ if (!_.isObject(value)) throw new Error(`${this.storeTitle} key "${key}" is not an object, value = [${value}].`);
+
+ return value;
+ }
+
+ async array(key) {
+ const value = await this.mustFind(key);
+ if (!_.isArray(value)) throw new Error(`${this.storeTitle} key "${key}" is not an array, value = [${value}].`);
+
+ return value;
+ }
+
+ async optionalString(key, defaults = '') {
+ const value = await this.value(key);
+ if (_.isNil(value)) return defaults;
+ if (!_.isString(value)) throw new Error(`${this.storeTitle} key "${key}" is not a string, value = [${value}].`);
+ if (_.isEmpty(value)) return defaults;
+
+ return value;
+ }
+
+ async optionalNumber(key, defaults = NaN) {
+ const value = await this.value(key);
+ if (_.isNil(value)) return defaults;
+ if (!_.isNumber(value)) throw new Error(`${this.storeTitle} key "${key}" is not a number, value = [${value}].`);
+
+ return value;
+ }
+
+ async optionalBoolean(key, defaults = false) {
+ const value = await this.value(key);
+ if (_.isNil(value)) return defaults;
+ if (!_.isBoolean(value)) throw new Error(`${this.storeTitle} key "${key}" is not a boolean, value = [${value}].`);
+
+ return value;
+ }
+
+ async optionalObject(key, defaults = {}) {
+ const value = await this.value(key);
+ if (_.isNil(value)) return defaults;
+ if (!_.isObject(value)) throw new Error(`${this.storeTitle} key "${key}" is not an object, value = [${value}].`);
+
+ return value;
+ }
+
+ async optionalArray(key, defaults = []) {
+ const value = await this.value(key);
+ if (_.isNil(value)) return defaults;
+ if (!_.isArray(value)) throw new Error(`${this.storeTitle} key "${key}" is not an array, value = [${value}].`);
+
+ return value;
+ }
+
+ // Returns the getter methods (bounded to this KeyGetterDelegate instance)
+ // This is useful if you have your own Store class and you want to add to your store
+ // the getter methods of this KeyGetterDelegate
+ getMethods() {
+ return {
+ string: this.string.bind(this),
+ number: this.number.bind(this),
+ boolean: this.boolean.bind(this),
+ object: this.object.bind(this),
+ array: this.array.bind(this),
+
+ optionalString: this.optionalString.bind(this),
+ optionalNumber: this.optionalNumber.bind(this),
+ optionalBoolean: this.optionalBoolean.bind(this),
+ optionalObject: this.optionalObject.bind(this),
+ optionalArray: this.optionalArray.bind(this),
+ };
+ }
+
+ // private
+ async value(key) {
+ if (this.loadFn) await this.loadFn(key);
+ return this.findFnAsync(key);
+ }
+
+ // private
+ async mustFind(key) {
+ const value = await this.value(key);
+
+ if (_.isUndefined(value)) throw new Error(`${this.storeTitle} "${key}" is not found.`);
+ return value;
+ }
+}
+
+module.exports = KeyGetterDelegate;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/helpers/utils.js b/addons/addon-base-workflow/packages/workflow-engine/lib/helpers/utils.js
new file mode 100644
index 0000000000..004b68d8cc
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/helpers/utils.js
@@ -0,0 +1,62 @@
+/*
+ * 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-console */
+const _ = require('lodash');
+
+// This function can receive an instance of Error or a string or an object with two properties: msg/message & stack.
+// Furthermore, if the error is an instance of Error and has a property 'root' as an object, then this root
+// object is expected to have two properties: msg & stack.
+//
+// This function returns an object with two properties: msg & stack (with stack being trimmed to 300 characters).
+function normalizeError(error = {}, { maxStackLength = 300 } = {}) {
+ if (_.isString(error) || _.isNil(error)) return { msg: error || 'UnknownError', stack: '' };
+
+ const toMsg = obj => obj.msg || obj.message || 'Unknown Error';
+ const toStack = obj => (obj.stack || '').substring(0, maxStackLength);
+ const toResult = obj => {
+ const output = _.omit({ ...obj, msg: toMsg(obj), stack: toStack(obj) }, ['message']);
+ return output;
+ };
+
+ console.log(error); // We are printing this here so that the full stack is shown
+ if (error instanceof Error) return _.isObject(error.root) ? toResult(error.root) : toResult(error);
+ return toResult(error);
+}
+
+// Just a function that protects against throwing an error
+const catchIfErrorAsync = async fn => {
+ try {
+ return await fn();
+ } catch (error) {
+ console.log(error);
+ return undefined;
+ }
+};
+
+const catchIfError = fn => {
+ try {
+ return fn();
+ } catch (error) {
+ console.log(error);
+ return undefined;
+ }
+};
+
+module.exports = {
+ normalizeError,
+ catchIfErrorAsync,
+ catchIfError,
+};
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/call-decision.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/call-decision.js
new file mode 100644
index 0000000000..ee63631508
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/call-decision.js
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+const Invoker = require('../invoker');
+
+class CallDecision {
+ constructor(invoker) {
+ this.type = 'call';
+ this.thenCall = invoker;
+ }
+
+ // The memento shape is:
+ // {
+ // "type": "call" // the type of the decision
+ // "tc": {...} // "tc" = thenCall invoker memento (when applicable)
+ // }
+
+ setMemento({ tc } = {}) {
+ this.thenCall = undefined; // ensure that it is empty
+ if (tc) this.thenCall = new Invoker().setMemento(tc);
+
+ return this;
+ }
+
+ getMemento() {
+ const result = {
+ type: 'call',
+ };
+ if (this.thenCall) result.tc = this.thenCall.getMemento();
+
+ return result;
+ }
+
+ get methodName() {
+ if (this.thenCall) return this.thenCall.methodName || 'unknown method name';
+ return 'unknown method name';
+ }
+
+ static is(decisionMemento = {}) {
+ return decisionMemento.type === 'call';
+ }
+}
+
+module.exports = CallDecision;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/goto-decision.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/goto-decision.js
new file mode 100644
index 0000000000..89f9db6e8a
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/goto-decision.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.
+ */
+
+/**
+ * A decision to go to a specific step in the workflow and execute workflow from that step.
+ */
+class GoToDecision {
+ constructor(stepIndex) {
+ this.type = 'goto';
+ this.stepIndex = stepIndex;
+ }
+
+ // The memento shape is:
+ // {
+ // "type": "goto" // the type of the decision
+ // "si": Number // "si" = stepIndex
+ // }
+
+ setMemento({ si } = {}) {
+ this.stepIndex = si;
+ return this;
+ }
+
+ getMemento() {
+ const result = {
+ type: 'goto',
+ si: this.stepIndex,
+ };
+ return result;
+ }
+
+ static is(decisionMemento = {}) {
+ return decisionMemento.type === 'goto';
+ }
+}
+
+module.exports = GoToDecision;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/pause-decision-builder.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/pause-decision-builder.js
new file mode 100644
index 0000000000..1d3d641a6f
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/pause-decision-builder.js
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+const Invoker = require('../invoker');
+const PauseDecision = require('./pause-decision');
+
+class PauseDecisionBuilder {
+ constructor(seconds) {
+ this.pauseDecision = new PauseDecision();
+ this.pauseDecision.seconds = seconds;
+ }
+
+ until(methodName, ...params) {
+ this.pauseDecision.check = new Invoker(methodName, ...params);
+ return this;
+ }
+
+ maxAttempts(max) {
+ this.pauseDecision.max = max;
+ this.pauseDecision.counter = max;
+ return this;
+ }
+
+ thenCall(methodName, ...params) {
+ this.pauseDecision.thenCall = new Invoker(methodName, ...params);
+ return this;
+ }
+
+ otherwiseCall(methodName, ...params) {
+ this.pauseDecision.otherwise = new Invoker(methodName, ...params);
+ return this;
+ }
+
+ toPauseDecision() {
+ // lets do a quick validation
+ if (this.pauseDecision.max !== undefined && this.pauseDecision.check === undefined) {
+ throw new Error(
+ 'The step specified a pause decision with a max attempt but without specifying the "until" function.',
+ );
+ }
+
+ if (this.pauseDecision.max === undefined && this.pauseDecision.check !== undefined) {
+ throw new Error(
+ 'The step specified a pause decision with the "until" function but without specifying "maxAttempts" count.',
+ );
+ }
+
+ if (this.pauseDecision.otherwiseCall !== undefined && this.pauseDecision.check === undefined) {
+ throw new Error('The step specified "otherwiseCall" function but without specifying the "until" function.');
+ }
+ return this.pauseDecision;
+ }
+}
+
+module.exports = PauseDecisionBuilder;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/pause-decision.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/pause-decision.js
new file mode 100644
index 0000000000..9cd6640d3c
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/pause-decision.js
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+const WaitDecision = require('./wait-decision');
+
+/**
+ * A decision to pause Step. This decision is very similar to the WaitDecision.
+ * The WaitDecision is intended to be used for situation when the Step is in-progress and waiting for some condition
+ * to become true. From StepLoop's perspective the Step is considered to be still in-progress while waiting so
+ * the StepLoop does not fire any status change related events.
+ *
+ * In case of the PauseDecision the Step is considered to be transitioning from "in_progress" status to a explicitly
+ * "paused" state. From StepLoop's perspective the Step is considered to be transitioning from "in_progress" to
+ * "paused" status so the StepLoop fires step status change related events. See "../step-loop.js" for more details.
+ */
+class PauseDecision extends WaitDecision {
+ constructor() {
+ super();
+ this.type = 'pause';
+ this.pauseReason = '';
+ }
+
+ // The memento shape is:
+ // {
+ // "type": "pause" // the type of the decision
+ // "s": int // "s" = seconds, this is the total wait in seconds (when applicable)
+ // "mx": int // "mx" = max check attempts (when applicable)
+ // "co": int // "co" = counter, used to count the check attempts (when applicable)
+ // "ch": {...} // "ch" = check, the check invoker memento (when applicable)
+ // "ot": {...} // "ot" = otherwise, the otherwise invoker memento (when applicable)
+ // "tc": {...} // "tc" = thenCall invoker memento (when applicable)
+ // "pr": string // "pr" = reason for pausing the step
+ // }
+
+ setMemento({ s, mx, co, ch, tc, ot, wt, pr } = {}) {
+ super.setMemento({ s, mx, co, ch, tc, ot, wt });
+ this.pauseReason = pr;
+ return this;
+ }
+
+ getMemento() {
+ const result = super.getMemento();
+ result.type = 'pause';
+
+ if (this.pauseReason !== undefined) result.pr = this.pauseReason;
+
+ return result;
+ }
+
+ static is(decisionMemento = {}) {
+ return decisionMemento.type === 'pause';
+ }
+}
+
+module.exports = PauseDecision;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/wait-decision-builder.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/wait-decision-builder.js
new file mode 100644
index 0000000000..614e663ae1
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/wait-decision-builder.js
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+const Invoker = require('../invoker');
+const WaitDecision = require('./wait-decision');
+
+class WaitDecisionBuilder {
+ constructor(seconds) {
+ this.waitDecision = new WaitDecision();
+ this.waitDecision.seconds = seconds;
+ }
+
+ until(methodName, ...params) {
+ this.waitDecision.check = new Invoker(methodName, ...params);
+ return this;
+ }
+
+ maxAttempts(max) {
+ this.waitDecision.max = max;
+ this.waitDecision.counter = max;
+ return this;
+ }
+
+ thenCall(methodName, ...params) {
+ this.waitDecision.thenCall = new Invoker(methodName, ...params);
+ return this;
+ }
+
+ otherwiseCall(methodName, ...params) {
+ this.waitDecision.otherwise = new Invoker(methodName, ...params);
+ return this;
+ }
+
+ toWaitDecision() {
+ // lets do a quick validation
+ if (this.waitDecision.max !== undefined && this.waitDecision.check === undefined) {
+ throw new Error(
+ 'The step specified a wait decision with a max attempt but without specifying the "until" function.',
+ );
+ }
+
+ if (this.waitDecision.max === undefined && this.waitDecision.check !== undefined) {
+ throw new Error(
+ 'The step specified a wait decision with the "until" function but without specifying "maxAttempts" count.',
+ );
+ }
+
+ if (this.waitDecision.otherwiseCall !== undefined && this.waitDecision.check === undefined) {
+ throw new Error('The step specified "otherwiseCall" function but without specifying the "until" function.');
+ }
+ return this.waitDecision;
+ }
+}
+
+module.exports = WaitDecisionBuilder;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/wait-decision.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/wait-decision.js
new file mode 100644
index 0000000000..436377c56f
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/decisions/wait-decision.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.
+ */
+
+const Invoker = require('../invoker');
+
+class WaitDecision {
+ constructor() {
+ this.type = 'wait';
+ this.seconds = undefined;
+ this.check = undefined;
+ this.thenCall = undefined;
+ this.otherwise = undefined;
+ this.max = undefined;
+ this.counter = undefined;
+ }
+
+ // The memento shape is:
+ // {
+ // "type": "wait" // the type of the decision
+ // "s": int // "s" = seconds, this is the total wait in seconds (when applicable)
+ // "mx": int // "mx" = max check attempts (when applicable)
+ // "co": int // "co" = counter, used to count the check attempts (when applicable)
+ // "ch": {...} // "ch" = check, the check invoker memento (when applicable)
+ // "ot": {...} // "ot" = otherwise, the otherwise invoker memento (when applicable)
+ // "tc": {...} // "tc" = thenCall invoker memento (when applicable)
+ // }
+
+ setMemento({ s, mx, co, ch, tc, ot, wt } = {}) {
+ this.seconds = s;
+ this.max = mx;
+ this.counter = co;
+ this.title = wt;
+
+ if (ch) this.check = new Invoker().setMemento(ch);
+ if (tc) this.thenCall = new Invoker().setMemento(tc);
+ if (ot) this.otherwise = new Invoker().setMemento(ot);
+
+ return this;
+ }
+
+ getMemento() {
+ const result = {
+ type: 'wait',
+ };
+
+ if (this.seconds !== undefined) result.s = this.seconds;
+ if (this.max !== undefined) result.mx = this.max;
+ if (this.counter !== undefined) result.co = this.counter;
+ if (this.check) result.ch = this.check.getMemento();
+ if (this.thenCall) result.tc = this.thenCall.getMemento();
+ if (this.otherwise) result.ot = this.otherwise.getMemento();
+
+ return result;
+ }
+
+ checkNotBooleanMessage() {
+ const methodName = this.getMethodName(this.check);
+ return `A check function ${methodName}() did not return a boolean.`;
+ }
+
+ maxReachedMessage() {
+ const methodName = this.getMethodName(this.check);
+ return `A "wait" decision with its check function ${methodName}() reached its maximum number of attempts "${this.max}".`;
+ }
+
+ decrement() {
+ if (this.counter !== undefined) {
+ this.counter -= 1;
+ }
+ return this;
+ }
+
+ reachedMax() {
+ if (this.max === undefined) return false;
+ if (this.counter === undefined) return false;
+ return this.counter <= 0;
+ }
+
+ // private
+ getMethodName(invoker = {}) {
+ return invoker.methodName || 'unknown';
+ }
+
+ static is(decisionMemento = {}) {
+ return decisionMemento.type === 'wait';
+ }
+}
+
+module.exports = WaitDecision;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/invoker.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/invoker.js
new file mode 100644
index 0000000000..f53337c210
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/invoker.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.
+ */
+
+const _ = require('lodash');
+
+// The memento shape is:
+// {
+// "m": string // "m" = method name
+// "p": json string // "p" = parameters in json
+// }
+class Invoker {
+ constructor(methodName, ...params) {
+ this.methodName = methodName;
+ const result = [];
+ _.forEach([...params], item => {
+ if (item instanceof Error) {
+ const obj = {};
+ _.forEach(Object.keys(item), key => {
+ obj[key] = item[key];
+ });
+ Object.getOwnPropertyNames(item).forEach(key => {
+ obj[key] = item[key];
+ });
+
+ result.push(obj);
+ } else result.push(item);
+ });
+ this.params = JSON.stringify(result);
+ }
+
+ setMemento({ m, p = '[]' }) {
+ this.methodName = m;
+ this.params = p;
+
+ return this;
+ }
+
+ getMemento() {
+ return {
+ m: this.methodName,
+ p: this.params,
+ };
+ }
+
+ async invoke(stepImplementation, ...rightParams) {
+ if (!this.methodName) return undefined;
+ const leftParams = JSON.parse(this.params);
+ const fn = stepImplementation[this.methodName];
+ if (!_.isFunction(fn)) throw new Error(`Was trying to call ${this.methodName}(), but this method is not defined.`);
+ return fn.call(stepImplementation, ...leftParams, ...rightParams);
+ }
+}
+
+module.exports = Invoker;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-base.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-base.js
new file mode 100644
index 0000000000..3199949835
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-base.js
@@ -0,0 +1,157 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const StepConfig = require('./step-config');
+const WaitDecisionBuilder = require('./decisions/wait-decision-builder');
+const PauseDecisionBuilder = require('./decisions/pause-decision-builder');
+const CallDecision = require('./decisions/call-decision');
+const GoToDecision = require('./decisions/goto-decision');
+const Invoker = require('./invoker');
+const KeyGetterDelegate = require('../helpers/key-getter-delegate');
+
+class StepBase {
+ constructor({ input, workflowInstance, workflowPayload, stepState, step, stepReporter, workflowStatus }) {
+ this.input = input;
+ this.workflowInstance = workflowInstance;
+ this.workflowPayload = workflowPayload; // private, use this.payload instead
+ this.workflowStatus = workflowStatus;
+ this.state = stepState;
+ this.step = step;
+ this.reporter = stepReporter;
+ this.config = new StepConfig(this.step.configs);
+ this.payload = this.buildPayload(workflowPayload, step);
+
+ const getterDelegate = new KeyGetterDelegate(
+ async key => {
+ let value = await this.payload.getValue(key);
+ if (_.isNil(value)) {
+ const rawConfig = await this.config.spread();
+ value = rawConfig[key];
+ }
+ return value;
+ },
+ { storeTitle: 'Merged Payload and Config for Step' },
+ );
+ this.payloadOrConfig = {};
+ Object.assign(this.payloadOrConfig, getterDelegate.getMethods());
+
+ const rawWorkflowMeta = workflowPayload.meta || {};
+ const getterDelegateForMeta = new KeyGetterDelegate(async key => rawWorkflowMeta[key], {
+ storeTitle: 'Workflow Metadata',
+ });
+ this.meta = {};
+ Object.assign(this.meta, getterDelegateForMeta.getMethods());
+ }
+
+ wait(seconds) {
+ return new WaitDecisionBuilder(seconds);
+ }
+
+ pause(seconds) {
+ return new PauseDecisionBuilder(seconds);
+ }
+
+ thenGoToStep(stepIndex) {
+ return new GoToDecision(stepIndex);
+ }
+
+ thenCall(methodName, ...params) {
+ return new CallDecision(new Invoker(methodName, ...params));
+ }
+
+ print(message, ...params) {
+ return this.reporter.print(message, ...params);
+ }
+
+ async statusMessage(message) {
+ return this.reporter.statusMessage(message);
+ }
+
+ async clearStatusMessage() {
+ return this.reporter.clearStatusMessage();
+ }
+
+ async clearPreviousStepsErrors() {
+ this.workflowStatus.clearErrors();
+ }
+
+ printError(error, ...params) {
+ return this.reporter.printError(error, ...params);
+ }
+
+ /**
+ * Returns a plain JavaScript object containing all key/value passed in the "meta"
+ * @returns {Promise<[unknown]>}
+ */
+ async toMetaContent() {
+ return this.workflowPayload.meta || {};
+ }
+
+ /**
+ * Returns a plain JavaScript object containing all key/value accumulated in the workflow payload so far
+ * @returns {Promise<[unknown]>}
+ */
+ async toPayloadContent() {
+ return this.workflowPayload.toPayloadContent();
+ }
+
+ // private
+ buildPayload(workflowPayload, step) {
+ const methodNames = [
+ 'load',
+ 'save',
+ 'string',
+ 'number',
+ 'boolean',
+ 'object',
+ 'optionalString',
+ 'optionalNumber',
+ 'optionalBoolean',
+ 'optionalObject',
+ 'getStepPayload',
+ ];
+ const payload = {};
+ _.forEach(methodNames, name => {
+ payload[name] = workflowPayload[name].bind(workflowPayload);
+ });
+
+ payload.getValue = async key => {
+ return workflowPayload.getValue(key);
+ };
+
+ payload.setKey = async (key, value) => {
+ const stepPayload = await workflowPayload.getStepPayload(step);
+ return stepPayload.setKey(key, value);
+ };
+
+ payload.removeKey = async (key, { limited = false } = {}) => {
+ const stepPayload = await workflowPayload.getStepPayload(step);
+ if (limited) return stepPayload.removeKey(key);
+ return workflowPayload.removeKey(key);
+ };
+
+ payload.removeAllKeys = async ({ limited = false } = {}) => {
+ const stepPayload = await workflowPayload.getStepPayload(step);
+ if (limited) return stepPayload.removeAllKeys();
+ return workflowPayload.removeAllKeys();
+ };
+
+ return payload;
+ }
+}
+
+module.exports = StepBase;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-config.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-config.js
new file mode 100644
index 0000000000..b46894d34f
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-config.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+const KeyGetterDelegate = require('../helpers/key-getter-delegate');
+
+class StepConfig {
+ constructor(configs = {}) {
+ this.configs = configs;
+ const getterDelegate = new KeyGetterDelegate(async key => this.configs[key], {
+ storeTitle: 'Step configuration',
+ });
+ Object.assign(this, getterDelegate.getMethods());
+ }
+
+ async spread() {
+ return this.configs;
+ }
+
+ // NOTE: all of the following methods are coming from getterDelegate.getMethods()
+ // async string(key)
+ // async number(key)
+ // async boolean(key)
+ // async object(key)
+ // async optionalString(key, defaults)
+ // async optionalNumber(key, defaults)
+ // async optionalBoolean(key, defaults)
+ // async optionalObject(key, defaults)
+}
+
+module.exports = StepConfig;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-loop-provider.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-loop-provider.js
new file mode 100644
index 0000000000..e50c1ec608
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-loop-provider.js
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+
+const DefaultStepLoop = require('./step-loop');
+const EventDelegate = require('../helpers/event-delegate');
+const { catchIfErrorAsync } = require('../helpers/utils');
+
+// Supported events
+const supportedEvents = ['stepLoopCreated'];
+
+// The memento shape is:
+// {
+// "ci": int // "ci" = current index, the index of the current step in the list of steps for this workflow
+// "sl": {...} // "sl" = stepLoop memento (if there is a current stepLoop)
+// }
+class StepLoopProvider {
+ constructor({ workflowInstance, stepClassProvider, StepLoopClass = DefaultStepLoop, RemoteStepLoopClass } = {}) {
+ // workflowStatus is be provided by the workflow loop via setWorkflowStatus()
+ this.steps = workflowInstance.steps;
+ this.stepClassProvider = stepClassProvider;
+ this.currentIndex = 0;
+ this.eventDelegate = new EventDelegate({ supportedEvents, sponsorName: 'StepLoopProvider' });
+ this.StepLoop = StepLoopClass; // StepLoop is a class
+ this.RemoteStepLoop = RemoteStepLoopClass; // RemoteStepRunner is a class
+ }
+
+ setMemento({ ci = 0, sl = {} } = {}) {
+ this.currentIndex = ci;
+ this.stepLoopMemento = sl;
+ if (this.stepLoop !== undefined) {
+ this.stepLoop.setMemento(sl);
+ }
+ return this;
+ }
+
+ getMemento() {
+ const output = {
+ ci: this.currentIndex,
+ };
+
+ if (this.stepLoop !== undefined) {
+ output.sl = this.stepLoop.getMemento();
+ }
+ return output;
+ }
+
+ setWorkflowStatus(workflowStatus) {
+ this.workflowStatus = workflowStatus;
+ }
+
+ on(name, fn) {
+ this.eventDelegate.on(name, fn);
+ return this;
+ }
+
+ async next() {
+ delete this.stepLoop;
+ delete this.stepLoopMemento;
+ this.currentIndex += 1;
+ }
+
+ async goToStep(stepIndex) {
+ delete this.stepLoop;
+ delete this.stepLoopMemento;
+ if (stepIndex < 0 && stepIndex >= this.steps.length) {
+ throw new Error(
+ `Invalid stepIndex specified to go to. There is no step at the specified stepIndex = ${stepIndex}`,
+ );
+ }
+ this.currentIndex = stepIndex;
+ }
+
+ async getStepLoop() {
+ const { steps = [], workflowStatus, currentIndex, stepClassProvider, StepLoop, RemoteStepLoop } = this;
+
+ if (currentIndex >= steps.length) {
+ delete this.stepLoop;
+ return undefined; // no more steps in the workflow
+ }
+
+ const step = steps[currentIndex];
+ let stepLoop;
+
+ if (step.remote) {
+ stepLoop = new RemoteStepLoop({ step, stepClassProvider, workflowStatus });
+ } else {
+ stepLoop = new StepLoop({ step, stepClassProvider, workflowStatus });
+ }
+
+ if (this.stepLoopMemento !== undefined) stepLoop.setMemento(this.stepLoopMemento);
+ this.stepLoop = stepLoop;
+
+ await catchIfErrorAsync(async () => this.fireEvent('stepLoopCreated', stepLoop));
+ return stepLoop;
+ }
+
+ // private
+ async fireEvent(name, ...params) {
+ return this.eventDelegate.fireEvent(name, ...params);
+ }
+}
+
+module.exports = StepLoopProvider;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-loop.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-loop.js
new file mode 100644
index 0000000000..a8451b3a5d
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-loop.js
@@ -0,0 +1,415 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const { normalizeError, catchIfErrorAsync } = require('../helpers/utils');
+const EventDelegate = require('../helpers/event-delegate');
+const WaitDecision = require('./decisions/wait-decision');
+const PauseDecision = require('./decisions/pause-decision');
+const CallDecision = require('./decisions/call-decision');
+const GoToDecision = require('./decisions/goto-decision');
+
+// A convenient map to shorten and un-shorten the state label
+const stateLabelMap = { s: 'start', w: 'wait', pa: 'pause', l: 'loop', p: 'pass', f: 'fail' };
+const stateLabels = {
+ encode(long) {
+ const map = _.invert(stateLabelMap);
+ return map[long] || 'f';
+ },
+
+ decode(short) {
+ const map = stateLabelMap;
+ return map[short] || 'start';
+ },
+};
+
+// Supported events
+const supportedEvents = [
+ 'stepLoopStarted',
+ 'stepLoopSkipped',
+ 'stepLoopMethodCall',
+ 'stepLoopQueueAdd',
+ 'stepLoopStepPausing',
+ 'stepLoopStepResuming',
+ 'stepLoopRequestingGoTo',
+ 'stepLoopStepMaxPauseReached',
+ 'stepLoopPassed',
+ 'stepLoopFailed',
+ 'beforeStepLoopTick',
+ 'afterStepLoopTick',
+];
+
+class StepLoop {
+ constructor({ step, stepClassProvider, workflowStatus }) {
+ this.step = step;
+ this.stepClassProvider = stepClassProvider;
+ this.workflowStatus = workflowStatus;
+ this.stateLabel = 'start';
+ this.decisionQueue = [];
+ this.eventDelegate = new EventDelegate({ supportedEvents, sponsorName: 'StepLoop' });
+ }
+
+ // The memento shape is:
+ // {
+ // "st": "s|w|pa|l|p|f", // state label
+ // // s="start", w="wait", pa="pause", l="loop", p="pass", f="fail"
+ // "dq": [{ decision memento }, ... ] // "dq" = decision queue memento
+ // }
+
+ setMemento({ st = 's', dq = [] } = {}) {
+ this.stateLabel = stateLabels.decode(st);
+ const supportedDecisions = [WaitDecision, PauseDecision, CallDecision];
+ this.decisionQueue = [];
+
+ dq.forEach(decisionMemento => {
+ let found = false;
+ supportedDecisions.forEach(DecisionClass => {
+ if (DecisionClass.is(decisionMemento)) {
+ const decision = new DecisionClass();
+ decision.setMemento(decisionMemento);
+ this.decisionQueue.push(decision);
+ found = true;
+ return false; // stop the inner loop
+ }
+ return undefined;
+ });
+ if (!found) throw new Error(`The decision loop contains an unknown decision ${decisionMemento}.`);
+ });
+
+ return this;
+ }
+
+ getMemento() {
+ const queue = []; // the memento version of the queue
+ const result = {
+ st: stateLabels.encode(this.stateLabel),
+ };
+
+ _.forEach(this.decisionQueue, decision => {
+ queue.push(decision.getMemento());
+ });
+
+ result.dq = queue;
+ return result;
+ }
+
+ on(name, fn) {
+ this.eventDelegate.on(name, fn);
+ return this;
+ }
+
+ // main user of this property is the workflow loop
+ get logPrefixStr() {
+ if (!this.step) return '["unknown step"]';
+ return this.step.logPrefixStr;
+ }
+
+ get logPrefixObj() {
+ if (!this.step) return {};
+ return this.step.logPrefixObj;
+ }
+
+ async tick() {
+ // we need to check if there were errors in pervious steps
+ // and if so, then we start the loop for this new step only and only if step.skippable = false
+ if (this.shouldSkip() && this.stateLabel === 'start') {
+ await this.safeFireEvent('stepLoopSkipped');
+ return this.passDecision();
+ }
+
+ return this.catchAndReport(async () => {
+ const { stateLabel } = this;
+ this.stepImplementation = undefined;
+ let decision;
+
+ switch (stateLabel) {
+ case 'start':
+ this.stepImplementation = await this.getStepImplementation();
+ await this.fireEvent('stepLoopStarted');
+ decision = await this.stepImplementation.start();
+ return this.processStepDecision(decision);
+ case 'wait':
+ case 'goto':
+ case 'pause':
+ case 'loop':
+ this.stepImplementation = await this.getStepImplementation();
+ return this.processDecisionQueue();
+ case 'pass':
+ throw new Error('Trying to run a step loop that has already passed.');
+ case 'fail':
+ throw new Error('Trying to run a step loop that has already failed.');
+ default:
+ throw new Error(`The step loop has an unsupported "${stateLabel}" state label.`);
+ }
+ });
+ }
+
+ // private
+ async processDecisionQueue() {
+ if (this.decisionQueue.length === 0) throw new Error('No decisions in the step Loop to process.');
+ const decision = this.decisionQueue[0];
+
+ // A helper function that inserts a call decision if we have "thenCall", otherwise,
+ // it returns a pass decision after calling onPass
+ const thenCallOrPass = async () => {
+ this.decisionQueue.shift(); // remove the first element
+ if (decision.thenCall) {
+ this.decisionQueue.unshift(new CallDecision(decision.thenCall));
+ return this.loopDecision();
+ }
+ await this.callOnPass();
+ return this.passDecision();
+ };
+
+ // A helper function that calls the "check" function
+ const callCheckFn = async () => {
+ const impl = this.stepImplementation;
+ await this.safeFireEvent('stepLoopMethodCall', decision.check.methodName);
+ const result = await decision.check.invoke(impl);
+ if (!_.isBoolean(result)) throw new Error(decision.checkNotBooleanMessage());
+ return result;
+ };
+
+ // A helper function that inserts a call decision for the "otherwise" function if it exists,
+ // otherwise, it throws an exceed max attempts error
+ const otherwiseOrError = async () => {
+ this.decisionQueue.shift(); // remove the first element
+ if (decision.otherwise) {
+ this.decisionQueue.unshift(new CallDecision(decision.otherwise));
+ return this.loopDecision();
+ }
+ throw new Error(decision.maxReachedMessage());
+ };
+
+ // The logic for processing the decision queue
+ if (WaitDecision.is(decision)) {
+ decision.decrement();
+ if (_.isNil(decision.max)) return thenCallOrPass();
+ const isTrue = await callCheckFn();
+ if (isTrue) return thenCallOrPass();
+ if (decision.reachedMax()) return otherwiseOrError();
+ return this.waitDecision(decision.seconds);
+ }
+
+ if (PauseDecision.is(decision)) {
+ decision.decrement();
+ if (_.isNil(decision.max)) return thenCallOrPass();
+ const isTrue = await callCheckFn();
+ if (isTrue) {
+ await this.safeFireEvent('stepLoopStepResuming', 'resume condition met');
+ return thenCallOrPass();
+ }
+ if (decision.reachedMax()) {
+ await this.safeFireEvent('stepLoopStepMaxPauseReached');
+ return otherwiseOrError();
+ }
+ return this.pauseDecision(decision.seconds);
+ }
+
+ if (CallDecision.is(decision)) {
+ this.decisionQueue.shift(); // remove the first element
+ await this.safeFireEvent('stepLoopMethodCall', decision.methodName);
+ const impl = this.stepImplementation;
+ const stepDecision = await decision.thenCall.invoke(impl);
+ return this.processStepDecision(stepDecision);
+ }
+
+ if (GoToDecision.is(decision)) {
+ this.decisionQueue.shift(); // remove the first element
+ await this.safeFireEvent('stepLoopRequestingGoTo', decision.stepIndex);
+ // Call "OnPass" on current step before returning GoTo and resuming workflow from other step
+ await this.callOnPass();
+ return this.goToDecision(decision.stepIndex);
+ }
+
+ throw new Error(`The step loop decision queue contains an unsupported decision "${JSON.stringify(decision)}".`);
+ }
+
+ // private
+ async processStepDecision(possibleDecision) {
+ let decision = possibleDecision;
+ let msg;
+
+ if (_.isEmpty(possibleDecision)) {
+ await this.callOnPass();
+ return this.passDecision();
+ }
+
+ // lets start with the possibility that a step is returning a wait builder instance instead of an instance of a wait decision
+ if (_.isFunction(possibleDecision.toWaitDecision)) {
+ decision = possibleDecision.toWaitDecision();
+ }
+ // if not wait builder, check if the step returned an instance of pause decision builder instead of an instance
+ // of pause decision
+ if (_.isFunction(possibleDecision.toPauseDecision)) {
+ decision = possibleDecision.toPauseDecision();
+ }
+
+ if (WaitDecision.is(decision)) {
+ msg = `StepLoop - adding a wait decision for ${decision.seconds} seconds to the step loop decision queue`;
+ await this.safeFireEvent('stepLoopQueueAdd', msg, decision);
+ this.decisionQueue.push(decision);
+ return this.waitDecision(decision.seconds);
+ }
+
+ if (PauseDecision.is(decision)) {
+ msg = `StepLoop - adding a pause decision for ${decision.seconds} seconds to the step loop decision queue`;
+ await this.safeFireEvent('stepLoopQueueAdd', msg, decision);
+ await this.safeFireEvent('stepLoopStepPausing', decision.pauseReason);
+ this.decisionQueue.push(decision);
+ return this.pauseDecision(decision.seconds);
+ }
+
+ if (CallDecision.is(decision)) {
+ msg = `StepLoop - adding a call decision for ${decision.methodName}() to the step loop decision queue`;
+ await this.safeFireEvent('stepLoopQueueAdd', msg, decision);
+ this.decisionQueue.push(decision);
+ return this.loopDecision();
+ }
+
+ if (GoToDecision.is(decision)) {
+ msg = `StepLoop - adding a goto decision to execute workflow from step at index ${decision.stepIndex} to the step loop decision queue`;
+ await this.safeFireEvent('stepLoopQueueAdd', msg, decision);
+ await this.safeFireEvent('stepLoopRequestingGoTo', decision.stepIndex);
+
+ this.decisionQueue.push(decision);
+ return this.goToDecision(decision.stepIndex);
+ }
+
+ throw new Error(`The step returned an unsupported decision/return object "${JSON.stringify(decision)}".`);
+ }
+
+ // private
+ shouldSkip() {
+ const hasErrors = this.workflowStatus.hasErrors();
+ return hasErrors && this.step.skippable;
+ }
+
+ // private
+ async getStepImplementation() {
+ const { step, stepClassProvider, workflowStatus } = this;
+ const impl = await stepClassProvider.getClass({ step, workflowStatus });
+ if (_.isNil(impl)) throw new Error('The step does not have an implementation');
+ step.implementation = impl; // create a reference to step implementation so that it can be accessed by reporter
+ if (_.isFunction(impl.initStep)) await impl.initStep();
+ if (!_.isFunction(impl.start)) throw new Error('The step does not have "start" method.');
+ return impl;
+ }
+
+ // private
+ async catchAndReport(fn) {
+ let afterStepLoopTickCalled = false;
+
+ try {
+ await this.fireEvent('beforeStepLoopTick');
+ const result = await fn();
+ if (_.isEmpty(result)) throw new Error('No results were returned when running one step loop tick.');
+ afterStepLoopTickCalled = true;
+ await this.fireEvent('afterStepLoopTick');
+ return result;
+ } catch (error) {
+ if (!afterStepLoopTickCalled) await this.fireEvent('afterStepLoopTick');
+ return this.callOnFail(error);
+ }
+ }
+
+ // private
+ async callOnPass() {
+ // clear the decision queue
+ this.decisionQueue = [];
+ const impl = this.stepImplementation;
+
+ if (impl && _.isFunction(impl.onPass)) {
+ await this.safeFireEvent('stepLoopMethodCall', 'onPass');
+ await impl.onPass();
+ }
+ await this.fireEvent('stepLoopPassed');
+ }
+
+ // private
+ async callOnFail(error) {
+ // clear the decision queue
+ this.decisionQueue = [];
+ const normalized = normalizeError(error);
+ const impl = this.stepImplementation;
+ let stepFailedEventCalled = false;
+
+ try {
+ if (impl && _.isFunction(impl.onFail)) {
+ await this.safeFireEvent('stepLoopMethodCall', 'onFail');
+ await impl.onFail(error);
+ }
+
+ stepFailedEventCalled = true;
+ await this.fireEvent('stepLoopFailed', normalized);
+ return this.failDecision(normalized);
+ } catch (error2) {
+ if (!stepFailedEventCalled) await this.fireEvent('stepLoopFailed', normalizeError(error2));
+ return this.failDecision(error2);
+ }
+ }
+
+ // private
+ async fireEvent(name, ...params) {
+ return this.eventDelegate.fireEvent(name, ...params);
+ }
+
+ // private
+ // Same as fireEvent but wrapped with catchIfErrorAsync. Sometime we do not want to catch the ignore the exceptions
+ // from the listeners and sometime we want to do that.
+ async safeFireEvent(name, ...params) {
+ return catchIfErrorAsync(async () => this.fireEvent(name, ...params));
+ }
+
+ // private
+ passDecision() {
+ this.stateLabel = 'pass';
+ return { type: 'pass' };
+ }
+
+ // private
+ waitDecision(waitInSeconds = 1) {
+ this.stateLabel = 'wait';
+ return { type: 'wait', wait: waitInSeconds };
+ }
+
+ // private
+ pauseDecision(waitInSeconds = 1) {
+ this.stateLabel = 'pause';
+ return { type: 'pause', wait: waitInSeconds };
+ }
+
+ // private
+ goToDecision(stepIndex) {
+ this.stateLabel = 'goto';
+ return { type: 'goto', stepIndex };
+ }
+
+ // private
+ loopDecision() {
+ this.stateLabel = 'loop';
+ return { type: 'loop' };
+ }
+
+ // private
+ failDecision(error) {
+ const normalized = normalizeError(error);
+ this.stateLabel = 'fail';
+ return { ...normalized, type: 'fail' };
+ }
+}
+
+module.exports = StepLoop;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-payload.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-payload.js
new file mode 100644
index 0000000000..f485e1e125
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-payload.js
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const KeyGetterDelegate = require('../helpers/key-getter-delegate');
+
+class StepPayload {
+ constructor({ step, workflowInstance }) {
+ this.workflowInstance = workflowInstance;
+ this.step = step;
+ this.content = {};
+ this.dirty = false;
+ this.loaded = false;
+ const getterDelegate = new KeyGetterDelegate(async key => this.content[key], {
+ loadFn: async key => this.load(key),
+ storeTitle: 'Step payload',
+ });
+
+ Object.assign(this, getterDelegate.getMethods());
+ }
+
+ // The memento shape is:
+ // {
+ // ... step payload content key/value pairs
+ // }
+
+ setMemento(content = {}) {
+ this.content = content;
+ return this;
+ }
+
+ getMemento() {
+ return this.content;
+ }
+
+ // NOTE: all of the following methods are coming from getterDelegate.getMethods()
+ // async string(key)
+ // async number(key)
+ // async boolean(key)
+ // async object(key)
+ // async optionalString(key, defaults)
+ // async optionalNumber(key, defaults)
+ // async optionalBoolean(key, defaults)
+ // async optionalObject(key, defaults)
+
+ async load() {
+ // since the store is kept in the memento (for now), there is no need
+ // to load the store from anywhere else
+ if (this.loaded) return;
+ this.loaded = true;
+ }
+
+ async save() {
+ // since the store is kept in the memento (for now), there is no need
+ // to save the store to anywhere else
+ if (!this.dirty) return;
+ this.dirty = false;
+ }
+
+ async spread() {
+ return this.content;
+ }
+
+ async allKeys() {
+ return _.keys(this.content);
+ }
+
+ async hasKey(key) {
+ return _.has(this.content, key);
+ }
+
+ async getValue(key) {
+ return this.content[key];
+ }
+
+ async setKey(key, value) {
+ this.content[key] = value;
+ this.dirty = true;
+ }
+
+ async removeKey(key) {
+ delete this.content[key];
+ this.dirty = true;
+ }
+
+ async removeAllKeys() {
+ this.content = {};
+ this.dirty = true;
+ }
+}
+
+module.exports = StepPayload;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-reporter.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-reporter.js
new file mode 100644
index 0000000000..79dd05f3e0
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-reporter.js
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const { normalizeError, catchIfError } = require('../helpers/utils');
+
+// --------------------------------------------------
+// StepReporter
+// --------------------------------------------------
+class StepReporter {
+ constructor({ workflowReporter, step }) {
+ this.workflowReporter = workflowReporter;
+ this.step = step;
+ this.log = workflowReporter.log || console;
+ this.logPrefixObj = { ...workflowReporter.logPrefixObj, ...step.logPrefixObj };
+ }
+
+ async stepStarted() {
+ this.printStepInformation('StepLoop - step started');
+ }
+
+ async stepSkipped() {
+ this.print('StepLoop - step skipped');
+ }
+
+ async stepPassed() {
+ this.print('StepLoop - step completed');
+ }
+
+ async stepPaused(reasonForPause) {
+ this.print(`StepLoop - step paused, Reason: ${reasonForPause}`);
+ }
+
+ async stepResumed(reasonForResume) {
+ this.print(`StepLoop - step resumed, Reason: ${reasonForResume}`);
+ }
+
+ // error is just an object (not necessarily an instance of Error) with the following two properties:
+ // - message & stack
+ async stepFailed(error) {
+ this.printError(error);
+ this.print('StepLoop - step failed');
+ }
+
+ async statusMessage(message) {
+ this.print(message);
+ }
+
+ async clearStatusMessage() {
+ // empty implementation
+ }
+
+ // prints step information such as src
+ printStepInformation(msg = 'Step information', ...params) {
+ const { workflowReporter } = this;
+ const obj = Object.assign({}, workflowReporter.logPrefixObj, this.step.info, { msg }, ...params);
+ this.logIt(obj);
+ }
+
+ print(msg, ...params) {
+ const obj = Object.assign({}, this.logPrefixObj, { msg }, ...params);
+ this.logIt(obj);
+ }
+
+ printError(raw = {}, ...params) {
+ const error = normalizeError(raw, { maxStackLength: 1000 });
+ this.log.error(raw);
+ const obj = Object.assign(
+ {},
+ this.logPrefixObj,
+ {
+ msg: error.msg || error.message || 'Unknown error',
+ stack: error.stack,
+ },
+ error,
+ ...params,
+ );
+
+ this.logItError(_.omit(obj, ['message']));
+ }
+
+ // private
+ logIt(obj) {
+ catchIfError(() => this.log.info(obj));
+ }
+
+ // private
+ logItError(obj) {
+ catchIfError(() => this.log.error(obj));
+ }
+}
+
+module.exports = StepReporter;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-state-provider.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-state-provider.js
new file mode 100644
index 0000000000..dfae9000d9
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-state-provider.js
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const StepState = require('./step-state');
+
+class StepStateProvider {
+ constructor() {
+ this.mementosStore = {}; // key is the 'stpIndex-stpTemplateId', value is the memento for the step state store
+ this.storeInstances = {}; // key is the 'stpIndex-stpTemplateId', value is the step state store instance
+ }
+
+ // The memento shape is:
+ // {
+ // "ms": {...} // "ms" = mementos store,
+ // where key is the 'stpIndex-stpTemplateId', value is the memento for the step state store
+ // }
+
+ setMemento({ ms = {} } = {}) {
+ this.mementosStore = ms;
+ this.storeInstances = {};
+ return this;
+ }
+
+ getMemento() {
+ const ms = { ...this.mementosStore };
+
+ _.forEach(this.storeInstances, (store, storeId) => {
+ ms[storeId] = store.getMemento();
+ });
+
+ return { ms };
+ }
+
+ async getStepState({ step }) {
+ const storeId = this.getMementoKey(step);
+ const existing = this.storeInstances[storeId];
+ if (existing) return existing;
+
+ const memento = this.mementosStore[storeId] || {};
+ const store = new StepState();
+ store.setMemento(memento);
+ this.storeInstances[storeId] = store;
+
+ return store;
+ }
+
+ // private
+ getMementoKey(step) {
+ return `${step.stpIndex}-${step.stpTmplId}`;
+ }
+}
+
+module.exports = StepStateProvider;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-state.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-state.js
new file mode 100644
index 0000000000..d1dca2baea
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step-state.js
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+const KeyGetterDelegate = require('../helpers/key-getter-delegate');
+
+class StepState {
+ constructor() {
+ this.content = {};
+ this.dirty = false;
+ this.loaded = false;
+ const getterStore = new KeyGetterDelegate(async key => this.content[key], {
+ loadFn: async key => this.load(key),
+ storeTitle: 'Step internal state',
+ });
+ Object.assign(this, getterStore.getMethods());
+ }
+
+ // The memento shape is:
+ // {
+ // ... state store content key/value pairs
+ // }
+
+ setMemento(content = {}) {
+ this.content = content;
+ return this;
+ }
+
+ getMemento() {
+ return this.content;
+ }
+
+ // NOTE: all of the following methods are coming from getterStore.getMethods()
+ // async string(key)
+ // async number(key)
+ // async boolean(key)
+ // async object(key)
+ // async optionalString(key, defaults)
+ // async optionalNumber(key, defaults)
+ // async optionalBoolean(key, defaults)
+ // async optionalObject(key, defaults)
+
+ async load() {
+ // since the content is kept in the memento (for now), there is no need
+ // to load the content from anywhere else
+ if (this.loaded) return;
+ this.loaded = true;
+ }
+
+ async save() {
+ // since the content is kept in the memento (for now), there is no need
+ // to save the content to anywhere else
+ if (!this.dirty) return;
+ this.dirty = false;
+ }
+
+ async spread() {
+ return this.content;
+ }
+
+ async setKey(key, value) {
+ this.content[key] = value;
+ this.dirty = true;
+ }
+
+ async removeKey(key) {
+ delete this.content[key];
+ this.dirty = true;
+ }
+
+ async removeAllKeys() {
+ this.content = {};
+ this.dirty = true;
+ }
+}
+
+module.exports = StepState;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/step/step.js b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step.js
new file mode 100644
index 0000000000..d57f823a81
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/step/step.js
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+// This class is a wrapper over the WorkflowSelectedStep json object
+class Step {
+ // index: the position of this step in the workflow
+ // workflowSelectedStep: the json object containing information about this step
+ constructor({ index, workflowSelectedStep } = {}) {
+ this.index = index;
+ this.selectedStep = workflowSelectedStep;
+ }
+
+ // step template id
+ get stepTemplateId() {
+ return this.selectedStep.stepTemplateId;
+ }
+
+ // step template version
+ get stepTemplateVer() {
+ return this.selectedStep.stepTemplateVer;
+ }
+
+ get title() {
+ return this.selectedStep.title || '';
+ }
+
+ get skippable() {
+ const value = this.selectedStep.skippable;
+ if (_.isNil(value)) return true; // default is true
+ return value;
+ }
+
+ get src() {
+ return this.selectedStep.src || {};
+ }
+
+ // is the step implementation a lambda instead of a builtin implementation
+ get remote() {
+ return !_.isEmpty(this.src.lambdaArn);
+ }
+
+ get lambdaArn() {
+ return this.src.lambdaArn;
+ }
+
+ get overrides() {
+ return this.selectedStep.overrides;
+ }
+
+ get configs() {
+ return this.selectedStep.configs;
+ }
+
+ // a shorthand for step template id
+ get stpTmplId() {
+ return this.stepTemplateId;
+ }
+
+ get stpTmplVer() {
+ return this.stepTemplateVer;
+ }
+
+ get stpIndex() {
+ return this.index;
+ }
+
+ get stpTitle() {
+ return this.title;
+ }
+
+ get logPrefixStr() {
+ return `["${this.stpIndex}"]["${this.stpTmplId}"]["${this.stpTmplVer}"]`;
+ }
+
+ get logPrefixObj() {
+ return { stpIndex: this.index, stpTmplId: this.stepTemplateId, stpTmplVer: this.stepTemplateVer };
+ }
+
+ get info() {
+ return _.pick(this, ['stpIndex', 'stpTmplId', 'stpTmplVer', 'stpTitle', 'src', 'configs', 'overrides']);
+ }
+}
+
+module.exports = Step;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-input.js b/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-input.js
new file mode 100644
index 0000000000..25f9d2814a
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-input.js
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const KeyGetterDelegate = require('./helpers/key-getter-delegate');
+
+class WorkflowInput {
+ constructor({ input }) {
+ this.content = input;
+ const getterDelegate = new KeyGetterDelegate(async key => this.content[key], {
+ storeTitle: 'Workflow input',
+ });
+ Object.assign(this, getterDelegate.getMethods());
+ }
+
+ // NOTE: all of the following methods are coming from getterDelegate.getMethods()
+ // async string(key)
+ // async number(key)
+ // async boolean(key)
+ // async object(key)
+ // async optionalString(key, defaults)
+ // async optionalNumber(key, defaults)
+ // async optionalBoolean(key, defaults)
+ // async optionalObject(key, defaults)
+
+ async hasKey(key) {
+ return _.has(this.content, key);
+ }
+
+ async allKeys() {
+ return _.keys(this.content);
+ }
+
+ async getValue(key) {
+ return this.content[key];
+ }
+}
+
+module.exports = WorkflowInput;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-instance.js b/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-instance.js
new file mode 100644
index 0000000000..57d6ede67a
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-instance.js
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const Step = require('./step/step');
+
+// This class is a wrapper over the WorkflowInstance json object
+class WorkflowInstance {
+ // workflowInstance: the json object containing information about this workflow instance
+ constructor({ workflowInstance = {} } = {}) {
+ this.wfInst = workflowInstance;
+ this.wf = workflowInstance.workflow || {};
+
+ // lets prepare the Steps now
+ this.steps = _.map(this.wf.selectedSteps, (entry, index) => new Step({ index, workflowSelectedStep: entry }));
+ }
+
+ get id() {
+ return this.wfInst.id;
+ }
+
+ get stepAttribs() {
+ return this.wfInst.stAttribs;
+ }
+
+ get workflowId() {
+ return this.wf.id;
+ }
+
+ get workflowVer() {
+ return this.wf.v;
+ }
+
+ get title() {
+ return this.wf.title;
+ }
+
+ // a shorthand form for logging purposes
+ get wfInstId() {
+ return this.wfInst.id;
+ }
+
+ get wfId() {
+ return this.workflowId;
+ }
+
+ get wfVer() {
+ return this.workflowVer;
+ }
+
+ get wfTitle() {
+ return this.title;
+ }
+
+ get logPrefixString() {
+ return `["${this.wfInstId}"]["${this.wfId}"]["${this.wfVer}"]`;
+ }
+
+ get logPrefixObj() {
+ return { wfInstId: this.wfInstId, wfId: this.wfId, wfVer: this.wfVer };
+ }
+
+ get info() {
+ return {
+ ..._.pick(this, ['wfInstId', 'wfId', 'wfVer', 'wfTitle']),
+ steps: _.map(this.steps, step => ({ ...step.logPrefixObj, stpTitle: step.stpTitle, stpConfigs: step.configs })),
+ };
+ }
+}
+
+module.exports = WorkflowInstance;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-loop.js b/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-loop.js
new file mode 100644
index 0000000000..7eaddfaf3f
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-loop.js
@@ -0,0 +1,276 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const { normalizeError } = require('./helpers/utils');
+const WorkflowStatus = require('./workflow-status');
+const EventDelegate = require('./helpers/event-delegate');
+
+// A convenient map to shorten and un-shorten the state label
+const stateLabelMap = { s: 'start', w: 'wait', pa: 'pause', l: 'loop', p: 'pass', f: 'fail' };
+const stateLabels = {
+ encode(long) {
+ const map = _.invert(stateLabelMap);
+ return map[long] || 'f'; // if we can't encode it, then it is 'fail'
+ },
+
+ decode(short) {
+ const map = stateLabelMap;
+ return map[short] || 'start'; // if we can't decode it, then it is 'start'
+ },
+};
+
+// Supported events
+const supportedEvents = [
+ 'workflowStarted',
+ 'workflowPaused',
+ 'workflowResuming',
+ 'workflowIsEmpty',
+ 'workflowPassed',
+ 'workflowFailed',
+ 'beforeWorkflowTick',
+ 'afterWorkflowTick',
+];
+
+// The class WorkflowLoop contains the main tick() logic
+// --------------------------------------------------
+// WorkflowLoop
+// --------------------------------------------------
+// The memento shape is:
+// {
+// "st": "s|w|l|p|f", // state label, used by the workflowLoop to determine where it is
+// // s=start, w=wait, l=loop, p=pass, f=fail
+// "ws": {...}, // "ws" = WorkflowStatus memento
+// }
+class WorkflowLoop {
+ constructor({ workflowInstance, stepLoopProvider }) {
+ this.workflowInstance = workflowInstance;
+ this.stepLoopProvider = stepLoopProvider;
+ this.workflowStatus = new WorkflowStatus({ workflowInstance });
+ this.stepLoopProvider.setWorkflowStatus(this.workflowStatus);
+ this.stepsCount = _.size(this.workflowInstance.steps);
+ this.eventDelegate = new EventDelegate({ supportedEvents, sponsorName: 'WorkflowLoop' });
+ }
+
+ setMemento({ st = 's', ws = {} } = {}) {
+ this.stateLabel = stateLabels.decode(st);
+ this.workflowStatus.setMemento(ws);
+ return this;
+ }
+
+ getMemento() {
+ return {
+ st: stateLabels.encode(this.stateLabel),
+ ws: this.workflowStatus.getMemento(),
+ };
+ }
+
+ async tick() {
+ this.stepLoop = undefined;
+ return this.catchAndReport(async () => {
+ const { stateLabel, stepLoopProvider } = this;
+
+ switch (stateLabel) {
+ case 'start':
+ await this.fireEvent('workflowStarted');
+ this.stepLoop = await stepLoopProvider.getStepLoop();
+ if (_.isEmpty(this.stepLoop)) {
+ // this means that we have a workflow without any steps information
+ await this.fireEvent('workflowIsEmpty');
+ return this.passDecision();
+ }
+ break;
+ case 'wait':
+ case 'pause':
+ this.stepLoop = await stepLoopProvider.getStepLoop();
+ break;
+ case 'loop':
+ this.stepLoop = await stepLoopProvider.getStepLoop();
+ break;
+ case 'pass':
+ throw new Error('Trying to run a workflow loop that has already passed.');
+ case 'fail':
+ throw new Error('Trying to run a workflow loop that has already failed.');
+ default:
+ throw new Error(`The workflowLoop has an unsupported "${stateLabel}" state label.`);
+ }
+
+ if (_.isUndefined(this.stepLoop)) throw new Error('No step loop is found.');
+
+ const decision = await this.stepLoop.tick();
+ if (_.isEmpty(decision) || !_.isObject(decision))
+ throw new Error(`The current step ${this.stepLoop.logPrefixStr} didn't return a decision object.`);
+ return this.processStepLoopDecision(decision, this.stepLoop);
+ });
+ }
+
+ on(name, fn) {
+ this.eventDelegate.on(name, fn);
+ return this;
+ }
+
+ // private
+ async processStepLoopDecision(decision, stepLoop) {
+ const type = decision.type;
+
+ switch (type) {
+ case 'wait':
+ return this.waitDecision(decision.wait);
+ case 'pause':
+ return this.pauseDecision(decision.wait);
+ case 'goto':
+ return this.goToStep(decision.stepIndex);
+ case 'pass':
+ return this.eitherLoopOrPass();
+ case 'loop':
+ return this.loopDecision();
+ case 'fail':
+ return this.eitherFailOrLoopOrPass(_.omit(decision, ['type']));
+ default:
+ throw new Error(`The current step ${stepLoop.logPrefixStr} returned an invalid decision type "${type}".`);
+ }
+ }
+
+ // private
+ async catchAndReport(fn) {
+ let afterWorkflowTickCalled = false;
+ try {
+ await this.fireEvent('beforeWorkflowTick');
+ const result = await fn();
+ if (_.isEmpty(result)) throw new Error('No results were returned when running one workflow loop tick.');
+ afterWorkflowTickCalled = true;
+ await this.fireEvent('afterWorkflowTick');
+ return result;
+ } catch (error) {
+ if (!afterWorkflowTickCalled) await this.fireEvent('afterWorkflowTick');
+ return this.eitherFailOrLoopOrPass(error);
+ }
+ }
+
+ // private
+ // We check with the step loop provider if there is a next step loop, if that is the case
+ // then we make a loopDecision otherwise we make a passDecision to end the workflow loop
+ async eitherLoopOrPass() {
+ const { stepLoopProvider } = this;
+ // when a step is passed, we move to the next one in the next tick, if there are more step loops
+ await stepLoopProvider.next(); // advance to the next stepLoop
+ const stepLoop = await stepLoopProvider.getStepLoop();
+ if (_.isEmpty(stepLoop)) {
+ // this means that we are done, there are no more steps to run
+ return this.passDecision();
+ }
+ return this.loopDecision();
+ }
+
+ // private
+ async goToStep(stepIndex) {
+ const { stepLoopProvider } = this;
+ // when a step has requested workflow to go to a specific step, we move to the specified step in the next tick
+ await stepLoopProvider.goToStep(stepIndex); // navigate to the specified stepLoop
+ const stepLoop = await stepLoopProvider.getStepLoop();
+ if (_.isEmpty(stepLoop)) {
+ // this means that the "stepIndex" specified here to go to points to a non existent step
+ throw new Error('Trying to go to a non-existent workflow step.');
+
+ // TODO: May be this can be treated as a request to terminate workflow.
+ // If yes, instead of throwing error here, we can create new termination decision and return that from here
+ }
+
+ // The goto decision in workflow-loop manifests itself as loopDecision to the main loop
+ // (i.e., to the workflow loop runner) so return loopDecision from here
+ return this.loopDecision();
+ }
+
+ // private
+ // We increment the error counter, if the error counter is more than the steps, then we make a failDecision
+ // to avoid an infinite loop, otherwise we do either a loopDecision or passDecision depending on if we have
+ // more step loops to process
+ async eitherFailOrLoopOrPass(error = {}) {
+ const { workflowStatus, stepLoop, stepsCount } = this;
+ workflowStatus.addError(error, stepLoop);
+
+ if (workflowStatus.errorCount > stepsCount) {
+ // we do this to avoid a possible infinite loop
+ const tooManyErrors = new Error(
+ `Too many errors (more than the number of steps "${stepsCount}") inside the workflow loop. Exiting.`,
+ );
+ return this.failDecision(tooManyErrors);
+ }
+
+ return this.eitherLoopOrPass();
+ }
+
+ // private
+ async fireEvent(name, ...params) {
+ return this.eventDelegate.fireEvent(name, ...params);
+ }
+
+ // private
+ async passDecision() {
+ if (this.stateLabel === 'pause') {
+ // if the previous state of the workflow was paused and now it is transitioning to "pass"
+ // then it means the workflow is not paused any more and is resuming execution
+ // fire "workflowResuming" event to notify any listeners for this transition
+ await this.fireEvent('workflowResuming');
+ }
+
+ // Before we make this decision we need to see if we have errors in the workflowStatus, if so, then we change the decision
+ // to failDecision picking the last error
+ const { workflowStatus } = this;
+ if (workflowStatus.hasErrors()) {
+ return this.failDecision(workflowStatus.lastError);
+ }
+ await this.fireEvent('workflowPassed');
+ this.stateLabel = 'pass';
+ return { type: 'pass' };
+ }
+
+ // private
+ async waitDecision(waitInSeconds = 1) {
+ this.stateLabel = 'wait';
+ return { type: 'wait', wait: waitInSeconds };
+ }
+
+ // private
+ async pauseDecision(waitInSeconds = 1) {
+ await this.fireEvent('workflowPaused');
+
+ this.stateLabel = 'pause';
+ return { type: 'pause', wait: waitInSeconds };
+ }
+
+ // private
+ async loopDecision() {
+ if (this.stateLabel === 'pause') {
+ // if the previous state of the workflow was paused and now it is transitioning to "loop"
+ // then it means the workflow is not paused any more and is resuming execution
+ // fire "workflowResuming" event to notify any listeners for this transition
+ await this.fireEvent('workflowResuming');
+ }
+ this.stateLabel = 'loop';
+ return { type: 'loop' };
+ }
+
+ // private
+ async failDecision(error) {
+ const normalized = normalizeError(error);
+ await this.fireEvent('workflowFailed', normalized);
+ this.stateLabel = 'fail';
+ return { ...normalized, type: 'fail' };
+ }
+}
+
+module.exports = WorkflowLoop;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-payload.js b/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-payload.js
new file mode 100644
index 0000000000..a64154b089
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-payload.js
@@ -0,0 +1,193 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const KeyGetterDelegate = require('./helpers/key-getter-delegate');
+const StepPayload = require('./step/step-payload');
+
+class WorkflowPayload {
+ constructor({ input, meta, workflowInstance }) {
+ this.input = input;
+ this.workflowInstance = workflowInstance;
+ this.store = []; // [ StepPayload ]
+ this.meta = meta;
+ this.loaded = false;
+
+ // lets build empty StepPayload objects using the information we have about the steps
+ _.forEach(workflowInstance.steps, step => {
+ this.store.push(new StepPayload({ step, workflowInstance }));
+ });
+
+ const getterDelegate = new KeyGetterDelegate(async key => this.getValue(key), {
+ loadFn: async key => this.load(key),
+ storeTitle: 'Workflow payload',
+ });
+ Object.assign(this, getterDelegate.getMethods());
+ }
+
+ // The memento shape is:
+ // {
+ // "s": [ step payload memento, ...] // "s" = store memento,
+ // }
+
+ setMemento({ s = [], m = {} } = {}) {
+ // IMPORTANT and DANGEROUS assumption:
+ // - We assume that the array of the "s" memento has the same step orders, which is a valid assumption
+ // unless we introduce a new feature in which the order of the steps changes after
+ // a tick() (this is unlikely a good feature, anyway)
+ _.forEach(s, (stepPayloadMemento, index) => {
+ if (index >= this.store.length)
+ throw new Error('The number of step payloads in the workflow is not the same as the last tick');
+ this.store[index].setMemento(stepPayloadMemento);
+ });
+ this.meta = m;
+ return this;
+ }
+
+ getMemento() {
+ return {
+ s: _.map(this.store, stepPayload => stepPayload.getMemento()),
+ m: this.meta || {},
+ };
+ }
+
+ get dirty() {
+ return !_.isEmpty(_.find(this.store, ['dirty', true]));
+ }
+
+ // NOTE: all of the following methods are coming from getterDelegate.getMethods()
+ // async string(key)
+ // async number(key)
+ // async boolean(key)
+ // async object(key)
+ // async optionalString(key, defaults)
+ // async optionalNumber(key, defaults)
+ // async optionalBoolean(key, defaults)
+ // async optionalObject(key, defaults)
+
+ async load() {
+ // Since the store is kept in the memento (for now), there is no need
+ // to load the store from anywhere else
+ if (this.loaded) return;
+ this.loaded = true;
+ }
+
+ async save() {
+ // Since the store is kept in the memento (for now), there is no need
+ // to save the store to anywhere else
+ if (!this.dirty) return;
+
+ // Currently, we are looping through each StepPayload and ask it to save itself,
+ // this does not do anything for now and the main reason we are doing this is to ensure
+ // that the StepPayload dirty flag is dealt with correctly
+ /* eslint-disable no-restricted-syntax */
+ for (const stepPayload of this.store) {
+ await stepPayload.save(); // eslint-disable-line no-await-in-loop
+ }
+ /* eslint-enable no-restricted-syntax */
+ }
+
+ // The search is in reverse order, the last step wins while the workflow input is the first to search.
+ // This allows steps to override previous steps keys
+ async getValue(key) {
+ const stores = this.searchableStores();
+ const size = stores.length;
+ let value;
+ let index = 0;
+
+ while (index < size && _.isUndefined(value)) {
+ const stepPayload = stores[index];
+ value = await stepPayload.getValue(key); // eslint-disable-line no-await-in-loop
+ index += 1;
+ }
+
+ return value;
+ }
+
+ async getStepPayload({ stpIndex } = {}) {
+ if (_.isNil(stpIndex) || stpIndex < 0)
+ throw new Error('You are trying to get the step payload but provided incorrect/missing step information');
+ if (stpIndex >= this.store.length) throw new Error(`No step payload is found for step index "${stpIndex}"`);
+
+ return this.store[stpIndex];
+ }
+
+ // Allow for the removal of a key from all steps payloads
+ async removeKey(key) {
+ /* eslint-disable no-restricted-syntax */
+ for (const stepPayload of this.store) {
+ await stepPayload.removeKey(key); // eslint-disable-line no-await-in-loop
+ }
+ /* eslint-enable no-restricted-syntax */
+ }
+
+ // Allow for the removal of all keys from all steps payloads
+ async removeAllKeys() {
+ /* eslint-disable no-restricted-syntax */
+ for (const stepPayload of this.store) {
+ await stepPayload.removeAllKeys(); // eslint-disable-line no-await-in-loop
+ }
+ /* eslint-enable no-restricted-syntax */
+ }
+
+ /**
+ * Returns an array of all keys that are available from the workflow payload so far
+ * @returns {Promise}
+ */
+ async allKeys() {
+ // Explanation:
+ // The "this.searchableStores()" below is an array of stores containing instances of StepPayload and WorkflowInput
+ //
+ // The "_.map(this.store, async store => store.allKeys())" below returns an array of promises where each
+ // promise maps to the keys from the corresponding StepPayload.
+ //
+ // The "await Promise.all(_.map(this.store, async store => store.allKeys())))" below awaits these promises to be
+ // resolved and since each of these promises are resolving to array of keys the resulting array will be array of
+ // arrays containing keys from all stores
+ //
+ // The _.flatten flattens them into a single array
+ // The _.uniq de-duplicates keys
+ const allSearchableStores = this.searchableStores();
+ return _.uniq(_.flatten(await Promise.all(_.map(allSearchableStores, async store => store.allKeys()))));
+ }
+
+ /**
+ * Returns a plain JavaScript object containing all key/value accumulated in the workflow payload so far
+ * @returns {Promise<[unknown]>}
+ */
+ async toPayloadContent() {
+ const allKeys = await this.allKeys();
+ const payloadContent = {};
+ await Promise.all(
+ _.map(allKeys, async key => {
+ payloadContent[key] = await this.getValue(key);
+ }),
+ );
+ return payloadContent;
+ }
+
+ // private
+ // The search is in reverse order, the last step wins while the workflow input is the first to search.
+ // This allows steps to override previous steps keys
+ searchableStores() {
+ const stores = _.reverse(this.store.slice());
+ stores.push(this.input);
+
+ return stores;
+ }
+}
+
+module.exports = WorkflowPayload;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-reporter.js b/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-reporter.js
new file mode 100644
index 0000000000..4b350bf87d
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-reporter.js
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const { normalizeError, catchIfError } = require('./helpers/utils');
+const StepReporter = require('./step/step-reporter');
+
+// --------------------------------------------------
+// WorkflowReporter
+// --------------------------------------------------
+class WorkflowReporter {
+ constructor({ workflowInstance = {}, log = console }) {
+ this.wfInstance = workflowInstance;
+ this.log = log;
+ this.logPrefixObj = workflowInstance.logPrefixObj;
+ }
+
+ async workflowStarted() {
+ this.printWorkflowInformation('WorkflowLoop - workflow started');
+ }
+
+ async workflowPaused() {
+ this.printWorkflowInformation('WorkflowLoop - workflow paused');
+ }
+
+ async workflowResuming() {
+ this.printWorkflowInformation('WorkflowLoop - workflow resuming');
+ }
+
+ async workflowIsEmpty() {
+ this.print('WorkflowLoop - workflow does NOT have any steps to run');
+ }
+
+ async workflowPassed() {
+ this.print('WorkflowLoop - workflow passed');
+ }
+
+ // error is just an object (not necessarily an instance of Error) with the following two properties:
+ // - message & stack
+ async workflowFailed(error) {
+ this.printError(error);
+ this.print('WorkflowLoop - workflow failed');
+ }
+
+ // prints workflow information such as title, steps, etc.
+ printWorkflowInformation(msg = 'Workflow information', ...params) {
+ const { wfInstance } = this;
+ const obj = Object.assign({}, wfInstance.info, { msg }, ...params);
+ this.logIt(obj);
+ }
+
+ print(msg, ...params) {
+ const obj = Object.assign({}, this.logPrefixObj, { msg }, ...params);
+ this.logIt(obj);
+ }
+
+ printError(raw = {}, ...params) {
+ const error = normalizeError(raw, { maxStackLength: 1000 });
+ const obj = Object.assign(
+ {},
+ this.logPrefixObj,
+ error,
+ {
+ logLevel: 'error',
+ msg: error.msg || error.message || 'Unknown error',
+ stack: error.stack,
+ },
+ error,
+ ...params,
+ );
+
+ this.logItError(_.omit(obj, ['message']));
+ }
+
+ getStepReporter({ step }) {
+ return new StepReporter({ workflowReporter: this, step });
+ }
+
+ // private
+ logIt(obj) {
+ catchIfError(() => this.log.info(obj));
+ }
+
+ // private
+ logItError(obj) {
+ catchIfError(() => this.log.error(obj));
+ }
+}
+
+module.exports = WorkflowReporter;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-status.js b/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-status.js
new file mode 100644
index 0000000000..aefc7f05d2
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/lib/workflow-status.js
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const { normalizeError } = require('./helpers/utils');
+
+class WorkflowStatus {
+ constructor({ workflowInstance }) {
+ this.workflowInstance = workflowInstance;
+ this.errorCount = 0;
+ this.errors = [];
+ this.logPrefixObj = { ...workflowInstance.logPrefixObj };
+ }
+
+ // The memento shape is:
+ // {
+ // "er": [ {msg, stack, wfInstId, wfId, wfVer, stpIndex*, stpTmplId*, stpTmplVer*} ] // "er" = errors mementos
+ // // stpIndex, stpTmplId and stpTmplVer are available only if the error came from a step loop
+ // "ec": int // "ec" = error count
+ // }
+
+ setMemento({ er = [], ec = 0 } = {}) {
+ this.errorCount = ec;
+ this.errors = er;
+ return this;
+ }
+
+ getMemento() {
+ return {
+ er: this.errors.slice(),
+ ec: this.errorCount,
+ };
+ }
+
+ hasErrors() {
+ return _.size(this.errors) > 0;
+ }
+
+ clearErrors() {
+ this.errors = [];
+ }
+
+ addError(error, stepLoop) {
+ this.errorCount += 1;
+ let entry = { ...this.logPrefixObj, ...normalizeError(error) };
+ if (stepLoop && _.isObject(stepLoop.logPrefixObj)) {
+ entry = { ...stepLoop.logPrefixObj, ...entry };
+ }
+
+ this.errors.push(entry);
+ }
+
+ get lastError() {
+ if (_.isEmpty(this.errors)) return {};
+ return this.errors[this.errors.length - 1];
+ }
+}
+
+module.exports = WorkflowStatus;
diff --git a/addons/addon-base-workflow/packages/workflow-engine/package.json b/addons/addon-base-workflow/packages/workflow-engine/package.json
new file mode 100644
index 0000000000..b5eb8e28d6
--- /dev/null
+++ b/addons/addon-base-workflow/packages/workflow-engine/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "@aws-ee/workflow-engine",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A simple workflow engine",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.1.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base/packages/serverless-backend-tools/.eslintrc.json b/addons/addon-base/packages/serverless-backend-tools/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/addons/addon-base/packages/serverless-backend-tools/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base/packages/serverless-backend-tools/.gitignore b/addons/addon-base/packages/serverless-backend-tools/.gitignore
new file mode 100644
index 0000000000..659959de8f
--- /dev/null
+++ b/addons/addon-base/packages/serverless-backend-tools/.gitignore
@@ -0,0 +1,16 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/addons/addon-base/packages/serverless-backend-tools/.prettierrc.json b/addons/addon-base/packages/serverless-backend-tools/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base/packages/serverless-backend-tools/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base/packages/serverless-backend-tools/index.js b/addons/addon-base/packages/serverless-backend-tools/index.js
new file mode 100644
index 0000000000..ff59f8960b
--- /dev/null
+++ b/addons/addon-base/packages/serverless-backend-tools/index.js
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+const chalk = require('chalk');
+const aws = require('aws-sdk');
+
+const LambdasOverrider = require('./lib/lambdas-overrider');
+
+class BackendTools {
+ constructor(serverless, options) {
+ this.serverless = serverless;
+ this.options = options;
+
+ this.hooks = {
+ 'before:offline:start': this.resolveOverrides.bind(this), // event fired by serverless-offline
+ 'before:invoke:local:loadEnvVars': this.resolveOverrides.bind(this), // event fired by the invoke plugin
+ };
+
+ this.cli = {
+ raw(message) {
+ serverless.cli.consoleLog(chalk.dim(message));
+ },
+ log(prefix = '', message) {
+ serverless.cli.consoleLog(`${prefix} ${chalk.yellowBright(message)}`);
+ },
+ warn(prefix = '', message) {
+ serverless.cli.consoleLog(`${prefix} ${chalk.redBright(message)}`);
+ },
+ };
+
+ this.lambdasOverrider = new LambdasOverrider({ serverless, options });
+ }
+
+ async resolveOverrides() {
+ const awsInstance = this.prepareAws();
+ return this.lambdasOverrider.overrideEnvironments({ aws: awsInstance });
+ }
+
+ prepareAws() {
+ const profile = this.serverless.service.custom.settings.awsProfile;
+ const region = this.serverless.service.custom.settings.awsRegion;
+ const credentials = new aws.SharedIniFileCredentials({ profile });
+
+ // setup profile and region
+ process.env.AWS_REGION = region;
+ process.env.AWS_PROFILE = profile;
+
+ aws.config.update({
+ maxRetries: 3,
+ region,
+ sslEnabled: true,
+ credentials,
+ });
+
+ return aws;
+ }
+}
+
+module.exports = BackendTools;
diff --git a/addons/addon-base/packages/serverless-backend-tools/jest.config.js b/addons/addon-base/packages/serverless-backend-tools/jest.config.js
new file mode 100644
index 0000000000..60c221fc1d
--- /dev/null
+++ b/addons/addon-base/packages/serverless-backend-tools/jest.config.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base/packages/serverless-backend-tools/jsconfig.json b/addons/addon-base/packages/serverless-backend-tools/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base/packages/serverless-backend-tools/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base/packages/serverless-backend-tools/lib/lambdas-overrider.js b/addons/addon-base/packages/serverless-backend-tools/lib/lambdas-overrider.js
new file mode 100644
index 0000000000..64bad1972a
--- /dev/null
+++ b/addons/addon-base/packages/serverless-backend-tools/lib/lambdas-overrider.js
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const chalk = require('chalk');
+
+const { formatObject } = require('./utils');
+
+class LambdasOverrider {
+ constructor({ serverless, options }) {
+ this.serverless = serverless;
+ this.options = options;
+ this.cli = {
+ log(message) {
+ serverless.cli.consoleLog(`[backend-tools] ${chalk.yellowBright(message)}`);
+ },
+ warn(message) {
+ serverless.cli.consoleLog(`[backend-tools] ${chalk.redBright(message)}`);
+ },
+ };
+ }
+
+ async overrideEnvironments({ aws }) {
+ const service = this.serverless.service;
+ const stackName = service.provider.stackName;
+ const lambdasOverrides = _.get(service.custom, 'backendTools.environmentOverrides.lambdas', {});
+ const providerOverrides = _.get(service.custom, 'backendTools.environmentOverrides.provider', {});
+ const names = _.keys(lambdasOverrides);
+
+ const stackOutput = await this.getStackOutput({ aws, stackName });
+ const cfnOutput = key => {
+ const value = stackOutput[key];
+ if (_.isUndefined(value))
+ throw new Error(`The stack output "${key}" is not found. You referenced it in environmentOverrides.`);
+ return value;
+ };
+ const resolveIt = value => _.template(value)({ cfnOutput });
+
+ // time to resolve all expressions in the environmentOverrides.provider
+ // and then merge it to the provider.environment
+ const resolvedProvider = {};
+ _.forEach(providerOverrides, (value, key) => {
+ resolvedProvider[key] = resolveIt(value);
+ });
+ service.provider.environment = { ...service.provider.environment, ...resolvedProvider };
+ this.cli.log(`"provider environment:\n${formatObject(service.provider.environment)}\n`);
+
+ // time to resolve all expressions in the environmentOverrides.lambdas
+ // we want a map of the lambda name and all its resolved expressions
+ // example: { 'apiHandler': { 'env1': '', 'env2': ' } , ...}
+ const resolvedMap = {};
+ _.forEach(names, name => {
+ const entries = lambdasOverrides[name].environment || {};
+ const envMap = {};
+ _.forEach(entries, (value, key) => {
+ envMap[key] = resolveIt(value);
+ });
+ resolvedMap[name] = envMap;
+ });
+
+ // we now loop through all the defined functions and override their environments
+ _.forEach(names, name => {
+ const lambdaEntry = _.get(service.functions, name);
+ if (_.isEmpty(lambdaEntry)) {
+ this.cli.warn(`Lambda "${name}" is not defined but yet it was specified in environmentOverrides`);
+ return;
+ }
+ lambdaEntry.environment = { ...lambdaEntry.environment, ...resolvedMap[name] };
+
+ // Now lets check if this is a local invoke
+ if (this.isInvokeLocal(name)) {
+ _.merge(process.env, lambdaEntry.environment);
+ }
+
+ this.cli.log(`"${name}" Lambda environment:\n${formatObject(lambdaEntry.environment)}\n`);
+ });
+ }
+
+ async getStackOutput({ aws, stackName }) {
+ const cfn = new aws.CloudFormation();
+ const raw = await cfn.describeStacks({ StackName: stackName }).promise();
+ const data = _.get(raw, 'Stacks[0]');
+ const outputs = _.get(data, 'Outputs', []);
+
+ const result = {};
+ _.forEach(outputs, item => {
+ result[item.OutputKey] = item.OutputValue;
+ });
+
+ return result;
+ }
+
+ // Here is the deal, the AwsInvokeLocal plugin captures the environments before we get a chance to override it for
+ // local dev. So, the solution is to inject the env variables directly to process.env, just like how the AwsInvokeLocal
+ // is doing it.
+ isInvokeLocal(fnName) {
+ return this.options.f === fnName || this.options.function === fnName;
+ }
+}
+
+module.exports = LambdasOverrider;
diff --git a/addons/addon-base/packages/serverless-backend-tools/lib/utils.js b/addons/addon-base/packages/serverless-backend-tools/lib/utils.js
new file mode 100644
index 0000000000..ddb0a639bf
--- /dev/null
+++ b/addons/addon-base/packages/serverless-backend-tools/lib/utils.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.
+ */
+
+const _ = require('lodash');
+const inspect = require('util').inspect;
+
+// a promise friendly delay function
+function delay(seconds) {
+ return new Promise(resolve => {
+ _.delay(resolve, seconds * 1000);
+ });
+}
+
+function formatObject(obj) {
+ return inspect(obj, { showHidden: false, depth: 7 });
+}
+
+module.exports = {
+ delay,
+ formatObject,
+};
diff --git a/addons/addon-base/packages/serverless-backend-tools/package.json b/addons/addon-base/packages/serverless-backend-tools/package.json
new file mode 100644
index 0000000000..85147d6210
--- /dev/null
+++ b/addons/addon-base/packages/serverless-backend-tools/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@aws-ee/base-serverless-backend-tools",
+ "version": "1.0.0",
+ "private": true,
+ "description": "A collection of serverless commands to help with the backend development and deployment",
+ "author": "aws-ee",
+ "main": "index.js",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "aws-sdk": "^2.647.0",
+ "chalk": "^2.4.2",
+ "fs-extra": "^8.1.0",
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base/packages/serverless-settings-helper/.eslintrc.json b/addons/addon-base/packages/serverless-settings-helper/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base/packages/serverless-settings-helper/.gitignore b/addons/addon-base/packages/serverless-settings-helper/.gitignore
new file mode 100644
index 0000000000..b512c09d47
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/.gitignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/addons/addon-base/packages/serverless-settings-helper/.prettierrc.json b/addons/addon-base/packages/serverless-settings-helper/.prettierrc.json
new file mode 100644
index 0000000000..d3846d96f3
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/.prettierrc.json
@@ -0,0 +1,16 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all",
+ "overrides": [
+ {
+ "files": ["*.yml", "*.yaml"],
+ "options": {
+ "singleQuote": false
+ }
+ }
+ ]
+}
+
diff --git a/addons/addon-base/packages/serverless-settings-helper/README.md b/addons/addon-base/packages/serverless-settings-helper/README.md
new file mode 100644
index 0000000000..abbcc2d97c
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/README.md
@@ -0,0 +1,64 @@
+# @aws-ee/base-serverless-settings-helper
+
+This package provides a helper to merge solution settings files with local and solution-level defaults.
+
+## Example Usage
+
+In `/sls-settings.defaults.yml`:
+
+```yaml
+solutionName: awesome-poc
+dbPrefix: ${opt:stage}-db-
+```
+
+In `/sls-settings.alice.yml`:
+
+```yaml
+awsProfile: awesome-poc-admin
+awsRegion: us-east-1
+dbPrefix: bob-db- # temporarily override to point at Bob's stack.
+```
+
+In `/serverless.yml`:
+
+```yaml
+service: ${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}-website
+provider:
+ name: aws
+ region: ${self:custom.settings.awsRegion}
+ profile: ${self:custom.settings.awsProfile}
+ stackName: ${self:custom.settings.envName}-${self:service}
+custom:
+ settings: ${file(./settings.js):merged}
+resources:
+ - Description: The infrastructure stack for [${self:custom.settings.solutionName}] and env [${self:custom.settings.envName}]
+```
+
+Finally, in `settings.js`:
+
+```javascript
+module.exports.merged = require('@aws-ee/base-serverless-settings-helper').mergeSettings(__dirname, [
+ './sls-settings.defaults.yml',
+ './sls-settings.${stage}.yml',
+]);
+```
+
+Now, if we run `serverless print -s alice`, we get:
+
+```yaml
+service: awesome-poc-website
+provider:
+ region: us-east-1
+ name: aws
+ profile: awesome-poc-admin
+ stackName: alice-awesome-poc-website
+custom:
+ settings:
+ solutionName: awesome-poc
+ envName: alice
+ dbPrefix: bob-db- # Note the override of `alice-db-` with `bob-db-`
+ awsProfile: awesome-poc-admin
+ awsRegion: us-east-1
+resources:
+ - Description: 'The infrastructure stack for [awesome-poc] and env [alice]'
+```
diff --git a/addons/addon-base/packages/serverless-settings-helper/__test__/integration.test.js b/addons/addon-base/packages/serverless-settings-helper/__test__/integration.test.js
new file mode 100644
index 0000000000..4b3594560b
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/__test__/integration.test.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+// const path = require('path');
+// const { promisify } = require('util');
+// const exec = promisify(require('child_process').exec);
+//
+// const exampleDirectory = path.resolve(__dirname, '../examples/basic');
+//
+// const serverlessBin = path.resolve(__dirname, '../node_modules/.bin/sls');
+
+// TODO: The following test fails because the setting helper needs to talk to STS to get current acc info.
+// The setting helper code executes in a separate process and can't be easily mocked. So, if the machine where the test is running does
+// not have default AWS profile or AWS IAM credentials with permissions to call STS available via the default
+// credentials provider chain the test fails
+// TODO: Figure out different way of testing this or mocking
+/*
+test('merges yaml settings files used by serverless', async () => {
+ // const { stdout } = await exec(`${serverlessBin} -s alice print`, {
+ // cwd: exampleDirectory,
+ // });
+ // expect(stdout).toMatchSnapshot();
+});
+*/
+test('TESTS IN THIS FILE ARE TEMPORARILY DISABLED', async () => {
+ // const { stdout } = await exec(`${serverlessBin} -s alice print`, {
+ // cwd: exampleDirectory,
+ // });
+ // expect(stdout).toMatchSnapshot();
+});
diff --git a/addons/addon-base/packages/serverless-settings-helper/examples/basic/config/settings.js b/addons/addon-base/packages/serverless-settings-helper/examples/basic/config/settings.js
new file mode 100644
index 0000000000..3589da61e1
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/examples/basic/config/settings.js
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+module.exports.merged = require('../../../lib').mergeSettings(__dirname, [
+ './sls-settings.defaults.yml', // load defaults first
+ './sls-settings.${stage}.yml', // eslint-disable-line
+]);
diff --git a/addons/addon-base/packages/serverless-settings-helper/examples/basic/config/sls-settings.alice.yml b/addons/addon-base/packages/serverless-settings-helper/examples/basic/config/sls-settings.alice.yml
new file mode 100644
index 0000000000..00d571abf0
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/examples/basic/config/sls-settings.alice.yml
@@ -0,0 +1,4 @@
+awsProfile: awesome-poc-admin
+awsRegion: us-east-1
+awsRegionShortName: euw1
+dbPrefix: bob-db- # temporarily override to point at Bob's stack.
diff --git a/addons/addon-base/packages/serverless-settings-helper/examples/basic/config/sls-settings.defaults.yml b/addons/addon-base/packages/serverless-settings-helper/examples/basic/config/sls-settings.defaults.yml
new file mode 100644
index 0000000000..e41a4cafd4
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/examples/basic/config/sls-settings.defaults.yml
@@ -0,0 +1,3 @@
+solutionName: awesome-poc
+envName: ${opt:stage}
+dbPrefix: ${self:custom.settings.envName}-db-
diff --git a/addons/addon-base/packages/serverless-settings-helper/examples/basic/package.json b/addons/addon-base/packages/serverless-settings-helper/examples/basic/package.json
new file mode 100644
index 0000000000..9ee0254991
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/examples/basic/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@aws-ee/base-my-awesome-poc",
+ "version": "0.0.1",
+ "private": true
+}
diff --git a/addons/addon-base/packages/serverless-settings-helper/examples/basic/serverless.yml b/addons/addon-base/packages/serverless-settings-helper/examples/basic/serverless.yml
new file mode 100644
index 0000000000..d29b8deb51
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/examples/basic/serverless.yml
@@ -0,0 +1,20 @@
+# For full config options, see docs.serverless.com
+# Note that most settings in here come from config/settings/*.yaml
+service: ${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}-website
+
+provider:
+ name: aws
+ region: ${self:custom.settings.awsRegion}
+ profile: ${self:custom.settings.awsProfile}
+ stackName: ${self:custom.settings.envName}-${self:service}
+ # All references beginning with ${self:*, ${opt:*, ${file:*, ${deep:*, and ${cf:* will be resolved by Serverless
+ # All other ${* references will be resolved by CloudFormation
+ # See https://forum.serverless.com/t/getting-handle-accountid-in-serverless-config/946/11 and
+ # See https://github.com/serverless/serverless/issues/5011
+ variableSyntax: '\$\{((((self|opt|deep|cf):)|file)((?!\$\{).)+?)}'
+
+custom:
+ settings: ${file(./config/settings.js):merged}
+
+resources:
+ - Description: The infrastructure stack for [${self:custom.settings.solutionName}] and env [${self:custom.settings.envName}]
diff --git a/addons/addon-base/packages/serverless-settings-helper/jest.config.js b/addons/addon-base/packages/serverless-settings-helper/jest.config.js
new file mode 100644
index 0000000000..60c221fc1d
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/jest.config.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base/packages/serverless-settings-helper/lib/aws-acc-context.js b/addons/addon-base/packages/serverless-settings-helper/lib/aws-acc-context.js
new file mode 100644
index 0000000000..4b497c4cd2
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/lib/aws-acc-context.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.
+ */
+
+const { createAwsSdkClient } = require('./utils');
+
+/**
+ * A function to return some context information about the AWS account being
+ * accessed by the caller.
+ *
+ * @param awsProfile
+ * @param awsRegion
+ * @returns {Promise<{awsAccountId: *}>}
+ */
+async function getAwsAccountInfo(awsProfile, awsRegion) {
+ const stsClient = createAwsSdkClient('STS', awsProfile, { apiVersion: '2011-06-15', region: awsRegion });
+ const response = await stsClient.getCallerIdentity().promise();
+ return {
+ awsAccountId: response.Account,
+ };
+}
+module.exports = { getAwsAccountInfo };
diff --git a/addons/addon-base/packages/serverless-settings-helper/lib/cloud-formation-cross-region-values.js b/addons/addon-base/packages/serverless-settings-helper/lib/cloud-formation-cross-region-values.js
new file mode 100644
index 0000000000..b23525c7b5
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/lib/cloud-formation-cross-region-values.js
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+
+const { createAwsSdkClient } = require('./utils');
+
+const settingRegexp = /\$\{([:._-\w]+)\}/g;
+
+/**
+ * Expands serverless settings in a string. It supports recursive lookups and path-based lookup
+ * with a maximum path length of two items.
+ *
+ * Example:
+ * ```javascript
+ * const env = { greeting: {welcome: 'hello', goodbye: 'bye}, intro: 'welcome', target: '${self:custom.settings.greeting.${self:custom.settings.intro}}' };
+ * substitute(env)('${self:custom.settings.target}-world'); // outputs: 'hello-world'
+ * ```
+ *
+ * @param {object} env an environment object containing keys and string values or an object containing string values.
+ * @throws if a variable is missing in the environment.
+ */
+const settingExpander = settings => {
+ const replacer = (input, limit = 4) => {
+ if (limit <= 0) {
+ return input;
+ }
+ const result = input.replace(settingRegexp, (_, braced) => {
+ const splitChar = braced.includes('.') ? '.' : ':';
+ const parts = braced.split(splitChar);
+ const key = parts[parts.length - 1];
+ if (key in settings && typeof settings[key] !== 'object') {
+ return settings[key];
+ }
+
+ // For now just support path lengths for the lookup of up to two
+ if (parts.length > 1) {
+ const nestedKey = parts[parts.length - 2];
+ if (nestedKey in settings) {
+ return settings[nestedKey][key];
+ }
+ }
+ throw new Error('Failed to expand input - either not found or the settings path length is > 2');
+ });
+
+ return result.match(settingRegexp) ? replacer(result, limit - 1) : result;
+ };
+
+ return replacer;
+};
+
+/**
+ * A function to get cross region CloudFormation outputs. The cloudFormationSettings parameter
+ * contains details of the CloudFormation outputs to retrieve:
+ *
+ * {
+ * ServerlessSettingWithStackName: [{
+ * settingName: 'nameOfServerlessSetting',
+ * outputKey: "StackOutputName"
+ * }]
+ * }
+ *
+ * The name of the stack is resolved from the current settings. The stack outputs are retrieved
+ * and the desired output is merged into the serverless settings with the desired name.
+ *
+ * @param stage
+ * @param awsProfile
+ * @param otherAwsRegion
+ * @param currentSettings
+ * @param cloudFormationSettings
+ * @returns {Promise<{*}>}
+ */
+async function getCloudFormationCrossRegionValues(
+ stage,
+ awsProfile,
+ otherAwsRegion,
+ currentSettings,
+ cloudFormationSettings,
+) {
+ const cf = createAwsSdkClient('CloudFormation', awsProfile, { apiVersion: '2010-05-15', region: otherAwsRegion });
+
+ // The settings for the other CloudFormation stack will be in the other region
+ const settingsForExpansion = { ...currentSettings, stage, awsRegion: otherAwsRegion };
+ const expander = settingExpander(settingsForExpansion);
+
+ const results = await Promise.all(
+ Object.entries(cloudFormationSettings).map(async ([stackSettingNameKey, cfVariables]) => {
+ const StackName = expander(currentSettings[stackSettingNameKey]);
+ const result = await cf.describeStacks({ StackName }).promise();
+ const {
+ Stacks: [{ Outputs }],
+ } = result;
+
+ const settingsToMerge = cfVariables.reduce((previous, { settingName, outputKey }) => {
+ const setting = Outputs.find(({ OutputKey }) => OutputKey === outputKey);
+ return { ...previous, [settingName]: setting.OutputValue };
+ }, {});
+
+ return settingsToMerge;
+ }),
+ );
+
+ return results.reduce((previous, current) => ({ ...previous, ...current }), {});
+}
+exports.getCloudFormationCrossRegionValues = getCloudFormationCrossRegionValues;
diff --git a/addons/addon-base/packages/serverless-settings-helper/lib/index.js b/addons/addon-base/packages/serverless-settings-helper/lib/index.js
new file mode 100644
index 0000000000..1db1445d2f
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/lib/index.js
@@ -0,0 +1,173 @@
+/*
+ * 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.
+ */
+
+const path = require('path');
+const fs = require('fs');
+
+const { getAwsAccountInfo } = require('./aws-acc-context');
+const { getCloudFormationCrossRegionValues } = require('./cloud-formation-cross-region-values');
+
+/**
+ * Expands environment variables in a string.
+ * Use `\$` to escape an otherwise valid substitution expression.
+ *
+ * Example:
+ * ```javascript
+ * const env = { greeting: 'hello', target: 'world', escaped: 'didnotreplace' };
+ * substitute(env)('$greeting, ${target}, \\${escaped}, ${notfound}'); // outputs: 'hello, world, ${escaped}, ${notfound}'
+ * ```
+ *
+ * @param {object} env an environment object containing keys and string values.
+ * @throws if a variable is missing in the environment.
+ */
+const newExpander = (env = process.env) => s =>
+ s.replace(/\\(\$)|\$([_\w]+)|\$\{([_\w]+)\}/g, (_, $, plain, braced) => {
+ if ($) {
+ return $;
+ }
+ const key = plain || braced;
+ const got = env[key];
+ if (typeof got === 'undefined') {
+ throw new Error(`missing substitution: ${JSON.stringify(key)}`);
+ }
+ return got;
+ });
+
+/**
+ * @returns true if a file consists of only blank lines, or if the file is a YAML file that consists of only blank lines and comments.
+ */
+const isEmptyFile = filename => {
+ const text = fs
+ .readFileSync(filename)
+ .toString()
+ .trim();
+ if (text) {
+ const ext = path.extname(filename);
+ if (ext === '.yml' || ext === '.yaml') {
+ const hasContent = text.split(/\r?\n/g).some(x => x.trim() && !x.trim().startsWith('#'));
+ return !hasContent;
+ }
+ return false;
+ }
+ return true;
+};
+
+/**
+ * @param {*} serverless a serverless object.
+ * @param {object} options options to pass to the loader.
+ * @param {boolean|'warn'} options.missingFiles indicates whether missing files are permissible.
+ * @param {boolean|'warn'} options.emptyFiles indicates whether empty files are permissible.
+ */
+const newFileLoader = (serverless, options = {}) => async filename => {
+ const { missingFiles = true, emptyFiles = true } = options;
+ if (missingFiles && !fs.existsSync(filename)) {
+ if (missingFiles === 'warn') {
+ console.warn(`WARNING: missing settings file: ${filename}`); // eslint-disable-line no-console
+ }
+ return {};
+ }
+ try {
+ return await serverless.yamlParser.parse(filename);
+ } catch (err) {
+ // The following is a kludge to support allowing empty settings files.
+ // serverless.yamlParser will throw an exception if the file is empty.
+ // Check if this is the case and return an empty object instead.
+ if (emptyFiles && isEmptyFile(filename)) {
+ if (emptyFiles === 'warn') {
+ console.warn(`WARNING: empty settings file: ${filename}`); // eslint-disable-line no-console
+ }
+ return {};
+ }
+ throw err;
+ }
+};
+
+module.exports = {
+ /**
+ * Usage example:
+ *
+ * In `./settings/.settings.js`:
+ *
+ * ```javascript
+ * module.exports.merged = require('serverless-settings-helper').mergeSettings(
+ * // current working directory for resolving relative paths
+ * __dirname,
+ * // list of YAML or JSON files to require and merge
+ * [
+ * '../../global-settings.yml',
+ * './local-defaults.yml',
+ * './some-other-settings.json',
+ * './${stage}.yml',
+ * ],
+ * )
+ * ```
+ *
+ * In `./serverless.yml`:
+ *
+ * ```yaml
+ * custom:
+ * mySettings: ${file(./settings/.settings.js):merged}
+ * ```
+ *
+ *
+ * @param {string} cwd the current working directory for resolving settings file paths.
+ * @param {string[]} files a list of files to merge.
+ * @param {*} options
+ * @param {boolean|"warn"} options.missingFiles allow missing files.
+ * @param {boolean|"warn"} options.emptyFiles allow empty files.
+ */
+ mergeSettings: (
+ cwd,
+ files,
+ { missingFiles = true, emptyFiles = true, crossRegionCloudFormation } = {},
+ ) => async serverless => {
+ const stage = serverless.variables.options.s || serverless.variables.options.stage || undefined;
+ const loadFile = newFileLoader(serverless, { missingFiles, emptyFiles });
+ const expandVariables = newExpander({ stage });
+ const resolvePath = filename => path.resolve(cwd, filename);
+ const objects = await Promise.all(
+ files
+ .map(expandVariables)
+ .map(resolvePath)
+ .map(loadFile),
+ );
+ const merged = Object.assign({}, ...objects);
+ const mergedSettingsObj =
+ Object.keys(merged).length === 0
+ ? { __suppressValidFileWarning: true } // prevents serverless from complaining about an empty object.
+ : merged;
+
+ const { awsProfile, awsRegion } = mergedSettingsObj;
+
+ // Adding AWS Account Context
+ mergedSettingsObj.awsAccountInfo = await getAwsAccountInfo(awsProfile, awsRegion);
+
+ // Enrich settings with any cross region variables
+ // But only if the user has supplied the otherAwsRegion setting
+ if (crossRegionCloudFormation && 'otherAwsRegion' in mergedSettingsObj) {
+ const { otherAwsRegion } = mergedSettingsObj;
+ const crossRegionSettings = await getCloudFormationCrossRegionValues(
+ stage,
+ awsProfile,
+ otherAwsRegion,
+ mergedSettingsObj,
+ crossRegionCloudFormation,
+ );
+ Object.assign(mergedSettingsObj, crossRegionSettings);
+ }
+
+ return mergedSettingsObj;
+ },
+};
diff --git a/addons/addon-base/packages/serverless-settings-helper/lib/utils.js b/addons/addon-base/packages/serverless-settings-helper/lib/utils.js
new file mode 100644
index 0000000000..e5c9b7e4da
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/lib/utils.js
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+const aws = require('aws-sdk');
+
+/**
+ * A utility function to construct AWS SDK client for various services.
+ * The function configures various options and optionally loads credentials
+ * based on aws credentials profile. If no profile is specified then it
+ * constructs the client without explicitly specified profile to allow the
+ * client to credentials from the default credentials chain
+ *
+ * @param whichClient
+ * @param awsProfile
+ * @param options
+ */
+function createAwsSdkClient(whichClient, awsProfile, options = {}) {
+ options.maxRetries = options.maxRetries || 3;
+ options.sslEnabled = true;
+
+ // if a an AWS SDK profile has been configured, use its credentials
+ if (awsProfile) {
+ const credentials = new aws.SharedIniFileCredentials({ profile: awsProfile });
+ options.credentials = credentials;
+ }
+ return new aws[whichClient](options);
+}
+exports.createAwsSdkClient = createAwsSdkClient;
diff --git a/addons/addon-base/packages/serverless-settings-helper/package.json b/addons/addon-base/packages/serverless-settings-helper/package.json
new file mode 100644
index 0000000000..673d978a42
--- /dev/null
+++ b/addons/addon-base/packages/serverless-settings-helper/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@aws-ee/base-serverless-settings-helper",
+ "private": true,
+ "version": "1.0.0",
+ "description": "This package provides a helper to merge solution settings files with local and solution-level defaults",
+ "author": "aws-ee",
+ "main": "lib/index.js",
+ "license": "Apache-2.0",
+ "files": [
+ "lib"
+ ],
+ "peerDependencies": {
+ "serverless": "^1.60.5"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.0.1",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-jsx-a11y": "^6.2.3",
+ "eslint-plugin-prettier": "^3.1.2",
+ "eslint-plugin-react": "^7.18.3",
+ "eslint-plugin-react-hooks": "^1.7.0",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "serverless": "^1.63.0"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "prepublishOnly": "pnpm run test",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ },
+ "dependencies": {
+ "aws-sdk": "^2.647.0"
+ }
+}
diff --git a/addons/addon-base/packages/services-container/.eslintrc.json b/addons/addon-base/packages/services-container/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/addons/addon-base/packages/services-container/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base/packages/services-container/.gitignore b/addons/addon-base/packages/services-container/.gitignore
new file mode 100644
index 0000000000..659959de8f
--- /dev/null
+++ b/addons/addon-base/packages/services-container/.gitignore
@@ -0,0 +1,16 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/addons/addon-base/packages/services-container/.prettierrc.json b/addons/addon-base/packages/services-container/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base/packages/services-container/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base/packages/services-container/jest.config.js b/addons/addon-base/packages/services-container/jest.config.js
new file mode 100644
index 0000000000..60c221fc1d
--- /dev/null
+++ b/addons/addon-base/packages/services-container/jest.config.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base/packages/services-container/jsconfig.json b/addons/addon-base/packages/services-container/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base/packages/services-container/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base/packages/services-container/lib/boom-error.js b/addons/addon-base/packages/services-container/lib/boom-error.js
new file mode 100644
index 0000000000..885d0ce3ee
--- /dev/null
+++ b/addons/addon-base/packages/services-container/lib/boom-error.js
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+class BoomError extends Error {
+ /**
+ * @param msg Error message
+ * @param safe A flag indicating if this error's message and its payload are
+ * safe to be transferred across system boundaries. If this flag is set to "false"
+ * then the code responsible for translating this error information into boundary
+ * layer error should NOT include the error's "message" and "payload" in the translated error.
+ * For example, if this flag is "false" and if this error is being converted to HTTP response
+ * (here, HTTP response represents a boundary layer error) this error's "message" and "payload"
+ * should be omitted from the HTTP response body.
+ * Note that this flag is a convenience flag for suggestion and the BoomError class itself does
+ * not do any enforcement of this flag. The interpretation of this flag is left to the clients of this class.
+ * @param code Error code
+ * @param status Status code number
+ */
+ constructor(msg = '', safe = false, code = 'badImplementation', status = 500) {
+ super(_.isError(msg) ? msg.message : _.get(msg, 'message', msg || ''));
+
+ this.boom = true;
+ this.code = code;
+ this.status = status;
+ this.safe = safe;
+ if (_.isError(msg)) {
+ this.root = msg;
+ }
+ }
+
+ /**
+ * A method to add extra payload information to the error. This payload can then be used by
+ * the clients to read additional information about the error.
+ * @param payload The payload to add to this error
+ *
+ * @returns {BoomError}
+ */
+ withPayload(payload) {
+ this.payload = payload;
+ return this;
+ }
+
+ cause(root) {
+ this.root = root;
+ return this;
+ }
+}
+
+module.exports = BoomError;
diff --git a/addons/addon-base/packages/services-container/lib/boom.js b/addons/addon-base/packages/services-container/lib/boom.js
new file mode 100644
index 0000000000..80d6d5ea05
--- /dev/null
+++ b/addons/addon-base/packages/services-container/lib/boom.js
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const BoomError = require('./boom-error');
+
+class Boom {
+ constructor() {
+ this.extend(
+ ['badRequest', 400],
+ ['concurrentUpdate', 400],
+ // Make sure you know the difference between forbidden and unauthorized
+ // (see https://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses)
+ ['unauthorized', 401],
+ ['forbidden', 403],
+ ['invalidToken', 403],
+ ['notFound', 404],
+ ['alreadyExists', 400],
+ // Used when a conflicting operation is being performed (e.g. updating an item when a newer
+ // revision of the same is updated by someone else before that)
+ ['outdatedUpdateAttempt', 409],
+ ['timeout', 408],
+ ['badImplementation', 500],
+ ['internalError', 500],
+ );
+ }
+
+ extend(...arr) {
+ _.forEach(arr, item => {
+ if (!_.isArray(item))
+ throw new Error(
+ `You tried to extend boom, but one of the elements you provided is not an array "${item}". You need to pass an array of arrays.`,
+ );
+ this[item[0]] = (msg, safe) => new BoomError(msg, safe, item[0], item[1]);
+ });
+ }
+
+ is(error, code) {
+ return (error || {}).boom && error.code === code;
+ }
+
+ code(error) {
+ return (error || {}).code || '';
+ }
+}
+
+module.exports = Boom;
diff --git a/addons/addon-base/packages/services-container/lib/request-context.js b/addons/addon-base/packages/services-container/lib/request-context.js
new file mode 100644
index 0000000000..db097ca467
--- /dev/null
+++ b/addons/addon-base/packages/services-container/lib/request-context.js
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+// Inspired by the IAM request context idea https://docs.aws.amazon.com/IAM/latest/UserGuide/intro-structure.html
+class RequestContext {
+ constructor() {
+ this.actions = [];
+ this.resources = [];
+ this.authenticated = false;
+ this.principal = undefined;
+ this.attr = {};
+ }
+}
+
+module.exports = RequestContext;
diff --git a/addons/addon-base/packages/services-container/lib/service.js b/addons/addon-base/packages/services-container/lib/service.js
new file mode 100644
index 0000000000..4d337cd517
--- /dev/null
+++ b/addons/addon-base/packages/services-container/lib/service.js
@@ -0,0 +1,349 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const Boom = require('./boom');
+
+/**
+ * This is the base class for any services you write. Here are a few examples of services that extend Service:
+ * ```
+ * // For a service that does not depend on other services and
+ * // does not need any initialization logic and has one method named "doSomething":
+ * class SimpleService extends Service {
+ * doSomething() {
+ * // add logic here
+ * }
+ * }
+ *
+ * // A more realistic example:
+ * class MyService extends Service {
+ * constructor() {
+ * super(); // don't forget to call the super class
+ *
+ * this.dependency(['ns:user', 'ns:aws', 'ns:cache']);
+ * // you are declaring dependency on the user, aws and cache services.
+ * // do not add any initialization logic here, instead you should add your initialization logic in
+ * // the 'init()' method.
+ * }
+ *
+ * async init() {
+ * await super.init(); // don't forget to call the super class
+ * const [ user, aws, cache ] = await this.service([ 'user', 'aws', 'cache']);
+ *
+ * // you can access a setting like this:
+ * const vpcId = this.settings.get('vpc.id');
+ *
+ * // to log a message
+ * this.log.info('This is the init() method of my service');
+ *
+ * // do whatever you need using the user, aws, cache
+ * this.log.info(`My name is ${user.getName()}`);
+ * }
+ * }
+ * ```
+ */
+class Service {
+ /**
+ * In general, you want to override the constructor to declare the service dependency by calling `dependency()` from within
+ * the constructor method. However, any other initialization logic should go to the `init()` method.
+ * So, keep the following in mind:
+ * - You don't have access to any services in the constructor, if you need access to the services
+ * then move your logic to the `init()` method.
+ * - You must call the `super()` constructor.
+ *
+ * For example:
+ * ```
+ * class MyService extends AnotherService {
+ * constructor() {
+ * super(); // don't forget to call the super class
+ *
+ * this.dependency(['user', 'aws', 'cache']);
+ * // you are declaring dependency on the user, aws and cache services.
+ * }
+ *
+ * async init() {
+ * await super.init(); // don't forget to call the super class
+ * const [ user, aws, cache ] = await this.service(['user', 'aws', 'cache']);
+ *
+ * // now do whatever you need using the user, aws, cache
+ * this.log.info(`My name is ${user.getName()}`);
+ * }
+ * }
+ * ```
+ */
+ constructor() {
+ this._deps = {};
+ this._optionalDeps = {};
+ this._boom = new Boom();
+ this.initialized = false;
+ this.initializationPromise = undefined;
+ // some internal instance variables that are used by the base service class
+ // this.serviceName
+ // this.container
+ }
+
+ // Do not override this function, instead override the 'init' function
+ async initService(container, { name }) {
+ // eslint-disable-line consistent-return
+ this.serviceName = name;
+ this.container = container;
+
+ // Guard against a reentry
+ // This could happen if service A has a method M and in that method we get service B
+ // for the first time. If service X does Promise.all with many promises calling service A with method M,
+ // then this results in calling service B init method as many times as the number of promises.
+ if (this.initializationPromise) {
+ return this.initializationPromise;
+ }
+
+ this.initializationPromise = (async () => {
+ if (name !== 'settings' && name !== 'log') {
+ this._settings = await this.container.find('settings');
+ this._log = await this.container.find('log');
+ }
+
+ await this.init();
+ this.initialized = true;
+
+ return this;
+ })();
+
+ return this.initializationPromise;
+ }
+
+ get deps() {
+ return { ...this._deps };
+ }
+
+ get optionalDeps() {
+ return { ...this._optionalDeps };
+ }
+
+ /**
+ * Override this method to include your initialization code. Keep in mind that this method is async.
+ */
+ async init() {
+ // override this method to add your own logic
+ }
+
+ /**
+ * Gives you access to the boom helper object. You can use this helper to return exceptions that follow
+ * the error handling micro pattern.
+ *
+ * There are a few things you can do with this helper:
+ * - You can use one of the existing methods to throw a boom error. There are four built-in error-code based methods:
+ * - `throw this.boom.badRequest('Your internal message goes here');`
+ * - `throw this.boom.forbidden('Your internal message goes here');`
+ * - `throw this.boom.notFound('Your internal message goes here');`
+ * - `throw this.boom.badImplementation('Your internal message goes here');`
+ * - If your service needs to use a different error-code based method, you can extend boom in the service constructor and,
+ * then, use the newly introduced error-code based method anywhere in your service. Let's look at an example:
+ *
+ * ```
+ * class MyService extends Service {
+ * constructor() {
+ * super();
+ * this.boom.extend(['dbError', 500], ['snsError', 500]);
+ * }
+ *
+ * doSomething() {
+ * throw this.boom.dbError('database is snoozing');
+ * // throw this.boom.snsError('sns is out of whack');
+ * }
+ * }
+ * ```
+ */
+ get boom() {
+ return this._boom;
+ }
+
+ /**
+ * Gives you access to the settings service. For example:
+ * ```
+ * const vpcId = this.settings.get('vpc.id');
+ * ```
+ */
+ get settings() {
+ if (!this.initializationPromise)
+ throw new Error('You tried to reference "settings" in a service but the service has not been initialized.');
+ return this._settings;
+ }
+
+ /**
+ * Gives you access to the log service. For example:
+ * ```
+ * this.log.info('sweet!);
+ * ```
+ */
+ get log() {
+ if (!this.initializationPromise)
+ throw new Error('You tried to reference "log" in a service but the service has not been initialized.');
+ return this._log;
+ }
+
+ _enforce(source = {}, target = []) {
+ const normalized = _.concat(target);
+ _.forEach(normalized, name => {
+ if (this.container.isRoot(name)) return;
+ if (!source[name])
+ throw new Error(
+ `The service "${this.serviceName}" tried to access the "${name}" service, but it was not declared as a dependency.`,
+ );
+ });
+ }
+
+ /**
+ * Gain access to a service or services. If you try to access a service that you did not declare as a dependency,
+ * an error is thrown.
+ *
+ * An example of accessing a service:
+ * ```
+ * const [user, aws, cache] = await this.service(['ns:user', 'ns:aws', 'ns:cache']);
+ * const account = await this.service('ns:account);
+ * ```
+ *
+ * @param {string | Array} nameOrNames the name of the service(s) you need access to
+ */
+ async service(nameOrNames) {
+ if (!this.initializationPromise)
+ throw new Error('You tried to use "service()" in a service but the service has not been initialized.');
+ this._enforce(this._deps, nameOrNames);
+ const result = await Promise.all(
+ _.concat(nameOrNames).map(async name => {
+ const service = await this.container.find(name); // eslint-disable-line no-await-in-loop
+ if (!service)
+ throw new Error(
+ `The service "${this.serviceName}" tried to access the "${name}" service, but the "${name}" service was not registered.`,
+ );
+ return service;
+ }),
+ );
+
+ if (!_.isArray(nameOrNames)) return _.head(result);
+ return result;
+ }
+
+ /**
+ * Gain access to a service or services. If you try to access a service that you did not declare as an optional dependency,
+ * an error is thrown. However, if the service itself is not registered, then `undefined` is returned.
+ *
+ * An example of accessing a service:
+ * ```
+ * const [user, aws, cache] = await this.optionalService(['ns:user', 'ns:aws', 'ns:cache']);
+ * const account = await this.optionalService('ns:account);
+ * ```
+ *
+ * @param {string | Array} nameOrNames the name of the service(s) you need access to
+ */
+ async optionalService(nameOrNames) {
+ if (!this.initializationPromise)
+ throw new Error('You tried to use "optionalService()" in a service but the service has not been initialized.');
+ this._enforce(this._optionalDeps, nameOrNames);
+ const result = Promise.all(
+ _.concat(nameOrNames).map(async name => {
+ const service = await this.container.find(name); // eslint-disable-line no-await-in-loop
+ return service;
+ }),
+ );
+
+ if (!_.isArray(nameOrNames)) return _.head(result);
+ return result;
+ }
+
+ /**
+ * Use this method to declare dependency on other services. Call this method inside the constructor of your service.
+ * For example:
+ * ```
+ * class MyService extends AnotherService {
+ * constructor() {
+ * super(); // important: don't forget to call the constructor of the super class
+ *
+ * this.dependency(['ns:user', 'ns:account', 'ns:cache']);
+ * // you are declaring dependency on the user, account and cache services.
+ * // where 'ns' is a string representing the namespace of the service.
+ *
+ * // if you just have one dependency
+ * this.dependency('ns:user');
+ * }
+ * }
+ * ```
+ *
+ * *NOTE*: You can call `dependency()` anytime before the service is initialized, once the service is initialized, calling
+ * `dependency()` will throw an exception.
+ *
+ * @param {string | Array } deps The dependency name(s), see the description for examples.
+ */
+ dependency(deps = []) {
+ if (this.initialized)
+ throw new Error(
+ `You are trying to add dependency to the "${this.serviceName}" service, but the service has already been initialized.`,
+ );
+ const arr = _.concat(deps); // this allows us to receive either one string or an array of strings
+ if (_.isEmpty(arr)) throw new Error('You are trying to add an empty dependency to a service.');
+ _.forEach(arr, item => {
+ if (_.isEmpty(item))
+ throw new Error('You tried to call "dependency()" in a service but you included an empty string.');
+ if (!_.isString(item))
+ throw new Error('You tried to call "dependency()" in a service but you included an item that is not a string.');
+ this._deps[item] = true;
+ });
+ }
+
+ /**
+ * Use this method to declare optional dependency on other services. Call this method inside the constructor of your service.
+ * For example:
+ * ```
+ * class MyService extends AnotherService {
+ * constructor() {
+ * super(); // important: don't forget to call the constructor of the super class
+ *
+ * this.optionalDependency(['ns:user', 'ns:account', 'ns:cache']);
+ * // you are declaring optional dependency on the user, account and cache services.
+ * // where 'ns' is a string representing the namespace of the service.
+ *
+ * // if you just have one dependency
+ * this.optionalDependency('ns:user');
+ * }
+ * }
+ * ```
+ *
+ * *NOTE*: You can call `optionalDependency()` anytime before the service is initialized, once the service is initialized, calling
+ * `optionalDependency()` will throw an exception.
+ *
+ * Once you declare a dependency, you can use gain access to it using 'optionalService()'
+ *
+ * @param {string | Array } deps The optional dependency name(s), see the description for examples.
+ */
+ optionalDependency(deps = []) {
+ if (this.initialized)
+ throw new Error(
+ `You are trying to add optional dependency to the "${this.serviceName}" service, but the service has already been initialized.`,
+ );
+ const arr = _.concat(deps); // this allows us to receive either one string or an array of strings
+ if (_.isEmpty(arr)) throw new Error('You are trying to add an empty optional dependency to a service.');
+ _.forEach(arr, item => {
+ if (_.isEmpty(item))
+ throw new Error('You tried to call "optionalDependency()" in a service but you included an empty string.');
+ if (!_.isString(item))
+ throw new Error(
+ 'You tried to call "optionalDependency()" in a service but you included an item that is not a string.',
+ );
+ this._optionalDeps[item] = true;
+ });
+ }
+}
+
+module.exports = Service;
diff --git a/addons/addon-base/packages/services-container/lib/services-container.js b/addons/addon-base/packages/services-container/lib/services-container.js
new file mode 100644
index 0000000000..698718562b
--- /dev/null
+++ b/addons/addon-base/packages/services-container/lib/services-container.js
@@ -0,0 +1,189 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const toposort = require('toposort');
+
+/**
+ * First of all, a services container has nothing to do with Docker.
+ *
+ * A services container is simply an instance of a JavaScript class that runs inside a node.js unix process.
+ *
+ * It is a container of services that has two main responsibilities:
+ * - A registry for the Service class instances.
+ * - Initializing the services on demand.
+ *
+ * This is a low level class that you usually do not interact with directly with the exception of calling
+ * the `register()` method. Higher level abstractions, such as the API Framework and the Service class, hide most of the
+ * interactions with this class from the rest of your service classes.
+ *
+ * **Usage**
+ *
+ * 
+ */
+class ServicesContainer {
+ /**
+ * @param {Array} roots An array listing all of the root dependencies that are automatically added to all the services.
+ *
+ * An example:
+ *
+ * ```
+ * const container = new ServicesContainer(['settings', 'log']);
+ * ```
+ */
+ constructor(roots = []) {
+ this.roots = {};
+ _.forEach(roots, item => {
+ this.roots[item] = true;
+ });
+
+ // this shape of the serviceMap is:
+ // {
+ // '': { lazy: true (default), instance },
+ // ...
+ // }
+ this.serviceMap = {};
+ this.initialized = false;
+ }
+
+ isRoot(name) {
+ return !!this.roots[name];
+ }
+
+ /**
+ * Register a service with the container.
+ *
+ * @param {string} name The name of the service to register
+ * @param {Service} instance The instance of the service to register
+ * @param {Object} [options] An object containing the option values
+ * @param {boolean} [options.lazy=true] True if the service should be initialized lazily
+ *
+ *
+ * An example:
+ *
+ * ```
+ * container.register('user', , { lazy: false });
+ * ```
+ */
+ register(name, instance, options = {}) {
+ const ops = { lazy: true, ...options, instance };
+ if (this.initialized)
+ throw new Error(
+ `You tried to register a service "${name}" after the service initialization stage had completed.`,
+ );
+ if (_.isEmpty(name)) throw new Error('You tried to register a service, but you did not provide a name.');
+ if (!_.isObject(instance))
+ throw new Error(
+ `You tried to register a service named "${name}", but you didn't provide an instance of the service.`,
+ );
+
+ // don't add a root service to itself otherwise we will be creating a cyclic dependency
+ // also, root dependencies can not depend on each other.
+ if (!this.roots[name]) {
+ // we add all the root dependencies to the service
+ _.forEach(this.roots, (_ignore, rootName) => {
+ if (instance.deps[rootName] || instance.optionalDeps[rootName]) return;
+ instance.dependency(rootName);
+ });
+ }
+
+ this.serviceMap[name] = ops;
+ }
+
+ /**
+ * Initialize the services that are marked with lazy = false.
+ */
+ async initServices() {
+ this.initialized = true;
+ this.validate();
+ const names = _.keys(this.serviceMap);
+ const services = [];
+ /* eslint-disable no-restricted-syntax, no-await-in-loop */
+ for (const name of names) {
+ const meta = this.serviceMap[name];
+ const found = !!meta;
+ if (!found)
+ throw new Error(`The service container could not be initialized because the "${name}" service does not exist`);
+
+ const instance = meta.instance;
+ if (found && !meta.lazy) {
+ if (!instance.initialized) await instance.initService(this, { name }); // eslint-disable-line no-await-in-loop
+ services.push({ name, instance });
+ }
+ }
+ /* eslint-enable no-restricted-syntax, no-await-in-loop */
+
+ return services;
+ }
+
+ /**
+ * Gain access to a service. Returns `undefined` if the service is not found. Throws an exception if you call `find` before the `initServices` was called.
+ *
+ * An example:
+ * ```
+ * const userService = await container.find('ns:user);
+ * ```
+ *
+ * @param {string} name The name of the service that you want to lookup and gain access to.
+ */
+ async find(name) {
+ if (!this.initialized) throw new Error('You tried to call find() before the service container was initialized.');
+ const meta = this.serviceMap[name];
+ if (!meta) return undefined;
+ const instance = meta.instance;
+ if (instance.initialized) return instance;
+ await instance.initService(this, { name });
+
+ return instance;
+ }
+
+ /**
+ * Validates that there is no circular dependencies and returns a list of service names sorted according to the dependency order.
+ * - Throws an exception if there is a circular dependency.
+ * - Throws an exception if a dependency is missing (not applicable for optional dependencies)
+ *
+ * An example:
+ * ```
+ * const list = container.validate();
+ * // list might contain elements as follows:
+ * // [ 'settings', 'user ]
+ * ```
+ */
+ validate() {
+ const edges = [];
+
+ _.forEach(this.serviceMap, (meta, serviceName) => {
+ const instance = meta.instance;
+ _.forEach(instance.deps, (_ignore, name) => {
+ const child = this.serviceMap[name];
+ if (!child)
+ throw new Error(
+ `The "${serviceName}" service has a dependency on the "${name}" service. But the "${name}" service was not registered.`,
+ );
+ edges.push([serviceName, name]);
+ });
+ _.forEach(instance.optionalDeps, (_ignore, name) => {
+ const child = this.serviceMap[name];
+ if (!child) return;
+ edges.push([serviceName, name]);
+ });
+ });
+
+ const ordered = toposort(edges).reverse();
+ return ordered;
+ }
+}
+
+module.exports = ServicesContainer;
diff --git a/addons/addon-base/packages/services-container/package.json b/addons/addon-base/packages/services-container/package.json
new file mode 100644
index 0000000000..36a62df395
--- /dev/null
+++ b/addons/addon-base/packages/services-container/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@aws-ee/base-services-container",
+ "version": "1.0.0",
+ "private": true,
+ "description": "A simple container for services",
+ "author": "aws-ee",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "aws-sdk": "^2.647.0",
+ "toposort": "^2.0.2"
+ },
+ "peerDependencies": {
+ "aws-sdk": "^2.647.0",
+ "lodash": "*"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "lodash": "^4.17.15",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/addons/addon-base/packages/services/.eslintrc.json b/addons/addon-base/packages/services/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/addons/addon-base/packages/services/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/addons/addon-base/packages/services/.gitignore b/addons/addon-base/packages/services/.gitignore
new file mode 100644
index 0000000000..05bd7c6845
--- /dev/null
+++ b/addons/addon-base/packages/services/.gitignore
@@ -0,0 +1,20 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+**/.webpack
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+/coverage/
+.build
diff --git a/addons/addon-base/packages/services/.prettierrc.json b/addons/addon-base/packages/services/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/addons/addon-base/packages/services/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/addons/addon-base/packages/services/jest.config.js b/addons/addon-base/packages/services/jest.config.js
new file mode 100644
index 0000000000..60c221fc1d
--- /dev/null
+++ b/addons/addon-base/packages/services/jest.config.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/addons/addon-base/packages/services/jsconfig.json b/addons/addon-base/packages/services/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/addons/addon-base/packages/services/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/addons/addon-base/packages/services/lib/audit/audit-writer-service.js b/addons/addon-base/packages/services/lib/audit/audit-writer-service.js
new file mode 100644
index 0000000000..c68303ac24
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/audit/audit-writer-service.js
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+/**
+ * Main audit logging writer service implementation that provides a standard interface for writing audit logs.
+ * The service is only responsible for writing audit events.
+ */
+class AuditWriterService extends Service {
+ constructor() {
+ super();
+ this.dependency(['pluginRegistryService']);
+ }
+
+ async init() {
+ this.pluginRegistryService = await this.service('pluginRegistryService');
+ }
+
+ /**
+ * Audit method responsible for writing the specified audit event. The method calls plugins for the "audit" extension
+ * point. Plugins are called and awaited in the same order as returned by the plugin registry.
+ *
+ * The method first prepares the given event by calling the "prepare" method of the plugins. Each plugin gets a chance
+ * to contribute to preparing the given audit event. The plugins can return the given audit event as is or modify it
+ * and return prepared audit event. The audit event returned by the last plugin from the "prepare" method is used as
+ * the effective audit event.
+ *
+ * After audit event is prepared, it writes the audit event by calling the "write" method of the plugins. Each plugin
+ * gets a chance to write the given audit event to their respective persistent layer.
+ *
+ * The method returns a promise that resolves only after all plugins for "audit" extension point are resolved
+ * (i.e., after the event has been written by all audit writer plugins). If any plugin throws an error, the method
+ * stops calling further plugins and fails (i.e., returns a Promise that rejects with the same error).
+ *
+ * @param requestContext The request context object containing principal (caller) information.
+ * The principal's identifier object is expected to be available as "requestContext.principalIdentifier"
+ *
+ * @param auditEvent The audit event.
+ * @param auditEvent.action The action that is being audited.
+ * @param auditEvent.body The body containing some information about the audit event. The body can be any javascript
+ * object containing extra information about the audit event.
+ * @param auditEvent.message Optional user friendly string message. This defaults to "auditEvent.action", if missing.
+ * @param auditEvent.actor Optional JavaScript object containing information about the actor who is performing the
+ * specified action. This defaults to the "requestContext.principalIdentifier" and should NOT be specified in most
+ * cases.
+ * @param auditEvent.timestamp Optional timestamp when the event occurred. This defaults to current time. The
+ * "auditEvent.timestamp" is in Epoch Unix timestamp format.
+ *
+ * @param args Additional arguments to pass to the plugins for the "audit" extension point.
+ *
+ * @returns {Promise<{auditEvent: *, status: string}>}
+ */
+ async write(requestContext, auditEvent, ...args) {
+ return this.writeAuditEvent(requestContext, auditEvent, false, ...args);
+ }
+
+ /**
+ * This method is very similar to the {@link write} method. The method also calls plugins for the "audit" extension
+ * point. Plugins are called and awaited in the same order as returned by the plugin registry.
+ * The main differences are:
+ * - The method fires writing audit event using the plugins and returns right away. The method returns a Promise that
+ * resolve immediately (i.e., does not wait for all plugins to finish writing)
+ * - In case, any plugin fails writing audit event, the method ignores that error and continues invoking other plugins
+ * from the plugin registry (for the "audit" extension point).
+ *
+ * @param requestContext
+ * @param auditEvent
+ * @param args
+ * @returns {Promise<{status: string}>}
+ */
+ async writeAndForget(requestContext, auditEvent, ...args) {
+ this.writeAuditEvent(requestContext, auditEvent, true, ...args).catch(e => this.log.error(e));
+ return { status: 'unknown' };
+ }
+
+ // ---- PROTECTED METHODS ----- //
+ async writeAuditEvent(requestContext, auditEvent, continueOnError, ...args) {
+ const preparedAuditEvent = await this.prepareAuditEvent(requestContext, auditEvent, continueOnError, ...args);
+
+ // Give all plugins a chance to write the audit event to their respective persistent locations
+ // (such as logs, db, ElasticSearch etc)
+ const writtenAuditEvent = await this.pluginRegistryService.visitPlugins(
+ 'audit',
+ 'write',
+ { payload: { requestContext, container: this.container, auditEvent: preparedAuditEvent }, continueOnError },
+ ...args,
+ );
+
+ return { status: 'success', auditEvent: writtenAuditEvent };
+ }
+
+ async prepareAuditEvent(requestContext, auditEvent, continueOnError, ...args) {
+ if (!auditEvent.message) {
+ // If no audit message is specified then default it to "action"
+ auditEvent.message = auditEvent.action;
+ }
+ if (!auditEvent.actor) {
+ // Add actor, if it's not there
+ auditEvent.actor = _.get(requestContext, 'principalIdentifier');
+ }
+ if (auditEvent.timestamp) {
+ // Add timestamp, if it's not there
+ auditEvent.timestamp = Date.now();
+ }
+
+ // Give all plugins a chance in preparing the audit event
+ // Each plugin will receive the following payload object with the shape {requestContext, container, auditEvent}
+ const result = await this.pluginRegistryService.visitPlugins(
+ 'audit',
+ 'prepare',
+ { payload: { requestContext, container: this.container, auditEvent }, continueOnError },
+ ...args,
+ );
+ return result ? result.auditEvent : {};
+ }
+}
+module.exports = AuditWriterService;
diff --git a/addons/addon-base/packages/services/lib/authorization/assertions.js b/addons/addon-base/packages/services/lib/authorization/assertions.js
new file mode 100644
index 0000000000..d8df8fe47e
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/authorization/assertions.js
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Boom = require('@aws-ee/base-services-container/lib/boom');
+
+const boom = new Boom();
+const internalAuthProviderId = 'internal';
+
+const isCurrentUser = (requestContext, username, ns) =>
+ requestContext.principalIdentifier.username === username && requestContext.principalIdentifier.ns === ns;
+
+async function ensureCurrentUserOrAdmin(requestContext, username, ns = internalAuthProviderId) {
+ const isCurrentUserOrAdmin = isCurrentUser(requestContext, username, ns) || requestContext.principal.isAdmin;
+ if (!isCurrentUserOrAdmin) {
+ throw boom.forbidden('You are not authorized to perform this operation', true);
+ }
+}
+
+async function ensureCurrentUser(requestContext, username, ns) {
+ const identifer = requestContext.principalIdentifier;
+
+ if (identifer.username !== username || identifer.ns !== ns) {
+ throw boom.forbidden('You are not authorized to perform this operation on another user', true);
+ }
+}
+
+async function ensureAdmin(requestContext) {
+ const isAdmin = _.get(requestContext, 'principal.isAdmin', false);
+ if (!isAdmin) {
+ throw boom.forbidden('You are not authorized to perform this operation', true);
+ }
+}
+
+async function ensureRoot(requestContext) {
+ const isRoot = _.get(requestContext, 'principal.userType') === 'root';
+ if (!isRoot) {
+ throw boom.forbidden('You are not authorized to perform this operation', true);
+ }
+}
+
+module.exports = {
+ ensureCurrentUserOrAdmin,
+ ensureAdmin,
+ isCurrentUser,
+ ensureCurrentUser,
+ ensureRoot,
+};
diff --git a/addons/addon-base/packages/services/lib/authorization/authorization-plugin-factory.js b/addons/addon-base/packages/services/lib/authorization/authorization-plugin-factory.js
new file mode 100644
index 0000000000..a555b459ec
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/authorization/authorization-plugin-factory.js
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+/**
+ * A utility authorization plugin factory that creates an authorization plugin that delegates authorization to the
+ * specified service. The plugin returned by this factory can be registered with the plugin registry for the "*-authz"
+ * extension points.
+ * The authorization-service invokes these plugins in the same order as they are registered in the plugin registry.
+ * Each plugin instance gets a chance to perform authorization.
+ * -- Each plugin is passed a plain JavaScript object containing the authorization result evaluated so far from other plugins.
+ * -- Each plugin gets a chance to inspect the authorization result (i.e., effect) from previous plugins and return its own authorization effect as "allow" or "deny".
+ * -- The authorization result with effect returned from the last plugin will be used as an effective authorization answer.
+ *
+ * The plugin returned by this factory delegates authorization to the authorization service specified by the
+ * "authorizationServiceName" argument. The plugin looks up the service using the specified "authorizationServiceName"
+ * from the services container.
+ *
+ * @param authorizationServiceName Name of the authorization service in the services container to delegate to.
+ * If a non-existent authorizationServiceName specified then the plugin skips calling the service and returns the
+ * permissions passed to it as is.
+ *
+ * @returns {{authorize: authorize}}
+ */
+const factory = authorizationServiceName => {
+ const plugin = {
+ /**
+ * @param requestContext The request context object containing principal (caller) information.
+ * The principal's identifier object is expected to be available as "requestContext.principalIdentifier" and the
+ *
+ * @param container The services container
+ *
+ * @param resource The resource for which the authorization needs to be performed (Optional).
+ *
+ * @param action The action for which the authorization needs to be performed
+ *
+ * @param effect Initial permission decision (e.g., effect = 'allow' or effect = 'deny'). This is optional argument
+ * as an initial default permissions decision. This will be undefined for the first plugin instance. This will be
+ * populated with permission evaluated from previous plugins in the plugins list (as returned by the plugin registry).
+
+ * @param reason An optional object containing information about a reason for a specific authorization decision.
+ * For example, if an authorization service denies a request it can populate the reason for denial. The reason has
+ * the following shape {message: string, safe: boolean}.
+ * -- The "reason.message" indicates some reasoning message as a string.
+ * -- The "reason.safe" is a flag indicating if it's safe to propagate this authorization reason across the service boundary (e.g., to the UI)
+ *
+ * @param args Other arguments. These arguments are passed "as is" to the underlying authorization service.
+ *
+ * @returns {Promise<{effect: *}|{reason: {message: string, safe: boolean}, effect: string}|{reason: *, resource: *, effect: *, action: *}|{effect: *}>}
+ */
+ authorize: async (requestContext, container, { resource, action, effect, reason }, ...args) => {
+ // if no authorizer is specified then return immediately with the current authorization decision collected so far
+ if (!authorizationServiceName) return { effect, reason }; // guard condition
+
+ // Lookup a service named as the given "authorizerName" after prefixing it with the specified scope
+ const resourceAuthorizationService = await container.find(authorizationServiceName);
+
+ // if no scoped authorizer is found then return immediately with the current authorization decision collected so far
+ if (!resourceAuthorizationService) return { effect, reason }; // guard condition
+
+ // if authorizer is found then give it a chance to perform its own authorization logic
+ const result = await resourceAuthorizationService.authorize(
+ requestContext,
+ { resource, action, effect, reason },
+ ...args,
+ );
+ return result;
+ },
+ };
+
+ return plugin;
+};
+
+module.exports = factory;
diff --git a/addons/addon-base/packages/services/lib/authorization/authorization-service.js b/addons/addon-base/packages/services/lib/authorization/authorization-service.js
new file mode 100644
index 0000000000..245dd1ae90
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/authorization/authorization-service.js
@@ -0,0 +1,161 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const { isDeny, deny } = require('./authorization-utils');
+
+/**
+ * Main authorization service implementation that performs authorization for the specified "action" by calling all the
+ * available plugins from various add-ons for the specified authorization extension point. These plugins are expected to
+ * implement "authorize" method.
+ */
+class AuthorizationService extends Service {
+ constructor() {
+ super();
+ this.dependency(['pluginRegistryService']);
+ }
+
+ /**
+ * Main authorization method responsible for authorizing the specified action.
+ *
+ * @param requestContext The request context object containing principal (caller) information.
+ * The principal's identifier object is expected to be available as "requestContext.principalIdentifier" and the
+ * principal object is expected to be available as "requestContext.principal"
+ *
+ * @param extensionPoint Name of authorization extension point specific to the resource for which the authorization
+ * needs to be performed. The method invokes all plugins registered to the specified extension point giving them a
+ * chance to perform their authorization logic.
+ * -- The plugins are called in the same order as returned by the registry.
+ * -- Each plugin is passed a plain JavaScript object containing the authorization result evaluated so far from previous plugins.
+ * -- Each plugin gets a chance to inspect the authorization result collected so far and return authorization effect as
+ * "allow" or "deny". i.e., each subsequent plugin has a chance to loosen or tighten the permissions returned by previous
+ * plugins.
+ * -- The authorization result with effect returned from the last plugin will be used as an effective authorization answer.
+ * These plugins are expected to implement "authorize" method. The method may be sync or async (i.e., it may return a promise).
+ * If it returns promise, it is awaited before calling the next plugin.
+ *
+ * @param resource The resource for which the authorization needs to be performed (Optional).
+ *
+ * @param action The action for which the authorization needs to be performed
+ *
+ * @param conditions Optional condition function or an array of functions. All conditions are assumed to be connected
+ * by AND i.e., all condition functions must return "allow" for the action to be authorized. These functions are
+ * invoked with the same arguments that the authorizer plugin is invoked with. i.e.,
+ * "(requestContext, container, permissionsSoFar, ...args)".
+ * The "permissionsSoFar" is permissions returned by the previous function in the array. It is an object with the
+ * shape {resource, action, effect, reason}. These condition functions can be sync or async
+ * (i.e., they can return a Promise). If the function returns a promise, it is awaited first before calling the next
+ * function in the array. (i.e., the next function is not invoked until the returned promise either resolves or rejects).
+ * The effective permissions as a result of evaluating all conditions (with implicit AND between them) is passed to
+ * the plugins registered against the specified "extension-point". These plugins can inspect these permissions and
+ * return permission as is or change it. In other words, the plugins can override permissions resulted by evaluating
+ * the conditions. This allows the plugins to loosen/tighten permissions as per their requirements.
+ *
+ * @param args Additional arguments to pass to the plugins for the specified extension point. These arguments are also
+ * passed to the condition functions.
+ *
+ * @returns {Promise<{reason, effect: string}>} A promise that resolves to effective permissions for the specified
+ * action and principal (the principal information is retrieved from requestContext)
+ */
+ async authorize(requestContext, { extensionPoint, resource, action, conditions }, ...args) {
+ const pluginRegistryService = await this.service('pluginRegistryService');
+ const plugins = (await pluginRegistryService.getPluginsWithMethod(extensionPoint, 'authorize')) || [];
+
+ const defaultAuthorizerPlugins = await this.toAuthorizerPlugins(conditions); // convert default authorizer functions to plugin objects
+ const authorizerPlugins = [...defaultAuthorizerPlugins, ...plugins]; // Merge default (inline) authorizers with the authorizer plugins from the registry
+
+ let effectSoFar;
+ let reasonSoFar;
+ // Give each plugin a chance to perform authorization.
+ // -- Each plugin is passed a plain JavaScript object containing the authorization result evaluated so far from other plugins.
+ // -- The plugins are called in the same order as returned by the registry.
+ // -- Each plugin gets a chance to inspect the authorization result collected so far and return authorization effect as "allow" or "deny".
+ // -- The authorization result with effect returned from the last plugin will be used as an effective authorization answer.
+ // eslint-disable-next-line no-restricted-syntax
+ for (const plugin of authorizerPlugins) {
+ const { effect: resultEffect, reason: resultReason } =
+ // need to await in strict order so disabling "no-await-in-loop" rule here
+ // eslint-disable-next-line no-await-in-loop
+ (await plugin.authorize(
+ requestContext,
+ this.container, // Pass services container to the plugins to allow them to find/use any services they require
+ { resource, action, effect: effectSoFar, reason: reasonSoFar }, // pass authorization result so far
+ ...args,
+ )) || {};
+
+ effectSoFar = resultEffect;
+ reasonSoFar = resultReason;
+ }
+
+ // if effectSoFar is still undefined then default to implicit "deny"
+ if (!effectSoFar) {
+ return deny('No plugins returned any permissions so returning implicit deny');
+ }
+
+ return { effect: effectSoFar, reason: reasonSoFar };
+ }
+
+ /**
+ * A method similar to the {@link authorize} method except that this method throws forbidden exception if the
+ * authorization results in "deny".
+ *
+ * @param requestContext
+ * @param extensionPoint
+ * @param resource
+ * @param action
+ * @param conditions
+ * @param args
+ * @returns {Promise}
+ *
+ * @see authorize
+ */
+ async assertAuthorized(requestContext, { extensionPoint, resource, action, conditions }, ...args) {
+ const result = await this.authorize(requestContext, { extensionPoint, resource, action, conditions }, ...args);
+ if (_.toLower(result.effect) !== 'allow') {
+ const isSafe = _.get(result, 'reason.safe');
+ const reasonMessage = _.get(result, 'reason.message');
+ const errorMessage = isSafe && reasonMessage ? reasonMessage : 'You are not authorized to perform this operation';
+ if (!isSafe) {
+ // Make sure to log the original authorization result with denial for troubleshooting, if
+ // the denial reason message is not safe to propagate beyond service boundary.
+ this.log.warn({ extensionPoint, resource, action, ...result });
+ }
+ // if the principal is not authorized to perform the specified action then throw error
+ throw this.boom.forbidden(errorMessage, true);
+ }
+ }
+
+ // Private methods
+ async toAuthorizerPlugins(conditionFns) {
+ if (conditionFns) {
+ const fns = _.isArray(conditionFns) ? conditionFns : [conditionFns];
+ const conditionsAsPlugins = _.map(fns, fn => ({
+ authorize: async (requestContext, container, { resource, action, effect, reason }, ...args) => {
+ // if effect is "deny" already (due to any of the previous plugins returning "deny") then return "deny" right away
+ if (isDeny({ effect })) return { resource, action, effect, reason };
+
+ // If not denied yet then call the condition function
+ return fn(requestContext, { resource, action, effect, reason }, ...args);
+ },
+ }));
+ return conditionsAsPlugins;
+ }
+ return [];
+ }
+}
+
+module.exports = AuthorizationService;
diff --git a/addons/addon-base/packages/services/lib/authorization/authorization-utils.js b/addons/addon-base/packages/services/lib/authorization/authorization-utils.js
new file mode 100644
index 0000000000..c3647a7981
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/authorization/authorization-utils.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.
+ */
+
+const _ = require('lodash');
+
+function isAdmin(requestContext) {
+ return _.get(requestContext, 'principal.isAdmin', false);
+}
+
+function isCurrentUser(requestContext, { username, ns }) {
+ return (
+ _.get(requestContext, 'principalIdentifier.username') === username &&
+ _.get(requestContext, 'principalIdentifier.ns') === ns
+ );
+}
+
+function isCurrentUserOrAdmin(requestContext, { username, ns }) {
+ return isAdmin(requestContext) || isCurrentUser(requestContext, { username, ns });
+}
+
+function isActive(requestContext) {
+ return _.toLower(_.get(requestContext, 'principal.status', '')) === 'active';
+}
+
+function isRoot(requestContext) {
+ return _.get(requestContext, 'principal.userType', '') === 'root';
+}
+
+function allow() {
+ return {
+ effect: 'allow',
+ };
+}
+
+function deny(message, safe = false) {
+ return {
+ effect: 'deny',
+ reason: {
+ message,
+ safe,
+ },
+ };
+}
+
+async function allowIfCurrentUserOrAdmin(requestContext, { action }, { username, ns }) {
+ if (!isCurrentUserOrAdmin(requestContext, { username, ns })) {
+ return deny(`Cannot perform the specified action "${action}". Only admins or current user can.`);
+ }
+ return allow();
+}
+
+async function allowIfCurrentUser(requestContext, { action }, { username, ns }) {
+ if (!isCurrentUser(requestContext, { username, ns })) {
+ return deny(`Cannot perform the specified action "${action}" on other user's resources.`);
+ }
+ return allow();
+}
+
+async function allowIfActive(requestContext, { action }) {
+ // Make sure the current user is active
+ if (!isActive(requestContext)) {
+ return deny(`Cannot perform the specified action "${action}". The caller is not active.`);
+ }
+ return allow();
+}
+
+async function allowIfAdmin(requestContext, { action }) {
+ if (!isAdmin(requestContext)) {
+ return deny(`Cannot perform the specified action "${action}". Only admins can.`);
+ }
+ return allow();
+}
+
+async function allowIfRoot(requestContext, { action }) {
+ if (!isRoot(requestContext)) {
+ return deny(`Cannot perform the specified action "${action}". Only root user can.`);
+ }
+ return allow();
+}
+
+function isAllow({ effect }) {
+ return _.toLower(effect) === 'allow';
+}
+
+function isDeny({ effect }) {
+ return _.toLower(effect) === 'deny';
+}
+
+module.exports = {
+ allow,
+ deny,
+
+ allowIfCurrentUserOrAdmin,
+ allowIfCurrentUser,
+ allowIfActive,
+ allowIfAdmin,
+ allowIfRoot,
+
+ isAllow,
+ isDeny,
+
+ isCurrentUser,
+ isCurrentUserOrAdmin,
+ isAdmin,
+ isActive,
+ isRoot,
+};
diff --git a/addons/addon-base/packages/services/lib/aws/aws-service.js b/addons/addon-base/packages/services/lib/aws/aws-service.js
new file mode 100644
index 0000000000..e91755aacf
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/aws/aws-service.js
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const settingKeys = {
+ awsRegion: 'awsRegion',
+ awsProfile: 'awsProfile',
+ useAwsProfile: 'useAwsProfile',
+};
+
+class AwsService extends Service {
+ async init() {
+ this._sdk = require('aws-sdk'); // eslint-disable-line global-require
+ if (process.env.IS_OFFLINE || process.env.IS_LOCAL) this.prepareForLocal(this._sdk);
+ }
+
+ get sdk() {
+ if (!this.initialized)
+ throw new Error('You tried to use "AwsService.sdk()" but the service has not been initialized.');
+ return this._sdk;
+ }
+
+ prepareForLocal(aws) {
+ const sslEnabled = true;
+ const maxRetries = 3;
+ const useProfile = this.settings.optionalBoolean(settingKeys.useAwsProfile, true);
+
+ let profile;
+ let region;
+ if (process.env.IS_OFFLINE) {
+ // For `sls offline`, get profile from environmentOverrides settings
+ if (useProfile) {
+ profile = this.settings.get(settingKeys.awsProfile);
+ }
+ region = this.settings.get(settingKeys.awsRegion);
+ } else if (process.env.IS_LOCAL) {
+ // For `sls invoke local`, serverless should set AWS_PROFILE from provider -> awsProfile setting
+ profile = process.env.AWS_PROFILE;
+ region = process.env.AWS_REGION || 'us-east-1';
+ }
+
+ if (useProfile) {
+ // see http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#cli-aws-help-config-vars
+ process.env.AWS_PROFILE = profile;
+ }
+ process.env.AWS_DEFAULT_REGION = region;
+ process.env.AWS_REGION = region;
+
+ aws.config.update({
+ region,
+ sslEnabled,
+ maxRetries,
+ logger: console,
+ });
+
+ if (useProfile) {
+ const creds = new aws.SharedIniFileCredentials({ profile });
+ process.env.AWS_ACCESS_KEY_ID = creds.accessKeyId;
+ process.env.AWS_SECRET_ACCESS_KEY = creds.secretAccessKey;
+ if (creds.sessionToken) process.env.AWS_SESSION_TOKEN = creds.sessionToken;
+ aws.config.update({
+ credentials: creds,
+ });
+ }
+ }
+}
+
+module.exports = AwsService;
diff --git a/addons/addon-base/packages/services/lib/db-password/db-password-service.js b/addons/addon-base/packages/services/lib/db-password/db-password-service.js
new file mode 100644
index 0000000000..e74154490c
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/db-password/db-password-service.js
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const crypto = require('crypto');
+const uuid = require('uuid/v4');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { ensureCurrentUserOrAdmin } = require('../authorization/assertions');
+
+const settingKeys = {
+ tableName: 'dbTablePasswords',
+};
+
+class DbPasswordService extends Service {
+ constructor() {
+ super();
+ this.dependency('dbService');
+ }
+
+ async passwordMatchesPasswordPolicy(password) {
+ // TODO: Support more comprehensive and configurable password policy
+ return password && _.isString(password) && password.length >= 4;
+ }
+
+ async assertValidPassword(password) {
+ const isValidPassword = await this.passwordMatchesPasswordPolicy(password);
+ if (!isValidPassword) {
+ throw this.boom.badRequest(
+ 'Can not save password. Invalid password specified. Please specify a valid password with at least 4 characters',
+ true,
+ );
+ }
+ }
+
+ async savePassword(requestContext, { username, password }) {
+ // Assert that the password is valid (i.e., it matches password policy)
+ await this.assertValidPassword(password);
+
+ // Allow only current user or admin to update (or create) the user's password
+ await ensureCurrentUserOrAdmin(requestContext, username);
+
+ const isValidPassword = await this.passwordMatchesPasswordPolicy(password);
+ if (!isValidPassword) {
+ throw this.boom.badRequest(
+ 'Can not save password. ' +
+ 'Invalid password specified. Please specify a valid password with at least 4 characters',
+ true,
+ );
+ }
+
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+ const salt = uuid();
+ const hashed = this.hash({ password, salt });
+
+ await dbService.helper
+ .updater()
+ .table(table)
+ .key({ username })
+ .item({ hashed, salt })
+ .update();
+ }
+
+ async exists({ username, password }) {
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+
+ const item = await dbService.helper
+ .getter()
+ .table(table)
+ .key('username', username)
+ .get();
+
+ if (item === undefined) return false;
+ const hashed = this.hash({ password, salt: item.salt });
+
+ return hashed === item.hashed && username === item.username;
+ }
+
+ hash({ password, salt }) {
+ const hash = crypto.createHash('sha256');
+ hash.update(`${password}${salt}`);
+ return hash.digest('hex');
+ }
+}
+
+module.exports = DbPasswordService;
diff --git a/addons/addon-base/packages/services/lib/db-service.js b/addons/addon-base/packages/services/lib/db-service.js
new file mode 100644
index 0000000000..ee41530c56
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/db-service.js
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const Scanner = require('./db/scanner');
+const Updater = require('./db/updater');
+const Getter = require('./db/getter');
+const Query = require('./db/query');
+const Deleter = require('./db/deleter');
+const unmarshal = require('./db/unmarshal');
+
+class DbService extends Service {
+ constructor() {
+ super();
+ this.dependency('aws');
+ }
+
+ async init() {
+ await super.init();
+ const aws = await this.service('aws');
+ this.dynamoDb = new aws.sdk.DynamoDB({ apiVersion: '2012-08-10' });
+ // Setting convertEmptyValues = true below, without this, if any item is asked to be updated with any attrib containing empty string
+ // the dynamo update operation fails with
+ // "ExpressionAttributeValues contains invalid value: One or more parameter values were invalid: An AttributeValue may not contain an empty string for key :desc" error
+ // See https://github.com/aws/aws-sdk-js/issues/833 for details
+ this.client = new aws.sdk.DynamoDB.DocumentClient({
+ convertEmptyValues: true,
+ });
+
+ this.helper = {
+ unmarshal,
+ scanner: () => new Scanner(this.log, this.client),
+ updater: () => new Updater(this.log, this.client),
+ getter: () => new Getter(this.log, this.client),
+ query: () => new Query(this.log, this.client),
+ deleter: () => new Deleter(this.log, this.client),
+ };
+ }
+}
+
+module.exports = DbService;
diff --git a/addons/addon-base/packages/services/lib/db/deleter.js b/addons/addon-base/packages/services/lib/db/deleter.js
new file mode 100644
index 0000000000..4c91135ffa
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/db/deleter.js
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const unmarshal = require('./unmarshal');
+
+// To handle get operation using DocumentClient
+// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#delete-property
+// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html
+// NOTE: The following properties are legacy and should not be used:
+// - ConditionalOperator
+// - Expected
+
+class DbDeleter {
+ constructor(log = console, client) {
+ this.log = log;
+ this.client = client;
+ this.params = {
+ // ReturnConsumedCapacity: 'INDEXES',
+ };
+ }
+
+ table(name) {
+ if (!_.isString(name) || _.isEmpty(_.trim(name)))
+ throw new Error(`DbDeleter.table("${name}" <== must be a string and can not be empty).`);
+ this.params.TableName = name;
+ return this;
+ }
+
+ // can be either key(key, value) or key({ key1: value1, key2: value2, ...})
+ key(...args) {
+ if (!this.params.Key) this.params.Key = {};
+
+ if (args.length > 1) this.params.Key[args[0]] = args[1];
+ else Object.assign(this.params.Key, ...args);
+ return this;
+ }
+
+ // can be either props(key, value) or props({ key1: value1, key2: value2, ...})
+ props(...args) {
+ if (args.length > 1) this.params[args[0]] = args[1];
+ else Object.assign(this.params, ...args);
+ return this;
+ }
+
+ // same as ConditionExpression
+ condition(str) {
+ if (this.params.ConditionExpression)
+ throw new Error(`DbDeleter.condition("${str}"), you already called condition() before this call.`);
+ this.params.ConditionExpression = str;
+ return this;
+ }
+
+ // same as ExpressionAttributeNames
+ names(obj = {}) {
+ if (!_.isObject(obj)) throw new Error(`DbDeleter.names("${obj}" <== must be an object).`);
+ this.params.ExpressionAttributeNames = {
+ ...this.params.ExpressionAttributeNames,
+ ...obj,
+ };
+ return this;
+ }
+
+ // same as ExpressionAttributeValues
+ values(obj = {}) {
+ if (!_.isObject(obj)) throw new Error(`DbDeleter.values("${obj}" <== must be an object).`);
+ this.params.ExpressionAttributeValues = {
+ ...this.params.ExpressionAttributeValues,
+ ...obj,
+ };
+ return this;
+ }
+
+ // same as ReturnValues: NONE | ALL_OLD
+ return(str) {
+ const upper = str.toUpperCase();
+ const allowed = ['NONE', 'ALL_OLD'];
+ if (!allowed.includes(upper))
+ throw new Error(`DbDeleter.return("${upper}" <== is not a valid value). Only ${allowed.join(',')} are allowed.`);
+ this.params.ReturnValues = upper;
+ return this;
+ }
+
+ // same as ReturnConsumedCapacity
+ capacity(str = '') {
+ const upper = str.toUpperCase();
+ const allowed = ['INDEXES', 'TOTAL', 'NONE'];
+ if (!allowed.includes(upper))
+ throw new Error(
+ `DbDeleter.capacity("${upper}" <== is not a valid value). Only ${allowed.join(',')} are allowed.`,
+ );
+ this.params.ReturnConsumedCapacity = upper;
+ return this;
+ }
+
+ // same as ReturnItemCollectionMetrics
+ metrics(str) {
+ const upper = str.toUpperCase();
+ const allowed = ['NONE', 'SIZE'];
+ if (!allowed.includes(upper))
+ throw new Error(`DbDeleter.metrics("${upper}" <== is not a valid value). Only ${allowed.join(',')} are allowed.`);
+ this.params.ReturnItemCollectionMetrics = upper;
+ return this;
+ }
+
+ async delete() {
+ const data = await this.client.delete(this.params).promise();
+ return unmarshal(data.Item);
+ }
+}
+
+module.exports = DbDeleter;
diff --git a/addons/addon-base/packages/services/lib/db/getter.js b/addons/addon-base/packages/services/lib/db/getter.js
new file mode 100644
index 0000000000..95677ea964
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/db/getter.js
@@ -0,0 +1,144 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const unmarshal = require('./unmarshal');
+
+// To handle get operation using DocumentClient
+// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#get-property
+// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html
+// NOTE: The following properties are legacy and should not be used:
+// - AttributesToGet
+// - AttributeUpdates
+// - ConditionalOperator
+// - Expected
+
+class DbGetter {
+ constructor(log = console, client) {
+ this.log = log;
+ this.client = client;
+ this.params = {
+ // ReturnConsumedCapacity: 'INDEXES',
+ };
+ }
+
+ table(name) {
+ if (!_.isString(name) || _.isEmpty(_.trim(name)))
+ throw new Error(`DbGetter.table("${name}" <== must be a string and can not be empty).`);
+ this.params.TableName = name;
+ return this;
+ }
+
+ // can be either key(key, value) or key({ key1: value1, key2: value2, ...})
+ key(...args) {
+ if (!this.params.Key) this.params.Key = {};
+
+ if (args.length > 1) this.params.Key[args[0]] = args[1];
+ else Object.assign(this.params.Key, ...args);
+ return this;
+ }
+
+ // must be keys([{ key1: value1, key2: value2, ... }, { keyA: valueA, keyB, valueB, ...}, ...])
+ // uses batchGet() API instead of just get()
+ keys(args) {
+ this.params.Keys = args;
+ return this;
+ }
+
+ // can be either props(key, value) or props({ key1: value1, key2: value2, ...})
+ props(...args) {
+ if (args.length > 1) this.params[args[0]] = args[1];
+ else Object.assign(this.params, ...args);
+ return this;
+ }
+
+ // same as ConsistentRead = true
+ strong() {
+ this.params.ConsistentRead = true;
+ return this;
+ }
+
+ // same as ExpressionAttributeNames
+ names(obj = {}) {
+ if (!_.isObject(obj)) throw new Error(`DbGetter.names("${obj}" <== must be an object).`);
+ this.params.ExpressionAttributeNames = {
+ ...this.params.ExpressionAttributeNames,
+ ...obj,
+ };
+ return this;
+ }
+
+ // same as ProjectionExpression
+ projection(expr) {
+ if (_.isEmpty(expr)) return this;
+ if (_.isString(expr)) {
+ if (this.params.ProjectionExpression)
+ this.params.ProjectionExpression = `${this.params.ProjectionExpression}, ${expr}`;
+ else this.params.ProjectionExpression = expr;
+ } else if (_.isArray(expr)) {
+ const names = {};
+ const values = [];
+ expr.forEach(key => {
+ names[`#${key}`] = key;
+ values.push(`#${key}`);
+ });
+ const str = values.join(', ');
+ if (this.params.ProjectionExpression)
+ this.params.ProjectionExpression = `${this.params.ProjectionExpression}, ${str}`;
+ else this.params.ProjectionExpression = str;
+ this.params.ExpressionAttributeNames = {
+ ...this.params.ExpressionAttributeNames,
+ ...names,
+ };
+ } else throw new Error(`DbGetter.projection("${expr}" <== must be a string or an array).`);
+
+ return this;
+ }
+
+ // same as ReturnConsumedCapacity
+ capacity(str = '') {
+ const upper = str.toUpperCase();
+ const allowed = ['INDEXES', 'TOTAL', 'NONE'];
+ if (!allowed.includes(upper))
+ throw new Error(`DbGetter.capacity("${upper}" <== is not a valid value). Only ${allowed.join(',')} are allowed.`);
+ this.params.ReturnConsumedCapacity = upper;
+ return this;
+ }
+
+ async get() {
+ if (this.params.Key && this.params.Keys)
+ throw new Error('DbGetter <== only key() or keys() may be called, not both');
+
+ let data;
+ if (this.params.Key) data = (await this.client.get(this.params).promise()).Item;
+ else if (this.params.Keys) {
+ // BatchGet
+ const batchParams = { RequestItems: {} };
+ batchParams.RequestItems[this.params.TableName] = { ...this.params };
+
+ if (this.params.ReturnConsumedCapacity) {
+ batchParams.RequestItems.ReturnConsumedCapacity = this.params.ReturnConsumedCapacity;
+ delete batchParams.RequestItems[this.params.TableName].ReturnConsumedCapacity;
+ }
+
+ data = (await this.client.batchGet(batchParams).promise()).Responses[this.params.TableName];
+ }
+
+ return unmarshal(data);
+ }
+}
+
+module.exports = DbGetter;
diff --git a/addons/addon-base/packages/services/lib/db/query.js b/addons/addon-base/packages/services/lib/db/query.js
new file mode 100644
index 0000000000..46578f8517
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/db/query.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.
+ */
+
+const _ = require('lodash');
+
+const unmarshal = require('./unmarshal');
+
+// To handle scan operation using DocumentClient
+// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#query-property
+// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html
+// NOTE: The following properties are legacy and should not be used:
+// - AttributesToGet
+// - AttributeUpdates
+// - ConditionalOperator
+// - Expected
+// - ScanFilter
+// - KeyConditions
+// - QueryFilter
+
+class DbQuery {
+ constructor(log = console, client) {
+ this.log = log;
+ this.client = client;
+ this.sortKeyName = undefined;
+ this.params = {
+ // ReturnConsumedCapacity: 'INDEXES',
+ };
+ }
+
+ // same as TableName
+ table(name) {
+ if (!_.isString(name) || _.isEmpty(_.trim(name)))
+ throw new Error(`DbQuery.table("${name}" <== must be a string and can not be empty).`);
+ this.params.TableName = name;
+ return this;
+ }
+
+ // same as IndexName
+ index(name) {
+ if (!_.isString(name) || _.isEmpty(_.trim(name)))
+ throw new Error(`DbQuery.index("${name}" <== must be a string and can not be empty).`);
+ this.params.IndexName = name;
+ return this;
+ }
+
+ // can be either props(key, value) or props({ key1: value1, key2: value2, ...})
+ props(...args) {
+ if (args.length > 1) this.params[args[0]] = args[1];
+ else Object.assign(this.params, ...args);
+ return this;
+ }
+
+ // helps with setting up KeyConditionExpression
+ // this is for the partition key only. If you also need to specify sort key, then use sortKey() then .eq(), .lt() or .gt(). However,
+ // if you use .condition() for the sort key expression, you will need to use values() and possibly names()
+ key(name, value) {
+ if (!_.isString(name) || _.isEmpty(_.trim(name)))
+ throw new Error(`DbQuery.key("${name}" <== must be a string and can not be empty).`);
+
+ const expression = `#${name} = :${name}`;
+ this._setCondition(expression);
+ this.names({ [`#${name}`]: name });
+ this.values({ [`:${name}`]: value });
+
+ return this;
+ }
+
+ sortKey(name) {
+ this.sortKeyName = name;
+ this.names({ [`#${name}`]: name });
+
+ return this;
+ }
+
+ // helps with setting up KeyConditionExpression
+ // this is for the sort key only. It results in an equal expression using the sort key "# = :". You only want to supply the value of the
+ // sort key here since we assume you called .sortKey(name) before calling this one
+ eq(value) {
+ if (!this.sortKeyName)
+ throw new Error('You tried to call DbQuery.eq(), however, you must call DbQuery.sortKey() first.');
+ return this._internalExpression('=', value);
+ }
+
+ // helps with setting up KeyConditionExpression
+ // this is for the sort key only. It results in an less than expression using the sort key "# < :". You only want to supply the value of the
+ // sort key here since we assume you called .sortKey(name) before calling this one
+ lt(value) {
+ if (!this.sortKeyName)
+ throw new Error('You tried to call DbQuery.lt(), however, you must call DbQuery.sortKey() first.');
+ return this._internalExpression('<', value);
+ }
+
+ // helps with setting up KeyConditionExpression
+ // this is for the sort key only. It results in an less than or equal expression using the sort key "# <= :". You only want to supply the value of the
+ // sort key here since we assume you called .sortKey(name) before calling this one
+ lte(value) {
+ if (!this.sortKeyName)
+ throw new Error('You tried to call DbQuery.lte(), however, you must call DbQuery.sortKey() first.');
+ return this._internalExpression('<=', value);
+ }
+
+ // helps with setting up KeyConditionExpression
+ // this is for the sort key only. It results in greater than expression using the sort key "# > :". You only want to supply the value of the
+ // sort key here since we assume you called .sortKey(name) before calling this one
+ gt(value) {
+ if (!this.sortKeyName)
+ throw new Error('You tried to call DbQuery.gt(), however, you must call DbQuery.sortKey() first.');
+ return this._internalExpression('>', value);
+ }
+
+ // helps with setting up KeyConditionExpression
+ // this is for the sort key only. It results in greater than or equal expression using the sort key "# >= :". You only want to supply the value of the
+ // sort key here since we assume you called .sortKey(name) before calling this one
+ gte(value) {
+ if (!this.sortKeyName)
+ throw new Error('You tried to call DbQuery.gte(), however, you must call DbQuery.sortKey() first.');
+ return this._internalExpression('>=', value);
+ }
+
+ // helps with setting up KeyConditionExpression
+ // this is for the sort key only. It results in the between expression using the sort key "# BETWEEN : AND :". You only want to supply
+ // the two between values for the sort key here since we assume you called .sortKey(name) before calling this one
+ between(value1, value2) {
+ if (!this.sortKeyName)
+ throw new Error('You tried to call DbQuery.between(), however, you must call DbQuery.sortKey() first.');
+
+ const expression = `#${this.sortKeyName} BETWEEN :${this.sortKeyName}1 AND :${this.sortKeyName}2`;
+ this._setCondition(expression);
+ this.values({
+ [`:${this.sortKeyName}1`]: value1,
+ [`:${this.sortKeyName}2`]: value2,
+ });
+ return this;
+ }
+
+ // helps with setting up KeyConditionExpression
+ // this is for the sort key only. It results begins_with expression using the sort key "begins_with( # ,: )". You only want to supply the value of the
+ // sort key here since we assume you called .sortKey(name) before calling this one
+ begins(value) {
+ if (!this.sortKeyName)
+ throw new Error('You tried to call DbQuery.begins(), however, you must call DbQuery.sortKey() first.');
+
+ const expression = `begins_with ( #${this.sortKeyName}, :${this.sortKeyName} )`;
+ this._setCondition(expression);
+ this.values({ [`:${this.sortKeyName}`]: value });
+
+ return this;
+ }
+
+ _internalExpression(expr, value) {
+ const expression = `#${this.sortKeyName} ${expr} :${this.sortKeyName}`;
+ this._setCondition(expression);
+ this.values({ [`:${this.sortKeyName}`]: value });
+
+ return this;
+ }
+
+ _setCondition(expression) {
+ if (this.params.KeyConditionExpression)
+ this.params.KeyConditionExpression = `${this.params.KeyConditionExpression} AND ${expression}`;
+ else this.params.KeyConditionExpression = expression;
+ }
+
+ // same as ExclusiveStartKey
+ start(key) {
+ if (!key) delete this.params.ExclusiveStartKey;
+ else this.params.ExclusiveStartKey = key;
+
+ return this;
+ }
+
+ // same as FilterExpression
+ filter(str) {
+ if (this.params.FilterExpression) this.params.FilterExpression = `${this.params.FilterExpression} ${str}`;
+ else this.params.FilterExpression = str;
+ return this;
+ }
+
+ // same as ConsistentRead = true
+ strong() {
+ this.params.ConsistentRead = true;
+ return this;
+ }
+
+ // same as ExpressionAttributeNames
+ names(obj = {}) {
+ if (!_.isObject(obj)) throw new Error(`DbQuery.names("${obj}" <== must be an object).`);
+ this.params.ExpressionAttributeNames = {
+ ...this.params.ExpressionAttributeNames,
+ ...obj,
+ };
+ return this;
+ }
+
+ // same as ExpressionAttributeValues
+ values(obj = {}) {
+ if (!_.isObject(obj)) throw new Error(`DbQuery.values("${obj}" <== must be an object).`);
+ this.params.ExpressionAttributeValues = {
+ ...this.params.ExpressionAttributeValues,
+ ...obj,
+ };
+ return this;
+ }
+
+ // same as ProjectionExpression
+ projection(expr) {
+ if (_.isEmpty(expr)) return this;
+ if (_.isString(expr)) {
+ if (this.params.ProjectionExpression)
+ this.params.ProjectionExpression = `${this.params.ProjectionExpression}, ${expr}`;
+ else this.params.ProjectionExpression = expr;
+ } else if (_.isArray(expr)) {
+ const names = {};
+ const values = [];
+ expr.forEach(key => {
+ names[`#${key}`] = key;
+ values.push(`#${key}`);
+ });
+ const str = values.join(', ');
+ if (this.params.ProjectionExpression)
+ this.params.ProjectionExpression = `${this.params.ProjectionExpression}, ${str}`;
+ else this.params.ProjectionExpression = str;
+ this.params.ExpressionAttributeNames = {
+ ...this.params.ExpressionAttributeNames,
+ ...names,
+ };
+ } else throw new Error(`DbQuery.projection("${expr}" <== must be a string or an array).`);
+
+ return this;
+ }
+
+ // same as Select: ALL_ATTRIBUTES | ALL_PROJECTED_ATTRIBUTES | SPECIFIC_ATTRIBUTES | COUNT
+ select(str) {
+ const upper = str.toUpperCase();
+ const allowed = ['ALL_ATTRIBUTES', 'ALL_PROJECTED_ATTRIBUTES', 'SPECIFIC_ATTRIBUTES', 'COUNT'];
+ if (!allowed.includes(upper))
+ throw new Error(`DbQuery.select("${upper}" <== is not a valid value). Only ${allowed.join(',')} are allowed.`);
+ this.params.Select = upper;
+ return this;
+ }
+
+ // same as Limit
+ limit(num) {
+ this.params.Limit = num;
+ return this;
+ }
+
+ // same as ScanIndexForward
+ forward(yesOrNo = true) {
+ this.params.ScanIndexForward = yesOrNo;
+ return this;
+ }
+
+ // same as ReturnConsumedCapacity
+ capacity(str = '') {
+ const upper = str.toUpperCase();
+ const allowed = ['INDEXES', 'TOTAL', 'NONE'];
+ if (!allowed.includes(upper))
+ throw new Error(`DbQuery.capacity("${upper}" <== is not a valid value). Only ${allowed.join(',')} are allowed.`);
+ this.params.ReturnConsumedCapacity = upper;
+ return this;
+ }
+
+ async query() {
+ let count = 0;
+ let result = [];
+
+ const done = () => {
+ const limit = this.params.Limit;
+ if (this.params.ExclusiveStartKey === undefined) return true;
+ if (limit === undefined) return false;
+ return limit <= count;
+ };
+
+ // An example of an output of one "this.client.query()" call
+ // {
+ // "Items": [
+ // {
+ // "firstName": "Alan",
+ // "lastName": "Turing",
+ // "username": "alan"
+ // }
+ // ],
+ // "Count": 1,
+ // "ScannedCount": 1,
+ // "LastEvaluatedKey": {
+ // "username": "alan"
+ // }
+ // }
+
+ do {
+ const data = await this.client.query(this.params).promise(); // eslint-disable-line no-await-in-loop
+
+ this.params.ExclusiveStartKey = data.LastEvaluatedKey;
+ count += data.Count;
+ if (data.Count > 0) {
+ result = _.concat(result, unmarshal(data.Items));
+ }
+ } while (!done());
+
+ return result;
+ }
+}
+
+module.exports = DbQuery;
diff --git a/addons/addon-base/packages/services/lib/db/scanner.js b/addons/addon-base/packages/services/lib/db/scanner.js
new file mode 100644
index 0000000000..159b289fa2
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/db/scanner.js
@@ -0,0 +1,211 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const unmarshal = require('./unmarshal');
+
+// To handle scan operation using DocumentClient
+// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#scan-property
+// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html
+// NOTE: The following properties are legacy and should not be used:
+// - AttributesToGet
+// - AttributeUpdates
+// - ConditionalOperator
+// - Expected
+// - ScanFilter
+
+class DbScanner {
+ constructor(log = console, client) {
+ this.log = log;
+ this.client = client;
+ this.params = {
+ // ReturnConsumedCapacity: 'INDEXES',
+ };
+ }
+
+ // same as TableName
+ table(name) {
+ if (!_.isString(name) || _.isEmpty(_.trim(name)))
+ throw new Error(`DbScanner.table("${name}" <== must be a string and can not be empty).`);
+ this.params.TableName = name;
+ return this;
+ }
+
+ // same as IndexName
+ index(name) {
+ if (!_.isString(name) || _.isEmpty(_.trim(name)))
+ throw new Error(`DbScanner.index("${name}" <== must be a string and can not be empty).`);
+ this.params.IndexName = name;
+ return this;
+ }
+
+ // can be either props(key, value) or props({ key1: value1, key2: value2, ...})
+ props(...args) {
+ if (args.length > 1) this.params[args[0]] = args[1];
+ else Object.assign(this.params, ...args);
+ return this;
+ }
+
+ // same as ExclusiveStartKey
+ start(key) {
+ if (!key) delete this.params.ExclusiveStartKey;
+ else this.params.ExclusiveStartKey = key;
+
+ return this;
+ }
+
+ // same as FilterExpression
+ filter(str) {
+ if (this.params.FilterExpression) this.params.FilterExpression = `${this.params.FilterExpression} ${str}`;
+ else this.params.FilterExpression = str;
+ return this;
+ }
+
+ // same as ConsistentRead = true
+ strong() {
+ this.params.ConsistentRead = true;
+ return this;
+ }
+
+ // same as ExpressionAttributeNames
+ names(obj = {}) {
+ if (!_.isObject(obj)) throw new Error(`DbScanner.names("${obj}" <== must be an object).`);
+ this.params.ExpressionAttributeNames = {
+ ...this.params.ExpressionAttributeNames,
+ ...obj,
+ };
+ return this;
+ }
+
+ // same as ExpressionAttributeValues
+ values(obj = {}) {
+ if (!_.isObject(obj)) throw new Error(`DbScanner.values("${obj}" <== must be an object).`);
+ this.params.ExpressionAttributeValues = {
+ ...this.params.ExpressionAttributeValues,
+ ...obj,
+ };
+ return this;
+ }
+
+ // same as ProjectionExpression
+ projection(expr) {
+ if (_.isEmpty(expr)) return this;
+ if (_.isString(expr)) {
+ if (this.params.ProjectionExpression)
+ this.params.ProjectionExpression = `${this.params.ProjectionExpression}, ${expr}`;
+ else this.params.ProjectionExpression = expr;
+ } else if (_.isArray(expr)) {
+ const names = {};
+ const values = [];
+ expr.forEach(key => {
+ names[`#${key}`] = key;
+ values.push(`#${key}`);
+ });
+ const str = values.join(', ');
+ if (this.params.ProjectionExpression)
+ this.params.ProjectionExpression = `${this.params.ProjectionExpression}, ${str}`;
+ else this.params.ProjectionExpression = str;
+ this.params.ExpressionAttributeNames = {
+ ...this.params.ExpressionAttributeNames,
+ ...names,
+ };
+ } else throw new Error(`DbScanner.projection("${expr}" <== must be a string or an array).`);
+
+ return this;
+ }
+
+ // same as Select: ALL_ATTRIBUTES | ALL_PROJECTED_ATTRIBUTES | SPECIFIC_ATTRIBUTES | COUNT
+ select(str) {
+ const upper = str.toUpperCase();
+ const allowed = ['ALL_ATTRIBUTES', 'ALL_PROJECTED_ATTRIBUTES', 'SPECIFIC_ATTRIBUTES', 'COUNT'];
+ if (!allowed.includes(upper))
+ throw new Error(`DbScanner.select("${upper}" <== is not a valid value). Only ${allowed.join(',')} are allowed.`);
+ this.params.Select = upper;
+ return this;
+ }
+
+ // same as Limit
+ limit(num) {
+ this.params.Limit = num;
+ return this;
+ }
+
+ // same as Segment
+ segment(num) {
+ this.params.Segment = num;
+ return this;
+ }
+
+ // same as TotalSegments
+ totalSegment(num) {
+ this.params.TotalSegment = num;
+ return this;
+ }
+
+ // same as ReturnConsumedCapacity
+ capacity(str = '') {
+ const upper = str.toUpperCase();
+ const allowed = ['INDEXES', 'TOTAL', 'NONE'];
+ if (!allowed.includes(upper))
+ throw new Error(
+ `DbScanner.capacity("${upper}" <== is not a valid value). Only ${allowed.join(',')} are allowed.`,
+ );
+ this.params.ReturnConsumedCapacity = upper;
+ return this;
+ }
+
+ async scan() {
+ let count = 0;
+ let result = [];
+
+ const done = () => {
+ const limit = this.params.Limit;
+ if (this.params.ExclusiveStartKey === undefined) return true;
+ if (limit === undefined) return false;
+ return limit <= count;
+ };
+
+ // An example of an output of one "this.client.scan()" call
+ // {
+ // "Items": [
+ // {
+ // "firstName": "Alan",
+ // "lastName": "Turing",
+ // "username": "alan"
+ // }
+ // ],
+ // "Count": 1,
+ // "ScannedCount": 1,
+ // "LastEvaluatedKey": {
+ // "username": "alan"
+ // }
+ // }
+
+ do {
+ const data = await this.client.scan(this.params).promise(); // eslint-disable-line no-await-in-loop
+
+ this.params.ExclusiveStartKey = data.LastEvaluatedKey;
+ count += data.Count;
+ if (data.Count > 0) {
+ result = _.concat(result, unmarshal(data.Items));
+ }
+ } while (!done());
+
+ return result;
+ }
+}
+
+module.exports = DbScanner;
diff --git a/addons/addon-base/packages/services/lib/db/unmarshal.js b/addons/addon-base/packages/services/lib/db/unmarshal.js
new file mode 100644
index 0000000000..b4177f22bd
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/db/unmarshal.js
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+function reader(obj) {
+ return key => {
+ if (!_.isObject(obj)) return undefined;
+ if (obj[key] && obj[key].wrapperName === 'Set') return obj[key].values;
+ return obj[key];
+ };
+}
+
+// This function assumes that the DocumentClient is used to do the initial unmarshalling, it
+// then does the unmarshalling for client.createSet() artifact, this is because the DocumentClient unmarshales Sets
+// into : { "wrapperName": "Set", "values": [ "something", ... ], "type": "String" }
+function process(obj) {
+ const read = reader(obj);
+ const keys = Object.keys(obj);
+ const result = {};
+ keys.forEach(key => {
+ result[key] = read(key);
+ });
+ return result;
+}
+
+function unmarshal(objOrArr) {
+ if (objOrArr === undefined) return objOrArr;
+ if (_.isArray(objOrArr)) return _.map(objOrArr, item => process(item));
+ if (_.size(objOrArr) === 0) return undefined;
+ return process(objOrArr);
+}
+
+module.exports = unmarshal;
diff --git a/addons/addon-base/packages/services/lib/db/updater.js b/addons/addon-base/packages/services/lib/db/updater.js
new file mode 100644
index 0000000000..ef30785384
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/db/updater.js
@@ -0,0 +1,318 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+const unmarshal = require('./unmarshal');
+
+// To handle get operation using DocumentClient
+// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#update-property
+// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html
+// NOTE: The following properties are legacy and should not be used:
+// - AttributesToGet
+// - AttributeUpdates
+// - ConditionalOperator
+// - Expected
+
+class DbUpdater {
+ constructor(log = console, client) {
+ this.log = log;
+ this.client = client; // this is the DynamoDB.DocumentClient. This allows us to use the client.createSet() as needed
+ this.params = {
+ // ReturnConsumedCapacity: 'INDEXES',
+ ReturnValues: 'ALL_NEW',
+ };
+ this.marked = {};
+ this.createdAtState = { enabled: true, processed: false, value: '' };
+ this.updatedAtState = { enabled: true, processed: false, value: '' };
+
+ const self = this;
+ this.internals = {
+ set: [],
+ add: [],
+ remove: [],
+ delete: [],
+ revGiven: false,
+ setConditionExpression: (expr, separator = 'AND') => {
+ if (self.params.ConditionExpression)
+ self.params.ConditionExpression = `${self.params.ConditionExpression} ${separator} ${expr}`;
+ else self.params.ConditionExpression = expr;
+ },
+ toParams() {
+ const updates = [];
+ if (!_.isEmpty(this.set)) updates.push(`SET ${this.set.join(', ')}`);
+ if (!_.isEmpty(this.add)) updates.push(`ADD ${this.add.join(', ')}`);
+ if (!_.isEmpty(this.remove)) updates.push(`REMOVE ${this.remove.join(', ')}`);
+ if (!_.isEmpty(this.delete)) updates.push(`DELETE ${this.delete.join(', ')}`);
+
+ delete self.params.UpdateExpression;
+ if (_.isEmpty(updates)) return self.params;
+ self.params.UpdateExpression = updates.join(' ');
+ return self.params;
+ },
+ };
+ }
+
+ table(name) {
+ if (!_.isString(name) || _.isEmpty(_.trim(name)))
+ throw new Error(`DbUpdater.table("${name}" <== must be a string and can not be empty).`);
+ this.params.TableName = name;
+ return this;
+ }
+
+ // mark the provided attribute names as being of type Set
+ mark(arr = []) {
+ if (this.params.UpdateExpression)
+ throw new Error(
+ 'You tried to call DbUpdater.mark() after you called DbUpdater.update(). Call mark() before calling update().',
+ );
+ arr.forEach(key => {
+ this.marked[key] = true;
+ });
+ return this;
+ }
+
+ // can be either key(key, value) or key({ key1: value1, key2: value2, ...})
+ key(...args) {
+ if (this.params.UpdateExpression)
+ throw new Error(
+ 'You tried to call DbUpdater.key() after you called DbUpdater.update(). Call key() before calling update().',
+ );
+ if (!this.params.Key) this.params.Key = {};
+
+ if (args.length > 1) this.params.Key[args[0]] = args[1];
+ else Object.assign(this.params.Key, ...args);
+ return this;
+ }
+
+ // can be either props(key, value) or props({ key1: value1, key2: value2, ...})
+ props(...args) {
+ if (args.length > 1) this.params[args[0]] = args[1];
+ else Object.assign(this.params, ...args);
+ return this;
+ }
+
+ disableCreatedAt() {
+ if (this.params.UpdateExpression)
+ throw new Error(
+ 'You tried to call DbUpdater.disableCreatedAt() after you called DbUpdater.update(). Call disableCreatedAt() before calling update().',
+ );
+ this.createdAtState.enabled = false;
+ return this;
+ }
+
+ createdAt(str) {
+ if (this.params.UpdateExpression)
+ throw new Error(
+ 'You tried to call DbUpdater.createdAt() after you called DbUpdater.update(). Call createdAt() before calling update().',
+ );
+ if (!_.isDate(str) && (!_.isString(str) || _.isEmpty(_.trim(str))))
+ throw new Error(`DbUpdater.createdAt("${str}" <== must be a string or Date and can not be empty).`);
+ this.createdAtState.enabled = true;
+ this.createdAtState.value = _.isDate(str) ? str.toISOString() : str;
+ return this;
+ }
+
+ disableUpdatedAt() {
+ if (this.params.UpdateExpression)
+ throw new Error(
+ 'You tried to call DbUpdater.disableUpdatedAt() after you called DbUpdater.update(). Call disableUpdatedAt() before calling update().',
+ );
+ this.updatedAtState.enabled = false;
+ return this;
+ }
+
+ updatedAt(str) {
+ if (this.params.UpdateExpression)
+ throw new Error(
+ 'You tried to call DbUpdater.updatedAt() after you called DbUpdater.update(). Call updatedAt() before calling update().',
+ );
+ if (!_.isDate(str) && (!_.isString(str) || _.isEmpty(_.trim(str))))
+ throw new Error(`DbUpdater.updatedAt("${str}" <== must be a string or Date and can not be empty).`);
+ this.updatedAtState.enabled = true;
+ this.updatedAtState.value = _.isDate(str) ? str.toISOString() : str;
+ return this;
+ }
+
+ // this is an additional method that helps us with using the optimistic locking technique, if you use this method,
+ // you NO longer need to add the 'and #rev = :rev' and 'SET #rev = #rev + :_addOne' expressions
+ rev(rev) {
+ if (_.isNil(rev)) return this;
+ const expression = '#rev = :rev';
+ this.internals.setConditionExpression(expression);
+ this.internals.revGiven = true;
+ this.names({ '#rev': 'rev' });
+ this.values({ ':rev': rev, ':_addOne': 1 });
+ this.internals.set.push('#rev = #rev + :_addOne');
+
+ return this;
+ }
+
+ // helps with setting up UpdateExpression
+ item(item) {
+ if (!item) return this;
+
+ // we loop through all the properties that are defined and add them to the
+ // update expression and to the expression values and that same time detect if they are marked as sets
+ const keys = Object.keys(item);
+ if (keys.length === 0) return this;
+
+ const assignments = [];
+ const values = {};
+ const names = {};
+
+ keys.forEach(key => {
+ const value = item[key];
+ if (value === undefined) return;
+ if (this.params.Key && this.params.Key.hasOwnProperty(key)) return; // eslint-disable-line no-prototype-builtins
+
+ if (this.createdAtState.enabled && key === 'createdAt') return;
+ if (this.updatedAtState.enabled && key === 'updatedAt') return;
+ if (this.internals.revGiven && key === 'rev') return;
+
+ names[`#${key}`] = key;
+ assignments.push(`#${key} = :${key}`);
+
+ if (this.marked[key] && _.isEmpty(value)) {
+ values[`:${key}`] = null;
+ } else if (this.marked[key]) {
+ values[`:${key}`] = this.client.createSet(value, { validate: true });
+ } else {
+ values[`:${key}`] = value;
+ }
+ });
+
+ if (assignments.length === 0) return this;
+
+ this.internals.set.push(assignments.join(', '));
+
+ let createdAt = this.createdAtState.value;
+ if (this.createdAtState.enabled && !this.createdAtState.processed) {
+ this.createdAtState.processed = true;
+ createdAt = _.isEmpty(createdAt) ? new Date().toISOString() : createdAt;
+ this.internals.set.push('#createdAt = if_not_exists(#createdAt, :createdAt)');
+ names['#createdAt'] = 'createdAt';
+ values[':createdAt'] = createdAt;
+ }
+
+ let updatedAt = this.updatedAtState.value;
+ if (this.updatedAtState.enabled && !this.updatedAtState.processed) {
+ this.updatedAtState.processed = true;
+ updatedAt = _.isEmpty(updatedAt) ? new Date().toISOString() : updatedAt;
+ this.internals.set.push('#updatedAt = :updatedAt');
+ names['#updatedAt'] = 'updatedAt';
+ values[':updatedAt'] = updatedAt;
+ }
+
+ this.names(names);
+ this.values(values);
+
+ return this;
+ }
+
+ // same as using UpdateExpression with the SET clause. IMPORTANT: your expression should NOT include the 'SET' keyword
+ set(expression) {
+ if (!_.isEmpty(expression)) this.internals.set.push(expression);
+ return this;
+ }
+
+ // same as using UpdateExpression with the ADD clause. IMPORTANT: your expression should NOT include the 'ADD' keyword
+ add(expression) {
+ if (!_.isEmpty(expression)) this.internals.add.push(expression);
+ return this;
+ }
+
+ // same as using UpdateExpression with the REMOVE clause. IMPORTANT: your expression should NOT include the 'REMOVE' keyword
+ remove(expression) {
+ if (!_.isEmpty(expression)) {
+ if (_.isArray(expression)) this.internals.remove.push(...expression);
+ else this.internals.remove.push(expression);
+ }
+ return this;
+ }
+
+ // same as using UpdateExpression with the DELETE clause. IMPORTANT: your expression should NOT include the 'DELETE' keyword
+ delete(expression) {
+ if (!_.isEmpty(expression)) this.internals.delete.push(expression);
+ return this;
+ }
+
+ // same as ExpressionAttributeNames
+ names(obj = {}) {
+ if (!_.isObject(obj)) throw new Error(`DbUpdater.names("${obj}" <== must be an object).`);
+ this.params.ExpressionAttributeNames = {
+ ...this.params.ExpressionAttributeNames,
+ ...obj,
+ };
+ return this;
+ }
+
+ // same as ExpressionAttributeValues
+ values(obj = {}) {
+ if (!_.isObject(obj)) throw new Error(`DbScanner.values("${obj}" <== must be an object).`);
+ this.params.ExpressionAttributeValues = {
+ ...this.params.ExpressionAttributeValues,
+ ...obj,
+ };
+ return this;
+ }
+
+ // same as ConditionExpression
+ condition(str, separator = 'AND') {
+ if (!_.isString(str) || _.isEmpty(_.trim(str)))
+ throw new Error(`DbUpdater.condition("${str}" <== must be a string and can not be empty).`);
+ this.internals.setConditionExpression(str, separator);
+ return this;
+ }
+
+ // same as ReturnValues: NONE | ALL_OLD | UPDATED_OLD | ALL_NEW | UPDATED_NEW,
+ return(str) {
+ const upper = str.toUpperCase();
+ const allowed = ['NONE', 'ALL_OLD', 'UPDATED_OLD', 'ALL_NEW', 'UPDATED_NEW'];
+ if (!allowed.includes(upper))
+ throw new Error(`DbUpdater.return("${upper}" <== is not a valid value). Only ${allowed.join(',')} are allowed.`);
+ this.params.ReturnValues = upper;
+ return this;
+ }
+
+ // same as ReturnItemCollectionMetrics
+ metrics(str) {
+ const upper = str.toUpperCase();
+ const allowed = ['NONE', 'SIZE'];
+ if (!allowed.includes(upper))
+ throw new Error(`DbUpdater.metrics("${upper}" <== is not a valid value). Only ${allowed.join(',')} are allowed.`);
+ this.params.ReturnItemCollectionMetrics = upper;
+ return this;
+ }
+
+ // same as ReturnConsumedCapacity
+ capacity(str = '') {
+ const upper = str.toUpperCase();
+ const allowed = ['INDEXES', 'TOTAL', 'NONE'];
+ if (!allowed.includes(upper))
+ throw new Error(
+ `DbUpdater.capacity("${upper}" <== is not a valid value). Only ${allowed.join(',')} are allowed.`,
+ );
+ this.params.ReturnConsumedCapacity = upper;
+ return this;
+ }
+
+ async update() {
+ const data = await this.client.update(this.internals.toParams()).promise();
+ return unmarshal(data.Attributes);
+ }
+}
+
+module.exports = DbUpdater;
diff --git a/addons/addon-base/packages/services/lib/helpers/system-context.js b/addons/addon-base/packages/services/lib/helpers/system-context.js
new file mode 100644
index 0000000000..40da81e570
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/helpers/system-context.js
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+const RequestContext = require('@aws-ee/base-services-container/lib/request-context');
+
+const internalAuthProviderId = 'internal'; // TODO - make this string comes from constants
+
+/**
+ * A helper function that helps create requestContext for system users.
+ * Most of the services accept "requestContext" argument which provides context about the service call (such as who is the caller of the service i.e., the "principal" etc)
+ * In case of system calls (i.e., calls not initiated by any "principal" but result of some system operation such as execution of post-deployment steps),
+ * the requestContext should contain information about the implicit "system" user.
+ * This method returns this "requestContext" that can be passed to services for system calls.
+ *
+ * @returns {Service}
+ */
+function getSystemRequestContext() {
+ const ctx = new RequestContext();
+
+ const systemUsername = '_system_';
+ const systemUserNamespace = internalAuthProviderId;
+
+ ctx.authenticated = true;
+ ctx.principal = {
+ username: systemUsername,
+ ns: systemUserNamespace,
+ isAdmin: true,
+ status: 'active',
+ };
+ ctx.principalIdentifier = {
+ username: systemUsername,
+ ns: systemUserNamespace,
+ };
+
+ return ctx;
+}
+
+module.exports = {
+ getSystemRequestContext,
+};
diff --git a/addons/addon-base/packages/services/lib/helpers/utils.js b/addons/addon-base/packages/services/lib/helpers/utils.js
new file mode 100644
index 0000000000..401140c5fb
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/helpers/utils.js
@@ -0,0 +1,176 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+function toVersionString(num) {
+ return `v${_.padStart(num, 4, '0')}_`;
+}
+
+function parseVersionString(str) {
+ return parseInt(str.substring(1), 10);
+}
+
+// Convenient function to wrap the db call with a catch for the ConditionalCheckFailedException
+async function runAndCatch(fn, handler, code = 'ConditionalCheckFailedException') {
+ try {
+ const result = await fn();
+ return result;
+ } catch (error) {
+ if (error && error.code === code) {
+ return handler(error);
+ }
+
+ throw error;
+ }
+}
+
+/**
+ * A utility interval function for exponential back-off strategy. (i.e., intervals of 1, 2, 4, 8, 16 .. seconds)
+ *
+ * @param {Number} attemptCount
+ * @param {Number} baseInterval
+ * @return {Number}
+ */
+function exponentialInterval(attemptCount, baseInterval = 500) {
+ return baseInterval * 2 ** attemptCount;
+}
+
+/**
+ * A utility interval function for liner back-off strategy. (i.e., intervals of 1, 2, 3, 4, 5 .. seconds)
+ *
+ * @param {Number} attemptCount
+ * @param {Number} baseInterval
+ * @return {Number}
+ */
+function linearInterval(attemptCount, baseInterval = 1000) {
+ return baseInterval * attemptCount;
+}
+
+/**
+ * Retries calling a function as many times as requested by the 'times' argument. The retries are done with
+ * back-offs specified by the 'intervalFn'. By default, it uses {@link exponentialInterval} function to pause
+ * between each retry with exponential back-off (i.e., intervals of 1, 2, 4, 8, 16 .. seconds)
+ *
+ * @param {Function} fn - the fn to retry if it is rejected ( "fn" must return a promise )
+ *
+ * @param {Number} maxAttempts - maximum number of attempts calling the function. This includes first attempt and all
+ * retries.
+ * @param {Function} intervalFn - The interval function to decide the pause between the attempts. The function is
+ * invoked with one argument 'attempt' number. The 'attempt' here is the count of calls attempted so far. For
+ * example, if the 'fn' fails during the first attempt then the 'intervalFn' is called with attempt = 1. The
+ * intervalFn is expected to return the pause time in milliseconds to wait before making the next 'fn' call attempt.
+ *
+ * @returns {Promise<*>} The promise returned by the 'fn'. The returned promise will be rejected with the error thrown
+ * by 'fn' if the 'fn' still fails after the specified number of attempts.
+ */
+async function retry(fn, maxAttempts = 3, intervalFn = exponentialInterval) {
+ let caughtError;
+ for (let i = 0; i < maxAttempts; i += 1) {
+ try {
+ // We need to await before making next attempt so disabling no-await-in-loop lint rule here
+ // eslint-disable-next-line no-await-in-loop
+ const result = await fn();
+ return result;
+ } catch (err) {
+ caughtError = err;
+ // eslint-disable-next-line no-await-in-loop
+ await sleep(intervalFn(i + 1));
+ }
+ }
+ // We reached here means we exhausted all attempts calling the "fn"
+ // Propagate error that was caught in the last unsuccessful attempt
+ throw caughtError;
+}
+
+/**
+ * A utility function to process given items in sequence of batches. Items in one batch are processed in-parallel but
+ * all batches are processed sequentially i..e, processing of the next batch is not started until the previous batch is
+ * complete.
+ *
+ * @param items Array of items to process
+ * @param batchSize Number of items in a batch
+ * @param processorFn A function to process the batch. The function is called with the item argument.
+ * The function is expected to return a Promise with some result of processing the item. If the "processorFn" throws an
+ * error for any item, the "processInBatches" function will fail immediately. Processing of other items in that batch
+ * may be already in-flight at that point. Due to this, if you need to handle partial batch failures or if you need
+ * fine grained error handling control at individual item level, you should handle errors in the "processorFn" itself
+ * (using try/catch or Promise.catch etc) and make sure that the "processorFn" does not throw any errors.
+ *
+ * @returns {Promise}
+ */
+async function processInBatches(items, batchSize, processorFn) {
+ const itemBatches = _.chunk(items, batchSize);
+
+ let results = [];
+
+ // Process all items in one batch in parallel and wait for the batch to
+ // complete before moving on to the next batch
+ for (let i = 0; i <= itemBatches.length; i += 1) {
+ const itemsInThisBatch = itemBatches[i];
+ // We need to await for each batch in loop to make sure they are awaited in sequence instead of
+ // firing them in parallel disabling eslint for "no-await-in-loop" due to this
+ // eslint-disable-next-line no-await-in-loop
+ const resultsFromThisBatch = await Promise.all(
+ // Fire promise for each item in this batch and let it be processed in parallel
+ _.map(itemsInThisBatch, processorFn),
+ );
+
+ // push all results from this batch into the main results array
+ results = _.concat(results, resultsFromThisBatch);
+ }
+ return results;
+}
+
+/**
+ * A utility function that processes items sequentially. The function uses the specified processorFn to process
+ * items in the given order i.e., it does not process next item in the given array until the promise returned for
+ * the processing of the previous item is resolved. If the processorFn throws error (or returns a promise rejection)
+ * this functions stops processing next item and the error is bubbled up to the caller (via a promise rejection).
+ *
+ * @param items Array of items to process
+ * @param processorFn A function to process the item. The function is called with the item argument.
+ * The function is expected to return a Promise with some result of processing the item.
+ *
+ * @returns {Promise}
+ */
+async function processSequentially(items, processorFn) {
+ return processInBatches(items, 1, processorFn);
+}
+
+/**
+ * Returns a promise that will be resolved in the requested time, ms.
+ * Example: await sleep(200);
+ * https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep/39914235#39914235
+ *
+ * @param ms wait time in milliseconds
+ *
+ * @returns a promise, that will be resolved in the requested time
+ */
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+module.exports = {
+ toVersionString,
+ parseVersionString,
+ runAndCatch,
+ exponentialInterval,
+ linearInterval,
+ retry,
+ processInBatches,
+ processSequentially,
+ sleep,
+};
diff --git a/addons/addon-base/packages/services/lib/input-manifest/__tests__/input-manifest.test.js b/addons/addon-base/packages/services/lib/input-manifest/__tests__/input-manifest.test.js
new file mode 100644
index 0000000000..9e0a792d20
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/input-manifest/__tests__/input-manifest.test.js
@@ -0,0 +1,190 @@
+/*
+ * 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.
+ */
+
+const { flattenSection, validateSection } = require('../input-manifest');
+
+describe('flattenSection', () => {
+ it('flattens an empty input manifest section into an empty array', () => {
+ expect(flattenSection({})).toEqual([]);
+ });
+
+ it('flattens an input manifest section into an array of entries', () => {
+ const got = flattenSection(
+ {
+ children: [
+ {
+ name: 'A',
+ children: [{ name: 'C' }, { name: 'D', condition: '<%= false %>' }],
+ },
+ {
+ name: 'B',
+ condition: 'false',
+ children: [{ name: 'shouldNotBeEmitted' }],
+ },
+ ],
+ },
+ {},
+ );
+ expect(got).toEqual([
+ {
+ name: 'A',
+ children: [{ name: 'C' }, { name: 'D', condition: '<%= false %>' }],
+ },
+ { name: 'C' },
+ ]);
+ });
+
+ it('evaluates the top level condition, if it is present', () => {
+ const got = flattenSection(
+ {
+ condition: '<%= shouldEmitCount > 0 %>',
+ children: [
+ {
+ name: 'foo',
+ },
+ ],
+ },
+ {
+ shouldEmitCount: 0,
+ },
+ );
+
+ expect(got).toEqual([]);
+ });
+
+ it('evaluates a conditions as true, if condition key is missing', () => {
+ const got = flattenSection(
+ {
+ children: [
+ {
+ name: 'foo',
+ },
+ ],
+ },
+ {
+ shouldEmitCount: 0,
+ },
+ );
+
+ expect(got).toEqual([{ name: 'foo' }]);
+ });
+
+ it('uses the passed in config to evaluate the top level condition', () => {
+ const got = flattenSection(
+ {
+ condition: '<%= someValue > 3 %>',
+ children: [{ name: 'foo' }],
+ },
+ { someValue: 2 },
+ );
+ expect(got).toEqual([]);
+ });
+
+ it('uses the passed in config to evaluate nested conditions', () => {
+ const got = flattenSection(
+ {
+ children: [
+ { name: 'foo', condition: '<%= someValue == 2 %>' },
+ { name: 'bar', condition: '<%= someValue < 0 %>' },
+ ],
+ },
+ { someValue: 2 },
+ );
+ expect(got).toEqual([
+ {
+ name: 'foo',
+ condition: '<%= someValue == 2 %>',
+ },
+ ]);
+ });
+});
+
+describe('validateSection', () => {
+ it('validates an input manifest section against a config object', () => {
+ const got = validateSection(
+ {
+ children: [
+ {
+ name: 'A',
+ rules: 'required|integer',
+ },
+ ],
+ },
+ {
+ A: 'some string',
+ },
+ );
+ expect(got).toEqual([
+ {
+ type: 'invalid',
+ message: 'The A must be an integer.',
+ },
+ ]);
+ });
+
+ it('validates that required keys are present and defined', () => {
+ const got = validateSection(
+ {
+ children: [
+ {
+ name: 'A',
+ rules: 'required|integer',
+ },
+ {
+ name: 'B',
+ rules: 'required|integer',
+ },
+ ],
+ },
+ {
+ A: undefined,
+ },
+ );
+ expect(got).toEqual([
+ {
+ type: 'invalid',
+ message: 'The A field is required.',
+ },
+ {
+ type: 'invalid',
+ message: 'The B field is required.',
+ },
+ ]);
+ });
+
+ it('validates that extra keys are not present if extraKeysAreInvalid is true', () => {
+ const got = validateSection(
+ {
+ children: [
+ {
+ name: 'A',
+ rules: 'required|integer',
+ },
+ ],
+ },
+ {
+ A: 1,
+ B: 'extra!',
+ },
+ true,
+ );
+ expect(got).toEqual([
+ {
+ type: 'extra',
+ message: 'The B is present in config but missing in manifest',
+ },
+ ]);
+ });
+});
diff --git a/addons/addon-base/packages/services/lib/input-manifest/input-manifest-validation-service.js b/addons/addon-base/packages/services/lib/input-manifest/input-manifest-validation-service.js
new file mode 100644
index 0000000000..9dd62e253f
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/input-manifest/input-manifest-validation-service.js
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { validateSection } = require('./input-manifest');
+
+module.exports = class InputManifestValidationService extends Service {
+ /**
+ * @returns Array of validation errors. If there are no errors, then returns an empty array.
+ */
+ async getValidationErrors(inputManifest, config) {
+ const { sections = [] } = inputManifest;
+ const errors = [];
+ sections.forEach(section => {
+ errors.push(...validateSection(section, config));
+ });
+ return errors;
+ }
+};
diff --git a/addons/addon-base/packages/services/lib/input-manifest/input-manifest.js b/addons/addon-base/packages/services/lib/input-manifest/input-manifest.js
new file mode 100644
index 0000000000..4d862fcd5d
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/input-manifest/input-manifest.js
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+const Validator = require('validatorjs');
+const { template: underscoreTemplate } = require('underscore');
+
+const flatten = (entry, config) => {
+ const out = [];
+ const { condition, children = [] } = entry;
+ if (!condition || underscoreTemplate(condition)(config) === 'true') {
+ out.push(entry);
+ children.forEach(child => {
+ out.push(...flatten(child, config));
+ });
+ }
+ return out;
+};
+
+const flattenSection = ({ condition, children = [] }, config) => {
+ const out = [];
+ if (!condition || underscoreTemplate(condition)(config) === 'true') {
+ children.forEach(child => {
+ out.push(...flatten(child, config));
+ });
+ }
+ return out;
+};
+
+const validateSection = (section, config, extraKeysAreInvalid = false) => {
+ const keysMissingInManifest = new Set(Object.keys(config));
+ const errors = flatten(section, config)
+ .map(({ name, value: defaultValue, rules }) => {
+ keysMissingInManifest.delete(name);
+ if (!rules || rules === '') {
+ return null;
+ }
+ const configValue = config[name];
+ const validation = new Validator(
+ {
+ [name]: typeof configValue === 'undefined' ? defaultValue : configValue,
+ },
+ {
+ [name]: rules,
+ },
+ );
+ return validation.passes()
+ ? null
+ : {
+ type: 'invalid',
+ message: validation.errors.first(name) || `The ${name} value is invalid`,
+ };
+ })
+ .filter(err => !!err);
+ if (extraKeysAreInvalid) {
+ errors.push(
+ ...Array.from(keysMissingInManifest).map(name => ({
+ type: 'extra',
+ message: `The ${name} is present in config but missing in manifest`,
+ })),
+ );
+ }
+ return errors;
+};
+
+module.exports = {
+ flattenSection,
+ validateSection,
+};
diff --git a/addons/addon-base/packages/services/lib/json-schema-validation-service.js b/addons/addon-base/packages/services/lib/json-schema-validation-service.js
new file mode 100644
index 0000000000..63d76a7703
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/json-schema-validation-service.js
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+const Ajv = require('ajv');
+
+class JsonSchemaValidationService extends Service {
+ /**
+ * @throws a boom.badRequest with a payload of validation errors if objectToValidate has validation errors.
+ */
+ async ensureValid(objectToValidate, schemaToValidateAgainst) {
+ const errors = await this.getValidationErrors(objectToValidate, schemaToValidateAgainst);
+ if (errors.length > 0) {
+ throw this.boom.badRequest('Input has validation errors', true).withPayload(
+ {
+ validationErrors: errors,
+ },
+ true,
+ );
+ }
+ }
+
+ /**
+ * @returns an array of validation errors. If there are no errors, getValidationErrors will return an empty array.
+ */
+ async getValidationErrors(objectToValidate, schemaToValidateAgainst) {
+ const ajv = new Ajv({ allErrors: true });
+ ajv.validate(schemaToValidateAgainst, objectToValidate);
+ return ajv.errors || [];
+ }
+}
+
+module.exports = JsonSchemaValidationService;
diff --git a/addons/addon-base/packages/services/lib/lock/lock-service.js b/addons/addon-base/packages/services/lib/lock/lock-service.js
new file mode 100644
index 0000000000..a9dbf9ca1d
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/lock/lock-service.js
@@ -0,0 +1,174 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const { runAndCatch, sleep } = require('../helpers/utils');
+const obtainWriteLockSchema = require('../schema/obtain-write-lock');
+const releaseWriteLockSchema = require('../schema/release-write-lock');
+
+const settingKeys = {
+ tableName: 'dbTableLocks',
+};
+
+class LockService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'dbService']);
+ }
+
+ async init() {
+ await super.init();
+ const [dbService] = await this.service(['dbService']);
+ const table = this.settings.get(settingKeys.tableName);
+
+ this._getter = () => dbService.helper.getter().table(table);
+ this._updater = () => dbService.helper.updater().table(table);
+ this._query = () => dbService.helper.query().table(table);
+ this._deleter = () => dbService.helper.deleter().table(table);
+ }
+
+ /**
+ * Exclusively obtains a write lock and returns a write token if the lock is available or returns undefined if the lock is not available.
+ *
+ * @param {{id: string, expiresIn: number}} lockInfo Lock info with 'id' of the lock and 'expiresIn' (in seconds).
+ * The 'id' can be any identifier to uniquely identify the lock within the system. At anytime, only one write lock with the same 'id' can be obtained.
+ * The 'expiresIn' indicates the lock expiry time in seconds AFTER the lock is successfully obtained. Any further calls to 'obtainWriteLock'
+ * will return undefined until either of the following conditions are met
+ * 1. Lock is released: The given lock is explicitly released using the "releaseWriteLock" method OR
+ * 2. Lock is expired: The 'expiresIn' number of seconds have passed since the lock was obtained.
+ *
+ * @returns write token or undefined if lock could not be obtained
+ */
+ async obtainWriteLock(lockInfo) {
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+
+ // Validate input
+ await validationService.ensureValid(lockInfo, obtainWriteLockSchema);
+ const { id, expiresIn } = lockInfo;
+ const nowInSeconds = Math.ceil(Date.now() / 1000);
+ const ttlInSeconds = nowInSeconds + expiresIn;
+
+ try {
+ await this._updater()
+ // .mark(['readLocks'])
+ // .condition('attribute_not_exists(writeLock) AND (attribute_not_exists(readLocks) OR attribute_type(readLocks, :isNull ))') // later when we implement read locks
+ .condition('attribute_not_exists(id) OR #ttl < :now ') // yes we need this
+ .key('id', id)
+ .names({ '#ttl': 'ttl' })
+ .values({ ':now': nowInSeconds })
+ .item({
+ ttl: ttlInSeconds,
+ })
+ .update();
+
+ return id;
+ } catch (err) {
+ // Yes, in most cases, catching an exception to simply ignore it, is not a good practice. But, this is by design.
+ return undefined;
+ }
+ }
+
+ /**
+ * Releases the write lock given the write token. The token is returned when you call "obtainWriteLock" or "tryWriteLock".
+ * The token should be passed here to release the corresponding lock.
+ *
+ * @param {{writeToken: string}} lockReleaseInfo An object containing "writeToken".
+ * @returns {Promise}
+ */
+ async releaseWriteLock(lockReleaseInfo) {
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+
+ // Validate input
+ await validationService.ensureValid(lockReleaseInfo, releaseWriteLockSchema);
+ const { writeToken } = lockReleaseInfo;
+
+ await runAndCatch(
+ async () => {
+ return this._deleter()
+ .condition('attribute_exists(id)')
+ .key('id', writeToken)
+ .delete();
+ },
+ async () => {
+ // we ignore the ConditionalCheckFailedException exception because it simply means that the entry might
+ // have already been removed
+ },
+ );
+ }
+
+ /**
+ * Attempts to obtain a lock given the number of attempts, with one second wait after each attempt (no backoff algorithm)
+ *
+ * @param {{id: string, expiresIn: number}} lockInfo Lock info with 'id' of the lock and 'expiresIn' (in seconds)
+ * @param {{attemptsCount:number}} Attempts info with attemptsCount indicating maximum number of attempts to obtain the lock with one second wait after each attempt.
+ * @returns {Promise<*>} write token or undefined if lock could not be obtained within the specified number of attempts
+ */
+ async tryWriteLock(lockInfo, { attemptsCount = 4 } = {}) {
+ let result;
+ for (let i = 0; i < attemptsCount; i += 1) {
+ try {
+ // We need to await in sequence so disabling "no-await-in-loop" rule here
+ // eslint-disable-next-line no-await-in-loop
+ result = await this.obtainWriteLock(lockInfo);
+ if (!result) throw this.boom.internalError('Could not obtain lock', true);
+ break;
+ } catch (error) {
+ // eslint-disable-next-line no-await-in-loop
+ await sleep(1000);
+ // ignore
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Attempts to obtain a lock given the number of attempts, with one second wait after each attempt (no backoff algorithm)
+ * and runs the specified function while holding the lock. Releases the lock after successful or failed (when the function throws any Error)
+ * function excution.
+ *
+ * @param {{id: string, expiresIn: number}} lockInfo Lock info with 'id' of the lock and 'expiresIn'.
+ * @param {function} fn The function to be executed while holding the obtained lock
+ * @param {{attemptsCount: number}} options options obj with max 'attemptsCount'
+ * @returns lock object or undefined if lock could not be obtained after the specified number of attempts
+ */
+ async tryWriteLockAndRun({ id, expiresIn = 25 } = {}, fn, { attemptsCount = 15 } = {}) {
+ // we attempt to obtain a lock (max 15 times with 1 second delay in between)
+ const lock = await this.tryWriteLock({ id, expiresIn }, { attemptsCount });
+ if (_.isUndefined(lock)) throw this.boom.internalError('Could not obtain a lock', true);
+
+ try {
+ return await fn();
+ } finally {
+ try {
+ await this.releaseWriteLock({ writeToken: lock });
+ } catch (error2) {
+ this.log.info(`The release lock has an issue ${error2}`);
+ // ignore this error
+ }
+ }
+ }
+
+ // TODO
+ // - read locks APIs
+ // - extendWriteLock (to extend the expire of a lock)
+ // - obtainReadLock
+ // - extendReadLock
+ // - releaseReadLock
+ // - vacuumExpiredLocks
+}
+
+module.exports = LockService;
diff --git a/addons/addon-base/packages/services/lib/logger/log-transformer.js b/addons/addon-base/packages/services/lib/logger/log-transformer.js
new file mode 100644
index 0000000000..1e9cc78a06
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/logger/log-transformer.js
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const cycle = require('cycle');
+
+class LogTransformer {
+ constructor(loggingContext = {}, fieldsToMask = ['x-amz-security-token', 'user', 'accessKey', 'password']) {
+ this.loggingContext = loggingContext;
+ this.fieldsToMask = fieldsToMask;
+ }
+
+ transformForInfo(logPayload) {
+ return this.transform(logPayload, 'info');
+ }
+
+ transformForLog(logPayload) {
+ return this.transform(logPayload, 'log');
+ }
+
+ transformForDebug(logPayload) {
+ return this.transform(logPayload, 'debug');
+ }
+
+ transformForWarn(logPayload) {
+ return this.transform(logPayload, 'warn');
+ }
+
+ transformForError(logPayload) {
+ return this.transform(logPayload, 'error');
+ }
+
+ transform(logPayload, logLevel) {
+ const transformedLogPayload = this.augment(this.maskDeep(logPayload, this.fieldsToMask), { logLevel });
+ return transformedLogPayload;
+ }
+
+ /**
+ * Augments the given data with standard logging metadata specified in the loggingContext
+ * (such as 'envType', 'envName', 'appName', 'functionName') etc.
+ *
+ * @param data The raw data to be logged.
+ * @param additionalContext Object containing additional logging contextual information as key, value pairs. This is in addition to the loggingContext.
+ * The payload of this additionalContext and the loggingContext (specified at the time constructing the LoggingTransformer object) will be added to the raw logging data.
+ *
+ * @return {string} A transformed logging data along with additional information from the loggingContext and the additionalContext as string.
+ */
+ augment(data, additionalContext = {}) {
+ let objToLog = {
+ ...this.loggingContext,
+ ...additionalContext,
+ };
+
+ if (_.isError(data)) {
+ objToLog.msg = data.message;
+ objToLog.stack = data.stack;
+ // Merge any other fields from the error object to the target object to log
+ objToLog = _.merge({}, objToLog, data);
+ } else if (_.isArray(data)) {
+ objToLog.msg = data;
+ } else if (_.isObject(data)) {
+ objToLog = _.merge({}, objToLog, data);
+ } else {
+ objToLog.msg = data;
+ }
+ try {
+ return JSON.stringify(objToLog, null, 2);
+ } catch (e) {
+ // the most likely error when stringifying could be due to circular references in the object
+ // in that case, try stringifying after decycling (i.e., replacing circular references)
+ return JSON.stringify(cycle.decycle(objToLog), null, 2);
+ }
+ }
+
+ /**
+ * The function returns a new deep copy of the given object with the properties that are specified in the
+ * keysToMask as masked
+ * @param object The object to deep copy from
+ * @param keysToMask The properties to be masked in the returned object
+ * @return {*} A deep copy of the object with the specified properties masked as ****
+ */
+ maskDeep(object, keysToMask) {
+ if (_.isError(object)) {
+ // the javascript errors don't have fields that can be masked so just return the error as is
+ return object;
+ }
+
+ function mask(_value, key) {
+ if (keysToMask && _.indexOf(keysToMask, key) >= 0) {
+ return '****';
+ }
+ return undefined;
+ }
+
+ return _.cloneDeepWith(object, mask);
+ }
+}
+
+module.exports = LogTransformer;
diff --git a/addons/addon-base/packages/services/lib/logger/logger-service.js b/addons/addon-base/packages/services/lib/logger/logger-service.js
new file mode 100644
index 0000000000..cda956027d
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/logger/logger-service.js
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+const LogTransformer = require('./log-transformer');
+
+class LoggerService extends Service {
+ constructor(
+ logger = console,
+ loggingContext = {},
+ fieldsToMask = ['x-amz-security-token', 'user', 'accessKey', 'password'],
+ ) {
+ super();
+ this.logger = logger;
+ this.logTransformer = new LogTransformer(loggingContext, fieldsToMask);
+ }
+
+ info(logPayload, ...args) {
+ const transformedLogPayload = this.logTransformer.transformForInfo(logPayload);
+ return this.logger.info(transformedLogPayload, ...args);
+ }
+
+ log(logPayload, ...args) {
+ const transformedLogPayload = this.logTransformer.transformForLog(logPayload);
+ return this.logger.log(transformedLogPayload, ...args);
+ }
+
+ debug(logPayload, ...args) {
+ const transformedLogPayload = this.logTransformer.transformForDebug(logPayload);
+ return this.logger.debug(transformedLogPayload, ...args);
+ }
+
+ warn(logPayload, ...args) {
+ const transformedLogPayload = this.logTransformer.transformForWarn(logPayload);
+ return this.logger.warn(transformedLogPayload, ...args);
+ }
+
+ error(logPayload, ...args) {
+ const transformedLogPayload = this.logTransformer.transformForError(logPayload);
+ return this.logger.error(transformedLogPayload, ...args);
+ }
+}
+
+module.exports = LoggerService;
diff --git a/addons/addon-base/packages/services/lib/plugin-registry/plugin-registry-service.js b/addons/addon-base/packages/services/lib/plugin-registry/plugin-registry-service.js
new file mode 100644
index 0000000000..66ec5eed4b
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/plugin-registry/plugin-registry-service.js
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const { processSequentially } = require('../helpers/utils');
+
+class PluginRegistryService extends Service {
+ constructor(pluginRegistry = {}) {
+ super();
+ this.pluginRegistry = pluginRegistry;
+ }
+
+ async init() {
+ await super.init();
+ if (!_.isFunction(this.pluginRegistry.getPlugins)) {
+ throw this.boom.internalError(
+ 'The pluginRegistry service requires that a registry object is passed to it in the constructor',
+ );
+ }
+ }
+
+ async getPlugins(extensionPoint) {
+ return (await this.pluginRegistry.getPlugins(extensionPoint)) || [];
+ }
+
+ async getPluginsWithMethod(extensionPoint, methodName) {
+ const plugins = await this.getPlugins(extensionPoint);
+ return _.filter(plugins, plugin => _.isFunction(plugin[methodName]));
+ }
+
+ async runPlugins(extensionPoint, methodName, ...args) {
+ const plugins = this.getPluginsWithMethod(extensionPoint, methodName);
+
+ // Each plugin needs to be executed in order. The plugin method may return a promise we need to await
+ // it in sequence.
+ return processSequentially(plugins, async plugin => plugin[methodName](...args));
+ }
+
+ /**
+ * A method to visit plugins that implement the specified method for the specified extension point. The specified
+ * plugin method may be regular sync method or may be "async" i.e., it may return a Promise.
+ * The method calls each plugin in the same order as registered in the plugin registry. If the plugin method
+ * returns a Promise then next plugin is only invoked after the promise has settled (i.e., resolved or rejected).
+ * It invokes the specified method on each plugin and passes a result returned by the previous plugin call.
+ * This gives each plugin a chance to contribute to the payload. Each plugin can inspect the given payload and return
+ * it "as is" or return a modified payload. The payload returned by the last plugin is considered the resultant payload
+ * and returned to the caller.
+ *
+ * @param extensionPoint Name of the extension point in the plugin registry mapped to corresponding plugins
+ *
+ * @param methodName Name of the plugin method to call. The plugin method will be invoked with the payload as the
+ * first argument followed by any other arguments specified by the "args". The plugin method may be regular sync
+ * method or "async" method (i.e., it may return a Promise)
+ *
+ * @param options Various options for this call
+ * @param options.payload Value of the initial payload to pass to the first plugin
+ * @param options.continueOnError Optional flag indicating if the method should continue (i.e., continue calling the
+ * next plugin) when a plugin throws error. Defaults to false.
+ * @param options.pluginInvokerFn An optional plugin invoker function that invokes plugin. Default plugin invoker
+ * calls the plugin method with the following arguments (payloadSoFar, ...args). The "payloadSoFar" is the payload
+ * collected so far from previous plugins. The "...args" are any other arguments passed to this method. The
+ * "pluginInvokerFn" can be used in cases where the plugin method signature does not match the "(payloadSoFar, ...args)"
+ * signature. A custom implementation of "pluginInvokerFn" can be passed in such cases to customize the way the
+ * plugin is called. The "pluginInvokerFn" is called with the following arguments "(pluginPayload, plugin, method, ...args)"
+ *
+ * @param args Any other arguments to pass to the plugins
+ *
+ * @returns {Promise<*>}
+ */
+ async visitPlugins(
+ extensionPoint,
+ methodName,
+ {
+ payload = {},
+ continueOnError = false,
+ pluginInvokerFn = async (pluginPayload, plugin, method, ...args) => plugin[method](pluginPayload, ...args),
+ } = {},
+ ...args
+ ) {
+ const plugins = await this.getPluginsWithMethod(extensionPoint, methodName);
+
+ let payloadSoFar = payload;
+ // eslint-disable-next-line no-restricted-syntax
+ for (const plugin of plugins) {
+ try {
+ // need to await in strict order so disabling "no-await-in-loop" rule here
+ // eslint-disable-next-line no-await-in-loop
+ payloadSoFar = await pluginInvokerFn(payloadSoFar, plugin, methodName, ...args);
+ } catch (err) {
+ if (!continueOnError) {
+ throw err;
+ }
+ }
+ }
+ return payloadSoFar;
+ }
+}
+
+module.exports = PluginRegistryService;
diff --git a/addons/addon-base/packages/services/lib/plugins/audit-plugin.js b/addons/addon-base/packages/services/lib/plugins/audit-plugin.js
new file mode 100644
index 0000000000..a57025bee9
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/plugins/audit-plugin.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.
+ */
+
+async function prepare({ requestContext, container, auditEvent }) {
+ if (!auditEvent.logEventType) {
+ auditEvent.logEventType = 'audit';
+ }
+ return { requestContext, container, auditEvent };
+}
+async function write({ requestContext, container, auditEvent }) {
+ const logger = await container.find('log');
+ logger.log(auditEvent);
+ return { requestContext, container, auditEvent };
+}
+
+/**
+ * A basic audit plugin that just logs audit events using the logger. The default logger writes logs using "console".
+ * In AWS Lambda based deployments, these logs will automatically go to AWS CloudWatch Logs.
+ * The plugin adds a field named "logEventType = audit" to the audit event. This allows searching for all audit log
+ * events using AWS CloudWatch Logs Insights using filter logEventType = 'audit'
.
+ *
+ * For example,
+ *
+ * fields @timestamp, @message
+ * | sort @timestamp desc
+ * | limit 20
+ * | filter logEventType = 'audit'
+ *
+ *
+ * @type {{prepare: (function({requestContext: *, container: *, auditEvent: *}): {container: *, requestContext: *, auditEvent: *}), write: (function({requestContext: *, container: *, auditEvent?: *}): {container: *, requestContext: *, auditEvent: *})}}
+ */
+const plugin = {
+ prepare,
+ write,
+};
+
+module.exports = plugin;
diff --git a/addons/addon-base/packages/services/lib/plugins/authorization-plugin.js b/addons/addon-base/packages/services/lib/plugins/authorization-plugin.js
new file mode 100644
index 0000000000..6e0ec58928
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/plugins/authorization-plugin.js
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+const authorizationPluginFactory = require('../authorization/authorization-plugin-factory');
+
+module.exports = authorizationPluginFactory('addon/base/authorizers');
diff --git a/addons/addon-base/packages/services/lib/s3-service.js b/addons/addon-base/packages/services/lib/s3-service.js
new file mode 100644
index 0000000000..de6c81a776
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/s3-service.js
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const path = require('path');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const moveS3ObjectSchema = require('./schema/move-s3-object');
+
+class S3Service extends Service {
+ constructor() {
+ super();
+ this.dependency(['aws', 'jsonSchemaValidationService']);
+ }
+
+ async init() {
+ await super.init();
+ const aws = await this.service('aws');
+ this.api = new aws.sdk.S3({ signatureVersion: 'v4' });
+ }
+
+ // files = [ {bucket, key}, {bucket, key} ]
+ async sign({ files = [], expireSeconds = 120 } = {}) {
+ const signIt = (bucket, key) => {
+ return new Promise((resolve, reject) => {
+ const params = {
+ Bucket: bucket,
+ Key: key,
+ Expires: expireSeconds,
+ };
+ this.api.getSignedUrl('getObject', params, (err, url) => {
+ if (err) return reject(err);
+ return resolve(url);
+ });
+ });
+ };
+
+ /* eslint-disable no-restricted-syntax, no-await-in-loop */
+ for (const file of files) {
+ const signedUrl = await signIt(file.bucket, file.key);
+ file.signedUrl = signedUrl;
+ }
+ /* eslint-enable no-restricted-syntax, no-await-in-loop */
+
+ return files;
+ }
+
+ async listObjects({ bucket, prefix }) {
+ const params = {
+ Bucket: bucket,
+ MaxKeys: 980,
+ Prefix: prefix,
+ };
+
+ const result = [];
+ let data;
+ do {
+ data = await this.api.listObjectsV2(params).promise(); // eslint-disable-line no-await-in-loop
+ params.ContinuationToken = data.NextContinuationToken;
+ const prefixSlash = _.endsWith(prefix, '/') ? prefix : `${prefix}/`;
+
+ _.forEach(data.Contents, item => {
+ // eslint-disable-line no-loop-func
+ if (item.Key === prefixSlash) return;
+
+ result.push({
+ key: item.Key,
+ bucket,
+ fullPath: item.Key.substring(prefixSlash.length),
+ isFolder: _.endsWith(item.Key, '/'),
+ filename: path.basename(item.Key),
+ updatedAt: item.LastModified,
+ etag: item.ETag,
+ size: item.Size,
+ storageClass: item.StorageClass,
+ });
+ });
+ } while (params.ContinuationToken);
+
+ return result;
+ }
+
+ /**
+ * Parses given s3 location URI in the form "s3://some-bucket/some/path" form and returns an object containing s3BucketName and s3Key.
+ * @param s3Location The s3 location uri in s3://some-bucket/some/path format
+ * @returns {{s3BucketName: string, s3Key: string}} A promise that resolves to an object with shape {s3BucketName, s3Key}
+ */
+ parseS3Details(s3Location) {
+ const s3Prefix = 's3://';
+ if (!_.startsWith(s3Location, s3Prefix)) {
+ throw new Error('Incorrect s3Location. Expecting s3Location to be in s3://bucketname/s3key format');
+ }
+ const s3Path = s3Location.substring(s3Prefix.length, s3Location.length);
+ const idxOfFirstSlash = s3Path.indexOf('/');
+ const s3BucketName = s3Path.substring(0, idxOfFirstSlash < 0 ? s3Path.length : idxOfFirstSlash);
+ const s3Key = s3Path.substring(idxOfFirstSlash + 1, idxOfFirstSlash < 0 ? idxOfFirstSlash : s3Path.length);
+
+ return { s3BucketName, s3Key };
+ }
+
+ /**
+ * Checks if the given s3Location in the form "s3://some-bucket/some-path" exists
+ * @param s3Location The s3 location uri in s3://some-bucket/some/path format
+ * @returns {Promise} A promise that resolves to a flag indicating whether the specified s3 location exists or not
+ */
+ async doesS3LocationExist(s3Location) {
+ const { s3BucketName, s3Key } = this.parseS3Details(_.trim(s3Location));
+ try {
+ if (s3Key && s3Key !== '/') {
+ // If s3Key is specified and if it is not just trailing forward slash then make sure the
+ // prefix specified by the s3Key exists
+ // For example, if the s3Location is s3://some-bucket-name/some-object-key then make sure the
+ // object or prefix with key "some-object-key" exists in the bucket "some-bucket-name"
+ const listingResult = await this.api
+ .listObjectsV2({
+ Bucket: s3BucketName,
+ Prefix: s3Key,
+ })
+ .promise();
+ return !_.isNil(listingResult) && listingResult.KeyCount > 0;
+ }
+
+ // If s3Key is not specified OR if it is just trailing forward slash then make sure the
+ // specified bucket exists
+ // For example, if the s3Location is "s3://some-bucket-name" or ""s3://some-bucket-name/" then make sure the
+ // bucket named "some-bucket-name" exists
+ const bucket = await this.api.headBucket({ Bucket: s3BucketName }).promise();
+ return !_.isNil(bucket);
+ } catch (err) {
+ if (err.code === 'NotFound' || err.code === 'NoSuchBucket' || err.code === 'NoSuchKey') {
+ // If the bucket does not exist or the S3 location path does not exist then return false
+ return false;
+ }
+ // in case of any other error, let it bubble up
+ throw err;
+ }
+ }
+
+ // Moves the object by first copying it to the new destination then deleting it from the source
+ async moveObject(rawData) {
+ const [validationService] = await this.service(['jsonSchemaValidationService']);
+
+ // Validate input
+ await validationService.ensureValid(rawData, moveS3ObjectSchema);
+ const { from, to } = rawData;
+
+ // we do a copy then we a delete
+ const copyParams = {
+ Bucket: to.bucket,
+ CopySource: `/${from.bucket}/${from.key}`,
+ Key: `${to.key}`,
+ };
+
+ await this.api.copyObject(copyParams).promise();
+ await this.api.deleteObject({ Bucket: from.bucket, Key: `${from.key}` }).promise();
+ }
+
+ async streamToS3(bucket, toKey, inputStream) {
+ return this.api
+ .upload({
+ Bucket: bucket,
+ Key: toKey,
+ Body: inputStream,
+ })
+ .promise();
+ }
+}
+
+module.exports = S3Service;
diff --git a/addons/addon-base/packages/services/lib/schema/create-user.json b/addons/addon-base/packages/services/lib/schema/create-user.json
new file mode 100644
index 0000000000..3cd18d667c
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/schema/create-user.json
@@ -0,0 +1,62 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "pattern": "^[A-Za-z0-9-_.]+$|^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"
+ },
+ "usernameInIdp": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "pattern": "^[A-Za-z0-9-_.]+$|^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"
+ },
+ "password": {
+ "type": "string"
+ },
+ "authenticationProviderId": {
+ "type": "string"
+ },
+ "identityProviderName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"
+ },
+ "firstName": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500,
+ "pattern": "^[A-Za-z0-9 .-]+$"
+ },
+ "lastName": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500,
+ "pattern": "^[A-Za-z0-9 .-]+$"
+ },
+ "userType": {
+ "type": "string",
+ "enum": ["root"]
+ },
+ "isSamlAuthenticatedUser": {
+ "type": "boolean"
+ },
+ "isAdmin": {
+ "type": "boolean"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["active", "inactive"]
+ },
+ "rev": {
+ "type": "number"
+ }
+ },
+ "required": ["username", "email", "firstName", "lastName"]
+}
diff --git a/addons/addon-base/packages/services/lib/schema/move-s3-object.json b/addons/addon-base/packages/services/lib/schema/move-s3-object.json
new file mode 100644
index 0000000000..3c8abdcf97
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/schema/move-s3-object.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "definitions": {
+ "s3Info": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "bucket": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1024
+ },
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1024
+ }
+ },
+ "required": ["bucket", "key"]
+ }
+ },
+
+ "properties": {
+ "from": {
+ "$ref": "#/definitions/s3Info"
+ },
+ "to": {
+ "$ref": "#/definitions/s3Info"
+ }
+ },
+ "required": ["from", "to"]
+}
diff --git a/addons/addon-base/packages/services/lib/schema/obtain-write-lock.json b/addons/addon-base/packages/services/lib/schema/obtain-write-lock.json
new file mode 100644
index 0000000000..5417c0b75c
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/schema/obtain-write-lock.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "expiresIn": {
+ "type": "integer",
+ "minimum": 1
+ }
+ },
+ "required": ["id", "expiresIn"]
+}
diff --git a/addons/addon-base/packages/services/lib/schema/release-write-lock.json b/addons/addon-base/packages/services/lib/schema/release-write-lock.json
new file mode 100644
index 0000000000..87ca878b6f
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/schema/release-write-lock.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "writeToken": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": ["writeToken"]
+}
diff --git a/addons/addon-base/packages/services/lib/schema/update-user.json b/addons/addon-base/packages/services/lib/schema/update-user.json
new file mode 100644
index 0000000000..e8fdd6f32b
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/schema/update-user.json
@@ -0,0 +1,59 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "pattern": "^[A-Za-z0-9-_]+$"
+ },
+ "usernameInIdp": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "pattern": "^[A-Za-z0-9-_]+$"
+ },
+ "authenticationProviderId": {
+ "type": "string"
+ },
+ "identityProviderName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"
+ },
+ "firstName": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500,
+ "pattern": "^[A-Za-z0-9 .-]+$"
+ },
+ "lastName": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500,
+ "pattern": "^[A-Za-z0-9 .-]+$"
+ },
+ "userType": {
+ "type": "string",
+ "enum": ["root"]
+ },
+ "isSamlAuthenticatedUser": {
+ "type": "boolean"
+ },
+ "isAdmin": {
+ "type": "boolean"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["active", "inactive"]
+ },
+ "rev": {
+ "type": "number"
+ }
+ },
+ "required": ["username", "rev"]
+}
diff --git a/addons/addon-base/packages/services/lib/settings/env-settings-service.js b/addons/addon-base/packages/services/lib/settings/env-settings-service.js
new file mode 100644
index 0000000000..72798b3b8c
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/settings/env-settings-service.js
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const { extract } = require('./env-vars');
+
+function normalize(value) {
+ if (_.isNil(value) || _.isString(value)) return value;
+ return JSON.stringify(value);
+}
+
+class EnvBasedSettingsService extends Service {
+ constructor({ provider } = {}) {
+ super();
+ this.normalized = {};
+ this.originals = {};
+ const vars = extract('APP_');
+ const add = (key, value) => {
+ const v = normalize(value);
+ this.normalized[key.toLowerCase()] = v;
+ this.originals[key] = v;
+ };
+
+ _.forEach(vars, (value, key) => {
+ add(key, value);
+ });
+
+ this.set('lambdaRoot', process.env.LAMBDA_TASK_ROOT);
+
+ if (provider) {
+ const map = provider.getDefaults(this);
+ _.forEach(map, (value, key) => {
+ if (!_.has(this.normalized, key.toLowerCase())) {
+ add(key, value);
+ }
+ });
+ }
+ }
+
+ get entries() {
+ return this.originals;
+ }
+
+ set(key, value) {
+ const v = normalize(value);
+ this.normalized[key.toLowerCase()] = v;
+ this.originals[key] = v;
+ }
+
+ get(rawKey) {
+ const key = rawKey.toLowerCase();
+ const value = this.normalized[key];
+ if (_.isEmpty(value))
+ throw new Error(
+ `The "${key}" setting value is required but it is either empty or not provided via the environment variables.`,
+ );
+
+ return value;
+ }
+
+ getObject(key) {
+ const value = this.get(key);
+ try {
+ return JSON.parse(value);
+ } catch (e) {
+ throw new Error(`The "${key}" setting value (${value}) is not a valid JSON object.`);
+ }
+ }
+
+ getBoolean(rawKey) {
+ const key = rawKey.toLowerCase();
+ const raw = this.normalized[key];
+ const error = () => new Error(`The "${key}" setting value (${raw}) is not a valid boolean.`);
+ if (_.isNil(raw)) throw error();
+
+ let value;
+ try {
+ value = JSON.parse(raw);
+ } catch (e) {
+ throw error();
+ }
+
+ if (!_.isBoolean(value)) throw error();
+
+ return value;
+ }
+
+ getNumber(rawKey) {
+ const key = rawKey.toLowerCase();
+ const raw = this.normalized[key];
+ const error = () => new Error(`The "${key}" setting value (${raw}) is not a valid number.`);
+ if (_.isNil(raw)) throw error();
+
+ let value;
+ try {
+ value = JSON.parse(raw);
+ } catch (e) {
+ throw error();
+ }
+
+ if (!_.isNumber(value)) throw error();
+
+ return value;
+ }
+
+ optional(rawKey, defaultValue) {
+ const key = rawKey.toLowerCase();
+ const value = this.normalized[key];
+ if (_.isEmpty(value)) return defaultValue;
+
+ return value;
+ }
+
+ optionalObject(rawKey, defaultValue) {
+ const key = rawKey.toLowerCase();
+ let value = this.normalized[key];
+ if (_.isEmpty(value)) return defaultValue;
+ const error = () => new Error(`The "${key}" setting value (${value}) is not a valid object.`);
+
+ try {
+ value = JSON.parse(value);
+ } catch (e) {
+ throw error();
+ }
+
+ if (_.isEmpty(value)) return defaultValue;
+ if (!_.isObject(value)) throw error();
+
+ return value;
+ }
+
+ optionalNumber(rawKey, defaultValue) {
+ const key = rawKey.toLowerCase();
+ let value = this.normalized[key];
+ if (_.isNil(value)) return defaultValue;
+ const error = () => new Error(`The "${key}" setting value (${value}) is not a valid number.`);
+
+ try {
+ value = JSON.parse(value);
+ } catch (e) {
+ throw error();
+ }
+
+ if (!_.isNumber(value)) throw error();
+
+ return value;
+ }
+
+ optionalBoolean(rawKey, defaultValue) {
+ const key = rawKey.toLowerCase();
+ let value = this.normalized[key];
+ if (_.isNil(value) || value === '') return defaultValue;
+ const error = () => new Error(`The "${key}" setting value (${value}) is not a valid boolean.`);
+
+ try {
+ value = JSON.parse(value);
+ } catch (e) {
+ throw error();
+ }
+
+ if (!_.isBoolean(value)) throw error();
+
+ return value;
+ }
+}
+
+module.exports = EnvBasedSettingsService;
diff --git a/addons/addon-base/packages/services/lib/settings/env-vars.js b/addons/addon-base/packages/services/lib/settings/env-vars.js
new file mode 100644
index 0000000000..412145c725
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/settings/env-vars.js
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+
+// Loops through all environment variables that start with given "prefix" and
+// return them all in one object map.
+//
+// For example, if the variable name is 'APP_AWS_REGION', and the prefix is "APP_",
+// this translates into the object:
+// {
+// 'awsRegion': '',
+// ... other key/value pairs
+// }
+function extract(prefix = '') {
+ const object = {};
+ _.forEach(process.env, (value, keyRaw = '') => {
+ if (!_.startsWith(keyRaw, prefix)) return;
+ const sliced = keyRaw.slice(prefix.length);
+ const key = _.camelCase(sliced);
+ object[key] = value;
+ });
+
+ return object;
+}
+
+module.exports = {
+ extract,
+};
diff --git a/addons/addon-base/packages/services/lib/settings/wrapper-settings-service.js b/addons/addon-base/packages/services/lib/settings/wrapper-settings-service.js
new file mode 100644
index 0000000000..75b5387e88
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/settings/wrapper-settings-service.js
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+class SettingsService extends Service {
+ constructor(settings) {
+ super();
+ this._original = settings;
+ }
+
+ get entries() {
+ return this._original.entries;
+ }
+
+ set(key, value) {
+ this._original.set(key, value);
+ }
+
+ get(key) {
+ return this._original.get(key);
+ }
+
+ getObject(key) {
+ return this._original.getObject(key);
+ }
+
+ getBoolean(key) {
+ return this._original.getBoolean(key);
+ }
+
+ getNumber(key) {
+ return this._original.getNumber(key);
+ }
+
+ optional(key, defaultValue) {
+ return this._original.optional(key, defaultValue);
+ }
+
+ optionalObject(key, defaultValue) {
+ return this._original.optionalObject(key, defaultValue);
+ }
+
+ optionalNumber(key, defaultValue) {
+ return this._original.optionalNumber(key, defaultValue);
+ }
+
+ optionalBoolean(key, defaultValue) {
+ return this._original.optionalBoolean(key, defaultValue);
+ }
+}
+
+module.exports = SettingsService;
diff --git a/addons/addon-base/packages/services/lib/user/helpers/user-namespace.js b/addons/addon-base/packages/services/lib/user/helpers/user-namespace.js
new file mode 100644
index 0000000000..fcd42259a7
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/user/helpers/user-namespace.js
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+const delimiter = '||||';
+
+/**
+ * Returns namespace for identifying users. A user with the same username may be authenticated by different authentication
+ * providers. An authentication provider itself may federate user across multiple identity providers.
+ * For example, a given Cognito User Pool can act as an authentication provider and the given user pool itself may
+ * federate users from multiple identity providers (such as multiple SAML IdPs or social IdPs such as google, facebook etc).
+ * To uniquely identify a user we need 2 things the username and username's namespace. A username has to be unique
+ * within a given namespace. The namespace itself is a composite of authenticationProviderId (for example, cognito
+ * user pool uri) and optionally identityProviderName.
+ *
+ * @param authenticationProviderId
+ * @param identityProviderName
+ * @returns {string|*}
+ */
+function toUserNamespace(authenticationProviderId, identityProviderName) {
+ if (identityProviderName) {
+ return `${identityProviderName}${delimiter}${authenticationProviderId}`;
+ }
+ return authenticationProviderId;
+}
+
+/**
+ * This is the inverse of the "toUserNamespace" function.
+ * It returns the "authenticationProviderId" and "identityProviderName" based on the given user namespace.
+ *
+ * @param userNamespace
+ * @returns {{authenticationProviderId: *, identityProviderName: *}}
+ */
+function fromUserNamespace(userNamespace) {
+ const parts = userNamespace.split(delimiter);
+ let authenticationProviderId;
+ let identityProviderName;
+ if (parts.length > 1) {
+ identityProviderName = parts[0];
+ authenticationProviderId = parts[1];
+ } else {
+ authenticationProviderId = parts[0];
+ }
+ return { authenticationProviderId, identityProviderName };
+}
+
+module.exports = { toUserNamespace, fromUserNamespace };
diff --git a/addons/addon-base/packages/services/lib/user/user-authz-service.js b/addons/addon-base/packages/services/lib/user/user-authz-service.js
new file mode 100644
index 0000000000..0be8328755
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/user/user-authz-service.js
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const {
+ isDeny,
+ allowIfActive,
+ allowIfAdmin,
+ allowIfRoot,
+ allowIfCurrentUserOrAdmin,
+ allow,
+ deny,
+} = require('../authorization/authorization-utils');
+const { toUserNamespace } = require('./helpers/user-namespace');
+
+class UserAuthzService extends Service {
+ async authorize(requestContext, { resource, action, effect, reason }, ...args) {
+ // if effect is "deny" already (due to any of the previous plugins returning "deny") then return "deny" right away
+ if (isDeny({ effect })) return { resource, action, effect, reason };
+
+ switch (action) {
+ case 'create':
+ return this.authorizeCreate(requestContext, { action }, ...args);
+ case 'delete':
+ return this.authorizeDelete(requestContext, { action }, ...args);
+ case 'update':
+ return this.authorizeUpdate(requestContext, { action }, ...args);
+ case 'updateAttributes':
+ return this.authorizeUpdateAttributes(requestContext, { action }, ...args);
+ default:
+ // This authorizer does not know how to perform authorizer for the specified action so return with the current
+ // authorization decision collected so far
+ return { effect };
+ }
+ }
+
+ // Protected methods
+ async authorizeCreate(requestContext, { action }) {
+ // Make sure the caller is active
+ let permissionSoFar = await allowIfActive(requestContext, { action });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ // Only admins can create users by default
+ permissionSoFar = await allowIfAdmin(requestContext, { action });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ // If code reached here then allow this call
+ return allow();
+ }
+
+ async authorizeDelete(requestContext, { action }, user) {
+ // basic authorization rules for delete user are same as create user at the moment
+ const result = await this.authorizeCreate(requestContext, { action });
+
+ // return right away if denying
+ if (isDeny(result)) return result; // return if denying
+
+ // Make sure root user can be deleted only by root user her/himself
+ if (_.get(user, 'userType') === 'root') {
+ const permissionSoFar = await allowIfRoot(requestContext, { action });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+ }
+
+ // If code reached here then allow this call
+ return allow();
+ }
+
+ async authorizeUpdate(requestContext, { action }, user) {
+ // Make sure the caller is active
+ let permissionSoFar = await allowIfActive(requestContext, { action });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ // User can update only their own attributes unless the user is an admin
+ const { username, authenticationProviderId, identityProviderName } = user;
+ const ns = toUserNamespace(authenticationProviderId, identityProviderName);
+ permissionSoFar = await allowIfCurrentUserOrAdmin(requestContext, { action }, { username, ns });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ return allow();
+ }
+
+ async authorizeUpdateAttributes(requestContext, { action }, user, existingUser) {
+ let permissionSoFar = allow();
+ // Inspect the attributes being updated and make sure the user has permissions to update those attributes
+ if (existingUser.isAdmin !== user.isAdmin || existingUser.status !== user.status) {
+ // The "isAdmin" and "status" properties should be updated only by admins
+ permissionSoFar = await allowIfAdmin(requestContext, { action });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+ }
+
+ if (existingUser.userType === 'root') {
+ // If the existing user is root user then make sure only root user is updating it
+ permissionSoFar = await allowIfRoot(requestContext, { action });
+ if (isDeny(permissionSoFar)) return permissionSoFar; // return if denying
+
+ // Certain properties on root user are immutable so should not be updated
+ if (
+ existingUser.authenticationProviderId !== user.authenticationProviderId ||
+ existingUser.identityProviderName !== user.identityProviderName ||
+ existingUser.isAdmin !== user.isAdmin
+ ) {
+ return deny('You are not authorized to update these fields on the root user', true);
+ }
+ }
+ return allow();
+ }
+}
+module.exports = UserAuthzService;
diff --git a/addons/addon-base/packages/services/lib/user/user-service.js b/addons/addon-base/packages/services/lib/user/user-service.js
new file mode 100644
index 0000000000..1f6c8e7083
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/user/user-service.js
@@ -0,0 +1,385 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+
+const { runAndCatch } = require('../helpers/utils');
+const { toUserNamespace } = require('./helpers/user-namespace');
+const createUserJsonSchema = require('../schema/create-user');
+const updateUserJsonSchema = require('../schema/update-user');
+
+const settingKeys = {
+ tableName: 'dbTableUsers',
+};
+
+class UserService extends Service {
+ constructor() {
+ super();
+ this.dependency([
+ 'dbService',
+ 'dbPasswordService',
+ 'authorizationService',
+ 'userAuthzService',
+ 'auditWriterService',
+ 'jsonSchemaValidationService',
+ ]);
+ }
+
+ async init() {
+ await super.init();
+ const [userAuthzService] = await this.service(['userAuthzService']);
+
+ // A private authorization condition function that just delegates to the userAuthzService
+ this.allowAuthorized = (requestContext, { resource, action, effect, reason }, ...args) =>
+ userAuthzService.authorize(requestContext, { resource, action, effect, reason }, ...args);
+ }
+
+ async createUser(requestContext, user) {
+ // ensure that the caller has permissions to create the user
+ // The following will result in checking permissions by calling the condition function "this.allowAuthorized" first
+ await this.assertAuthorized(requestContext, { action: 'create', conditions: [this.allowAuthorized] }, user);
+
+ // Validate input
+ await this.validateCreateUser(requestContext, user);
+
+ const { username, password } = user;
+ const authenticationProviderId = user.authenticationProviderId || 'internal';
+ if (password) {
+ // If password is specified then make sure this is for adding user to internal authentication provider only
+ // Password cannot be specified for any other auth providers
+ if (authenticationProviderId !== 'internal') {
+ throw this.boom.badRequest('Cannot specify password when adding federated users', true);
+ }
+ // Save password salted hash for the user in internal auth provider (i.e., in passwords table)
+ const dbPasswordService = await this.service('dbPasswordService');
+ await dbPasswordService.savePassword(requestContext, { username, password });
+ }
+
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+
+ const by = _.get(requestContext, 'principalIdentifier');
+
+ const { identityProviderName } = user;
+ const ns = toUserNamespace(authenticationProviderId, identityProviderName);
+
+ // Set default attributes (such as "isAdmin" flag and "status") on the user being created
+ await this.setDefaultAttributes(requestContext, user);
+
+ const existingUser = await this.findUser({
+ username,
+ authenticationProviderId,
+ identityProviderName,
+ });
+ let result;
+ if (existingUser) {
+ throw this.boom.alreadyExists('Cannot add user. The user already exists.', true);
+ } else {
+ // user does not exist so create it
+ result = await dbService.helper
+ .updater()
+ .table(table)
+ .key({ username, ns })
+ .item({
+ ...user,
+ authenticationProviderId,
+ rev: 0,
+ createdBy: by,
+ })
+ .update();
+ }
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'create-user', body: result });
+ return result;
+ }
+
+ async updateUser(requestContext, user) {
+ // ensure that the caller has permissions to update the user
+ // The following will result in checking permissions by calling the condition function "this.allowAuthorized" first
+ await this.assertAuthorized(requestContext, { action: 'update', conditions: [this.allowAuthorized] }, user);
+
+ // Validate input
+ await this.validateUpdateUser(requestContext, user);
+
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+
+ const by = _.get(requestContext, 'principalIdentifier');
+
+ const { username, identityProviderName } = user;
+ const authenticationProviderId = user.authenticationProviderId || 'internal';
+ const ns = toUserNamespace(authenticationProviderId, identityProviderName);
+ const existingUser = await this.findUser({
+ username,
+ authenticationProviderId,
+ identityProviderName,
+ });
+
+ let result;
+ if (existingUser) {
+ // ensure that the caller has permissions to update the user
+ // The following will result in checking permissions by calling the condition function "this.allowAuthorized" first
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'updateAttributes', conditions: [this.allowAuthorized] },
+ user,
+ existingUser,
+ );
+
+ // Validate the user attributes being updated
+ await this.validateUpdateAttributes(requestContext, user, existingUser);
+
+ // user exists, so update it
+ result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .updater()
+ .table(table)
+ .key({ username, ns })
+ .item(_.omit({ ...existingUser, ...user, updatedBy: by }, ['rev'])) // Remove 'rev' from the item. The "rev" method call below adds it correctly in update expression
+ .rev(user.rev)
+ .update();
+ },
+ async () => {
+ throw this.boom.outdatedUpdateAttempt(
+ `User "${username}" was just updated before your request could be processed, please refresh and try again`,
+ true,
+ );
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'update-user', body: result });
+ } else {
+ throw this.boom.notFound(`Cannot update user "${username}". The user does not exist`, true);
+ }
+ return result;
+ }
+
+ async deleteUser(requestContext, user) {
+ const { username, authenticationProviderId, identityProviderName } = user;
+ const existingUser = await this.mustFindUser({ username, authenticationProviderId, identityProviderName });
+
+ // ensure that the caller has permissions to delete the user
+ // The following will result in checking permissions by calling the condition function "this.allowAuthorized" first
+ await this.assertAuthorized(requestContext, { action: 'delete', conditions: [this.allowAuthorized] }, existingUser);
+
+ const ns = toUserNamespace(authenticationProviderId, identityProviderName);
+
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+
+ const result = await runAndCatch(
+ async () => {
+ return dbService.helper
+ .deleter()
+ .table(table)
+ .condition('attribute_exists(ns)') // yes we need this
+ .key({ username, ns })
+ .delete();
+ },
+ async () => {
+ throw this.boom.notFound(`The user with username "${username}" does not exist`, true);
+ },
+ );
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'delete-user', body: user });
+
+ return result;
+ }
+
+ async validateUsers(users) {
+ if (!Array.isArray(users)) {
+ throw this.boom.badRequest(`invalid users type`, true);
+ }
+
+ // ensure there are no duplicates
+ const distinctUsers = new Set(users.map(u => `${u.username}||||${u.ns}`));
+ if (distinctUsers.size < users.length) {
+ throw this.boom.badRequest('user list contains duplicates', true);
+ }
+
+ const findUserPromises = users.map(user => {
+ const { username, ns } = user;
+ return this.getUser({ username, ns });
+ });
+
+ const findUserResults = await Promise.all(findUserPromises);
+ const findUserExistsStatus = findUserResults.map((user, index) => {
+ return { usersIndex: index, exists: !!user };
+ });
+ const nonExistingUsers = findUserExistsStatus
+ .filter(item => !item.exists)
+ .map(item => users[item.usersIndex].username);
+
+ if (nonExistingUsers.length) {
+ throw this.boom.badRequest(`non available user: [${nonExistingUsers}]`, true);
+ }
+ }
+
+ async getUser({ username, ns, fields = [] }) {
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+ return dbService.helper
+ .getter()
+ .table(table)
+ .key({ username, ns })
+ .projection(fields)
+ .get();
+ }
+
+ async findUser({ username, authenticationProviderId, identityProviderName, fields = [] }) {
+ const ns = toUserNamespace(authenticationProviderId, identityProviderName);
+ return this.getUser({ username, ns, fields });
+ }
+
+ async exists({ username, authenticationProviderId, identityProviderName }) {
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+ const ns = toUserNamespace(authenticationProviderId, identityProviderName);
+ const item = await dbService.helper
+ .getter()
+ .table(table)
+ .key({ username, ns })
+ .get();
+
+ if (item === undefined) return false;
+ return username === item.username;
+ }
+
+ async isCurrentUserActive(requestContext) {
+ return this.isUserActive(requestContext.principal);
+ }
+
+ async isUserActive(user) {
+ return user.status && user.status.toLowerCase() === 'active';
+ }
+
+ async mustFindUser({ username, authenticationProviderId, identityProviderName, fields = [] }) {
+ const user = await this.findUser({
+ username,
+ authenticationProviderId,
+ identityProviderName,
+ fields,
+ });
+ if (!user) throw this.boom.notFound(`The user "${username}" is not found`, true);
+ return user;
+ }
+
+ async listUsers(requestContext, { fields = [] } = {}) {
+ const dbService = await this.service('dbService');
+ const table = this.settings.get(settingKeys.tableName);
+ // TODO: Handle pagination
+ const users = await dbService.helper
+ .scanner()
+ .table(table)
+ .limit(1000)
+ .projection(fields)
+ .scan();
+
+ const isAdmin = _.get(requestContext, 'principal.isAdmin', false);
+ return isAdmin ? users : users.map(user => _.omit(user, ['isAdmin']));
+ }
+
+ // Protected methods
+ /**
+ * Method to set default attributes to the given user object.
+ * For example, if the user does not have "isAdmin" flag set, the method defaults it to "false" (i.e., create non-admin user, by default)
+ *
+ * @param requestContext
+ * @param user
+ * @returns {Promise}
+ */
+ async setDefaultAttributes(requestContext, user) {
+ const setDefaultIfNil = (attribName, defaultValue) => {
+ if (_.isNil(user[attribName])) {
+ user[attribName] = defaultValue;
+ }
+ };
+ setDefaultIfNil('isAdmin', false);
+ setDefaultIfNil('status', 'active');
+ }
+
+ /**
+ * Validates the input for createUser api. The base version just does JSON schema validation using the schema
+ * returned by the "getCreateUserJsonSchema" method. Subclasses, can override this method to perform any additional
+ * validations.
+ *
+ * @param requestContext
+ * @param input
+ * @returns {Promise}
+ */
+ async validateCreateUser(requestContext, input) {
+ const jsonSchemaValidationService = await this.service('jsonSchemaValidationService');
+ const schema = await this.getCreateUserJsonSchema();
+ await jsonSchemaValidationService.ensureValid(input, schema);
+ }
+
+ /**
+ * Validates the input for updateUser api. The base version just does JSON schema validation using the schema
+ * returned by the "getUpdateUserJsonSchema" method. Subclasses, can override this method to perform any additional
+ * validations.
+ *
+ * @param requestContext
+ * @param input
+ * @returns {Promise}
+ */
+ async validateUpdateUser(requestContext, input) {
+ const jsonSchemaValidationService = await this.service('jsonSchemaValidationService');
+ const schema = await this.getUpdateUserJsonSchema();
+ await jsonSchemaValidationService.ensureValid(input, schema);
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ async validateUpdateAttributes(requestContext, user, existingUser) {
+ // No-op at base level
+ }
+
+ async getCreateUserJsonSchema() {
+ return createUserJsonSchema;
+ }
+
+ async getUpdateUserJsonSchema() {
+ return updateUserJsonSchema;
+ }
+
+ async assertAuthorized(requestContext, { action, conditions }, ...args) {
+ const authorizationService = await this.service('authorizationService');
+
+ // The "authorizationService.assertAuthorized" below will evaluate permissions by calling the "conditions" functions first
+ // It will then give a chance to all registered plugins (if any) to perform their authorization.
+ // The plugins can even override the authorization decision returned by the conditions
+ // See "authorizationService.authorize" method for more details
+ await authorizationService.assertAuthorized(
+ requestContext,
+ { extensionPoint: 'user-authz', action, conditions },
+ ...args,
+ );
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail main call if audit writing fails for some reason
+ // If the main call also needs to fail in case writing to any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+}
+
+module.exports = UserService;
diff --git a/addons/addon-base/packages/services/lib/utils/services-registration-util.js b/addons/addon-base/packages/services/lib/utils/services-registration-util.js
new file mode 100644
index 0000000000..4af37070bc
--- /dev/null
+++ b/addons/addon-base/packages/services/lib/utils/services-registration-util.js
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+
+const _ = require('lodash');
+const EnvSettingsService = require('../settings/env-settings-service');
+const LoggerService = require('../logger/logger-service');
+
+/**
+ * Utility function to register services by calling each service registration plugin in order.
+ * @param {*} container An instance of ServicesContainer
+ * @param {getPlugins} pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ * Each 'service' plugin in the returned array is an object containing "registerServices" method.
+ *
+ * @returns {Promise}
+ */
+async function registerServices(container, pluginRegistry) {
+ // Get all services plugins from the services plugin registry
+ // Each plugin is an object containing "registerServices" method
+ const plugins = await pluginRegistry.getPlugins('service');
+
+ if (!_.isArray(plugins)) {
+ throw new Error('Expecting plugins to be an array');
+ }
+
+ // Register settings service first
+ const settingsService = await registerSettingsService(container, plugins, pluginRegistry);
+
+ // Next, register logger service
+ await registerLoggerService(container, plugins, settingsService, pluginRegistry);
+
+ // Finally, register all other services
+ await registerOtherServices(container, plugins, pluginRegistry);
+}
+
+async function registerOtherServices(container, plugins, pluginRegistry) {
+ await visitPlugins(plugins, 'registerServices', container, pluginRegistry);
+}
+
+async function registerSettingsService(container, plugins, pluginRegistry) {
+ // Now, register default implementation of the settings service that provides settings from environment variables
+ const settingsService = new EnvSettingsService({
+ provider: {
+ getDefaults: settings => {
+ // Ask each plugin to return their static settings. Each plugin is passed a plain JavaScript object containing the
+ // static settings collected so far from other plugins. The plugins are called in the same order as returned by the
+ // registry.
+ // Each plugin gets a chance to add, remove, update, or delete static settings by mutating the provided staticSettings object.
+ const initialStaticSettings = {};
+ const staticSettings = _.reduce(
+ plugins,
+ (staticSettingsSoFar, plugin) => {
+ if (_.isFunction(plugin.getStaticSettings)) {
+ return plugin.getStaticSettings(staticSettingsSoFar, settings, pluginRegistry);
+ }
+ return staticSettingsSoFar;
+ },
+ initialStaticSettings,
+ );
+ return staticSettings;
+ },
+ },
+ });
+ container.register('settings', settingsService);
+
+ // Now, give every plugin a chance to swap out the settings service implementation with its own implementation
+ // of settings service by calling the "registerSettingsService" method. The plugins may or may not
+ // implement this method.
+ // The plugins are called in the same order as returned by the registry.
+ // Each plugin gets a chance to swap out the settings service implementation registered by previous plugins.
+ await visitPlugins(plugins, 'registerSettingsService', container);
+
+ // TODO: Capture and return the "settings" service registered by the last plugin instead of returning "settingsService"
+ return settingsService;
+}
+
+async function registerLoggerService(container, plugins, settingsService, pluginRegistry) {
+ // We can use "settingsService" here because the settingsService is dependency free and has no "init" method and does not
+ // require initialization by the services container. Usually other services that implement the "init" method can not be used
+ // before they are initialized by the services container
+ const solutionName = settingsService.optional('solutionName', '');
+ const envType = settingsService.optional('envType', '');
+ const envName = settingsService.optional('envName', '');
+ const initialLoggingContext = { solutionName, envType, envName };
+ // Ask each plugin to return their logging context. Each plugin is passed a plain JavaScript object containing the
+ // loggingContext collected so far from other plugins. The plugins are called in the same order as returned by the
+ // registry.
+ // Each plugin gets a chance to add, remove, update, or delete logging context items by mutating the provided loggingContext object.
+ const loggingContext = await _.reduce(
+ plugins,
+ async (loggingContextSoFar, plugin) => {
+ if (_.isFunction(plugin.getLoggingContext)) {
+ return plugin.getLoggingContext(await loggingContextSoFar, pluginRegistry);
+ }
+ return loggingContextSoFar;
+ },
+ Promise.resolve(initialLoggingContext),
+ );
+
+ // Now, give each plugin a chance to return their sensitive key names for masking.
+ // Each plugin is passed an array containing the names of the fields to mask. The plugins are called in the same order as returned by the
+ // registry.
+ // Each plugin gets a chance to add, remove, update, or delete fields to mask array by mutating the provided fieldsToMask array.
+ const initialFieldsToMask = ['x-amz-security-token', 'user', 'accessKey', 'password']; // initialize with default fields to mask
+ const fieldsToMask = await _.reduce(
+ plugins,
+ async (fieldsToMaskSoFar, plugin) => {
+ if (_.isFunction(plugin.getFieldsToMaskInLog)) {
+ return plugin.getFieldsToMaskInLog(await fieldsToMaskSoFar, pluginRegistry);
+ }
+ return fieldsToMaskSoFar;
+ },
+ Promise.resolve(initialFieldsToMask),
+ );
+ // Now, register default implementation of the logger service
+ container.register('log', new LoggerService(console, loggingContext, fieldsToMask), {
+ lazy: false,
+ });
+ // Now, give every plugin a chance to swap out the logger service implementation with its own implementation
+ // by calling the "registerLoggerService" method. The plugins may or may not implement this method.
+ // The plugins are called in the same order as returned by the registry.
+ // Each plugin gets a chance to swap out the logger service implementation registered by previous plugins.
+ await visitPlugins(plugins, 'registerLoggerService', container, settingsService, pluginRegistry);
+}
+
+async function visitPlugins(plugins, methodName, ...args) {
+ // visit each plugin in strict order
+ for (let i = 0; i < plugins.length; i += 1) {
+ const plugin = plugins[i];
+ // check if the visit method exists and is a function
+ if (_.isFunction(plugin[methodName])) {
+ // We need to await specified method call in strict sequence of plugins so awaiting in loop
+ // eslint-disable-next-line no-await-in-loop
+ await plugin[methodName](...args);
+ }
+ }
+}
+
+module.exports = { registerServices };
diff --git a/addons/addon-base/packages/services/package.json b/addons/addon-base/packages/services/package.json
new file mode 100644
index 0000000000..ee4944a459
--- /dev/null
+++ b/addons/addon-base/packages/services/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@aws-ee/base-services",
+ "private": true,
+ "version": "1.0.0",
+ "description": "A library containing base set of services to be used with solutions based on addons",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-services-container": "workspace:*",
+ "ajv": "^6.11.0",
+ "aws-sdk": "^2.647.0",
+ "cycle": "^1.0.3",
+ "jsonwebtoken": "^8.5.1",
+ "jwk-to-pem": "^2.0.3",
+ "jwt-decode": "^2.2.0",
+ "lodash": "^4.17.15",
+ "request": "^2.88.2",
+ "underscore": "^1.9.2",
+ "uuid": "^3.4.0",
+ "validatorjs": "^3.18.1"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/main/.prettierrc.json b/main/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/main/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/main/cicd/.gitignore b/main/cicd/.gitignore
new file mode 100644
index 0000000000..c8257abdf2
--- /dev/null
+++ b/main/cicd/.gitignore
@@ -0,0 +1,19 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug..og
+**/.webpack
+
+# Serverless directories
+.serverless
+
+local-events/*
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/main/cicd/README.md b/main/cicd/README.md
new file mode 100644
index 0000000000..0d68b5e173
--- /dev/null
+++ b/main/cicd/README.md
@@ -0,0 +1,121 @@
+# CI/CD Pipeline for the solution
+
+## Terms
+
+**Source Account:** AWS Account containing CodeCommit repository with the source code
+
+**Target Account:** AWS Account where the solution needs to be deployed to. The CodePipeline is also deployed in the target account.
+
+**Staging Environment:** Solution environment created to run integration tests or manual tests before deploying the solution to final target environment.
+
+**Target Environment:** Target solution environment where the code needs to be deployed.
+
+## How does the pipeline work?
+At high level the pipeline works as follows:
+ 1. Every commit in the source account on the configured repo for the specified branch triggers a CloudWatch event
+ 2. A CloudWatch Rule pushes the event to the default event bus of the target account
+ 3. A CloudWatch Rule in the target account triggers the CodePipeline.
+ 4. The pipeline contains the following stages and executes them in order.
+The pipeline stops upon failure of any stage and notifies user via configured SNS topic.
+
+ 4.1 **Source:** This stage takes the code from the CodeCommit repository from the specified branch and uploads it to
+ an S3 bucket. This S3 bucket is called artifacts bucket and is used by the CodePipeline to pass artifacts from one
+ stage to other stage.
+
+ 4.2 **Build-And-Deploy-To-Stagging-Env:** This stage uses [AWS CodeBuild](https://aws.amazon.com/codebuild/) to
+ perform build and deployment. It downloads the code from the artifacts S3 bucket and installs dependencies, performs
+ the static code analysis, runs unit tests, and deploys to the staging environment. This stage is only created if
+ `createStagingEnv` setting is set to `true` in settings file. Developers can set `createStagingEnv` to `false` to skip
+ creation and deployment to staging environment and directly push changes to their target development environment. This
+ flag should be set to `true` for higher environments (such as `demo` or `production`) to make sure code is deployed and
+ tested in a staging environment before pushing to target environment.
+
+ 4.3 **Test-Staging-Env:** This stage uses [AWS CodeBuild](https://aws.amazon.com/codebuild/) to execute integration
+ tests against the staging environment. This stage is only created if `createStagingEnv` setting is set to `true` in
+ settings file. Developers can set `createStagingEnv` to `false` to skip creation and deployment to staging environment
+ and directly push changes to their target development environment.
+
+ 4.4 **Push-To-Target-Env:** This stage is for manual approval to deploy to target environment. The pipeline will pause
+ at this stage and wait for manual approval. The user will receive an email notification via configured SNS topic. The
+ notification email address is configured via the setting `emailForNotifications` in the settings file. This stage is
+ only created if `requireManualApproval` setting is set to `true` in settings file. Setting `requireManualApproval`
+ to `false` will cause auto-propagation to the target environment.
+
+ 4.5 **Build-and-Deploy-to-Target-Env:** This stage uses [AWS CodeBuild](https://aws.amazon.com/codebuild/) to perform
+ build and deployment. It downloads the code from the artifacts S3 bucket and installs dependencies, performs the
+ static code analysis, runs unit tests, and deploys to the target environment.
+
+ 4.6 **Test-Target-Env:** This stage uses [AWS CodeBuild](https://aws.amazon.com/codebuild/) to execute integration
+ tests against the target environment. This stage is only created if `runTestsAgainstTargetEnv` setting is set to
+ `true` in settings file. Developers can set `createStagingEnv` to `false`, `requireManualApproval` to `false`, and
+ `runTestsAgainstTargetEnv` to `true` to skip creation and deployment to staging environment and directly push changes
+ to their target development environment without requiring manual approval and run integration tests directly against
+ their target development environment.
+
+## How to deploy the CI/CD Pipeline
+
+1. Deploy `cicd-source` stack to the Source Account
+
+ * Create a settings file in `cicd/cicd-source/config/settings` for the environment for which you want to create the
+ CI/CD pipeline. For example, to create the CI/CD pipeline for `dev` environment, create `dev.yml` file in
+ `cicd/cicd-source/config/settings/`. You can create the settings file by copying the sample `demo.yml` file.
+ Please adjust the settings as per your environment. Read inline comments in the file for information about each
+ setting.
+
+ Set the following settings as `"*"`
+ ```
+ artifactsS3BucketArn: "*"
+ artifactsKmsKeyArn: "*"
+ ```
+
+ * Deploy the `cicd-source` stack.
+ ```bash
+ cd cicd/cicd-source
+
+ pnpx sls deploy --stage
+ ```
+
+2. Deploy `cicd-pipeline` stack to the Target Account
+
+ * Create a settings file in `cicd/cicd-pipeline/config/settings` for the environment for which you want to create the
+ CI/CD pipeline. For example, to create the CI/CD pipeline for `dev` environment, create `dev.yml` file in
+ `cicd/cicd-pipeline/config/settings/`. You can create the settings file by copying the sample `demo.yml` file.
+ Please adjust the settings as per your environment. Read inline comments in the file for information about each
+ setting.
+
+ * Deploy the `cicd-pipeline` stack.
+ ```bash
+ cd cicd/cicd-pipeline
+
+ pnpx sls deploy --stage
+ ```
+
+3. Re-deploy `cicd-source` stack to the Source Account to lock down permissions for the artifacts bucket
+
+ * Note down the CloudFormation stack output variables `AppArtifactBucketArn` and `ArtifactBucketKeyArn` from the
+ `cicd-pipeline` stack you deployed in step 2 above.
+
+ You can use `sls info` command with `--verbose` flag to print stack output variables as follows.
+
+ * CD to the `cicd/cicd-pipeline`
+ ```bash
+ pnpx sls info --verbose -s
+ ```
+
+ * Set the CloudFormation stack output variables `AppArtifactBucketArn`
+ and `ArtifactBucketKeyArn` that you obtained above in the settings `artifactsS3BucketArn` and `artifactsKmsKeyArn`
+ in settings file `cicd/cicd-source/config/settings/demo.yml` then re-deploy the `cicd-source`
+ stack to lock down the permissions in `CodeCommitSourceRole`
+ ```
+ artifactsS3BucketArn: ""
+ artifactsKmsKeyArn: ""
+ ```
+
+ * CD to the `cicd/cicd-source`
+
+ * Deploy the `cicd-source` stack.
+ ```bash
+ cd cicd/cicd-source
+
+ pnpx sls deploy --stage
+ ```
\ No newline at end of file
diff --git a/main/cicd/cicd-pipeline/.eslintrc.json b/main/cicd/cicd-pipeline/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/main/cicd/cicd-pipeline/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/main/cicd/cicd-pipeline/.prettierrc.json b/main/cicd/cicd-pipeline/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/main/cicd/cicd-pipeline/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/main/cicd/cicd-pipeline/config/buildspec/buildspec-int-tests.yml b/main/cicd/cicd-pipeline/config/buildspec/buildspec-int-tests.yml
new file mode 100644
index 0000000000..e7f0bed992
--- /dev/null
+++ b/main/cicd/cicd-pipeline/config/buildspec/buildspec-int-tests.yml
@@ -0,0 +1,38 @@
+version: 0.2
+
+phases:
+
+ install:
+ runtime-versions:
+ nodejs: 12
+
+ pre_build:
+ commands:
+ - echo "Installing dependencies"
+ - npm add -g pnpm
+ - ./scripts/install.sh
+ # Need to build all packages explicitly here.
+ # This is required because somehow the "pnpm run build" script in "prepare" hook does not work in AWS CodeBuild
+ # The "prepare" hook script is executed fine but the "build" script referenced in "prepare" does not execute
+ - ./scripts/build-all-packages.sh
+ build:
+ commands:
+ - printf "\n\nRunning integration tests for environment "$ENV_NAME"\n"
+ - ./scripts/run-integration-tests.sh "$ENV_NAME"
+ - printf "\n\n"
+
+reports:
+ integrationTests:
+ files:
+ - 'main/integration-tests/.build/test/junit.xml'
+ discard-paths: yes
+ file-format: JunitXml
+
+cache:
+ paths:
+ - node_modules/
+ - addons/*/packages/*/node_modules/
+ - main/solution/*/node_modules/
+ - main/cicd/*/node_modules/
+ - main/integration-tests/node_modules/
+ - main/packages/*/node_modules
diff --git a/main/cicd/cicd-pipeline/config/buildspec/buildspec.yml b/main/cicd/cicd-pipeline/config/buildspec/buildspec.yml
new file mode 100644
index 0000000000..b1365159fd
--- /dev/null
+++ b/main/cicd/cicd-pipeline/config/buildspec/buildspec.yml
@@ -0,0 +1,90 @@
+version: 0.2
+
+phases:
+
+ install:
+ runtime-versions:
+ nodejs: 12
+
+ pre_build:
+ commands:
+ - echo "Installing dependencies"
+ - npm add -g pnpm
+ - ./scripts/install.sh
+ - echo ""
+ - echo "Building packages"
+ # Need to build all packages explicitly here.
+ # This is required because somehow the "pnpm run build" script in "prepare" hook does not work in AWS CodeBuild
+ # The "prepare" hook script is executed fine but the "build" script referenced in "prepare" does not execute
+ - ./scripts/build-all-packages.sh
+ build:
+ commands:
+ - echo "Running static code analysis"
+ - ./scripts/run-static-code-analysis.sh
+ - printf "\n\n"
+
+ - echo "Running unit tests"
+ - ./scripts/run-unit-tests.sh
+ - printf "\n\n"
+
+ - echo "Building and deploying $ENV_NAME"
+ - ./scripts/environment-deploy.sh "$ENV_NAME"
+ - printf "\n\n"
+
+reports:
+ servicesContainerTests:
+ files:
+ - 'addons/addon-base/packages/services-container/.build/test/junit.xml'
+ discard-paths: yes
+ file-format: JunitXml
+
+ servicesTests:
+ files:
+ - 'addons/addon-base/packages/services/.build/test/junit.xml'
+ discard-paths: yes
+ file-format: JunitXml
+
+ settingsHelperTests:
+ files:
+ - 'addons/addon-base/packages/serverless-settings-helper/.build/test/junit.xml'
+ discard-paths: yes
+ file-format: JunitXml
+
+ backendToolsTests:
+ files:
+ - 'addons/addon-base/packages/serverless-backend-tools/.build/test/junit.xml'
+ discard-paths: yes
+ file-format: JunitXml
+
+ apiHandlerTests:
+ files:
+ - 'addons/addon-base-rest-api/packages/api-handler-factory/.build/test/junit.xml'
+ discard-paths: yes
+ file-format: JunitXml
+
+ uiToolsTests:
+ files:
+ - 'addons/addon-base-ui/packages/serverless-ui-tools/.build/test/junit.xml'
+ discard-paths: yes
+ file-format: JunitXml
+
+ backendTests:
+ files:
+ - 'main/solution/backend/.build/test/junit.xml'
+ discard-paths: yes
+ file-format: JunitXml
+
+ postDeploymentTests:
+ files:
+ - 'main/solution/post-deployment/.build/test/junit.xml'
+ discard-paths: yes
+ file-format: JunitXml
+
+cache:
+ paths:
+ - node_modules/
+ - addons/*/packages/*/node_modules/
+ - main/solution/*/node_modules/
+ - main/cicd/*/node_modules/
+ - main/integration-tests/node_modules/
+ - main/packages/*/node_modules
diff --git a/main/cicd/cicd-pipeline/config/infra/cloudformation.yml b/main/cicd/cicd-pipeline/config/infra/cloudformation.yml
new file mode 100644
index 0000000000..e7c947715f
--- /dev/null
+++ b/main/cicd/cicd-pipeline/config/infra/cloudformation.yml
@@ -0,0 +1,580 @@
+Parameters:
+
+ GitHubOAuthToken:
+ Description: OAuth token used by AWS CodePipeline to connect to GitHub
+ Type: String
+ Default: 'to-be-specified'
+ NoEcho: true
+
+
+Conditions:
+ UseCodeCommit: !Equals ['${self:custom.settings.githubOwner}', '']
+ CreateStagingEnv: !Equals ['${self:custom.settings.createStagingEnv}', true]
+ RunTestsAgainstTargetEnv: !Equals ['${self:custom.settings.runTestsAgainstTargetEnv}', true]
+ AddManualApproval: !Equals ['${self:custom.settings.requireManualApproval}', true]
+ SubscribeNotificationEmail: !Not
+ - !Equals ['${self:custom.settings.emailForNotifications}', '']
+
+
+Resources:
+ # SNS Topic to receive various notifications from the pipeline
+ PipelineNotificationsTopic:
+ Type: AWS::SNS::Topic
+ Properties: !If
+ - SubscribeNotificationEmail
+ - Subscription:
+ - Endpoint: ${self:custom.settings.emailForNotifications}
+ Protocol: email
+ - !Ref AWS::NoValue
+
+ # SNS Topic Policy to allow CloudWatch Event Service to send notifications to the topic
+ PipelineNotificationsTopicPolicy:
+ Type: AWS::SNS::TopicPolicy
+ Properties:
+ PolicyDocument:
+ Id: !Ref AWS::StackName
+ Version: '2012-10-17'
+ Statement:
+ Effect: Allow
+ Principal:
+ Service: events.amazonaws.com
+ Resource:
+ - !Ref PipelineNotificationsTopic
+ Action:
+ - sns:Publish
+ Topics:
+ - !Ref PipelineNotificationsTopic
+
+ # KMS key to be used for encrypting/decrypting the pipeline artifacts
+ # We cannot use default S3 encryption (SSE-S3) as the default S3 keys are account specific
+ ArtifactBucketKey:
+ Type: AWS::KMS::Key
+ Properties:
+ Description: Code & Deployment Artifact Key
+ EnableKeyRotation: true
+ KeyPolicy:
+ Version: '2012-10-17'
+ Id: !Ref AWS::StackName
+ Statement:
+ - Sid: Allows adminstration of the key to the account root user
+ Effect: Allow
+ Principal:
+ AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
+ Action:
+ - kms:Create*
+ - kms:Describe*
+ - kms:Enable*
+ - kms:List*
+ - kms:Put*
+ - kms:Update*
+ - kms:Revoke*
+ - kms:Disable*
+ - kms:Get*
+ - kms:Delete*
+ - kms:TagResource
+ - kms:UntagResource
+ - kms:ScheduleKeyDeletion
+ - kms:CancelKeyDeletion
+ Resource: '*'
+ - Sid: Allow use of the key in the source account for upload and CodeBuild in pipeline account to download the code
+ Effect: Allow
+ Principal:
+ AWS:
+ - !If
+ - UseCodeCommit
+ - ${self:custom.settings.sourceRoleArn} # Allows CodePipeline's source stage to encrypt while uploading to artifact bucket
+ - !Ref AWS::NoValue
+ - !GetAtt AppDeployerRole.Arn # Allows CodeBuild (the deploy stage in pipeline) to decrypt code when downloading
+ - !GetAtt AppPipelineRole.Arn # Allows CodePipeline to encrypt code when uploading
+ Action:
+ - kms:Encrypt
+ - kms:Decrypt
+ - kms:ReEncrypt*
+ - kms:GenerateDataKey*
+ - kms:DescribeKey
+ Resource: '*'
+
+ # The artifacts S3 bucket to hold pipeline artifacts
+ AppArtifactBucket:
+ Type: AWS::S3::Bucket
+ Properties:
+ VersioningConfiguration:
+ Status: Enabled
+ PublicAccessBlockConfiguration: # Block all public access configuration for the S3 bucket
+ BlockPublicAcls: true
+ BlockPublicPolicy: true
+ IgnorePublicAcls: true
+ RestrictPublicBuckets: true
+ LifecycleConfiguration:
+ Rules:
+ - ExpirationInDays: 365 # Delete old artifacts from S3 after 1 year to save costs
+ Status: Enabled
+
+ # The artifacts bucket S3 policy to allow CodePipeline's source stage to upload artifacts
+ AppArtifactBucketPolicy:
+ Type: AWS::S3::BucketPolicy
+ Properties:
+ Bucket: !Ref AppArtifactBucket
+ PolicyDocument:
+ Statement:
+ - Action:
+ - s3:PutObject
+ - s3:GetBucketPolicy
+ - s3:GetObject
+ - s3:ListBucket
+ Effect: Allow
+ Resource:
+ - !Sub arn:aws:s3:::${AppArtifactBucket}
+ - !Sub arn:aws:s3:::${AppArtifactBucket}/*
+ Principal:
+ AWS:
+ - !Ref AWS::AccountId
+ - !If
+ - UseCodeCommit
+ - ${self:custom.settings.sourceRoleArn}
+ - !Ref AWS::NoValue
+
+ # The AWS IAM role to be assumed by the AWS CodePipeline.
+ # The role specified in each stage is assumed for that specific stage in the pipeline.
+ # This role is assumed by the CodePipeline service itself.
+ AppPipelineRole:
+ Type: AWS::IAM::Role
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service:
+ - codepipeline.amazonaws.com
+ Action: sts:AssumeRole
+ Policies:
+ - PolicyName: PipelineOperationalPermissions
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Action:
+ - codecommit:GetBranch
+ - codecommit:GetCommit
+ Effect: Allow
+ Resource:
+ - !Sub 'arn:${AWS::Partition}:codecommit:${AWS::Region}:${self:custom.settings.sourceAccountId}:${self:custom.settings.repoName}'
+ - Action:
+ - logs:CreateLogGroup
+ - logs:CreateLogStream
+ - logs:PutLogEvents
+ Effect: Allow
+ Resource: '*'
+ - Action:
+ - s3:GetObject
+ - s3:PutObject
+ - s3:DeleteObject
+ - s3:GetObjectVersion
+ - s3:ListBucket
+ - s3:GetBucketPolicy
+ Effect: Allow
+ Resource:
+ - !Sub arn:aws:s3:::${AppArtifactBucket}
+ - !Sub arn:aws:s3:::${AppArtifactBucket}/*
+ - Action:
+ - codebuild:BatchGetBuilds
+ - codebuild:StartBuild
+ Effect: Allow
+ Resource:
+ - !If
+ - CreateStagingEnv
+ - !GetAtt StgEnvDeployProject.Arn
+ - !Ref AWS::NoValue
+ - !GetAtt TestStgEnvProject.Arn
+ - !GetAtt TargetEnvDeployProject.Arn
+ - !GetAtt TestTargetEnvProject.Arn
+ - !If
+ - UseCodeCommit
+ - Action:
+ - sts:AssumeRole
+ Effect: Allow
+ Resource: ${self:custom.settings.sourceRoleArn}
+ - !Ref AWS::NoValue
+ - Action:
+ - sns:Publish
+ Effect: Allow
+ Resource: !Ref PipelineNotificationsTopic
+
+ AppDeployerRole:
+ Type: AWS::IAM::Role
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service:
+ - codebuild.amazonaws.com
+ Action: sts:AssumeRole
+ ManagedPolicyArns:
+ # The deployer role needs permissions to deploy CFN stacks and all actions those stacks are performing
+ # The permissions required by stacks are very fluid and dependent on which resources are declared in those
+ # stacks
+ - arn:aws:iam::aws:policy/PowerUserAccess
+ Policies:
+ - PolicyName: CodeBuildDeployerPermissions
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Action:
+ - iam:AttachRolePolicy
+ - iam:CreateRole
+ - iam:DeleteRole
+ - iam:DeleteRolePolicy
+ - iam:DetachRolePolicy
+ - iam:GetRole
+ - iam:GetRolePolicy
+ - iam:PassRole
+ - iam:PutRolePolicy
+ - iam:*TagRole*
+ Resource: '*'
+ Effect: Allow
+
+ # Role that allows triggering the CodePipeline. This role is assumed by CloudWatch Events from the Source AWS Account
+ # where the source code is located (i.e., the account containing the AWS CodeCommit repo with the source code)
+ # If you are not using AWS CodeCommit then this role needs to be removed or updated accordingly
+ PipelineTriggerRole:
+ Condition: UseCodeCommit
+ Type: AWS::IAM::Role
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ AWS: arn:aws:iam::${self:custom.settings.sourceAccountId}:root
+ Action: sts:AssumeRole
+ Policies:
+ - PolicyName: cwe-pipeline-execution
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action: codepipeline:StartPipelineExecution
+ Resource: !Sub 'arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${AppPipeline}'
+
+ AppPipeline:
+ Type: AWS::CodePipeline::Pipeline
+ Properties:
+ Name: ${self:custom.settings.pipelineName}
+ RoleArn: !GetAtt AppPipelineRole.Arn
+ ArtifactStore:
+ Type: S3
+ Location: !Ref AppArtifactBucket
+ EncryptionKey:
+ Id: !GetAtt ArtifactBucketKey.Arn
+ Type: KMS
+ Stages:
+ # Pull code from AWS CodeCommit and upload to artifacts S3 bucket
+ # Assume CodeCommitSourceRole for this operation that grants the permissions to download code from CodeCommit
+ # repo from source account and allows uploading it to the artifacts S3 bucket and encrypt the artifact using
+ # KMS key
+ - !If
+ - UseCodeCommit
+ - Name: Source
+ Actions:
+ - Name: SourceAction
+ RunOrder: 1
+ ActionTypeId:
+ Category: Source
+ Owner: AWS
+ Provider: CodeCommit
+ Version: '1'
+ Configuration:
+ RepositoryName: ${self:custom.settings.repoName}
+ BranchName: ${self:custom.settings.repoBranch}
+ PollForSourceChanges: 'false'
+ OutputArtifacts:
+ - Name: SourceArtifact
+ Region: ${self:custom.settings.sourceAwsRegion}
+ RoleArn: ${self:custom.settings.sourceRoleArn}
+ - Name: Source
+ Actions:
+ - Name: GitHubSource
+ ActionTypeId:
+ Category: Source
+ Owner: ThirdParty
+ Provider: GitHub
+ Version: '1'
+ Configuration:
+ OAuthToken: !Ref GitHubOAuthToken
+ Owner: ${self:custom.settings.githubOwner}
+ Repo: ${self:custom.settings.repoName}
+ Branch: ${self:custom.settings.repoBranch}
+ PollForSourceChanges: true
+ OutputArtifacts:
+ - Name: SourceArtifact
+
+ # Add stage to deploy to staging env if CreateStagingEnv condition is true
+ - !If
+ - CreateStagingEnv
+ - Name: Build-And-Deploy-To-Staging-Env-${self:custom.settings.stgEnvName}
+ Actions:
+ - Name: Build-And-Deploy-To-Staging-Env
+ RunOrder: 1
+ ActionTypeId:
+ Category: Build
+ Owner: AWS
+ Provider: CodeBuild
+ Version: '1'
+ Configuration:
+ ProjectName: !Ref StgEnvDeployProject
+ InputArtifacts:
+ - Name: SourceArtifact
+ - !Ref AWS::NoValue
+ # Add stage to run integration tests against the staging env if CreateStagingEnv condition is true
+ - !If
+ - CreateStagingEnv
+ - Name: Test-Staging-Env-${self:custom.settings.stgEnvName}
+ Actions:
+ - Name: Test-Staging-Env
+ RunOrder: 1
+ ActionTypeId:
+ Category: Build
+ Owner: AWS
+ Provider: CodeBuild
+ Version: '1'
+ Configuration:
+ ProjectName: !Ref TestStgEnvProject
+ InputArtifacts:
+ - Name: SourceArtifact
+ - !Ref AWS::NoValue
+ # Add manual approval stage only if AddManualApproval condition is true
+ - !If
+ - AddManualApproval
+ - Name: Push-To-Target-Env-${self:custom.settings.envName}
+ Actions:
+ - Name: Build-And-Deploy-To-Target-Env
+ RunOrder: 1
+ ActionTypeId:
+ Category: Approval
+ Owner: AWS
+ Provider: Manual
+ Version: '1'
+ Configuration:
+ NotificationArn: !Ref PipelineNotificationsTopic
+ - !Ref AWS::NoValue
+ # Deploy to target environment after manual approval
+ - Name: Build-and-Deploy-to-Target-Env-${self:custom.settings.envName}
+ Actions:
+ - Name: Build-and-Deploy
+ RunOrder: 1
+ ActionTypeId:
+ Category: Build
+ Owner: AWS
+ Provider: CodeBuild
+ Version: '1'
+ Configuration:
+ ProjectName: !Ref TargetEnvDeployProject
+ InputArtifacts:
+ - Name: SourceArtifact
+ # Add a stage for integration testing against target env if the RunTestsAgainstTargetEnv condition is true
+ - !If
+ - RunTestsAgainstTargetEnv
+ - Name: Test-Target-Env-${self:custom.settings.envName}
+ Actions:
+ - Name: Test-Target-Env
+ RunOrder: 1
+ ActionTypeId:
+ Category: Build
+ Owner: AWS
+ Provider: CodeBuild
+ Version: '1'
+ Configuration:
+ ProjectName: !Ref TestTargetEnvProject
+ InputArtifacts:
+ - Name: SourceArtifact
+ - !Ref AWS::NoValue
+
+
+ # A CodeBuild project to deploy solution to the staging environment before deploying it to target env
+ # Create this CodeBuild project only if the condition CreateStagingEnv is set to true
+ StgEnvDeployProject:
+ Condition: CreateStagingEnv
+ Type: AWS::CodeBuild::Project
+ Properties:
+ Artifacts:
+ Type: CODEPIPELINE
+ Source:
+ Type: CODEPIPELINE
+ BuildSpec: main/cicd/cicd-pipeline/config/buildspec/buildspec.yml
+ Environment:
+ ComputeType: BUILD_GENERAL1_LARGE
+ Type: LINUX_CONTAINER
+ Image: aws/codebuild/amazonlinux2-x86_64-standard:2.0
+ EnvironmentVariables:
+ - Name: ENV_NAME
+ Value: ${self:custom.settings.stgEnvName}
+ ServiceRole: !GetAtt AppDeployerRole.Arn
+ Cache:
+ # Use local caching to cache dirs specified in buildspec.yml (i.e., the node_modules dirs)
+ # See https://docs.aws.amazon.com/codebuild/latest/userguide/build-caching.html for various build caching options
+ Type: LOCAL
+ Modes:
+ - LOCAL_SOURCE_CACHE
+ - LOCAL_CUSTOM_CACHE
+
+ # A CodeBuild project to deploy solution to the main target environment
+ TargetEnvDeployProject:
+ Type: AWS::CodeBuild::Project
+ Properties:
+ Artifacts:
+ Type: CODEPIPELINE
+ Source:
+ Type: CODEPIPELINE
+ BuildSpec: main/cicd/cicd-pipeline/config/buildspec/buildspec.yml
+ Environment:
+ ComputeType: BUILD_GENERAL1_LARGE
+ Type: LINUX_CONTAINER
+ Image: aws/codebuild/amazonlinux2-x86_64-standard:2.0
+ EnvironmentVariables:
+ - Name: ENV_NAME
+ Value: ${self:custom.settings.envName}
+ ServiceRole: !GetAtt AppDeployerRole.Arn
+ Cache:
+ # Use local caching to cache dirs specified in buildspec.yml (i.e., the node_modules dirs)
+ # See https://docs.aws.amazon.com/codebuild/latest/userguide/build-caching.html for various build caching options
+ Type: LOCAL
+ Modes:
+ - LOCAL_SOURCE_CACHE
+ - LOCAL_CUSTOM_CACHE
+
+ # A CodeBuild project to test staging environment solution
+ TestStgEnvProject:
+ Type: AWS::CodeBuild::Project
+ Properties:
+ Artifacts:
+ Type: CODEPIPELINE
+ Source:
+ Type: CODEPIPELINE
+ BuildSpec: main/cicd/cicd-pipeline/config/buildspec/buildspec-int-tests.yml
+ Environment:
+ ComputeType: BUILD_GENERAL1_LARGE
+ Type: LINUX_CONTAINER
+ Image: aws/codebuild/amazonlinux2-x86_64-standard:2.0
+ EnvironmentVariables:
+ - Name: ENV_NAME
+ Value: ${self:custom.settings.stgEnvName}
+ ServiceRole: !GetAtt AppDeployerRole.Arn
+ Cache:
+ # Use local caching to cache dirs specified in buildspec.yml (i.e., the node_modules dirs)
+ # See https://docs.aws.amazon.com/codebuild/latest/userguide/build-caching.html for various build caching options
+ Type: LOCAL
+ Modes:
+ - LOCAL_SOURCE_CACHE
+ - LOCAL_CUSTOM_CACHE
+
+ # A CodeBuild project to test target environment solution
+ TestTargetEnvProject:
+ Type: AWS::CodeBuild::Project
+ Properties:
+ Artifacts:
+ Type: CODEPIPELINE
+ Source:
+ Type: CODEPIPELINE
+ BuildSpec: main/cicd/cicd-pipeline/config/buildspec/buildspec-int-tests.yml
+ Environment:
+ ComputeType: BUILD_GENERAL1_LARGE
+ Type: LINUX_CONTAINER
+ Image: aws/codebuild/amazonlinux2-x86_64-standard:2.0
+ EnvironmentVariables:
+ - Name: ENV_NAME
+ Value: ${self:custom.settings.envName}
+ ServiceRole: !GetAtt AppDeployerRole.Arn
+ Cache:
+ # Use local caching to cache dirs specified in buildspec.yml (i.e., the node_modules dirs)
+ # See https://docs.aws.amazon.com/codebuild/latest/userguide/build-caching.html for various build caching options
+ Type: LOCAL
+ Modes:
+ - LOCAL_SOURCE_CACHE
+ - LOCAL_CUSTOM_CACHE
+
+ # IAM role to be assumed by CloudWatch events service to trigger the CodePipeline
+ CodeCommitSourceEventRole:
+ Condition: UseCodeCommit
+ Type: AWS::IAM::Role
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: events.amazonaws.com
+ Action: sts:AssumeRole
+ Policies:
+ - PolicyName: PipelineOperationalPermissions
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action: codepipeline:StartPipelineExecution
+ Resource: !Sub 'arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${AppPipeline}'
+
+ # CloudWatch event rule to trigger the CodePipeline
+ PipelineTriggerRule:
+ Condition: UseCodeCommit
+ Type: AWS::Events::Rule
+ Properties:
+ Description: CloudWatch event rule to trigger the CodePipeline for [${self:custom.settings.solutionName}] and env [${self:custom.settings.envName}]
+ EventPattern:
+ source: [aws.codecommit]
+ detail-type: [CodeCommit Repository State Change]
+ resources:
+ - !Sub 'arn:${AWS::Partition}:codecommit:${AWS::Region}:${self:custom.settings.sourceAccountId}:${self:custom.settings.repoName}'
+ detail:
+ event: [referenceCreated, referenceUpdated]
+ referenceType: [branch]
+ referenceName:
+ - ${self:custom.settings.repoBranch}
+ State: ENABLED
+ Targets:
+ - Id: !Ref AppPipeline
+ Arn: !Sub 'arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${AppPipeline}'
+ RoleArn: !GetAtt CodeCommitSourceEventRole.Arn
+
+ # CloudWatch event rule to notify SNS topic whenever there is a failure in AWS CodePipeline
+ PipelineFailuresRule:
+ Type: AWS::Events::Rule
+ Properties:
+ Description: CloudWatch event rule to notify SNS topic in case of AWS CodePipeline failures for [${self:custom.settings.solutionName}] and env [${self:custom.settings.envName}]
+ EventPattern:
+ source:
+ - aws.codepipeline
+ detail-type:
+ - CodePipeline Pipeline Execution State Change
+ detail:
+ state:
+ - FAILED
+ pipeline:
+ - !Ref AppPipeline
+ State: ENABLED
+ Targets:
+ - Id: PipelineNotificationsTopic
+ Arn: !Ref PipelineNotificationsTopic
+ InputTransformer:
+ InputPathsMap:
+ pipeline : "$.detail.pipeline"
+ InputTemplate: "{\"The Pipeline has failed.\":}"
+
+ # A resource level policy for the default event bus in the target account (target account = the AWS account where code need to be deployed)
+ # to allow CodeCommit events to be published from the source account (source account = the AWS account containing the AWS CodeCommit repo with the source code)
+ EventBusPolicy:
+ Condition: UseCodeCommit
+ Type: AWS::Events::EventBusPolicy
+ Properties:
+ StatementId: ${self:custom.settings.namespace}-ebp
+ Action: events:PutEvents
+ Principal: ${self:custom.settings.sourceAccountId}
+
+Outputs:
+ AppArtifactBucketName: { Value: !Ref AppArtifactBucket }
+ AppArtifactBucketArn: { Value: !GetAtt AppArtifactBucket.Arn }
+ ArtifactBucketKeyArn: { Value: !GetAtt ArtifactBucketKey.Arn }
+ AppPipelineName: { Value: !Ref AppPipeline }
+ AppPipelineArn: { Value: !Sub 'arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${AppPipeline}' }
+ PipelineNotificationsTopic: { Value: !Ref PipelineNotificationsTopic }
diff --git a/main/cicd/cicd-pipeline/config/settings/.defaults.yml b/main/cicd/cicd-pipeline/config/settings/.defaults.yml
new file mode 100644
index 0000000000..4eef7feee4
--- /dev/null
+++ b/main/cicd/cicd-pipeline/config/settings/.defaults.yml
@@ -0,0 +1,43 @@
+# AWS Region of the source repository, only used for CodeCommit; defaults to same region as pipeline
+sourceAwsRegion: ${self:custom.settings.awsRegion}
+
+# Name of the repository in either CodeCommit or Github
+repoName: aws-galileo-gateway
+
+# The git branch name of the source code repository the code pipeline should build and deploy
+repoBranch: master
+
+# Path to repo token (e.g. Github access token) in parameter store
+tokenName: /${self:custom.settings.envName}/${self:custom.settings.solutionName}/github/token
+
+# Name of the AWS CodePipeline instance
+pipelineName: ${self:custom.settings.namespace}-${self:custom.settings.repoBranch}
+
+# Either specify a Github owner (and an OAuth Token in CloudFormation) or source account and role (for CodeCommit)
+githubOwner: ''
+sourceAccountId: ''
+sourceRoleArn: ''
+
+# Flag indicating whether to create a staging environment.
+# If this flag is set to true the pipeline will first deploy the solution to a staging environment before deploying the
+# solution to the target environment. By default, the staging environment is named by suffixing "stg" to the
+# target environment name.
+# For example, if the name of the target environment is "prod" the staging env will be named "prodstg".
+# You can change the name of the staging environment by specifying "stgEnvName" setting
+# If this flag is set to true then you MUST also provide settings file for the staging env.
+# For example, if createStagingEnv=true and if the stgEnvName=t"prodstg" then make sure you have
+# "prodstg.yml" file in top level "config/settings" directory
+createStagingEnv: true
+
+# Flag indicating whether to require manual approval before deploying to target environment
+requireManualApproval: true
+
+# Flag indicating whether to run integration tests against the target environment
+# Set this to false if you do not want to run automated tests against target env (such as production)
+# This flag is only to control tests against target env. The tests are always run against staging env
+# when "createStagingEnv" is "true" irrespective of this flag.
+runTestsAgainstTargetEnv: false
+
+# Name of the staging environment to run the integration tests against.
+# This setting is ignored when createStagingEnv is not true.
+stgEnvName: ${self:custom.settings.envName}stg
diff --git a/main/cicd/cicd-pipeline/config/settings/.settings.js b/main/cicd/cicd-pipeline/config/settings/.settings.js
new file mode 100644
index 0000000000..0f0a5acf3b
--- /dev/null
+++ b/main/cicd/cicd-pipeline/config/settings/.settings.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports.merged = require('@aws-ee/base-serverless-settings-helper').mergeSettings(__dirname, [
+ '../../../../config/settings/.defaults.yml',
+ './.defaults.yml',
+ '../../../../config/settings/${stage}.yml',
+ './${stage}.yml',
+]);
diff --git a/main/cicd/cicd-pipeline/package.json b/main/cicd/cicd-pipeline/package.json
new file mode 100644
index 0000000000..f05a8b49b4
--- /dev/null
+++ b/main/cicd/cicd-pipeline/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@aws-ee/cicd-pipeline",
+ "version": "1.0.0",
+ "private": true,
+ "description": "CI/CD pipeline implementation for the PoC",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "@aws-ee/base-serverless-settings-helper": "workspace:*",
+ "serverless": "^1.63.0",
+ "serverless-deployment-bucket": "^1.1.0",
+ "serverless-plugin-scripts": "^1.0.2"
+ },
+ "optionalDependencies": {
+ "fsevents": "*"
+ }
+}
diff --git a/main/cicd/cicd-pipeline/scripts/deploy-github-token.bash b/main/cicd/cicd-pipeline/scripts/deploy-github-token.bash
new file mode 100755
index 0000000000..bcb8a6c079
--- /dev/null
+++ b/main/cicd/cicd-pipeline/scripts/deploy-github-token.bash
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -e
+
+
+stack_name="$1"
+token_name="$2"
+
+
+token=$(aws ssm get-parameter --name ${token_name} --with-decryption --output text --query Parameter.Value 2> /dev/null || echo 'not-found')
+
+if [[ $token != 'not-found' ]]; then
+ aws cloudformation update-stack \
+ --stack-name ${stack_name} \
+ --capabilities CAPABILITY_IAM \
+ --use-previous-template \
+ --parameters ParameterKey=GitHubOAuthToken,ParameterValue=${token}
+fi
diff --git a/main/cicd/cicd-pipeline/serverless.yml b/main/cicd/cicd-pipeline/serverless.yml
new file mode 100644
index 0000000000..34bb594521
--- /dev/null
+++ b/main/cicd/cicd-pipeline/serverless.yml
@@ -0,0 +1,34 @@
+# For full config options, see docs.serverless.com
+# Note that most settings in here come from config/settings/*.yaml
+service: ${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}-cicd-pipeline
+
+provider:
+ name: aws
+ region: ${self:custom.settings.awsRegion}
+ profile: ${self:custom.settings.awsProfile}
+ stackName: ${self:custom.settings.envName}-${self:service}
+ deploymentBucket:
+ name: ${self:custom.settings.deploymentBucketName}
+ serverSideEncryption: AES256
+ stackTags: ${self:custom.tags}
+ # All references beginning with ${self:*, ${opt:*, ${file:*, ${deep:*, and ${cf:* will be resolved by Serverless
+ # All other ${* references will be resolved by CloudFormation
+ # See https://forum.serverless.com/t/getting-handle-accountid-in-serverless-config/946/11 and
+ # See https://github.com/serverless/serverless/issues/5011
+ variableSyntax: '\$\{((((self|opt|deep|cf):)|file)((?!\$\{).)+?)}'
+
+custom:
+ settings: ${file(./config/settings/.settings.js):merged}
+ tags:
+ Name: ${self:custom.settings.envName}-${self:service}
+ scripts:
+ hooks:
+ 'aws:deploy:finalize:cleanup': scripts/deploy-github-token.bash ${self:provider.stackName} ${self:custom.settings.tokenName}
+
+resources:
+ - Description: Galileo-Gateway ${self:custom.settings.version} ${self:custom.settings.solutionName} ${self:custom.settings.envName} CICD-Pipeline
+ - ${file(./config/infra/cloudformation.yml)}
+
+plugins:
+ - serverless-deployment-bucket
+ - serverless-plugin-scripts
diff --git a/main/cicd/cicd-source/.eslintrc.json b/main/cicd/cicd-source/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/main/cicd/cicd-source/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/main/cicd/cicd-source/.prettierrc.json b/main/cicd/cicd-source/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/main/cicd/cicd-source/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/main/cicd/cicd-source/README.md b/main/cicd/cicd-source/README.md
new file mode 100644
index 0000000000..6fe87bf966
--- /dev/null
+++ b/main/cicd/cicd-source/README.md
@@ -0,0 +1,12 @@
+A helper component to configure the AWS Source Account containing the code commit repository for CI/CD
+
+The component deploys a cloud formation stack in the source account that creates the following resources in the source account:
+
+- `CodeCommitSourceRole`:
+ A role that is assumed by the pipeline in the target account to allow the pipeline
+ to access the CodeCommit repository and copy source code into an artifact bucket in the target account.
+- `CodeCommitEventRole`:
+ A role that is assumed by Cloudwatch events to allow Cloudwatch events to publish code change events
+ to the default event bus in the target account.
+- `CodeCommitEventRule`:
+ A Cloudwatch Event rule that forwards code change events to the default event bus in the target account.
diff --git a/main/cicd/cicd-source/config/infra/cloudformation.yml b/main/cicd/cicd-source/config/infra/cloudformation.yml
new file mode 100644
index 0000000000..0a3424eae0
--- /dev/null
+++ b/main/cicd/cicd-source/config/infra/cloudformation.yml
@@ -0,0 +1,108 @@
+Conditions:
+ AllowAllArtifactBuckets: !Equals ["${self:custom.settings.artifactsS3BucketArn}", "*"]
+
+Resources:
+
+ # This role grants the permissions to checkout from CodeCommit and upload code to artifacts bucket
+ # and encrypt it using the KMS key. The artifacts bucket is created in the AWS account where the
+ # CodePipeline is running. CodePipeline will assume this role to checkout the code and upload it to the artifact
+ # bucket
+ CodeCommitSourceRole:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ AWS: arn:aws:iam::${self:custom.settings.targetAccountId}:root
+ Action: sts:AssumeRole
+ Policies:
+ - PolicyName: CopySourceCodePolicy
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Sid: GetSourceCodePermission
+ Effect: Allow
+ Action:
+ - codecommit:CancelUploadArchive
+ - codecommit:GetBranch
+ - codecommit:GitPull
+ - codecommit:GetCommit
+ - codecommit:GetUploadArchiveStatus
+ - codecommit:UploadArchive
+ Resource: !Sub "arn:${AWS::Partition}:codecommit:${AWS::Region}:${AWS::AccountId}:${self:custom.settings.repoName}"
+ - Sid: CopySourceCodeToArtifactBucketPermission
+ Effect: Allow
+ Action:
+ - s3:GetObject*
+ - s3:PutObject
+ - s3:PutObjectAcl
+ Resource: !If [AllowAllArtifactBuckets, "*", "${self:custom.settings.artifactsS3BucketArn}/*"]
+ - Sid: ListArtifactBucketPermission
+ Effect: Allow
+ Action:
+ - s3:ListBucket
+ Resource: "${self:custom.settings.artifactsS3BucketArn}"
+ - Sid: CopySourceCodeToArtifactBucketKMSPermission
+ Effect: Allow
+ Action:
+ - kms:Encrypt
+ - kms:Decrypt
+ - kms:ReEncrypt*
+ - kms:GenerateDataKey*
+ - kms:DescribeKey
+ Resource: "${self:custom.settings.artifactsKmsKeyArn}"
+
+ # Role to grant AWS CloudWatch Events Service permissions to publish CloudWatch Events to the default event bus
+ # of the target AWS Account
+ # See https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEvents-CrossAccountEventDelivery.html
+ # for more details about how to send/receive cross-account CloudWatch events
+ CodeCommitEventRole:
+ Type: AWS::IAM::Role
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: events.amazonaws.com
+ Action: sts:AssumeRole
+ Policies:
+ - PolicyName: pipeline-execution
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ Effect: Allow
+ Action: events:PutEvents
+ Resource:
+ - !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/default"
+ - !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${self:custom.settings.targetAccountId}:event-bus/default"
+
+ # AWS CloudWatch Event's Rule to publish the AWS CodeCommit changes related events to the default event bus of the
+ # target account. A matching event rule will be created in the target AWS account to trigger the pipeline
+ # See https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEvents-CrossAccountEventDelivery.html
+ # for more details about how to send/receive cross-account CloudWatch events
+ CodeCommitSourceEventRule:
+ Type: AWS::Events::Rule
+ Properties:
+ Description: CodeCommit Event Rule to trigger AWS CodePipeline in target account
+ EventPattern:
+ source: [aws.codecommit]
+ detail-type: [CodeCommit Repository State Change]
+ resources:
+ - !Sub 'arn:${AWS::Partition}:codecommit:${AWS::Region}:${AWS::AccountId}:${self:custom.settings.repoName}'
+ detail:
+ event: [referenceCreated, referenceUpdated]
+ referenceType: [branch]
+ referenceName:
+ - ${self:custom.settings.repoBranch}
+ State: ENABLED
+ Targets:
+ - Id: !Sub "CodeCommitCrossAccount${AWS::AccountId}"
+ Arn: !Sub "arn:${AWS::Partition}:events:${AWS::Region}:${self:custom.settings.targetAccountId}:event-bus/default"
+ RoleArn: !GetAtt CodeCommitEventRole.Arn
+
+Outputs:
+ CodeCommitSourceRoleArn:
+ Value: !GetAtt CodeCommitSourceRole.Arn
diff --git a/main/cicd/cicd-source/config/settings/.defaults.yml b/main/cicd/cicd-source/config/settings/.defaults.yml
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/main/cicd/cicd-source/config/settings/.settings.js b/main/cicd/cicd-source/config/settings/.settings.js
new file mode 100644
index 0000000000..0f0a5acf3b
--- /dev/null
+++ b/main/cicd/cicd-source/config/settings/.settings.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports.merged = require('@aws-ee/base-serverless-settings-helper').mergeSettings(__dirname, [
+ '../../../../config/settings/.defaults.yml',
+ './.defaults.yml',
+ '../../../../config/settings/${stage}.yml',
+ './${stage}.yml',
+]);
diff --git a/main/cicd/cicd-source/package.json b/main/cicd/cicd-source/package.json
new file mode 100644
index 0000000000..a76175ddee
--- /dev/null
+++ b/main/cicd/cicd-source/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@aws-ee/cicd-source",
+ "version": "1.0.0",
+ "private": true,
+ "description": "CI/CD Source Code Account Configuration Component for the PoC",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "@aws-ee/base-serverless-settings-helper": "workspace:*",
+ "aws-sdk": "^2.647.0",
+ "serverless": "^1.63.0",
+ "serverless-deployment-bucket": "^1.1.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "*"
+ }
+}
diff --git a/main/cicd/cicd-source/serverless.yml b/main/cicd/cicd-source/serverless.yml
new file mode 100644
index 0000000000..b9ae544923
--- /dev/null
+++ b/main/cicd/cicd-source/serverless.yml
@@ -0,0 +1,35 @@
+# For full config options, see docs.serverless.com
+# Note that most settings in here come from config/settings/*.yaml
+service: ${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}-cicd-src
+
+provider:
+ name: aws
+ region: ${self:custom.settings.awsRegion}
+ profile: ${self:custom.settings.awsProfile}
+ stackName: ${self:custom.settings.envName}-${self:service}
+ deploymentBucket:
+ # The deployment bucket is named "${self:custom.settings.deploymentBucketName}" for all other serverless projects
+ # under the "solution" directory as they all get deployed to one account and can share the deployment bucket
+ # from that account. This project (i.e., cicd-source-role project) creates resources in the source account where
+ # the code is located and hence it can not use the same bucket name as the rest of the solution projects
+ # (as the s3 bucket names are global).
+ # Because of this, using different name "${self:custom.settings.globalNamespace}-src-artifacts" here instead
+ name: ${self:custom.settings.globalNamespace}-src-artifacts
+ stackTags: ${self:custom.tags}
+ # All references beginning with ${self:*, ${opt:*, ${file:*, ${deep:*, and ${cf:* will be resolved by Serverless
+ # All other ${* references will be resolved by CloudFormation
+ # See https://forum.serverless.com/t/getting-handle-accountid-in-serverless-config/946/11 and
+ # See https://github.com/serverless/serverless/issues/5011
+ variableSyntax: '\$\{((((self|opt|deep|cf):)|file)((?!\$\{).)+?)}'
+
+custom:
+ settings: ${file(./config/settings/.settings.js):merged}
+ tags:
+ Name: ${self:custom.settings.envName}-${self:service}
+
+resources:
+ - Description: Galileo-Gateway ${self:custom.settings.version} ${self:custom.settings.solutionName} ${self:custom.settings.envName} CICD-Source
+ - ${file(./config/infra/cloudformation.yml)}
+
+plugins:
+ - serverless-deployment-bucket
diff --git a/main/config/settings/.defaults.yml b/main/config/settings/.defaults.yml
new file mode 100644
index 0000000000..d73c8f245a
--- /dev/null
+++ b/main/config/settings/.defaults.yml
@@ -0,0 +1,100 @@
+version: 1.0.0
+
+regionShortNamesMap:
+ "us-east-2": "oh"
+ "us-east-1": "va"
+ "us-west-1": "ca"
+ "us-west-2": "or"
+ "ap-east-1": "hk"
+ "ap-south-1": "mum"
+ "ap-northeast-3": "osa"
+ "ap-northeast-2": "sel"
+ "ap-southeast-1": "sg"
+ "ap-southeast-2": "syd"
+ "ap-northeast-1": "ty"
+ "ca-central-1": "ca"
+ "cn-north-1": "cn"
+ "cn-northwest-1": "nx"
+ "eu-central-1": "fr"
+ "eu-west-1": "irl"
+ "eu-west-2": "ldn"
+ "eu-west-3": "par"
+ "eu-north-1": "sth"
+ "me-south-1": "bhr"
+ "sa-east-1": "sao"
+ "us-gov-east-1": "gce"
+ "us-gov-west-1": "gcw"
+
+# The default region to deploy to
+awsRegion: us-east-1
+
+# Short region name
+# This is used in the namespace to avoid naming collisions to allow deploying the same solution across multiple regions
+# Currently using the ISO country code or state code or city abbreviation as short name of the region
+# See "regionShortNamesMap" defined above.
+# The above mapping needs to be updated when deploying to any region other than the ones listed above in future
+awsRegionShortName: ${self:custom.settings.regionShortNamesMap.${self:custom.settings.awsRegion}}
+
+# This prefix is used for naming various resources
+namespace: ${self:custom.settings.envName}-${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}
+
+# This is the namespace for naming resources that have global namespace such as S3 bucket names
+globalNamespace: ${self:custom.settings.awsAccountInfo.awsAccountId}-${self:custom.settings.namespace}
+
+# Name of the deployment bucket. The serverless framework uploads various artifacts to this bucket.
+# These artifacts include things like Lambda function code ZIP files, AWS CloudFormation Templates etc
+deploymentBucketName: ${self:custom.settings.globalNamespace}-artifacts
+
+# Bucket policy for the deployment bucket.
+deploymentBucketPolicy:
+ {
+ "Version": "2008-10-17",
+ "Statement":
+ [
+ {
+ "Sid": "Deny requests that do not use TLS",
+ "Effect": "Deny",
+ "Principal": "*",
+ "Action": "s3:*",
+ "Resource": "arn:aws:s3:::${self:custom.settings.deploymentBucketName}/*",
+ "Condition": { "Bool": { "aws:SecureTransport": "false" } },
+ },
+ {
+ "Sid": "Deny requests that do not use SigV4",
+ "Effect": "Deny",
+ "Principal": "*",
+ "Action": "s3:*",
+ "Resource": "arn:aws:s3:::${self:custom.settings.deploymentBucketName}/*",
+ "Condition":
+ {
+ "StringNotEquals": { "s3:signatureversion": "AWS4-HMAC-SHA256" },
+ },
+ },
+ ],
+ }
+
+# The S3 bucket name used to host environment bootstrap scripts
+environmentsBootstrapBucketName: ${self:custom.settings.globalNamespace}-environments-bootstrap-scripts
+
+# This prefix is used to construct the full name of a table
+dbTablePrefix: ${self:custom.settings.namespace}
+
+# Root path in parameter store for parameters used by this solution.
+paramStoreRoot: "${self:custom.settings.envName}/${self:custom.settings.solutionName}"
+
+# Information about the root admin user.
+# The root admin user is created by default in the internal authentication provider
+# The root user can be used for provisioning additional authentication providers
+# Root user's username
+rootUserName: root
+# Root user's first name
+rootUserFirstName: root
+# Root user's last name
+rootUserLastName: root
+
+# Enable/disable external researchers feature
+# When enableExternalResearchers = true, it allows users to be assigned to an "External Researcher" role.
+# These users can launch analytics workspaces in external AWS accounts.
+# When enableExternalResearchers = false (default setting), it does NOT allow any user to be assigned with
+# "External Researcher" role (any existing users who have "External Researcher" role will no longer be able to login).
+enableExternalResearchers: false
\ No newline at end of file
diff --git a/main/documentation/aws-accounts-readme.md b/main/documentation/aws-accounts-readme.md
new file mode 100644
index 0000000000..e836be97c6
--- /dev/null
+++ b/main/documentation/aws-accounts-readme.md
@@ -0,0 +1,281 @@
+# AWS account(s) terminology
+## Overview
+
+There are 3 types of AWS accounts roles in play.
+
+* The __Main__ AWS account. This is where the RaaS application is running in.
+* The __Master__ AWS account. This (optional) account can be the same as the Main AWS account or a different one. It hosts the AWS Organizations, needed if the AWS account creation functionality is exercised.
+* The __Member__ (or researcher) account(s). These accounts are exactly where the analytics run. These accounts are either member accounts in the organization ('created' accounts) or standalone accounts ('invited' or 'added' accounts).
+
+
+## Creating an AWS account (Member):
+The Main AWS account will assume a (master) role in the Master AWS account, then create a Member AWS account there. Then it will assume role in the Member AWS account and launch an AWS CloudFormation stack in there to build resources (VPC, Subnet, cross account execution role). See `solution/packages/cfn-templates/lib/templates/onboard-account.yaml`
+
+## Inviting an AWS account (Member)
+Invited account should provide a VPC and one Subnet in it, along with a cross account execution role. Trust permissions and execution permissions associated with this cross account execution role should be a superset than those described under _'The cross account execution Role'_ section further in this document.
+
+## Launching an EMR in a Member account:
+The Main AWS account assumes cross account execution role in Member AWS account and launches/reaps resources.
+
+
+## The Master Role
+* Resides in Master AWS account.
+* Is assumed by the Main AWS account.
+
+### Trust Policy
+```
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": "arn:aws:iam::${MainAccount}:root"
+ },
+ "Action": "sts:AssumeRole",
+ "Condition": {
+ "StringEquals": {
+ "sts:ExternalId": ${ExternalId}
+ }
+ }
+ }
+ ]
+}
+```
+### Permissions
+Managed Policy: AWSOrganizationsFullAccess
+
+```
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": "organizations:*",
+ "Resource": "*"
+ }
+ ]
+}
+```
+The above can be restricted to `createAccount`, `describeCreateAccountStatus` and `describeAccount` only.
+
+Inline Policy: sts:AssumeRole for the controlling role between Master AWS account and Member AWS account: OrganizationAccountAccessRole
+
+```
+{
+ "Version": "2012-10-17",
+ "Statement": {
+ "Effect": "Allow",
+ "Action": "sts:AssumeRole",
+ "Resource": "arn:aws:iam::*:role/OrganizationAccountAccessRole"
+ }
+}
+```
+
+## The cross account execution Role
+* Resides in Member AWS account.
+* Is assumed by the Main AWS account.
+* When creating a Member AWS account in the organization of the Master AWS account, this role is created by the `solution/packages/cfn-templates/lib/templates/onboard-account.yaml` template.
+
+### Trust Policy
+```
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": [
+ "arn:aws:iam::${MainAccount}:role/${ApiHandlerRole}",
+ "arn:aws:iam::${MainAccount}:role/${WorkflowLoopRunnerRole}",
+ "arn:aws:iam::${MemberAccount}:root"
+ ]
+ },
+ "Action": "sts:AssumeRole",
+ "Condition": {
+ "StringEquals": {
+ "sts:ExternalId": ${ExternalId}
+ }
+ }
+ }
+ ]
+}
+```
+The principals listed above are:
+
+* ApiHandlerRole: A role in the Main AWS account associated with the RaaS backend API execution.
+* WorkflowLoopRunnerRole: A role in the Main AWS account associated with background workflow execution as initiated by backend API calls.
+* The Member AWS account itself.
+
+### Permissions
+These policies support running analytics.
+
+* cloud formation
+
+```
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Action": [
+ "cloudformation:CreateStack",
+ "cloudformation:DeleteStack",
+ "cloudformation:DescribeStacks",
+ "cloudformation:DescribeStackEvents"
+ ],
+ "Resource": "*",
+ "Effect": "Allow"
+ }
+ ]
+}
+```
+
+* cost explorer
+
+```
+{
+ "Statement": {
+ "Action": [
+ "ce:*"
+ ],
+ "Resource": "*",
+ "Effect": "Allow"
+ }
+}
+```
+
+* EC2
+
+```
+{
+ "Statement": {
+ "Action": [
+ "ec2:*"
+ ],
+ "Resource": "*",
+ "Effect": "Allow"
+ }
+}
+```
+
+* EMR
+
+```
+{
+ "Statement": {
+ "Action": [
+ "elasticmapreduce:*"
+ ],
+ "Resource": "*",
+ "Effect": "Allow"
+ }
+}
+```
+
+* IAM
+
+```
+{
+ "Statement":[{
+ "Sid": "iamRoleAccess",
+ "Effect": "Allow",
+ "Action": [
+ "iam:GetRole",
+ "iam:CreateRole",
+ "iam:TagRole",
+ "iam:GetRolePolicy",
+ "iam:PutRolePolicy",
+ "iam:DeleteRolePolicy",
+ "iam:DeleteRole",
+ "iam:PassRole"
+ ],
+ "Resource": "arn:aws:iam:::role/analysis-*"
+ },
+ {
+ "Sid": "iamInstanceProfileAccess",
+ "Effect": "Allow",
+ "Action": [
+ "iam:AddRoleToInstanceProfile",
+ "iam:CreateInstanceProfile",
+ "iam:GetInstanceProfile",
+ "iam:DeleteInstanceProfile",
+ "iam:RemoveRoleFromInstanceProfile"
+ ],
+ "Resource": "arn:aws:iam:::instance-profile/analysis-*"
+ },
+ {
+ "Sid": "iamRoleServicePolicyAccess",
+ "Effect": "Allow",
+ "Action": [
+ "iam:AttachRolePolicy",
+ "iam:DetachRolePolicy"
+ ],
+ "Resource": "arn:aws:iam:::role/analysis-*",
+ "Condition": {
+ "ArnLike": {
+ "iam:PolicyARN": "arn:aws:iam::aws:policy/service-role/AmazonElasticMapReduceRole"
+ }
+ }
+ },
+ {
+ "Sid": "iamServiceLinkedRoleCreateAccess",
+ "Effect": "Allow",
+ "Action": [
+ "iam:CreateServiceLinkedRole",
+ "iam:PutRolePolicy"
+ ],
+ "Resource": "arn:aws:iam::*:role/aws-service-role/elasticmapreduce.amazonaws.com*/AWSServiceRoleForEMRCleanup*",
+ "Condition": {
+ "StringLike": {
+ "iam:AWSServiceName": [
+ "elasticmapreduce.amazonaws.com",
+ "elasticmapreduce.amazonaws.com.cn"
+ ]
+ }
+ }
+ }]
+}
+```
+
+* S3
+
+```
+{
+ "Statement": {
+ "Action": [
+ "s3:*"
+ ],
+ "Resource": "*",
+ "Effect": "Allow"
+ }
+}
+```
+
+* SageMaker
+
+```
+{
+ "Statement": {
+ "Action": [
+ "sagemaker:*"
+ ],
+ "Resource": "*",
+ "Effect": "Allow"
+ }
+}
+```
+
+* SSM
+
+```
+{
+ "Statement": {
+ "Action": [
+ "ssm:*"
+ ],
+ "Resource": "*",
+ "Effect": "Allow"
+ }
+}
+```
+
diff --git a/main/integration-tests/.eslintrc.json b/main/integration-tests/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/main/integration-tests/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/main/integration-tests/.gitignore b/main/integration-tests/.gitignore
new file mode 100644
index 0000000000..46e569f050
--- /dev/null
+++ b/main/integration-tests/.gitignore
@@ -0,0 +1,21 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+**/.webpack
+
+# Serverless directories
+.serverless
+
+.build
+
+local-events/*
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/main/integration-tests/.prettierrc.json b/main/integration-tests/.prettierrc.json
new file mode 100644
index 0000000000..d3846d96f3
--- /dev/null
+++ b/main/integration-tests/.prettierrc.json
@@ -0,0 +1,16 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all",
+ "overrides": [
+ {
+ "files": ["*.yml", "*.yaml"],
+ "options": {
+ "singleQuote": false
+ }
+ }
+ ]
+}
+
diff --git a/main/integration-tests/__test__/api-tests/public-auth-provider-api.test.js b/main/integration-tests/__test__/api-tests/public-auth-provider-api.test.js
new file mode 100644
index 0000000000..3ba89784a6
--- /dev/null
+++ b/main/integration-tests/__test__/api-tests/public-auth-provider-api.test.js
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+const request = require('request-promise-native');
+
+describe('GET /api/authentication/public/provider/configs should,', () => {
+ it('return at least one auth provider', async () => {
+ const apiBaseUrl = process.env.API_ENDPOINT;
+ const response = await request({
+ uri: `${apiBaseUrl}/api/authentication/public/provider/configs`,
+ json: true,
+ });
+
+ expect(response).not.toBeNull();
+ expect(response).toEqual(
+ expect.arrayContaining([
+ {
+ id: 'internal',
+ title: 'Default Login',
+ type: 'internal',
+ credentialHandlingType: 'submit',
+ signInUri: 'api/authentication/id-tokens',
+ },
+ ]),
+ );
+ expect(response.length).toBeGreaterThanOrEqual(1);
+ });
+});
diff --git a/main/integration-tests/jest.config.js b/main/integration-tests/jest.config.js
new file mode 100644
index 0000000000..3f7ffc8068
--- /dev/null
+++ b/main/integration-tests/jest.config.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/main/integration-tests/package.json b/main/integration-tests/package.json
new file mode 100644
index 0000000000..64197c7ffb
--- /dev/null
+++ b/main/integration-tests/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@aws-ee/integration-tests",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Integration tests for the base-poc solution",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "scripts": {
+ "intTest": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "intTestWatch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.0.1",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^23.7.0",
+ "eslint-plugin-jsx-a11y": "^6.2.3",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^25.1.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "request": "^2.88.0",
+ "request-promise-native": "^1.0.8"
+ }
+}
diff --git a/main/packages/.gitignore b/main/packages/.gitignore
new file mode 100644
index 0000000000..1fc3d6a576
--- /dev/null
+++ b/main/packages/.gitignore
@@ -0,0 +1,25 @@
+**/jspm_packages
+
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# IntelliJ Idea Module Files
+*.iml
+
+# Some scratch files
+scratch*.*
diff --git a/main/packages/services/.eslintrc.json b/main/packages/services/.eslintrc.json
new file mode 100644
index 0000000000..c700cf03a8
--- /dev/null
+++ b/main/packages/services/.eslintrc.json
@@ -0,0 +1,26 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "import/no-unresolved": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/main/packages/services/.gitignore b/main/packages/services/.gitignore
new file mode 100644
index 0000000000..f2fb153198
--- /dev/null
+++ b/main/packages/services/.gitignore
@@ -0,0 +1,19 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+/coverage/
+.build
diff --git a/main/packages/services/.prettierrc.json b/main/packages/services/.prettierrc.json
new file mode 100644
index 0000000000..4ee7b34147
--- /dev/null
+++ b/main/packages/services/.prettierrc.json
@@ -0,0 +1,8 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
+
diff --git a/main/packages/services/jest.config.js b/main/packages/services/jest.config.js
new file mode 100644
index 0000000000..70544476d7
--- /dev/null
+++ b/main/packages/services/jest.config.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+};
diff --git a/main/packages/services/jsconfig.json b/main/packages/services/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/main/packages/services/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/main/packages/services/lib/hello/hello-service.js b/main/packages/services/lib/hello/hello-service.js
new file mode 100644
index 0000000000..b0439e94ea
--- /dev/null
+++ b/main/packages/services/lib/hello/hello-service.js
@@ -0,0 +1,82 @@
+/*
+ * 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 */
+// const _ = require('lodash');
+const Service = require('@aws-ee/base-services-container/lib/service');
+const { allowIfActive, allowIfAdmin } = require('@aws-ee/base-services/lib/authorization/authorization-utils');
+
+// const sayHelloSchema = require('../schema/say-hello'); // your input schema
+
+// See ../plugins/services-plugin.js for an example of how to register this service
+class HelloService extends Service {
+ constructor() {
+ super();
+ this.dependency(['jsonSchemaValidationService', 'authorizationService', 'auditWriterService']);
+ }
+
+ async init() {
+ await super.init();
+ // If you need to do any initialization, do it here
+ }
+
+ async sayHello(requestContext, rawData) {
+ // Do your input validation here
+ // const [validationService] = await this.service(['jsonSchemaValidationService']);
+ // await validationService.ensureValid(rawData, sayHelloSchema);
+
+ // Do your authorization checks here. For example, below is an authorization assertion
+ // where the user has to be active and an admin.
+ await this.assertAuthorized(
+ requestContext,
+ { action: 'sayHello', conditions: [allowIfActive, allowIfAdmin] },
+ rawData,
+ );
+
+ // This is just an example result. Your result can be something else.
+ const result = { message: 'hello world' };
+
+ // Write audit event
+ await this.audit(requestContext, { action: 'sayHello', body: result });
+
+ return result;
+ }
+
+ async audit(requestContext, auditEvent) {
+ const auditWriterService = await this.service('auditWriterService');
+ // Calling "writeAndForget" instead of "write" to allow main call to continue without waiting for audit logging
+ // and not fail the main call if audit writing fails for some reason
+ // If the main call also needs to fail if any audit destination fails then switch to "write" method as follows
+ // return auditWriterService.write(requestContext, auditEvent);
+ return auditWriterService.writeAndForget(requestContext, auditEvent);
+ }
+
+ async assertAuthorized(requestContext, { action, conditions }, ...args) {
+ const authorizationService = await this.service('authorizationService');
+
+ // The "authorizationService.assertAuthorized" below will evaluate permissions by calling the "conditions" functions first
+ // It will then give a chance to all registered plugins (if any) to perform their authorization.
+ // The plugins can even override the authorization decision returned by the conditions
+ // See "authorizationService.authorize" method for more details
+ // NOTE: if you don't want to have an extension point for this, you can remove the extensionPoint property
+ await authorizationService.assertAuthorized(
+ requestContext,
+ { extensionPoint: 'sample-extension', action, conditions },
+ ...args,
+ );
+ }
+}
+
+module.exports = HelloService;
diff --git a/main/packages/services/lib/plugins/services-plugin.js b/main/packages/services/lib/plugins/services-plugin.js
new file mode 100644
index 0000000000..6a0d219097
--- /dev/null
+++ b/main/packages/services/lib/plugins/services-plugin.js
@@ -0,0 +1,219 @@
+/*
+ * 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.
+ */
+
+/**
+ * Function to register solution specific services to the services container
+ * @param container An instance of ServicesContainer to register services to
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerServices(container, pluginRegistry) {
+ // This is where you can register your services
+ // Example:
+ // container.register('helloService',new HelloService());
+ // container.register('service2',new Service2());
+ // TODO: Register additional services as per your solution requirements
+}
+
+/**
+ * Function to register solution specific static settings. "static settings" is a plain JavaScript object containing
+ * settings as key/value. In Lambda environment, the settings are provided by environment variables.
+ * There is 4K limit to the env variables that can be passed to a Lambda. The default settings service impl provided by the
+ * "@aws-ee/base-services" package reads settings from env variables.
+ * In addition to those, any other settings that be derived via convention should be passed as "static settings" to
+ * avoid occupying space in env variables space.
+ *
+ * @param existingStaticSettings An existing static settings plain javascript object containing settings as key/value contributed by other plugins
+ * @param settingsService Default instance of settings service that resolves settings from environment variables
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to static settings object
+ */
+// eslint-disable-next-line no-unused-vars
+function getStaticSettings(existingStaticSettings, settingsService, pluginRegistry) {
+ // This is where you can
+ // 1. register your static settings, to register your static settings
+ //
+ // const staticSettings = {
+ // ...existingStaticSettings,
+ // // add other static settings here as follows
+ // 'staticSetting1':'static-setting-1-value',
+ // 'staticSetting2':'static-setting-2-value',
+ // }
+ // return staticSettings;
+ //
+ // 2. modify any static settings
+ // existingStaticSettings['the-existing-static-setting-you-want-to-replace'] = 'new-value';
+ // return existingStaticSettings;
+ //
+ // 3. delete any existing static setting, to delete existing static setting
+ //
+ // existingStaticSettings.delete('the-existing-static-setting-you-want-to-delete');
+ //
+
+ // TODO: Register additional static settings as per your solution requirements here
+ const staticSettings = {
+ ...existingStaticSettings,
+ };
+ // DO NOT forget to return staticSettings here. If you do not return here no static settings will be configured
+ return staticSettings;
+}
+
+/**
+ * Function to register solution specific logging context. "logging context" is a plain JavaScript object containing
+ * key/values. These key/values are automatically added to logs by the loggingService.
+ * These additional context items can be useful for debugging and monitoring especially when aggregating logs from
+ * multiple lambdas, across multiple environments into a single dashboard or log analytics environment such as ELK stack.
+ *
+ * @param existingLoggingContext An existing logging context plain javascript object containing logging context items as key/value(s)
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to logging context object
+ */
+// eslint-disable-next-line no-unused-vars
+async function getLoggingContext(existingLoggingContext, pluginRegistry) {
+ // This is where you can
+ // 1. register your logging context items, to register your logging context items
+ //
+ // const loggingContext = {
+ // ...existingLoggingContext,
+ //
+ // // add other items here as follows, for example,
+ // 'someKey':'someValue',
+ // }
+ // return loggingContext;
+ //
+ // 2. modify any logging context items
+ // existingLoggingContext['the-existing-logging-context-item-you-want-to-replace'] = 'new-value';
+ // return existingLoggingContext;
+ //
+ // 3. delete any existing logging context, to delete existing logging context item
+ //
+ // existingLoggingContext.delete('the-existing-logging-context-item-you-want-to-delete');
+ //
+
+ // TODO: Register additional logging context items as per your solution requirements here
+ const loggingContext = {
+ ...existingLoggingContext,
+ };
+
+ // DO NOT forget to return loggingContext here. If you do not return here no logging context will be configured
+ return loggingContext;
+}
+
+/**
+ * Function to add solution specific fields for masking in logs.
+ * "fields to mask" is an array containing field names to mask in logs. The logingService will mask these fields as
+ * '****' in logs. The service will look for these fields in deeply nested objects too. Note that the masking only works
+ * when logging JavaScript objects.
+ *
+ * For example, let's say you want to mask "ssn" numbers from the logs so the fields to mask is ['ssn'].
+ *
+ * const objToLog = { key1: 'value1', ssn:'some-ssn'}
+ * this.log.info(objToLog); // The ssn will be masked here
+ *
+ * const objWithNestedSsn = { key1: 'value1', nested: { deepNested: {'ssn':'some-ssn-value'}}}
+ * this.log.info(objWithNestedSsn); // The ssn will be masked here as well
+ *
+ * but
+ *
+ * this.log.info(`value of ssn is ${someSssn}`); // The ssn will NOT be masked here
+ *
+ * These additional context items can be useful for debugging and monitoring especially when aggregating logs from
+ * multiple lambdas, across multiple environments into a single dashboard or log analytics environment such as ELK stack.
+ *
+ * @param existingFieldsToMask An existing array of field names to mask
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to an array of field names to mask when logging
+ */
+// eslint-disable-next-line no-unused-vars
+async function getFieldsToMask(existingFieldsToMask, pluginRegistry) {
+ // This is where you can
+ // 1. add your additional fields to mask here
+ //
+ // const fieldsToMask = [
+ // ...existingFieldsToMask,
+ //
+ // // add other fields to mask
+ // 'someField1',
+ // 'someField2'
+ // ]
+ // return fieldsToMask;
+ //
+ // 3. remove any existing field(s) from masking by returning an array without that field(s).
+ // This field will be removed from masking list (i.e., it will be logged as is)
+ //
+ // return _.filter(fieldsToMask,_.negate(fieldName => fieldName === fieldNameToNotMask));
+ //
+
+ // TODO: Register additional fieldsToMask as per your solution requirements here
+ const fieldsToMask = {
+ ...existingFieldsToMask,
+ };
+ // DO NOT forget to return fieldsToMask here. If you do not return here no fields will be masked
+ return fieldsToMask;
+}
+
+/**
+ * Function to register solution specific implementation for settings service. This is an optional function.
+ * By default, an implementation of settings service (i.e., "@aws-ee/base-services/lib/settings/env-settings-service")
+ * that resolves settings from environment variables is already registered in the "container"
+ *
+ * @param container Services container
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerSettingsService(container, pluginRegistry) {
+ // The container has default settings service already registered
+ // If you want to register your own settings service implementation then
+ // register it with the key "settings" as follows
+ //
+ // container.register('settings', yourSettingsServiceImpl);
+}
+
+/**
+ * Function to register solution specific implementation for logger service. This is an optional function.
+ * By default, an implementation of logger service (i.e., "@aws-ee/base-services/lib/logger/logger-service") is
+ * already registered in the "container"
+ *
+ * @param container Services container
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerLoggerService(container, pluginRegistry) {
+ // The container has default logger service already registered
+ // If you want to register your own logger service implementation then
+ // register it with the key "log" as follows
+ //
+ // container.register('log', yourLoggerServiceImpl);
+}
+
+const plugin = {
+ registerServices,
+ getStaticSettings,
+ getLoggingContext,
+ getFieldsToMaskInLog: getFieldsToMask,
+ registerSettingsService,
+ registerLoggerService,
+};
+
+module.exports = plugin;
diff --git a/main/packages/services/package.json b/main/packages/services/package.json
new file mode 100644
index 0000000000..058c1a326e
--- /dev/null
+++ b/main/packages/services/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "services",
+ "private": true,
+ "version": "1.0.0",
+ "description": "The solution services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-services": "workspace:*",
+ "@aws-ee/base-services-container": "workspace:*",
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.1.0",
+ "eslint-config-airbnb-base": "^14.1.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "source-map-support": "^0.5.16"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --quiet --ignore-path .gitignore . || true",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' || true",
+ "format": "pnpm run format:eslint; pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . || true",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' || true"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/main/solution/.gitignore b/main/solution/.gitignore
new file mode 100644
index 0000000000..be6937d9a7
--- /dev/null
+++ b/main/solution/.gitignore
@@ -0,0 +1,25 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# Used by the backend tools plugin
+.layers
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# IntelliJ Idea Module Files
+*.iml
+
+# Some scratch files
+scratch*.*
diff --git a/main/solution/backend/.eslintrc.json b/main/solution/backend/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/main/solution/backend/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/main/solution/backend/.gitignore b/main/solution/backend/.gitignore
new file mode 100644
index 0000000000..b2c4241e3d
--- /dev/null
+++ b/main/solution/backend/.gitignore
@@ -0,0 +1,20 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+local-events/
+
+# Serverless directories
+.serverless
+
+# Webpack generated directories
+.webpack
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/main/solution/backend/.prettierrc.json b/main/solution/backend/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/main/solution/backend/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/main/solution/backend/README.md b/main/solution/backend/README.md
new file mode 100644
index 0000000000..594e67c472
--- /dev/null
+++ b/main/solution/backend/README.md
@@ -0,0 +1,45 @@
+# Backend
+
+## Packaging and deploying
+
+To package cfn without deploying it
+
+```bash
+$ pnpx sls package --stage
+```
+
+To deploy:
+
+```bash
+$ pnpx sls deploy --stage
+```
+
+## Useful commands
+
+To list all resolved variables:
+
+```bash
+$ pnpx sls print --stage
+```
+
+## Overview of Lambda Functions
+
+The Research Portal is a serverless solution. Its backend application logic is implemented on API Gateway and AWS Lambda. Some of the backend AWS Lambda functions are:
+
+- API Handler
+
+ This is responsible for processing API calls received via API Gateway. It runs the Express server. API requests are handled by controllers, which delegate execution of business logic to application services. These services interface with the AWS Services via the AWS SDK for JavaScript and Node.js.
+
+ This Lambda function implements the route handling for all /api/_ API calls made by clients. This Lambda function is invoked, after authentication and authorization, every time a client makes a /api/_ HTTP request to the backend. Clients include the web UI as well as any CLI tools that were built, or will in the future be built.
+
+- Authentication Handler
+
+ A Lambda authorizer (formerly known as a custom authorizer) is an API Gateway feature that uses a Lambda function to control access to an API.
+
+ This Lambda function provides an authentication layer in front of the API. It ensures that all requests come from authenticated clients before they hit the backend API. This is the first layer of a common microservices pattern protecting your business logic. IMPORTANT: this lambda does NOT perform any application authorization logic.
+
+ This lambda function is the Custom Authorizer for the Research Portal. It's an entry point for the authentication layer for the API, as it intercepts authentication token, then delegates to authenticationService for authentication decisions.
+
+- Workflow Loop Runner
+
+ This is called by the AWS Step Functions' State Machine. Handles workflow input and configuration, payload, states, wait conditions, and execution of workflow steps, which could be long-running (in contrast, AWS Lambda runtime is limited to 15 minutes). This allows for composable and flexible workflows within the application.
diff --git a/main/solution/backend/config/build/webpack.config.js b/main/solution/backend/config/build/webpack.config.js
new file mode 100644
index 0000000000..cbe84523b2
--- /dev/null
+++ b/main/solution/backend/config/build/webpack.config.js
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+const CopyPlugin = require('copy-webpack-plugin'); // see https://github.com/boazdejong/webpack-plugin-copy
+const slsw = require('serverless-webpack');
+const nodeExternals = require('webpack-node-externals');
+
+const plugins = [new CopyPlugin([])];
+
+module.exports = {
+ entry: slsw.lib.entries,
+ target: 'node',
+ mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
+ optimization: {
+ minimize: false,
+ },
+ performance: {
+ hints: false,
+ },
+ devtool: 'nosources-source-map',
+ externals: [
+ /aws-sdk/, // Available on AWS Lambda
+ slsw.lib.webpack.isLocal && nodeExternals(),
+ ].filter(x => !!x),
+ plugins,
+ node: {
+ __dirname: false,
+ },
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: [
+ [
+ '@babel/preset-env',
+ {
+ targets: { node: '10' }, // Node version on AWS Lambda
+ modules: 'commonjs',
+ },
+ ],
+ ],
+ plugins: ['source-map-support'],
+ },
+ },
+ },
+ {
+ test: /\.ya?ml$/,
+ use: 'js-yaml-loader',
+ },
+ ],
+ },
+};
diff --git a/main/solution/backend/config/infra/cloudformation.yml b/main/solution/backend/config/infra/cloudformation.yml
new file mode 100644
index 0000000000..61f79bcac5
--- /dev/null
+++ b/main/solution/backend/config/infra/cloudformation.yml
@@ -0,0 +1,1091 @@
+Conditions:
+ IsDev: !Equals ['${self:custom.settings.envType}', 'dev']
+
+Resources:
+ # =============================================================================================
+ # S3
+ # =============================================================================================
+
+ # S3 Bucket used for storing uploaded study data
+ StudyDataBucket:
+ Type: AWS::S3::Bucket
+ Properties:
+ BucketName: ${self:custom.settings.studyDataBucketName}
+ BucketEncryption:
+ ServerSideEncryptionConfiguration:
+ - ServerSideEncryptionByDefault:
+ SSEAlgorithm: aws:kms
+ KMSMasterKeyID: !Ref StudyDataEncryptionKey
+ LoggingConfiguration:
+ DestinationBucketName: ${self:custom.settings.loggingBucketName}
+ LogFilePrefix: studydata/
+ VersioningConfiguration:
+ Status: Enabled
+ CorsConfiguration:
+ CorsRules:
+ - AllowedOrigins:
+ - ${self:custom.settings.websiteUrl}
+ - !If
+ - IsDev
+ - http://localhost:3000
+ - !Ref 'AWS::NoValue'
+ AllowedMethods:
+ - POST
+ ExposedHeaders:
+ - ETag
+ PublicAccessBlockConfiguration: # Block all public access configuration for the S3 bucket
+ BlockPublicAcls: true
+ BlockPublicPolicy: true
+ IgnorePublicAcls: true
+ RestrictPublicBuckets: true
+
+ StudyDataBucketPolicy:
+ Type: AWS::S3::BucketPolicy
+ Properties:
+ Bucket: !Ref StudyDataBucket
+ PolicyDocument:
+ Version: '2012-10-17'
+ Id: PutObjectPolicy
+ Statement:
+ - Sid: Deny object uploads not using default encryption settings
+ Effect: Deny
+ Principal: '*'
+ Action: s3:PutObject
+ Resource: !Join ['/', [!GetAtt StudyDataBucket.Arn, '*']]
+ Condition:
+ # The Null-condition allows uploads without encryption information in the request
+ # (i.e., requests with default S3 bucket encryption) and the
+ # StringNotEquals-condition denies uploads with invalid encryption information.
+ # Note that using StringNotEqualsIfExists doesn’t work for uploads without encryption information.
+ # The condition evaluates to true and denies the upload because of the Deny-effect.
+ 'Null':
+ s3:x-amz-server-side-encryption: false
+ StringNotEqualsIfExists:
+ s3:x-amz-server-side-encryption: 'aws:kms'
+ - Sid: Deny object uploads not using default encryption settings
+ Effect: Deny
+ Principal: '*'
+ Action: s3:PutObject
+ Resource: !Join ['/', [!GetAtt StudyDataBucket.Arn, '*']]
+ Condition:
+ StringNotEquals:
+ s3:x-amz-server-side-encryption: 'aws:kms'
+ StringNotEqualsIfExists:
+ s3:x-amz-server-side-encryption-aws-kms-key-id: !GetAtt StudyDataEncryptionKey.Arn
+ - Sid: Deny requests that do not use TLS
+ Effect: Deny
+ Principal: '*'
+ Action: s3:*
+ Resource: !Join ['/', [!GetAtt StudyDataBucket.Arn, '*']]
+ Condition:
+ Bool:
+ aws:SecureTransport: false
+ - Sid: Deny requests that do not use SigV4
+ Effect: Deny
+ Principal: '*'
+ Action: s3:*
+ Resource: !Join ['/', [!GetAtt StudyDataBucket.Arn, '*']]
+ Condition:
+ StringNotEquals:
+ s3:signatureversion: 'AWS4-HMAC-SHA256'
+
+ StudyDataEncryptionKey:
+ Type: AWS::KMS::Key
+ Properties:
+ Description: >-
+ Master key used to encrypt objects stored in the
+ ${self:custom.settings.studyDataBucketName} bucket
+ KeyPolicy:
+ Version: '2012-10-17'
+ Id: study-data-kms-policy
+ Statement:
+ - Sid: Allow root
+ Effect: Allow
+ Principal:
+ AWS:
+ - !Sub 'arn:aws:iam::${AWS::AccountId}:root'
+ Action:
+ - 'kms:*'
+ Resource: '*'
+ - Sid: Allow API access to create object and update policy for new workspaces
+ Effect: Allow
+ Principal:
+ AWS:
+ - !GetAtt [RoleApiHandler, Arn]
+ Action:
+ - kms:GenerateDataKey
+ - kms:DescribeKey
+ - kms:GetKeyPolicy
+ - kms:PutKeyPolicy
+ Resource: '*'
+ - Sid: Allow workflows to update key policy for new workspaces
+ Effect: Allow
+ Principal:
+ AWS:
+ - !GetAtt [RoleWorkflowLoopRunner, Arn]
+ Action:
+ - kms:DescribeKey
+ - kms:GetKeyPolicy
+ - kms:PutKeyPolicy
+ Resource: '*'
+
+ StudyDataEncryptionKeyAlias:
+ Type: AWS::KMS::Alias
+ Properties:
+ AliasName: alias/${self:custom.settings.studyDataKmsKeyAlias}
+ TargetKeyId: !Ref StudyDataEncryptionKey
+
+ ExternalCfnTemplatesBucket:
+ Type: AWS::S3::Bucket
+ Properties:
+ BucketName: ${self:custom.settings.externalCfnTemplatesBucketName}
+ BucketEncryption:
+ ServerSideEncryptionConfiguration:
+ - ServerSideEncryptionByDefault:
+ # Using default SSE-S3 instead of KMS encryption due to an issue with serverless-s3-sync plugin
+ # See "https://github.com/k1LoW/serverless-s3-sync/issues/23" for details
+ SSEAlgorithm: AES256
+ LoggingConfiguration:
+ DestinationBucketName: ${self:custom.settings.loggingBucketName}
+ LogFilePrefix: external-cfn-templates/
+ PublicAccessBlockConfiguration: # Block all public access configuration for the S3 bucket
+ BlockPublicAcls: true
+ BlockPublicPolicy: true
+ IgnorePublicAcls: true
+ RestrictPublicBuckets: true
+
+ ExternalCfnTemplatesBucketPolicy:
+ Type: AWS::S3::BucketPolicy
+ Properties:
+ Bucket: !Ref ExternalCfnTemplatesBucket
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Sid: Deny requests that do not use TLS
+ Effect: Deny
+ Principal: '*'
+ Action: s3:*
+ Resource: !Join ['/', [!GetAtt ExternalCfnTemplatesBucket.Arn, '*']]
+ Condition:
+ Bool:
+ aws:SecureTransport: false
+ - Sid: Deny requests that do not use SigV4
+ Effect: Deny
+ Principal: '*'
+ Action: s3:*
+ Resource: !Join ['/', [!GetAtt ExternalCfnTemplatesBucket.Arn, '*']]
+ Condition:
+ StringNotEquals:
+ s3:signatureversion: 'AWS4-HMAC-SHA256'
+
+ # S3 bucket used to store environment bootstrap scripts
+ EnvironmentsBootstrapBucket:
+ Type: AWS::S3::Bucket
+ Properties:
+ BucketName: ${self:custom.settings.environmentsBootstrapBucketName}
+ BucketEncryption:
+ ServerSideEncryptionConfiguration:
+ - ServerSideEncryptionByDefault:
+ SSEAlgorithm: AES256
+ PublicAccessBlockConfiguration:
+ BlockPublicAcls: true
+ BlockPublicPolicy: true
+ IgnorePublicAcls: true
+ RestrictPublicBuckets: true
+
+ EnvironmentsBootstrapBucketPolicy:
+ Type: AWS::S3::BucketPolicy
+ Properties:
+ Bucket: !Ref EnvironmentsBootstrapBucket
+ PolicyDocument:
+ Version: '2012-10-17'
+ Id: PutObjectPolicy
+ Statement:
+ - Sid: Deny requests that do not use TLS
+ Effect: Deny
+ Principal: '*'
+ Action: s3:*
+ Resource: !Join ['/', [!GetAtt EnvironmentsBootstrapBucket.Arn, '*']]
+ Condition:
+ Bool:
+ aws:SecureTransport: false
+ - Sid: Deny requests that do not use SigV4
+ Effect: Deny
+ Principal: '*'
+ Action: s3:*
+ Resource: !Join ['/', [!GetAtt EnvironmentsBootstrapBucket.Arn, '*']]
+ Condition:
+ StringNotEquals:
+ s3:signatureversion: 'AWS4-HMAC-SHA256'
+
+ # =============================================================================================
+ # IAM Roles
+ # =============================================================================================
+
+ # IAM Role for the authenticationLayerHandler Function
+ RoleAuthenticationLayerHandler:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: lambda.amazonaws.com
+ Action: 'sts:AssumeRole'
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+ Policies:
+ - PolicyName: db-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - dynamodb:DeleteItem
+ - dynamodb:GetItem
+ - dynamodb:PutItem
+ - dynamodb:Query
+ - dynamodb:Scan
+ - dynamodb:UpdateItem
+ Resource:
+ - !GetAtt [DbPasswords, Arn]
+ - !GetAtt [DbUserApiKeys, Arn]
+ - !GetAtt [DbUsers, Arn]
+ - !GetAtt [DbAuthenticationProviderTypes, Arn]
+ - !GetAtt [DbAuthenticationProviderConfigs, Arn]
+ - !GetAtt [DbRevokedTokens, Arn]
+ - !GetAtt [DbUserRoles, Arn]
+ - PolicyName: param-store-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - ssm:GetParameter
+ Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${self:custom.settings.paramStoreRoot}/*'
+
+ # IAM Role for the apiHandler Function
+ RoleApiHandler:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ RoleName: ${self:custom.settings.apiHandlerRoleName}
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: lambda.amazonaws.com
+ Action: 'sts:AssumeRole'
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+ Policies:
+ - PolicyName: db-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - dynamodb:DeleteItem
+ - dynamodb:GetItem
+ - dynamodb:BatchGetItem
+ - dynamodb:PutItem
+ - dynamodb:Query
+ - dynamodb:Scan
+ - dynamodb:UpdateItem
+ Resource:
+ - !GetAtt [DbAuthenticationProviderTypes, Arn]
+ - !GetAtt [DbAuthenticationProviderConfigs, Arn]
+ - !GetAtt [DbRevokedTokens, Arn]
+ - !GetAtt [DbUsers, Arn]
+ - !GetAtt [DbPasswords, Arn]
+ - !GetAtt [DbUserApiKeys, Arn]
+ - !GetAtt [DbLocks, Arn]
+ - !GetAtt [DbStudies, Arn]
+ - !GetAtt [DbStudyPermissions, Arn]
+ - !Join ['', [!GetAtt [DbStudies, Arn], '/index/*']]
+ - !GetAtt [DbStepTemplates, Arn]
+ - !GetAtt [DbWorkflowTemplates, Arn]
+ - !GetAtt [DbWorkflowTemplateDrafts, Arn]
+ - !Join ['', [!GetAtt [DbWorkflowTemplateDrafts, Arn], '/index/*']]
+ - !GetAtt [DbWorkflows, Arn]
+ - !GetAtt [DbWorkflowDrafts, Arn]
+ - !Join ['', [!GetAtt [DbWorkflowDrafts, Arn], '/index/*']]
+ - !GetAtt [DbWorkflowInstances, Arn]
+ - !Join ['', [!GetAtt [DbWorkflowInstances, Arn], '/index/*']]
+ - !GetAtt [DbWfAssignments, Arn]
+ - !Join ['', [!GetAtt [DbWfAssignments, Arn], '/index/*']]
+ - !GetAtt [DbStudies, Arn]
+ - !GetAtt [DbStudyPermissions, Arn]
+ - !Join ['', [!GetAtt [DbStudies, Arn], '/index/*']]
+ - !GetAtt [DbEnvironments, Arn]
+ - !GetAtt [DbUserRoles, Arn]
+ - !GetAtt [DbAwsAccounts, Arn]
+ - !GetAtt [DbIndexes, Arn]
+ - !GetAtt [DbCostApiCaches, Arn]
+ - !GetAtt [DbProjects, Arn]
+ - !GetAtt [DbAccounts, Arn]
+
+ - PolicyName: param-store-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - ssm:GetParameter
+ Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${self:custom.settings.paramStoreRoot}/*'
+
+ - PolicyName: step-functions-invocation
+ PolicyDocument:
+ Statement:
+ - Effect: Allow
+ Action:
+ - states:StartExecution
+ Resource:
+ - !Ref SMWorkflow
+
+ - PolicyName: s3-access
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action:
+ - s3:*
+ Resource:
+ - 'arn:aws:s3:::${self:custom.settings.studyDataBucketName}/*'
+ - Effect: Allow
+ Action:
+ - s3:ListBucket
+ Resource:
+ - 'arn:aws:s3:::${self:custom.settings.studyDataBucketName}'
+ - Effect: Allow
+ Action:
+ - kms:GenerateDataKey
+ Resource:
+ # NOTE: Can't use '!GetAtt StudyDataEncryptionKey.Arn' due to
+ # circular dependency in StudyDataEncryptionKey's KeyPolicy =[
+ - !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/${self:custom.settings.studyDataKmsKeyAlias}
+ - Effect: Allow
+ Action:
+ - s3:*
+ Resource:
+ - 'arn:aws:s3:::${self:custom.settings.externalCfnTemplatesBucketName}/*'
+ - Effect: 'Allow'
+ Action:
+ - s3:GetBucketPolicy
+ - s3:PutBucketPolicy
+ Resource:
+ - 'arn:aws:s3:::${self:custom.settings.environmentsBootstrapBucketName}'
+ - 'arn:aws:s3:::${self:custom.settings.studyDataBucketName}'
+
+ - PolicyName: sagemaker-access
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action:
+ - sagemaker:CreatePresignedNotebookInstanceUrl
+ Resource:
+ - '*'
+
+ - PolicyName: ec2-access
+ PolicyDocument:
+ Statement:
+ - Effect: 'Allow'
+ Action:
+ - ec2:*
+ Resource: '*'
+
+ - PolicyName: cost-explorer
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: 'Allow'
+ Action:
+ - ce:*
+ Resource: '*'
+
+ - PolicyName: assume-role
+ PolicyDocument:
+ Statement:
+ Action: 'sts:AssumeRole'
+ Effect: Allow
+ Resource: '*' # TODO: LOCK THIS DOWN!
+
+ - PolicyName: study-kms-policy-update
+ PolicyDocument:
+ Statement:
+ - Effect: 'Allow'
+ Action:
+ - kms:DescribeKey
+ - kms:GetKeyPolicy
+ - kms:PutKeyPolicy
+ Resource:
+ # NOTE: Can't use '!GetAtt StudyDataEncryptionKey.Arn' due to
+ # circular dependency in StudyDataEncryptionKey's KeyPolicy =[
+ - !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/${self:custom.settings.studyDataKmsKeyAlias}
+
+ # IAM Role for the workflowLoopRunner Function
+ RoleWorkflowLoopRunner:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ RoleName: ${self:custom.settings.workflowLoopRunnerRoleName}
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: lambda.amazonaws.com
+ Action: 'sts:AssumeRole'
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+ Policies:
+ - PolicyName: db-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - dynamodb:DeleteItem
+ - dynamodb:GetItem
+ - dynamodb:BatchGetItem
+ - dynamodb:PutItem
+ - dynamodb:Query
+ - dynamodb:Scan
+ - dynamodb:UpdateItem
+ Resource:
+ - !GetAtt [DbPasswords, Arn]
+ - !GetAtt [DbUsers, Arn]
+ - !GetAtt [DbUserApiKeys, Arn]
+ - !GetAtt [DbLocks, Arn]
+ - !GetAtt [DbStepTemplates, Arn]
+ - !GetAtt [DbWorkflowTemplates, Arn]
+ - !GetAtt [DbWorkflowTemplateDrafts, Arn]
+ - !Join ['', [!GetAtt [DbWorkflowTemplateDrafts, Arn], '/index/*']]
+ - !GetAtt [DbWorkflows, Arn]
+ - !GetAtt [DbWorkflowDrafts, Arn]
+ - !Join ['', [!GetAtt [DbWorkflowDrafts, Arn], '/index/*']]
+ - !GetAtt [DbWorkflowInstances, Arn]
+ - !Join ['', [!GetAtt [DbWorkflowInstances, Arn], '/index/*']]
+ - !GetAtt [DbWfAssignments, Arn]
+ - !Join ['', [!GetAtt [DbWfAssignments, Arn], '/index/*']]
+ - !GetAtt [DbEnvironments, Arn]
+ - !GetAtt [DbStudies, Arn]
+ - !GetAtt [DbStudyPermissions, Arn]
+ - !GetAtt [DbAccounts, Arn]
+ - !GetAtt [DbAwsAccounts, Arn]
+
+ - PolicyName: param-store-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - ssm:GetParameter
+ - ssm:PutParameter
+ - ssm:DeleteParameter
+ Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${self:custom.settings.paramStoreRoot}/*'
+
+ - PolicyName: keypair-creation-access
+ PolicyDocument:
+ Statement:
+ - Effect: 'Allow'
+ Action:
+ - ec2:CreateKeyPair
+ - ec2:DeleteKeyPair
+ Resource: '*'
+ - PolicyName: study-s3-policy-update
+ PolicyDocument:
+ Statement:
+ - Effect: 'Allow'
+ Action:
+ - s3:GetBucketPolicy
+ - s3:PutBucketPolicy
+ Resource:
+ - 'arn:aws:s3:::${self:custom.settings.studyDataBucketName}'
+ - 'arn:aws:s3:::${self:custom.settings.deploymentBucketName}'
+ - 'arn:aws:s3:::${self:custom.settings.environmentsBootstrapBucketName}'
+ - PolicyName: study-kms-policy-update
+ PolicyDocument:
+ Statement:
+ - Effect: 'Allow'
+ Action:
+ - kms:DescribeKey
+ - kms:GetKeyPolicy
+ - kms:PutKeyPolicy
+ Resource:
+ # NOTE: Can't use '!GetAtt StudyDataEncryptionKey.Arn' due to
+ # circular dependency in StudyDataEncryptionKey's KeyPolicy =[
+ - !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/${self:custom.settings.studyDataKmsKeyAlias}
+ - PolicyName: cfn-access
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action:
+ - cloudformation:CreateStack
+ - cloudformation:DeleteStack
+ - cloudformation:DescribeStacks
+ Resource: '*'
+
+ - PolicyName: assume-role
+ PolicyDocument:
+ Statement:
+ Action: 'sts:AssumeRole'
+ Effect: Allow
+ Resource: '*' # TODO: LOCK THIS DOWN!
+
+ # IAM Role for Step Functions to invoke lambda
+ RoleStepFunctionsWorkflow:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: !Sub 'states.${AWS::Region}.amazonaws.com'
+ Action: 'sts:AssumeRole'
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+ Policies:
+ - PolicyName: lambda
+ PolicyDocument:
+ Statement:
+ - Effect: Allow
+ Action: 'lambda:InvokeFunction'
+ Resource:
+ - !GetAtt 'WorkflowLoopRunnerLambdaFunction.Arn'
+
+ # IAM Role for the openDataScrapeHandler Function
+ RoleOpenDataScrapeHandler:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: lambda.amazonaws.com
+ Action: 'sts:AssumeRole'
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+ Policies:
+ - PolicyName: db-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - dynamodb:DeleteItem
+ - dynamodb:GetItem
+ - dynamodb:PutItem
+ - dynamodb:Query
+ - dynamodb:Scan
+ - dynamodb:UpdateItem
+ Resource:
+ - !GetAtt [DbStudies, Arn]
+
+ # =============================================================================================
+ # Step Functions
+ # =============================================================================================
+
+ # Workflow State Machine
+ SMWorkflow:
+ Type: 'AWS::StepFunctions::StateMachine'
+ Properties:
+ StateMachineName: ${self:custom.settings.workflowStateMachineName}
+ DefinitionString: !Sub |
+ {
+ "Comment": "Workflow State Machine",
+ "StartAt": "WorkflowLoopRunner",
+ "Version": "1.0",
+ "States": {
+ "WorkflowLoopRunner": {
+ "Type": "Task",
+ "Resource": "${WorkflowLoopRunnerLambdaFunction.Arn}",
+ "ResultPath": "$.loop",
+ "Next": "MakeAChoice",
+ "Catch": [{
+ "ErrorEquals": ["States.ALL"],
+ "ResultPath": "$.error",
+ "Next": "Failed"
+ }]
+ },
+ "MakeAChoice": {
+ "Type": "Choice",
+ "Choices": [{
+ "Variable": "$.loop.shouldWait",
+ "NumericEquals": 1,
+ "Next": "LetsWait"
+ }, {
+ "Variable": "$.loop.shouldLoop",
+ "NumericEquals": 1,
+ "Next": "WorkflowLoopRunner"
+ }, {
+ "Variable": "$.loop.shouldPass",
+ "NumericEquals": 1,
+ "Next": "Passed"
+ }, {
+ "Variable": "$.loop.shouldFail",
+ "NumericEquals": 1,
+ "Next": "Failed"
+ }],
+ "Default": "Failed"
+ },
+ "LetsWait": {
+ "Type": "Wait",
+ "SecondsPath": "$.loop.wait",
+ "Next": "WorkflowLoopRunner"
+ },
+ "Passed": {
+ "Type": "Pass",
+ "End": true
+ },
+ "Failed": {
+ "Type": "Fail"
+ }
+ }
+ }
+ RoleArn: !GetAtt 'RoleStepFunctionsWorkflow.Arn'
+
+ # =============================================================================================
+ # Dynamo DB
+ # =============================================================================================
+
+ DbPasswords:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTablePasswords}
+ AttributeDefinitions:
+ - AttributeName: 'username'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'username'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '10'
+
+ DbUserApiKeys:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableUserApiKeys}
+ AttributeDefinitions:
+ - AttributeName: 'unameWithNs' # Username with Namespace (ns)
+ AttributeType: 'S'
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'unameWithNs'
+ KeyType: 'HASH'
+ - AttributeName: 'id'
+ KeyType: 'RANGE'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '20'
+ WriteCapacityUnits: '10'
+
+ DbUsers:
+ Type: AWS::DynamoDB::Table
+ DependsOn: DbPasswords
+ Properties:
+ TableName: ${self:custom.settings.dbTableUsers}
+ AttributeDefinitions:
+ - AttributeName: 'username'
+ AttributeType: 'S'
+ - AttributeName: 'ns'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'username'
+ KeyType: 'HASH'
+ - AttributeName: 'ns'
+ KeyType: 'RANGE'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '15'
+ WriteCapacityUnits: '15'
+
+ DbAuthenticationProviderTypes:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableAuthenticationProviderTypes}
+ AttributeDefinitions:
+ - AttributeName: 'type'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'type'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '5'
+ WriteCapacityUnits: '5'
+
+ DbAuthenticationProviderConfigs:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableAuthenticationProviderConfigs}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '5'
+
+ DbRevokedTokens:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableRevokedTokens}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ TimeToLiveSpecification:
+ AttributeName: 'ttl'
+ Enabled: true
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '10'
+
+ DbLocks:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableLocks}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ TimeToLiveSpecification:
+ AttributeName: 'ttl'
+ Enabled: true
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '10'
+
+ DbStudies:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableStudies}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ - AttributeName: 'category'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '5'
+ WriteCapacityUnits: '5'
+ GlobalSecondaryIndexes:
+ - IndexName: ${self:custom.settings.dbTableStudiesCategoryIndex}
+ KeySchema:
+ - AttributeName: 'category'
+ KeyType: 'HASH'
+ Projection:
+ ProjectionType: 'ALL'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '5'
+ WriteCapacityUnits: '5'
+
+ DbStepTemplates:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableStepTemplates}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ - AttributeName: 'ver'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ - AttributeName: 'ver'
+ KeyType: 'RANGE'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '5'
+
+ DbWorkflowTemplates:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableWorkflowTemplates}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ - AttributeName: 'ver'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ - AttributeName: 'ver'
+ KeyType: 'RANGE'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '5'
+
+ DbWorkflows:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableWorkflows}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ - AttributeName: 'ver'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ - AttributeName: 'ver'
+ KeyType: 'RANGE'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '5'
+
+ DbWorkflowTemplateDrafts:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableWorkflowTemplateDrafts}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ - AttributeName: 'username'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '5'
+ GlobalSecondaryIndexes:
+ - IndexName: 'UsernameIndex'
+ KeySchema:
+ - AttributeName: 'username'
+ KeyType: 'HASH'
+ Projection:
+ ProjectionType: 'ALL'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '15'
+ WriteCapacityUnits: '15'
+
+ DbWorkflowDrafts:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableWorkflowDrafts}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ - AttributeName: 'username'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '5'
+ GlobalSecondaryIndexes:
+ - IndexName: 'UsernameIndex'
+ KeySchema:
+ - AttributeName: 'username'
+ KeyType: 'HASH'
+ Projection:
+ ProjectionType: 'ALL'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '15'
+ WriteCapacityUnits: '15'
+
+ DbWorkflowInstances:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableWorkflowInstances}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ - AttributeName: 'createdAt'
+ AttributeType: 'S'
+ - AttributeName: 'wf'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ TimeToLiveSpecification:
+ AttributeName: 'ttl'
+ Enabled: true
+ ProvisionedThroughput:
+ ReadCapacityUnits: '15'
+ WriteCapacityUnits: '10'
+ GlobalSecondaryIndexes:
+ - IndexName: 'WorkflowIndex'
+ KeySchema:
+ - AttributeName: 'wf'
+ KeyType: 'HASH'
+ - AttributeName: 'createdAt'
+ KeyType: 'RANGE'
+ Projection:
+ ProjectionType: 'ALL'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '20'
+ WriteCapacityUnits: '20'
+
+ DbWfAssignments:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableWfAssignments}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ - AttributeName: 'triggerType' # trigger type
+ AttributeType: 'S'
+ - AttributeName: 'triggerTypeData' # trigger type data (qualifier)
+ AttributeType: 'S'
+ - AttributeName: 'wf' # workflow id
+ AttributeType: 'S'
+ - AttributeName: 'createdAt'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '10'
+ GlobalSecondaryIndexes:
+ - IndexName: 'WorkflowIndex'
+ KeySchema:
+ - AttributeName: 'wf'
+ KeyType: 'HASH'
+ - AttributeName: 'createdAt'
+ KeyType: 'RANGE'
+ Projection:
+ ProjectionType: 'ALL'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '10'
+ - IndexName: 'TypeIndex'
+ KeySchema:
+ - AttributeName: 'triggerType'
+ KeyType: 'HASH'
+ - AttributeName: 'triggerTypeData'
+ KeyType: 'RANGE'
+ Projection:
+ ProjectionType: 'ALL'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '10'
+
+ DbEnvironments:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableEnvironments}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '10'
+
+ DbUserRoles:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableUserRoles}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '15'
+ WriteCapacityUnits: '15'
+
+ DbAwsAccounts:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableAwsAccounts}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '5'
+ WriteCapacityUnits: '5'
+
+ DbIndexes:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableIndexes}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '5'
+ WriteCapacityUnits: '5'
+
+ DbCostApiCaches:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableCostApiCaches}
+ AttributeDefinitions:
+ - AttributeName: 'indexId'
+ AttributeType: 'S'
+ - AttributeName: 'query'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'indexId'
+ KeyType: 'HASH'
+ - AttributeName: 'query'
+ KeyType: 'RANGE'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '5'
+ WriteCapacityUnits: '5'
+
+ DbAccounts:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableAccounts}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '5'
+ WriteCapacityUnits: '5'
+
+ DbProjects:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableProjects}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '5'
+ WriteCapacityUnits: '5'
+
+ DbStudyPermissions:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableStudyPermissions}
+ AttributeDefinitions:
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'id'
+ KeyType: 'HASH'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '10'
+
+# =============================================================================================
+# Outputs
+# =============================================================================================
+Outputs:
diff --git a/main/solution/backend/config/infra/functions.yml b/main/solution/backend/config/infra/functions.yml
new file mode 100644
index 0000000000..f548d79399
--- /dev/null
+++ b/main/solution/backend/config/infra/functions.yml
@@ -0,0 +1,118 @@
+authenticationLayerHandler:
+ handler: src/lambdas/authentication-layer-handler/handler.handler
+ role: RoleAuthenticationLayerHandler
+ tags: ${self:custom.tags}
+ description: Handles the authentication layer for API handlers.
+ environment:
+ APP_PARAM_STORE_JWT_SECRET: ${self:custom.settings.paramStoreJwtSecret}
+ APP_JWT_OPTIONS: ${self:custom.settings.jwtOptions}
+
+openDataScrapeHandler:
+ handler: src/lambdas/open-data-scrape-handler/handler.handler
+ role: RoleOpenDataScrapeHandler
+ tags: ${self:custom.tags}
+ description: Handles scraping the metadata from the AWS open data registry.
+ environment:
+ APP_DB_TABLE_STUDIES_CATEGORY_INDEX: ${self:custom.settings.dbTableStudiesCategoryIndex}
+ APP_STUDY_DATA_BUCKET_NAME: ${self:custom.settings.studyDataBucketName}
+ events:
+ - schedule:
+ rate: rate(1 day)
+ description: 'Invokes the lambda function that scrapes the awslabs/open-data-registry to find new studies and insert them into DynamoDB.'
+
+apiHandler:
+ handler: src/lambdas/api-handler/handler.handler
+ role: RoleApiHandler
+ tags: ${self:custom.tags}
+ description: The API handler for all /api/* APIs
+ events:
+ # Public APIs
+ - http:
+ path: /api/authentication/public/provider/configs
+ method: GET
+ cors: true
+ - http:
+ path: /api/authentication/id-tokens
+ method: POST
+ cors: true
+ # Protected APIs
+ - http:
+ authorizer: authenticationLayerHandler
+ path: /api
+ method: GET
+ cors: true
+ - http:
+ authorizer: authenticationLayerHandler
+ path: /api/{proxy+}
+ method: GET
+ cors: true
+ - http:
+ authorizer: authenticationLayerHandler
+ path: /api
+ method: POST
+ cors: true
+ - http:
+ authorizer: authenticationLayerHandler
+ path: /api/{proxy+}
+ method: POST
+ cors: true
+ - http:
+ authorizer: authenticationLayerHandler
+ path: /api
+ method: PUT
+ cors: true
+ - http:
+ authorizer: authenticationLayerHandler
+ path: /api/{proxy+}
+ method: PUT
+ cors: true
+ - http:
+ authorizer: authenticationLayerHandler
+ path: /api
+ method: DELETE
+ cors: true
+ - http:
+ authorizer: authenticationLayerHandler
+ path: /api/{proxy+}
+ method: DELETE
+ cors: true
+ environment:
+ APP_WEBSITE_URL: ${self:custom.settings.websiteUrl}
+ APP_CORS_WHITELIST: ${self:custom.settings.corsWhiteList}
+ APP_CORS_WHITELIST_LOCAL: ${self:custom.settings.corsWhiteListLocal}
+ APP_PARAM_STORE_JWT_SECRET: ${self:custom.settings.paramStoreJwtSecret}
+ APP_JWT_OPTIONS: ${self:custom.settings.jwtOptions}
+ APP_SM_WORKFLOW: ${self:custom.settings.workflowStateMachineArn}
+ APP_PARAM_STORE_ROOT: ${self:custom.settings.paramStoreRoot}
+ APP_EC2_LINUX_AMI_PREFIX: ${self:custom.settings.ec2LinuxAmiPrefix}
+ APP_EC2_WINDOWS_AMI_PREFIX: ${self:custom.settings.ec2WindowsAmiPrefix}
+ APP_EMR_AMI_PREFIX: ${self:custom.settings.emrAmiPrefix}
+ APP_DB_TABLE_STUDIES_CATEGORY_INDEX: ${self:custom.settings.dbTableStudiesCategoryIndex}
+ APP_STUDY_DATA_BUCKET_NAME: ${self:custom.settings.studyDataBucketName}
+ APP_WORKFLOW_ROLE_ARN: ${self:custom.settings.workflowLoopRunnerRoleArn}
+ APP_API_HANDLER_ARN: ${self:custom.settings.apiHandlerRoleArn}
+ APP_EXTERNAL_CFN_TEMPLATES_BUCKET_NAME: ${self:custom.settings.externalCfnTemplatesBucketName}
+ APP_ENVIRONMENT_INSTANCE_FILES: ${self:custom.settings.environmentInstanceFiles}
+ APP_STUDY_DATA_KMS_KEY_ALIAS: ${self:custom.settings.studyDataKmsKeyAlias}
+ APP_STUDY_DATA_KMS_KEY_ARN: ${self:custom.settings.studyDataKmsKeyAliasArn}
+ APP_STUDY_DATA_KMS_POLICY_WORKSPACE_SID: ${self:custom.settings.studyDataKmsPolicyWorkspaceSid}
+
+workflowLoopRunner:
+ handler: src/lambdas/workflow-loop-runner/handler.handler
+ role: RoleWorkflowLoopRunner
+ timeout: 900 # 15 min
+ tags: ${self:custom.tags}
+ description: The workflow loop runner, it is expected to be invoked by AWS Step Functions and not directly
+ environment:
+ # We cannot use "!Ref SMWorkflow" below as that will create circular dependency
+ APP_SM_WORKFLOW: ${self:custom.settings.workflowStateMachineArn}
+ APP_PARAM_STORE_ROOT: ${self:custom.settings.paramStoreRoot}
+ APP_PARAM_STORE_JWT_SECRET: ${self:custom.settings.paramStoreJwtSecret}
+ APP_JWT_OPTIONS: ${self:custom.settings.jwtOptions}
+ APP_DB_TABLE_STUDIES_CATEGORY_INDEX: ${self:custom.settings.dbTableStudiesCategoryIndex}
+ APP_STUDY_DATA_BUCKET_NAME: ${self:custom.settings.studyDataBucketName}
+ APP_ARTIFACTS_BUCKET_NAME: ${self:custom.settings.deploymentBucketName}
+ APP_ENVIRONMENT_INSTANCE_FILES: ${self:custom.settings.environmentInstanceFiles}
+ APP_STUDY_DATA_KMS_KEY_ALIAS: ${self:custom.settings.studyDataKmsKeyAlias}
+ APP_STUDY_DATA_KMS_KEY_ARN: !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/${self:custom.settings.studyDataKmsKeyAlias}
+ APP_STUDY_DATA_KMS_POLICY_WORKSPACE_SID: ${self:custom.settings.studyDataKmsPolicyWorkspaceSid}
diff --git a/main/solution/backend/config/settings/.defaults.yml b/main/solution/backend/config/settings/.defaults.yml
new file mode 100644
index 0000000000..64a8badae0
--- /dev/null
+++ b/main/solution/backend/config/settings/.defaults.yml
@@ -0,0 +1,156 @@
+# Options used when issuing JWT token such as which algorithm to use for hashing and how long to keep the tokens alive etc
+jwtOptions: '{"algorithm":"HS256","expiresIn":"2 days"}'
+
+# Name of the parameter in parameter store containing secret key for JWT. This key is used for signing and validating JWT tokens
+# issued by data lake authentication providers
+paramStoreJwtSecret: '/${self:custom.settings.paramStoreRoot}/jwt/secret'
+
+# TODO: Remove this dependency from the "infrastructure" stack and set the default "corsWhiteList" to empty array
+# Move these settings that are dependent on "infrastructure" stack to the "addon-base-ui" once the addon cli task to
+# insert settings is available. Until then, the "addon-rest-api" cannot be installed stand alone without also having
+# to install the "addon-base-ui"
+#
+# The stack name of the 'infrastructure' serverless service
+infrastructureStackName: ${self:custom.settings.namespace}-infrastructure
+
+# a list of domain names to whitelist in the API
+corsWhiteList: '["https://${cf:${self:custom.settings.infrastructureStackName}.CloudFrontEndpoint}"]'
+
+# a list of domain names to whitelist in the API while in dev (envType == dev)
+corsWhiteListLocal: '["http://localhost:3000"]'
+
+# URL of the website
+websiteUrl: ${cf:${self:custom.settings.infrastructureStackName}.WebsiteUrl}
+
+# The S3 bucket name used to host the static website
+websiteBucketName: ${cf:${self:custom.settings.infrastructureStackName}.WebsiteBucket}
+
+# The S3 bucket name used for S3 access logging
+loggingBucketName: ${cf:${self:custom.settings.infrastructureStackName}.LoggingBucket}
+
+# The S3 bucket name to be used to the data
+dataBucketName: ${self:custom.settings.globalNamespace}-data
+
+# The S3 bucket name to be where the external CloudFormation templates will be stored
+externalCfnTemplatesBucketName: ${self:custom.settings.globalNamespace}-external-templates
+
+# The name of the S3 bucket used to store uploaded study data
+studyDataBucketName: ${self:custom.settings.globalNamespace}-studydata
+
+# The alias used for the KMS key created to encrypt/decrypt study data
+studyDataKmsKeyAlias: ${self:custom.settings.globalNamespace}/s3/studydata
+
+# The arn of the KMS Key alias
+studyDataKmsKeyAliasArn: arn:aws:kms:${self:custom.settings.awsRegion}:${self:custom.settings.awsAccountInfo.awsAccountId}:alias/${self:custom.settings.studyDataKmsKeyAlias}
+
+# The statement ID in the generated KMS key's key policy that controls which workspaces
+# can decrypt data in the study data bucket
+studyDataKmsPolicyWorkspaceSid: Allow workspace environments to retrieve S3 objects
+
+# The name of the IAM role created for the Lambda API handler
+apiHandlerRoleName: ${self:custom.settings.namespace}-ApiHandler
+
+# The IAM role arn for the Lambda API handler, we need to define it in the settings because it is being passed to lambdas as an env var
+apiHandlerRoleArn: 'arn:aws:iam::${self:custom.settings.awsAccountInfo.awsAccountId}:role/${self:custom.settings.apiHandlerRoleName}'
+
+# Enable or disable workflow processing
+workflowsEnabled: true
+
+# The workflow state machine name
+workflowStateMachineName: ${self:custom.settings.namespace}-workflow
+
+# The workflow state machine arn, we need to define it in the settings because it is being passed to lambdas as an env var
+workflowStateMachineArn: 'arn:aws:states:${self:custom.settings.awsRegion}:${self:custom.settings.awsAccountInfo.awsAccountId}:stateMachine:${self:custom.settings.workflowStateMachineName}'
+
+# The name of the IAM role created for the workflow Lambda function
+workflowLoopRunnerRoleName: ${self:custom.settings.namespace}-WorkflowLoopRunner
+
+# The IAM role arn for the Lambda API handler, we need to define it in the settings because it is being passed to lambdas as an env var
+workflowLoopRunnerRoleArn: 'arn:aws:iam::${self:custom.settings.awsAccountInfo.awsAccountId}:role/${self:custom.settings.workflowLoopRunnerRoleName}'
+
+# The prefix of the ami used to create environments
+ec2LinuxAmiPrefix: ${self:custom.settings.namespace}-EC2-LINUX-AMI
+ec2WindowsAmiPrefix: ${self:custom.settings.namespace}-EC2-WINDOWS-AMI
+emrAmiPrefix: ${self:custom.settings.namespace}-EMR-AMI
+
+# S3 location of files copied to an environment instance along with bootstrap scripts
+environmentInstanceFiles: s3://${self:custom.settings.environmentsBootstrapBucketName}/environment-files
+
+# ================================ DB Settings ===========================================
+
+# DynamoDB table name for supported authentication provider types
+dbTableAuthenticationProviderTypes: ${self:custom.settings.dbTablePrefix}-DbAuthenticationProviderTypes
+
+# DynamoDB table name for authentication provider configurations
+dbTableAuthenticationProviderConfigs: ${self:custom.settings.dbTablePrefix}-DbAuthenticationProviderConfigs
+
+# DynamoDB table name for data lake users
+dbTableUsers: ${self:custom.settings.dbTablePrefix}-DbUsers
+
+# DynamoDB table name for passwords for the internal data lake users
+# (applicable only to the users authenticated by internal authentication provider)
+dbTablePasswords: ${self:custom.settings.dbTablePrefix}-DbPasswords
+
+# DynamoDB table name for User's API Keys. These keys are different from the user's passwords.
+# These keys allow the user invoke Data Lake APIs directly outside of the UI.
+dbTableUserApiKeys: ${self:custom.settings.dbTablePrefix}-DbUserApiKeys
+
+# DyanmoDB table name for Token Revocation Table
+dbTableRevokedTokens: ${self:custom.settings.dbTablePrefix}-DbRevokedTokens
+
+# DynamoDB table name for Locks
+dbTableLocks: ${self:custom.settings.dbTablePrefix}-DbLocks
+
+# DynamoDB table name for Step Templates
+dbTableStepTemplates: ${self:custom.settings.dbTablePrefix}-DbStepTemplates
+
+# DynamoDB table name for Workflow Templates
+dbTableWorkflowTemplates: ${self:custom.settings.dbTablePrefix}-DbWorkflowTemplates
+
+# DynamoDB table name for Workflow Templates Drafts
+dbTableWorkflowTemplateDrafts: ${self:custom.settings.dbTablePrefix}-DbWorkflowTemplateDrafts
+
+# DynamoDB table name for Workflows
+dbTableWorkflows: ${self:custom.settings.dbTablePrefix}-DbWorkflows
+
+# DynamoDB table name for Workflow Drafts
+dbTableWorkflowDrafts: ${self:custom.settings.dbTablePrefix}-DbWorkflowDrafts
+
+# DynamoDB table name for Workflow Instances
+dbTableWorkflowInstances: ${self:custom.settings.dbTablePrefix}-DbWorkflowInstances
+
+# DynamoDB table name for WfAssignments
+dbTableWfAssignments: ${self:custom.settings.dbTablePrefix}-DbWfAssignments
+
+# DynamoDB table name for Studies
+dbTableStudies: ${self:custom.settings.dbTablePrefix}-DbStudies
+
+# DynamoDB table global secondary index name for the "category" field
+dbTableStudiesCategoryIndex: CategoryIndex
+
+# DynamoDB table name for Environments
+dbTableEnvironments: ${self:custom.settings.dbTablePrefix}-DbEnvironments
+
+# DynamoDB table name for UserRoles
+dbTableUserRoles: ${self:custom.settings.dbTablePrefix}-DbUserRoles
+
+# DynamoDB table name for AwsAccounts
+dbTableAwsAccounts: ${self:custom.settings.dbTablePrefix}-DbAwsAccounts
+
+# DynamoDB table name for Indexes
+dbTableIndexes: ${self:custom.settings.dbTablePrefix}-DbIndexes
+
+# DynamoDB table name for CostApiCaches
+dbTableCostApiCaches: ${self:custom.settings.dbTablePrefix}-DbCostApiCaches
+
+# DynamoDB table name for Accounts
+dbTableAccounts: ${self:custom.settings.dbTablePrefix}-DbAccounts
+
+# DynamoDB table name for Projects
+dbTableProjects: ${self:custom.settings.dbTablePrefix}-DbProjects
+
+# DynamoDB table name for EnvironmentTokens
+dbTableEnvironmentTokens: ${self:custom.settings.dbTablePrefix}-DbEnvironmentTokens
+
+# DynamoDB table name for StudyPermissions
+dbTableStudyPermissions: ${self:custom.settings.dbTablePrefix}-DbStudyPermissions
diff --git a/main/solution/backend/config/settings/.settings.js b/main/solution/backend/config/settings/.settings.js
new file mode 100644
index 0000000000..0f0a5acf3b
--- /dev/null
+++ b/main/solution/backend/config/settings/.settings.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports.merged = require('@aws-ee/base-serverless-settings-helper').mergeSettings(__dirname, [
+ '../../../../config/settings/.defaults.yml',
+ './.defaults.yml',
+ '../../../../config/settings/${stage}.yml',
+ './${stage}.yml',
+]);
diff --git a/main/solution/backend/jest.config.js b/main/solution/backend/jest.config.js
new file mode 100644
index 0000000000..3f7ffc8068
--- /dev/null
+++ b/main/solution/backend/jest.config.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/main/solution/backend/jsconfig.json b/main/solution/backend/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/main/solution/backend/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/main/solution/backend/package.json b/main/solution/backend/package.json
new file mode 100644
index 0000000000..e972fa0722
--- /dev/null
+++ b/main/solution/backend/package.json
@@ -0,0 +1,93 @@
+{
+ "name": "@aws-ee/backend",
+ "version": "1.0.0",
+ "private": true,
+ "description": "The API implementation for the backend",
+ "author": "Amazon Web Services",
+ "license": "Apache 2.0",
+ "dependencies": {
+ "@aws-ee/base-api-handler": "workspace:*",
+ "@aws-ee/base-api-handler-factory": "workspace:*",
+ "@aws-ee/base-api-services": "workspace:*",
+ "@aws-ee/base-authn-handler": "workspace:*",
+ "@aws-ee/base-controllers": "workspace:*",
+ "@aws-ee/base-raas-cfn-templates": "workspace:*",
+ "@aws-ee/base-raas-rest-api": "workspace:*",
+ "@aws-ee/base-raas-services": "workspace:*",
+ "@aws-ee/base-raas-workflow-steps": "workspace:*",
+ "@aws-ee/base-raas-workflows": "workspace:*",
+ "@aws-ee/base-services": "workspace:*",
+ "@aws-ee/base-services-container": "workspace:*",
+ "@aws-ee/base-workflow-api": "workspace:*",
+ "@aws-ee/base-workflow-core": "workspace:*",
+ "@aws-ee/base-workflow-steps": "workspace:*",
+ "services": "workspace:*",
+ "aws-sdk": "^2.647.0",
+ "js-yaml": "^3.13.1",
+ "jwt-decode": "^2.2.0",
+ "lodash": "^4.17.15",
+ "node-fetch": "^2.6.0"
+ },
+ "devDependencies": {
+ "@aws-ee/base-serverless-backend-tools": "workspace:*",
+ "@aws-ee/base-serverless-settings-helper": "workspace:*",
+ "@babel/core": "^7.8.4",
+ "@babel/plugin-transform-runtime": "^7.8.3",
+ "@babel/preset-env": "^7.8.4",
+ "babel-jest": "^24.9.0",
+ "babel-loader": "^8.0.6",
+ "babel-plugin-source-map-support": "^2.1.1",
+ "copy-webpack-plugin": "^5.1.1",
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "fsevents": "*",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "js-yaml-loader": "^1.2.2",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "serverless": "^1.63.0",
+ "serverless-deployment-bucket": "^1.1.0",
+ "serverless-offline": "^5.12.1",
+ "serverless-s3-sync": "^1.12.0",
+ "serverless-webpack": "^5.3.1",
+ "source-map-support": "^0.5.16",
+ "webpack": "^4.41.5",
+ "webpack-cli": "^3.3.10",
+ "webpack-node-externals": "^1.7.2"
+ },
+ "optionalDependencies": {
+ "fsevents": "*"
+ },
+ "jest": {
+ "transform": {
+ "/src/.+\\.js$": "babel-jest"
+ }
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "start-debug": "node --inspect node_modules/.bin/serverless offline start",
+ "start": "sls offline --stage=$USER",
+ "invoke": "sls invoke local --stage=$USER -f",
+ "deploy": "sls deploy --stage=$USER",
+ "package": "sls package --stage=$USER",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/main/solution/backend/serverless.yml b/main/solution/backend/serverless.yml
new file mode 100644
index 0000000000..d98b306392
--- /dev/null
+++ b/main/solution/backend/serverless.yml
@@ -0,0 +1,69 @@
+# For full config options, see docs.serverless.com
+# Note that most settings in here come from config/settings/*.yaml
+service: ${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}-backend
+
+package:
+ individually: true
+ excludeDevDependencies: true
+
+provider:
+ name: aws
+ runtime: nodejs12.x
+ region: ${self:custom.settings.awsRegion}
+ profile: ${self:custom.settings.awsProfile}
+ stackName: ${self:custom.settings.envName}-${self:service}
+ deploymentBucket:
+ name: ${self:custom.settings.deploymentBucketName}
+ serverSideEncryption: AES256
+ stackTags: ${self:custom.tags}
+ versionFunctions: false # see https://medium.com/@mayconbordin/lessons-learned-building-a-large-serverless-project-on-aws-74d40f5b0b46
+ # All references beginning with ${self:*, ${opt:*, ${file:*, ${deep:*, and ${cf:* will be resolved by Serverless
+ # All other ${* references will be resolved by CloudFormation
+ # See https://forum.serverless.com/t/getting-handle-accountid-in-serverless-config/946/11 and
+ # See https://github.com/serverless/serverless/issues/5011
+ variableSyntax: '\$\{((((self|opt|deep|cf):)|file)((?!\$\{).)+?)}'
+ environment:
+ APP_ENV_TYPE: ${self:custom.settings.envType}
+ APP_ENV_NAME: ${self:custom.settings.envName}
+ APP_AWS_REGION: ${self:custom.settings.awsRegion}
+ APP_SOLUTION_NAME: ${self:custom.settings.solutionName}
+ APP_DB_TABLE_PREFIX: ${self:custom.settings.dbTablePrefix}
+
+custom:
+ settings: ${file(./config/settings/.settings.js):merged}
+ tags:
+ Name: ${self:custom.settings.envName}-${self:service}
+ deploymentBucket:
+ policy: ${self:custom.settings.deploymentBucketPolicy}
+ serverless-offline:
+ port: 4000
+ webpack:
+ webpackConfig: ./config/build/webpack.config.js
+ packager: pnpm
+ keepOutputDirectory: true
+ excludeFiles: src/**/*.test.js
+ backendTools:
+ environmentOverrides: # when running locally
+ provider:
+ APP_AWS_REGION: ${self:custom.settings.awsRegion} # this is needed for local development
+ APP_AWS_PROFILE: ${self:custom.settings.awsProfile} # this is needed for local development
+ APP_USE_AWS_PROFILE: ${self:custom.settings.useAwsProfile}
+ IS_OFFLINE: true
+ lambdas:
+
+ s3Sync:
+ - bucketName: ${self:custom.settings.externalCfnTemplatesBucketName} # required
+ localDir: ../../../addons/addon-base-raas/packages/base-raas-cfn-templates/src/templates/external
+
+functions: ${file(./config/infra/functions.yml)}
+
+resources:
+ - Description: Galileo-Gateway ${self:custom.settings.version} ${self:custom.settings.solutionName} ${self:custom.settings.envName} Backend
+ - ${file(./config/infra/cloudformation.yml)}
+
+plugins:
+ - serverless-webpack
+ - serverless-offline
+ - serverless-deployment-bucket
+ - serverless-s3-sync
+ - '@aws-ee/base-serverless-backend-tools'
diff --git a/main/solution/backend/src/lambdas/api-handler/handler.js b/main/solution/backend/src/lambdas/api-handler/handler.js
new file mode 100644
index 0000000000..ad139cedcf
--- /dev/null
+++ b/main/solution/backend/src/lambdas/api-handler/handler.js
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+const handlerFactory = require('@aws-ee/base-api-handler-factory');
+const {
+ registerServices: registerServicesUtil,
+} = require('@aws-ee/base-services/lib/utils/services-registration-util');
+const { registerRoutes: registerRoutesUtil } = require('@aws-ee/base-api-handler/lib/routes-registration-util');
+
+const pluginRegistry = require('./plugins/plugin-registry');
+
+/**
+ * Registers services by calling each service registration plugin in order.
+ *
+ * @param container An instance of ServicesContainer
+ * @returns {Promise}
+ */
+async function registerServices(container) {
+ return registerServicesUtil(container, pluginRegistry);
+}
+
+/**
+ * Configures the given express router by collecting routes contributed by all route plugins.
+ * @param context An instance of AppContext from api-handler-factory
+ * @param router Top level Express router
+ *
+ * @returns {Promise}
+ */
+async function registerRoutes(context, router) {
+ return registerRoutesUtil(context, router, pluginRegistry);
+}
+
+// The main lambda handler function. This is the entry point of the lambda function
+// Calls handlerFactory that creates a Lambda function
+// 1. by creating an Express JS application instance and registering all API routes by calling the "registerRoutes" function we pass here
+// 2. by initializing a services container instance and registering all service instances to the container by calling the "registerServices" function we pass here
+// The handler function returned by the "handlerFactory" has the classical Lambda handler function signature of (event, context) => Promise
+const handler = handlerFactory({ registerServices, registerRoutes });
+
+module.exports.handler = handler;
diff --git a/main/solution/backend/src/lambdas/api-handler/plugins/plugin-registry.js b/main/solution/backend/src/lambdas/api-handler/plugins/plugin-registry.js
new file mode 100644
index 0000000000..3d89a07e74
--- /dev/null
+++ b/main/solution/backend/src/lambdas/api-handler/plugins/plugin-registry.js
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+const baseAuditPlugin = require('@aws-ee/base-services/lib/plugins/audit-plugin');
+const baseServicesPlugin = require('@aws-ee/base-api-handler/lib/plugins/services-plugin');
+const baseRoutesPlugin = require('@aws-ee/base-controllers/lib/plugins/routes-plugin');
+const baseWfServicesPlugin = require('@aws-ee/base-workflow-api/lib/plugins/services-plugin');
+const baseWfRoutesPlugin = require('@aws-ee/base-workflow-api/lib/plugins/routes-plugin');
+const bassRaasServicesPlugin = require('@aws-ee/base-raas-rest-api/lib/plugins/services-plugin');
+const baseRaasRoutesPlugin = require('@aws-ee/base-raas-rest-api/lib/plugins/routes-plugin');
+const baseRaasCfnTemplatesPlugin = require('@aws-ee/base-raas-cfn-templates/dist/plugins/cfn-templates-plugin');
+const baseRaasUserAuthzPlugin = require('@aws-ee/base-raas-services/lib/user/user-authz-plugin');
+const servicesPlugin = require('services/lib/plugins/services-plugin');
+
+const routesPlugin = require('./routes-plugin');
+
+const extensionPoints = {
+ 'service': [baseServicesPlugin, baseWfServicesPlugin, bassRaasServicesPlugin, servicesPlugin],
+ 'route': [baseRoutesPlugin, baseWfRoutesPlugin, baseRaasRoutesPlugin, routesPlugin],
+ 'audit': [baseAuditPlugin],
+ 'authentication-provider-type': [], // No plugins at this point. The built in authentication provider types are registered by "addon-base-rest-api/packages/services/lib/authentication-providers/authentication-provider-type-service.js" service
+ 'cfn-templates': [baseRaasCfnTemplatesPlugin],
+ 'user-authz': [baseRaasUserAuthzPlugin],
+ 'user-role-management-authz': [], // No plugins at this point. All user-role-management authz is happening inline in 'user-roles-service'
+ 'environment-authz': [], // No plugins at this point. All environment authz is happening inline in 'environment-service' using the 'environment-authz-service'
+ 'project-authz': [], // No plugins at this point. All project authz is happening inline in 'project-service'
+ 'index-authz': [], // No plugins at this point. All index authz is happening inline in 'index-service'
+ 'account-authz': [], // No plugins at this point. All account authz is happening inline in 'account-service'
+ 'aws-account-authz': [], // No plugins at this point. All aws-account authz is happening inline in 'aws-account-service'
+ 'cost-authz': [], // No plugins at this point. All cost authz is happening inline in 'costs-service'
+};
+
+async function getPlugins(extensionPoint) {
+ return extensionPoints[extensionPoint];
+}
+
+const registry = {
+ getPlugins,
+};
+
+module.exports = registry;
diff --git a/main/solution/backend/src/lambdas/api-handler/plugins/routes-plugin.js b/main/solution/backend/src/lambdas/api-handler/plugins/routes-plugin.js
new file mode 100644
index 0000000000..e0df5e21b8
--- /dev/null
+++ b/main/solution/backend/src/lambdas/api-handler/plugins/routes-plugin.js
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+/**
+ * Adds routes to the given routesMap.
+ * This function is called last after adding routes to the routesMap from all other installed addons.
+ *
+ * @param routesMap A Map containing routes. This object is a Map that has route paths as
+ * keys and an array of functions that configure the router as value.
+ *
+ * Each function in the array is expected have the following signature. The function accepts context and router
+ * arguments and returns a configured router.
+ *
+ * (context, router) => configured router
+ *
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} Returns a Map with the mapping of routes as keys and their router configurer functions array as values
+ */
+// eslint-disable-next-line no-unused-vars
+async function getRoutes(routesMap, pluginRegistry) {
+ // This is where you can
+ // 1. register your routes, to register your routes
+ //
+ // const routes = new Map([
+ // ...routesMap,
+ // // Add your routes here
+ // ['/your/routes', [ someMiddlewareFn1, someMiddlewareFn2, ..., someControllerFn ]],
+ // ]);
+ // return routes;
+ //
+ // 2. modify any existing routes
+ // 2.1 by completely replacing them, to replace a route
+ //
+ // routesMap.set('the/route/you/want/to/replace',[ comma separated list of middleware functions, some controller function ]);
+ // return routesMap;
+ //
+ // 2.2 by updating their middlewares or controllers, to update middlewares or controllers of existing routes
+ //
+ // const existingMiddlewares = routesMap.get('the/route/whose/middlewares/or/controllers/to/modify');
+ // // Manipulate existingMiddlewares containing middleware and controller functions as per your need
+ // routesMap.set('the/route/whose/middlewares/or/controllers/to/modify',updatedMiddlewareFunctions);
+ // return routesMap;
+ //
+ // 3. delete any existing route, to delete existing route
+ //
+ // routesMap.delete('the/route/you/want/to/delete');
+ //
+
+ // TODO: Register additional routes and their controllers as per your solution requirements
+
+ // DO NOT forget to return routesMap here. If you do not return here no routes will be configured in Express router
+ return routesMap;
+}
+
+const plugin = {
+ getRoutes,
+};
+
+module.exports = plugin;
diff --git a/main/solution/backend/src/lambdas/authentication-layer-handler/__tests__/apigw.test.js b/main/solution/backend/src/lambdas/authentication-layer-handler/__tests__/apigw.test.js
new file mode 100644
index 0000000000..626440a1cf
--- /dev/null
+++ b/main/solution/backend/src/lambdas/authentication-layer-handler/__tests__/apigw.test.js
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+const { parseMethodArn, stringifyMethodArn } = require('../apigw');
+
+describe('parseMethodArn', () => {
+ it('parses an API Gateway method arn into constituent parts', () => {
+ const arg = 'arn:aws:execute-api:us-east-1:123456789012:api-id/test/GET/mydemoresource/foo';
+ const got = parseMethodArn(arg);
+ expect(got).toEqual({
+ arnPrefix: 'arn:aws:execute-api:us-east-1:123456789012:api-id',
+ stageName: 'test',
+ httpMethod: 'GET',
+ path: '/mydemoresource/foo',
+ });
+ });
+
+ it('returns undefined if it receives an invalid method arn', () => {
+ const got = parseMethodArn('some invalid method arn');
+ expect(got).toBeUndefined();
+ });
+});
+
+describe('stringifyMethodArn', () => {
+ it('converts an object of method arn parts into a string', () => {
+ const arg = {
+ arnPrefix: 'arn:aws:execute-api:us-east-1:123456789012:api-id',
+ stageName: 'test',
+ httpMethod: 'GET',
+ path: '/mydemoresource/foo',
+ };
+ const got = stringifyMethodArn(arg);
+ expect(got).toEqual('arn:aws:execute-api:us-east-1:123456789012:api-id/test/GET/mydemoresource/foo');
+ });
+
+ it('replaces missing parts with asterisks', () => {
+ const arg = {
+ arnPrefix: 'arn:aws:execute-api:us-east-1:123456789012:api-id',
+ stageName: 'test',
+ path: '/mydemoresource/foo',
+ };
+ const got = stringifyMethodArn(arg);
+ expect(got).toEqual('arn:aws:execute-api:us-east-1:123456789012:api-id/test/*/mydemoresource/foo');
+ });
+});
diff --git a/main/solution/backend/src/lambdas/authentication-layer-handler/__tests__/handler-impl.test.js b/main/solution/backend/src/lambdas/authentication-layer-handler/__tests__/handler-impl.test.js
new file mode 100644
index 0000000000..275899ffd5
--- /dev/null
+++ b/main/solution/backend/src/lambdas/authentication-layer-handler/__tests__/handler-impl.test.js
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+
+const newHandler = require('../handler-impl');
+
+describe('handler', () => {
+ let handler;
+ let authenticationService;
+
+ beforeEach(async () => {
+ authenticationService = {
+ authenticate: jest.fn(() => Promise.resolve({})),
+ };
+ handler = newHandler({
+ authenticationService,
+ });
+ });
+
+ it('throws an internal error if the method arn is not a valid API gateway method arn', async () => {
+ return expect(
+ handler({
+ methodArn: 'some-invalid-method-arn',
+ }),
+ ).rejects.toThrow();
+ });
+
+ it('removes the bearer prefix from authorizationTokens containing a Bearer prefix', async () => {
+ authenticationService.authenticate = jest.fn(() => Promise.resolve({ authenticated: true }));
+ await handler({
+ methodArn: 'arn:aws:execute-api:us-east-1:123456789012:api-id/test/GET/mydemoresource/foo',
+ authorizationToken: 'Bearer foo',
+ });
+ expect(authenticationService.authenticate).toHaveBeenCalledWith('foo');
+ });
+
+ it('throws an unauthorized error if a token is not authenticated', async () => {
+ authenticationService.authenticate = jest.fn(() => Promise.resolve({ authenticated: false }));
+ return expect(
+ handler({
+ methodArn: 'arn:aws:execute-api:us-east-1:123456789012:api-id/test/GET/mydemoresource/foo',
+ authorizationToken: 'foo',
+ }),
+ ).rejects.toThrow('Unauthorized');
+ });
+
+ it('returns an allow policy with context variables if the token is authenticated', async () => {
+ authenticationService.authenticate = jest.fn(() =>
+ Promise.resolve({
+ authenticated: true,
+ username: 'me',
+ authenticationProviderId: 'my.provider.id',
+ identityProviderId: 'my.idp.com',
+ }),
+ );
+ const result = await handler({
+ methodArn: 'arn:aws:execute-api:us-east-1:123456789012:api-id/test/GET/mydemoresource/foo',
+ authorizationToken: 'foo',
+ });
+ expect(result).toEqual({
+ principalId: 'me',
+ policyDocument: {
+ Version: '2012-10-17',
+ Statement: [
+ {
+ Effect: 'Allow',
+ Action: 'execute-api:Invoke',
+ Resource: 'arn:aws:execute-api:us-east-1:123456789012:api-id/test/*/*',
+ },
+ ],
+ },
+ context: {
+ authenticationProviderId: 'my.provider.id',
+ identityProviderId: 'my.idp.com',
+ username: 'me',
+ },
+ });
+ });
+
+ it('removes invalid entries from the returned context object', async () => {
+ authenticationService.authenticate = jest.fn(() =>
+ Promise.resolve({
+ authenticated: true,
+ username: 'me',
+ authenticationProviderId: 'my.provider.id',
+ identityProviderId: 'my.idp.com',
+ some: { invalid: { context: 'object' } },
+ }),
+ );
+ const result = await handler({
+ methodArn: 'arn:aws:execute-api:us-east-1:123456789012:api-id/test/GET/mydemoresource/foo',
+ authorizationToken: 'foo',
+ });
+ expect(result).toEqual({
+ principalId: 'me',
+ policyDocument: {
+ Version: '2012-10-17',
+ Statement: [
+ {
+ Effect: 'Allow',
+ Action: 'execute-api:Invoke',
+ Resource: 'arn:aws:execute-api:us-east-1:123456789012:api-id/test/*/*',
+ },
+ ],
+ },
+ context: {
+ authenticationProviderId: 'my.provider.id',
+ identityProviderId: 'my.idp.com',
+ username: 'me',
+ },
+ });
+ });
+});
diff --git a/main/solution/backend/src/lambdas/authentication-layer-handler/apigw.js b/main/solution/backend/src/lambdas/authentication-layer-handler/apigw.js
new file mode 100644
index 0000000000..91bc3c80d6
--- /dev/null
+++ b/main/solution/backend/src/lambdas/authentication-layer-handler/apigw.js
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+// AWS ARN consists of 'arn', partition, service, region, accountID, resource (separated by ':').
+// API Gateway method arn consists of AWS ARN as above, but resource futher split into stage, method, and path (separated by '/').
+const methodArnRegexp = /^(arn:[\w-]+:[\w-]+:[\w-]+:[\w-]+:[\w-]+)\/([^/]+)\/([^/]+)(.+)$/;
+
+const parseMethodArn = s => {
+ const matched = methodArnRegexp.exec(s);
+ if (!matched) {
+ return undefined;
+ }
+ const [_orig, arnPrefix, stageName, httpMethod, path] = matched;
+ return {
+ arnPrefix,
+ stageName,
+ httpMethod,
+ path,
+ };
+};
+
+const stringifyMethodArn = ({ arnPrefix, stageName = '*', httpMethod = '*', path = '/*' }) =>
+ `${arnPrefix}/${stageName}/${httpMethod}${path}`;
+
+const buildRestApiPolicy = ({ arnPrefix, stageName }, Effect = 'Deny') => ({
+ Version: '2012-10-17',
+ Statement: [
+ {
+ Effect,
+ Action: 'execute-api:Invoke',
+ Resource: stringifyMethodArn({
+ arnPrefix,
+ stageName,
+ httpMethod: '*',
+ path: '/*',
+ }),
+ },
+ ],
+});
+
+const newUnauthorizedError = () => new Error('Unauthorized');
+
+const customAuthorizerResponse = ({ principalId, policyDocument, context = {} }) => ({
+ principalId,
+ policyDocument,
+ context,
+});
+
+module.exports = {
+ stringifyMethodArn,
+ parseMethodArn,
+ buildRestApiPolicy,
+ newUnauthorizedError,
+ customAuthorizerResponse,
+};
diff --git a/main/solution/backend/src/lambdas/authentication-layer-handler/handler-impl.js b/main/solution/backend/src/lambdas/authentication-layer-handler/handler-impl.js
new file mode 100644
index 0000000000..4daadb1bcf
--- /dev/null
+++ b/main/solution/backend/src/lambdas/authentication-layer-handler/handler-impl.js
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+const { parseMethodArn, buildRestApiPolicy, newUnauthorizedError, customAuthorizerResponse } = require('./apigw');
+
+const bearerPrefix = 'Bearer ';
+
+const getToken = authorizationHeader => {
+ if (!authorizationHeader) {
+ return '';
+ }
+ if (authorizationHeader.startsWith(bearerPrefix)) {
+ return authorizationHeader.slice(bearerPrefix.length);
+ }
+ return authorizationHeader;
+};
+
+const sanitizeResponseContext = context => {
+ return Object.entries(context)
+ .filter(([_, value]) => typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number')
+ .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
+};
+
+const noopAuthenticationService = {
+ async authenticate() {
+ return { authenticated: false };
+ },
+};
+
+const consoleLogger = {
+ info(...args) {
+ // eslint-disable-next-line no-console
+ console.log(...args);
+ },
+};
+
+module.exports = function newHandler({ authenticationService = noopAuthenticationService, log = consoleLogger } = {}) {
+ return async ({ methodArn: rawMethodArn, authorizationToken }) => {
+ const methodArn = parseMethodArn(rawMethodArn);
+ if (!methodArn) {
+ throw new Error(`invalid method arn: ${rawMethodArn}`);
+ }
+ const token = getToken(authorizationToken);
+ const result = await authenticationService.authenticate(token);
+ const { authenticated, error, ...claims } = result;
+ if (error) {
+ log.info(
+ `authentication error for ${claims.username || ''}/${claims.authenticationProviderId ||
+ ''}: ${error.toString()}`,
+ );
+ }
+ if (!authenticated) {
+ throw newUnauthorizedError();
+ }
+ return customAuthorizerResponse({
+ principalId: claims.username,
+ policyDocument: buildRestApiPolicy(methodArn, 'Allow'),
+ context: sanitizeResponseContext(claims),
+ });
+ };
+};
diff --git a/main/solution/backend/src/lambdas/authentication-layer-handler/handler.js b/main/solution/backend/src/lambdas/authentication-layer-handler/handler.js
new file mode 100644
index 0000000000..2500eeebfd
--- /dev/null
+++ b/main/solution/backend/src/lambdas/authentication-layer-handler/handler.js
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+const ServicesContainer = require('@aws-ee/base-services-container/lib/services-container');
+const { registerServices } = require('@aws-ee/base-services/lib/utils/services-registration-util');
+
+const newHandler = require('./handler-impl');
+const pluginRegistry = require('./plugins/plugin-registry');
+
+const initHandler = (async () => {
+ const container = new ServicesContainer(['settings', 'log']);
+ // registerServices - Registers services by calling each service registration plugin in order.
+ await registerServices(container, pluginRegistry);
+ await container.initServices();
+ const authenticationService = await container.find('authenticationService');
+ const log = await container.find('log');
+ return newHandler({ authenticationService, log });
+})();
+
+// eslint-disable-next-line import/prefer-default-export
+module.exports.handler = async (...args) => {
+ return (await initHandler)(...args);
+};
diff --git a/main/solution/backend/src/lambdas/authentication-layer-handler/plugins/plugin-registry.js b/main/solution/backend/src/lambdas/authentication-layer-handler/plugins/plugin-registry.js
new file mode 100644
index 0000000000..764f805469
--- /dev/null
+++ b/main/solution/backend/src/lambdas/authentication-layer-handler/plugins/plugin-registry.js
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+const baseAuditPlugin = require('@aws-ee/base-services/lib/plugins/audit-plugin');
+const baseServicesPlugin = require('@aws-ee/base-authn-handler/lib/plugins/services-plugin');
+const bassRaasServicesPlugin = require('@aws-ee/base-raas-rest-api/lib/plugins/authn-handler-services-plugin');
+const baseRaasUserAuthzPlugin = require('@aws-ee/base-raas-services/lib/user/user-authz-plugin');
+const baseRaasAuthnPlugin = require('@aws-ee/base-raas-rest-api/lib/plugins/authentication-plugin');
+
+const servicesPlugin = require('./services-plugin');
+
+const extensionPoints = {
+ 'service': [baseServicesPlugin, bassRaasServicesPlugin, servicesPlugin],
+ 'audit': [baseAuditPlugin],
+ 'user-authz': [baseRaasUserAuthzPlugin],
+ 'authentication': [baseRaasAuthnPlugin],
+};
+
+async function getPlugins(extensionPoint) {
+ return extensionPoints[extensionPoint];
+}
+
+const registry = {
+ getPlugins,
+};
+
+module.exports = registry;
diff --git a/main/solution/backend/src/lambdas/authentication-layer-handler/plugins/services-plugin.js b/main/solution/backend/src/lambdas/authentication-layer-handler/plugins/services-plugin.js
new file mode 100644
index 0000000000..e8fb76b4d1
--- /dev/null
+++ b/main/solution/backend/src/lambdas/authentication-layer-handler/plugins/services-plugin.js
@@ -0,0 +1,219 @@
+/*
+ * 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.
+ */
+
+/**
+ * Function to register solution specific services to the services container
+ * @param container An instance of ServicesContainer to register services to
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerServices(container, pluginRegistry) {
+ // This is where you can register your services
+ // Example:
+ // container.register('service1',new Service1());
+ // container.register('service2',new Service2());
+ // TODO: Register additional services as per your solution requirements
+}
+
+/**
+ * Function to register solution specific static settings. "static settings" is a plain JavaScript object containing
+ * settings as key/value. In Lambda environment, the settings are provided by environment variables.
+ * There is 4K limit to the env variables that can be passed to a Lambda. The default settings service impl provided by the
+ * "@aws-ee/base-services" package reads settings from env variables.
+ * In addition to those, any other settings that be derived via convention should be passed as "static settings" to
+ * avoid occupying space in env variables space.
+ *
+ * @param existingStaticSettings An existing static settings plain javascript object containing settings as key/value contributed by other plugins
+ * @param settingsService Default instance of settings service that resolves settings from environment variables
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to static settings object
+ */
+// eslint-disable-next-line no-unused-vars
+function getStaticSettings(existingStaticSettings, settingsService, pluginRegistry) {
+ // This is where you can
+ // 1. register your static settings, to register your static settings
+ //
+ // const staticSettings = {
+ // ...existingStaticSettings,
+ // // add other static settings here as follows
+ // 'staticSetting1':'static-setting-1-value',
+ // 'staticSetting2':'static-setting-2-value',
+ // }
+ // return staticSettings;
+ //
+ // 2. modify any static settings
+ // existingStaticSettings['the-existing-static-setting-you-want-to-replace'] = 'new-value';
+ // return existingStaticSettings;
+ //
+ // 3. delete any existing static setting, to delete existing static setting
+ //
+ // existingStaticSettings.delete('the-existing-static-setting-you-want-to-delete');
+ //
+
+ // TODO: Register additional static settings as per your solution requirements here
+ const staticSettings = {
+ ...existingStaticSettings,
+ };
+ // DO NOT forget to return staticSettings here. If you do not return here no static settings will be configured
+ return staticSettings;
+}
+
+/**
+ * Function to register solution specific logging context. "logging context" is a plain JavaScript object containing
+ * key/values. These key/values are automatically added to logs by the loggingService.
+ * These additional context items can be useful for debugging and monitoring especially when aggregating logs from
+ * multiple lambdas, across multiple environments into a single dashboard or log analytics environment such as ELK stack.
+ *
+ * @param existingLoggingContext An existing logging context plain javascript object containing logging context items as key/value(s)
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to logging context object
+ */
+// eslint-disable-next-line no-unused-vars
+async function getLoggingContext(existingLoggingContext, pluginRegistry) {
+ // This is where you can
+ // 1. register your logging context items, to register your logging context items
+ //
+ // const loggingContext = {
+ // ...existingLoggingContext,
+ //
+ // // add other items here as follows, for example,
+ // 'someKey':'someValue',
+ // }
+ // return loggingContext;
+ //
+ // 2. modify any logging context items
+ // existingLoggingContext['the-existing-logging-context-item-you-want-to-replace'] = 'new-value';
+ // return existingLoggingContext;
+ //
+ // 3. delete any existing logging context, to delete existing logging context item
+ //
+ // existingLoggingContext.delete('the-existing-logging-context-item-you-want-to-delete');
+ //
+
+ // TODO: Register additional logging context items as per your solution requirements here
+ const loggingContext = {
+ ...existingLoggingContext,
+ };
+
+ // DO NOT forget to return loggingContext here. If you do not return here no logging context will be configured
+ return loggingContext;
+}
+
+/**
+ * Function to add solution specific fields for masking in logs.
+ * "fields to mask" is an array containing field names to mask in logs. The logingService will mask these fields as
+ * '****' in logs. The service will look for these fields in deeply nested objects too. Note that the masking only works
+ * when logging JavaScript objects.
+ *
+ * For example, let's say you want to mask "ssn" numbers from the logs so the fields to mask is ['ssn'].
+ *
+ * const objToLog = { key1: 'value1', ssn:'some-ssn'}
+ * this.log.info(objToLog); // The ssn will be masked here
+ *
+ * const objWithNestedSsn = { key1: 'value1', nested: { deepNested: {'ssn':'some-ssn-value'}}}
+ * this.log.info(objWithNestedSsn); // The ssn will be masked here as well
+ *
+ * but
+ *
+ * this.log.info(`value of ssn is ${someSssn}`); // The ssn will NOT be masked here
+ *
+ * These additional context items can be useful for debugging and monitoring especially when aggregating logs from
+ * multiple lambdas, across multiple environments into a single dashboard or log analytics environment such as ELK stack.
+ *
+ * @param existingFieldsToMask An existing array of field names to mask
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to an array of field names to mask when logging
+ */
+// eslint-disable-next-line no-unused-vars
+async function getFieldsToMask(existingFieldsToMask, pluginRegistry) {
+ // This is where you can
+ // 1. add your additional fields to mask here
+ //
+ // const fieldsToMask = [
+ // ...existingFieldsToMask,
+ //
+ // // add other fields to mask
+ // 'someField1',
+ // 'someField2'
+ // ]
+ // return fieldsToMask;
+ //
+ // 3. remove any existing field(s) from masking by returning an array without that field(s).
+ // This field will be removed from masking list (i.e., it will be logged as is)
+ //
+ // return _.filter(fieldsToMask,_.negate(fieldName => fieldName === fieldNameToNotMask));
+ //
+
+ // TODO: Register additional fieldsToMask as per your solution requirements here
+ const fieldsToMask = {
+ ...existingFieldsToMask,
+ };
+ // DO NOT forget to return fieldsToMask here. If you do not return here no fields will be masked
+ return fieldsToMask;
+}
+
+/**
+ * Function to register solution specific implementation for settings service. This is an optional function.
+ * By default, an implementation of settings service (i.e., "@aws-ee/base-services/lib/settings/env-settings-service")
+ * that resolves settings from environment variables is already registered in the "container"
+ *
+ * @param container Services container
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerSettingsService(container, pluginRegistry) {
+ // The container has default settings service already registered
+ // If you want to register your own settings service implementation then
+ // register it with the key "settings" as follows
+ //
+ // container.register('settings', yourSettingsServiceImpl);
+}
+
+/**
+ * Function to register solution specific implementation for logger service. This is an optional function.
+ * By default, an implementation of logger service (i.e., "@aws-ee/base-services/lib/logger/logger-service") is
+ * already registered in the "container"
+ *
+ * @param container Services container
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerLoggerService(container, pluginRegistry) {
+ // The container has default logger service already registered
+ // If you want to register your own logger service implementation then
+ // register it with the key "log" as follows
+ //
+ // container.register('log', yourLoggerServiceImpl);
+}
+
+const plugin = {
+ registerServices,
+ getStaticSettings,
+ getLoggingContext,
+ getFieldsToMaskInLog: getFieldsToMask,
+ registerSettingsService,
+ registerLoggerService,
+};
+
+module.exports = plugin;
diff --git a/main/solution/backend/src/lambdas/open-data-scrape-handler/handler-impl.js b/main/solution/backend/src/lambdas/open-data-scrape-handler/handler-impl.js
new file mode 100644
index 0000000000..3ef7794bb3
--- /dev/null
+++ b/main/solution/backend/src/lambdas/open-data-scrape-handler/handler-impl.js
@@ -0,0 +1,191 @@
+/*
+ * 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.
+ */
+
+//
+// 1. Get and parse yaml files from aws open data
+// 2. Filter for the desired tags
+// 3. Write to study-service
+//
+const { getSystemRequestContext } = require('@aws-ee/base-services/lib/helpers/system-context');
+
+const consoleLogger = {
+ info(...args) {
+ // eslint-disable-next-line no-console
+ console.log(...args);
+ },
+};
+
+const _ = require('lodash');
+let fetch = require('node-fetch');
+const yaml = require('js-yaml');
+
+const studyCategory = 'Open Data';
+
+// Webpack messes with the fetch function import and it breaks in lambda.
+if (typeof fetch !== 'function' && fetch.default && typeof fetch.default === 'function') {
+ fetch = fetch.default;
+}
+
+module.exports = function newHandler({ studyService, log = consoleLogger } = {}) {
+ const scrape = {
+ githubApiUrl: 'https://api.github.com',
+ rawGithubUrl: 'https://raw.githubusercontent.com',
+ owner: 'awslabs',
+ repository: 'open-data-registry',
+ ref: 'master',
+ subtree: 'datasets',
+ filterTags: ['genetic', 'genomic'],
+ };
+
+ function normalizeValue(value) {
+ if (_.isArray(value)) {
+ return value.map(normalizeValue);
+ }
+ if (_.isObject(value)) {
+ return normalizeKeys(value);
+ }
+ return value;
+ }
+
+ function normalizeKeys(obj) {
+ const normalized = Object.entries(obj).reduce((result, [key, value]) => {
+ // lowercase the first letter of the words in the key, unless the whole word is uppercase
+ // in which case lowercase the entire word
+ const normalizedKey = key
+ .split(' ')
+ .map(word => (/^[A-Z]*$/.test(word) ? word.toLowerCase() : `${word.slice(0, 1).toLowerCase()}${word.slice(1)}`))
+ .join(' ');
+ const normalizedValue = normalizeValue(value);
+ return { ...result, [normalizedKey]: normalizedValue };
+ }, {});
+
+ return normalized;
+ }
+
+ async function fetchDatasetFiles() {
+ const { githubApiUrl, rawGithubUrl, owner, repository, ref, subtree } = scrape;
+
+ log.info(`Fetching ${owner}/${repository}/${ref}/${subtree} file list`);
+ const refResponse = await fetch(`${githubApiUrl}/repos/${owner}/${repository}/git/refs/heads/${ref}`);
+
+ if (!refResponse.ok) {
+ throw new Error('Failed to fetch git refs');
+ }
+
+ const {
+ object: { url: commitUrl },
+ } = await refResponse.json();
+
+ const commitResponse = await fetch(commitUrl);
+
+ if (!commitResponse.ok) {
+ throw new Error('Failed to fetch git commit');
+ }
+
+ const {
+ tree: { url: treeUrl },
+ } = await commitResponse.json();
+
+ const baseTreeResponse = await fetch(treeUrl);
+
+ if (!baseTreeResponse.ok) {
+ throw new Error('Failed to fetch base git tree');
+ }
+
+ const { tree: baseTree } = await baseTreeResponse.json();
+
+ // The tree is an array of entries like:
+ // {
+ // path: 'datasets',
+ // mode: '040000',
+ // type: 'tree',
+ // sha: '82e29cc3cd11cdfedfbcfc756d132414f95dc8c2',
+ // url:
+ // 'https://api.github.com/repos/awslabs/open-data-registry/git/trees/82e29cc3cd11cdfedfbcfc756d132414f95dc8c2'
+ // }
+ // Find and list the datasets tree, fetching all content
+ const datasetsDir = baseTree.find(({ path: p }) => p === subtree);
+
+ if (!(datasetsDir && datasetsDir.url)) {
+ throw new Error('Failed to find the datasets directory');
+ }
+
+ const datasetsTreeResponse = await fetch(datasetsDir.url);
+
+ if (!datasetsTreeResponse.ok) {
+ throw new Error('Failed to fetch datasets git tree');
+ }
+
+ const { tree } = await datasetsTreeResponse.json();
+
+ const blobs = tree.filter(({ type }) => type === 'blob');
+
+ const rawContentUrls = blobs.map(({ path, sha }) => ({
+ id: path.replace('.yaml', ''),
+ sha,
+ url: `${rawGithubUrl}/${owner}/${repository}/${ref}/${subtree}/${path}`,
+ }));
+
+ return rawContentUrls;
+ }
+
+ async function fetchFile({ url, id, sha }) {
+ const res = await fetch(url);
+
+ if (!res.ok) {
+ throw new Error(`Failed to fetch ${url}`);
+ }
+
+ const text = await res.text();
+
+ const doc = yaml.safeLoad(text, { filename: url });
+
+ return normalizeKeys({ ...doc, id, sha });
+ }
+
+ async function fetchOpenData({ fileUrls, requiredTags }) {
+ log.info(`Fetching ${fileUrls.length} metadata files`);
+ const metadata = await Promise.all(fileUrls.map(fetchFile));
+
+ log.info(`Filtering for ${requiredTags} tags`);
+ const filtered = metadata.filter(({ tags }) => requiredTags.some(filterTag => tags.includes(filterTag)));
+
+ return filtered;
+ }
+
+ function basicProjection({ id, sha, name, description, resources }) {
+ return {
+ id,
+ name,
+ description,
+ category: studyCategory,
+ sha,
+ resources: resources.map(({ arn }) => ({ arn })),
+ };
+ }
+
+ return async () => {
+ const fileUrls = await fetchDatasetFiles();
+ const opendata = await fetchOpenData({ fileUrls, requiredTags: scrape.filterTags });
+
+ const simplified = opendata.map(basicProjection);
+
+ log.info('Updating studies');
+ // TODO: create or update existing record
+ await Promise.all(simplified.map(study => studyService.create(getSystemRequestContext(), study)));
+
+ return simplified;
+ };
+};
diff --git a/main/solution/backend/src/lambdas/open-data-scrape-handler/handler.js b/main/solution/backend/src/lambdas/open-data-scrape-handler/handler.js
new file mode 100644
index 0000000000..27760f898b
--- /dev/null
+++ b/main/solution/backend/src/lambdas/open-data-scrape-handler/handler.js
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+const ServicesContainer = require('@aws-ee/base-services-container/lib/services-container');
+const { registerServices } = require('@aws-ee/base-services/lib/utils/services-registration-util');
+
+const newHandler = require('./handler-impl');
+const pluginRegistry = require('./plugins/plugin-registry');
+
+const initHandler = (async () => {
+ const container = new ServicesContainer(['settings', 'log']);
+ // registerServices - Registers services by calling each service registration plugin in order.
+ await registerServices(container, pluginRegistry);
+ await container.initServices();
+ const studyService = await container.find('studyService');
+ const log = await container.find('log');
+ return newHandler({ studyService, log });
+})();
+
+// eslint-disable-next-line import/prefer-default-export
+module.exports.handler = async (...args) => {
+ return (await initHandler)(...args);
+};
diff --git a/main/solution/backend/src/lambdas/open-data-scrape-handler/plugins/plugin-registry.js b/main/solution/backend/src/lambdas/open-data-scrape-handler/plugins/plugin-registry.js
new file mode 100644
index 0000000000..0a6d1bbedf
--- /dev/null
+++ b/main/solution/backend/src/lambdas/open-data-scrape-handler/plugins/plugin-registry.js
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+const baseAuditPlugin = require('@aws-ee/base-services/lib/plugins/audit-plugin');
+const baseServicesPlugin = require('@aws-ee/base-api-handler/lib/plugins/services-plugin');
+const bassRaasServicesPlugin = require('@aws-ee/base-raas-rest-api/lib/plugins/services-plugin');
+const baseWfServicesPlugin = require('@aws-ee/base-workflow-api/lib/plugins/services-plugin');
+const baseRaasCfnTemplatesPlugin = require('@aws-ee/base-raas-cfn-templates/dist/plugins/cfn-templates-plugin');
+const baseRaasUserAuthzPlugin = require('@aws-ee/base-raas-services/lib/user/user-authz-plugin');
+
+const servicesPlugin = require('./services-plugin');
+
+const extensionPoints = {
+ 'service': [baseServicesPlugin, baseWfServicesPlugin, bassRaasServicesPlugin, servicesPlugin],
+ 'audit': [baseAuditPlugin],
+ 'cfn-templates': [baseRaasCfnTemplatesPlugin],
+ 'user-authz': [baseRaasUserAuthzPlugin],
+ 'user-role-management-authz': [], // No plugins at this point. All user-role-management authz is happening inline in 'user-roles-service'
+ 'environment-authz': [], // No plugins at this point. All environment authz is happening inline in 'environment-service' using the 'environment-authz-service'
+ 'project-authz': [], // No plugins at this point. All project authz is happening inline in 'project-service'
+ 'index-authz': [], // No plugins at this point. All index authz is happening inline in 'index-service'
+ 'account-authz': [], // No plugins at this point. All account authz is happening inline in 'account-service'
+ 'aws-account-authz': [], // No plugins at this point. All aws-account authz is happening inline in 'aws-account-service'
+ 'cost-authz': [], // No plugins at this point. All cost authz is happening inline in 'costs-service'
+};
+
+async function getPlugins(extensionPoint) {
+ return extensionPoints[extensionPoint];
+}
+
+const registry = {
+ getPlugins,
+};
+
+module.exports = registry;
diff --git a/main/solution/backend/src/lambdas/open-data-scrape-handler/plugins/services-plugin.js b/main/solution/backend/src/lambdas/open-data-scrape-handler/plugins/services-plugin.js
new file mode 100644
index 0000000000..e8fb76b4d1
--- /dev/null
+++ b/main/solution/backend/src/lambdas/open-data-scrape-handler/plugins/services-plugin.js
@@ -0,0 +1,219 @@
+/*
+ * 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.
+ */
+
+/**
+ * Function to register solution specific services to the services container
+ * @param container An instance of ServicesContainer to register services to
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerServices(container, pluginRegistry) {
+ // This is where you can register your services
+ // Example:
+ // container.register('service1',new Service1());
+ // container.register('service2',new Service2());
+ // TODO: Register additional services as per your solution requirements
+}
+
+/**
+ * Function to register solution specific static settings. "static settings" is a plain JavaScript object containing
+ * settings as key/value. In Lambda environment, the settings are provided by environment variables.
+ * There is 4K limit to the env variables that can be passed to a Lambda. The default settings service impl provided by the
+ * "@aws-ee/base-services" package reads settings from env variables.
+ * In addition to those, any other settings that be derived via convention should be passed as "static settings" to
+ * avoid occupying space in env variables space.
+ *
+ * @param existingStaticSettings An existing static settings plain javascript object containing settings as key/value contributed by other plugins
+ * @param settingsService Default instance of settings service that resolves settings from environment variables
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to static settings object
+ */
+// eslint-disable-next-line no-unused-vars
+function getStaticSettings(existingStaticSettings, settingsService, pluginRegistry) {
+ // This is where you can
+ // 1. register your static settings, to register your static settings
+ //
+ // const staticSettings = {
+ // ...existingStaticSettings,
+ // // add other static settings here as follows
+ // 'staticSetting1':'static-setting-1-value',
+ // 'staticSetting2':'static-setting-2-value',
+ // }
+ // return staticSettings;
+ //
+ // 2. modify any static settings
+ // existingStaticSettings['the-existing-static-setting-you-want-to-replace'] = 'new-value';
+ // return existingStaticSettings;
+ //
+ // 3. delete any existing static setting, to delete existing static setting
+ //
+ // existingStaticSettings.delete('the-existing-static-setting-you-want-to-delete');
+ //
+
+ // TODO: Register additional static settings as per your solution requirements here
+ const staticSettings = {
+ ...existingStaticSettings,
+ };
+ // DO NOT forget to return staticSettings here. If you do not return here no static settings will be configured
+ return staticSettings;
+}
+
+/**
+ * Function to register solution specific logging context. "logging context" is a plain JavaScript object containing
+ * key/values. These key/values are automatically added to logs by the loggingService.
+ * These additional context items can be useful for debugging and monitoring especially when aggregating logs from
+ * multiple lambdas, across multiple environments into a single dashboard or log analytics environment such as ELK stack.
+ *
+ * @param existingLoggingContext An existing logging context plain javascript object containing logging context items as key/value(s)
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to logging context object
+ */
+// eslint-disable-next-line no-unused-vars
+async function getLoggingContext(existingLoggingContext, pluginRegistry) {
+ // This is where you can
+ // 1. register your logging context items, to register your logging context items
+ //
+ // const loggingContext = {
+ // ...existingLoggingContext,
+ //
+ // // add other items here as follows, for example,
+ // 'someKey':'someValue',
+ // }
+ // return loggingContext;
+ //
+ // 2. modify any logging context items
+ // existingLoggingContext['the-existing-logging-context-item-you-want-to-replace'] = 'new-value';
+ // return existingLoggingContext;
+ //
+ // 3. delete any existing logging context, to delete existing logging context item
+ //
+ // existingLoggingContext.delete('the-existing-logging-context-item-you-want-to-delete');
+ //
+
+ // TODO: Register additional logging context items as per your solution requirements here
+ const loggingContext = {
+ ...existingLoggingContext,
+ };
+
+ // DO NOT forget to return loggingContext here. If you do not return here no logging context will be configured
+ return loggingContext;
+}
+
+/**
+ * Function to add solution specific fields for masking in logs.
+ * "fields to mask" is an array containing field names to mask in logs. The logingService will mask these fields as
+ * '****' in logs. The service will look for these fields in deeply nested objects too. Note that the masking only works
+ * when logging JavaScript objects.
+ *
+ * For example, let's say you want to mask "ssn" numbers from the logs so the fields to mask is ['ssn'].
+ *
+ * const objToLog = { key1: 'value1', ssn:'some-ssn'}
+ * this.log.info(objToLog); // The ssn will be masked here
+ *
+ * const objWithNestedSsn = { key1: 'value1', nested: { deepNested: {'ssn':'some-ssn-value'}}}
+ * this.log.info(objWithNestedSsn); // The ssn will be masked here as well
+ *
+ * but
+ *
+ * this.log.info(`value of ssn is ${someSssn}`); // The ssn will NOT be masked here
+ *
+ * These additional context items can be useful for debugging and monitoring especially when aggregating logs from
+ * multiple lambdas, across multiple environments into a single dashboard or log analytics environment such as ELK stack.
+ *
+ * @param existingFieldsToMask An existing array of field names to mask
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to an array of field names to mask when logging
+ */
+// eslint-disable-next-line no-unused-vars
+async function getFieldsToMask(existingFieldsToMask, pluginRegistry) {
+ // This is where you can
+ // 1. add your additional fields to mask here
+ //
+ // const fieldsToMask = [
+ // ...existingFieldsToMask,
+ //
+ // // add other fields to mask
+ // 'someField1',
+ // 'someField2'
+ // ]
+ // return fieldsToMask;
+ //
+ // 3. remove any existing field(s) from masking by returning an array without that field(s).
+ // This field will be removed from masking list (i.e., it will be logged as is)
+ //
+ // return _.filter(fieldsToMask,_.negate(fieldName => fieldName === fieldNameToNotMask));
+ //
+
+ // TODO: Register additional fieldsToMask as per your solution requirements here
+ const fieldsToMask = {
+ ...existingFieldsToMask,
+ };
+ // DO NOT forget to return fieldsToMask here. If you do not return here no fields will be masked
+ return fieldsToMask;
+}
+
+/**
+ * Function to register solution specific implementation for settings service. This is an optional function.
+ * By default, an implementation of settings service (i.e., "@aws-ee/base-services/lib/settings/env-settings-service")
+ * that resolves settings from environment variables is already registered in the "container"
+ *
+ * @param container Services container
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerSettingsService(container, pluginRegistry) {
+ // The container has default settings service already registered
+ // If you want to register your own settings service implementation then
+ // register it with the key "settings" as follows
+ //
+ // container.register('settings', yourSettingsServiceImpl);
+}
+
+/**
+ * Function to register solution specific implementation for logger service. This is an optional function.
+ * By default, an implementation of logger service (i.e., "@aws-ee/base-services/lib/logger/logger-service") is
+ * already registered in the "container"
+ *
+ * @param container Services container
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerLoggerService(container, pluginRegistry) {
+ // The container has default logger service already registered
+ // If you want to register your own logger service implementation then
+ // register it with the key "log" as follows
+ //
+ // container.register('log', yourLoggerServiceImpl);
+}
+
+const plugin = {
+ registerServices,
+ getStaticSettings,
+ getLoggingContext,
+ getFieldsToMaskInLog: getFieldsToMask,
+ registerSettingsService,
+ registerLoggerService,
+};
+
+module.exports = plugin;
diff --git a/main/solution/backend/src/lambdas/workflow-loop-runner/handler.js b/main/solution/backend/src/lambdas/workflow-loop-runner/handler.js
new file mode 100644
index 0000000000..1b241207ed
--- /dev/null
+++ b/main/solution/backend/src/lambdas/workflow-loop-runner/handler.js
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+const {
+ registerServices: registerServicesUtil,
+} = require('@aws-ee/base-services/lib/utils/services-registration-util');
+const handlerFactory = require('@aws-ee/base-workflow-core/lib/runner/handler');
+
+const pluginRegistry = require('./plugins/plugin-registry');
+
+/**
+ * Registers services by calling each service registration plugin in order.
+ *
+ * @param container An instance of ServicesContainer
+ * @returns {Promise}
+ */
+async function registerServices(container) {
+ return registerServicesUtil(container, pluginRegistry);
+}
+
+const handler = handlerFactory({ registerServices });
+
+module.exports.handler = handler;
diff --git a/main/solution/backend/src/lambdas/workflow-loop-runner/plugins/plugin-registry.js b/main/solution/backend/src/lambdas/workflow-loop-runner/plugins/plugin-registry.js
new file mode 100644
index 0000000000..e0415fa61e
--- /dev/null
+++ b/main/solution/backend/src/lambdas/workflow-loop-runner/plugins/plugin-registry.js
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+const baseAuditPlugin = require('@aws-ee/base-services/lib/plugins/audit-plugin');
+const baseWfServicesPlugin = require('@aws-ee/base-workflow-core/lib/runner/plugins/services-plugin');
+const baseWfStepsPlugin = require('@aws-ee/base-workflow-steps/steps/workflow-steps-plugin');
+const bassRaasServicesPlugin = require('@aws-ee/base-raas-rest-api/lib/plugins/services-plugin');
+const baseRaasCfnTemplatesPlugin = require('@aws-ee/base-raas-cfn-templates/dist/plugins/cfn-templates-plugin');
+const baseRaasWfStepsPlugin = require('@aws-ee/base-raas-workflow-steps/lib/plugins/workflow-steps-plugin');
+const baseRaasWorkflowsPlugin = require('@aws-ee/base-raas-workflows/lib/plugins/workflows-plugin');
+const baseRaasUserAuthzPlugin = require('@aws-ee/base-raas-services/lib/user/user-authz-plugin');
+
+const servicesPlugin = require('./services-plugin');
+
+const extensionPoints = {
+ 'service': [baseWfServicesPlugin, bassRaasServicesPlugin, servicesPlugin],
+ 'audit': [baseAuditPlugin],
+ 'workflow-steps': [baseWfStepsPlugin, baseRaasWfStepsPlugin],
+ 'workflow-templates': [],
+ 'workflows': [baseRaasWorkflowsPlugin],
+ 'workflow-assignments': [],
+ 'cfn-templates': [baseRaasCfnTemplatesPlugin],
+ 'user-authz': [baseRaasUserAuthzPlugin],
+ 'user-role-management-authz': [], // No plugins at this point. All user-role-management authz is happening inline in 'user-roles-service'
+ 'environment-authz': [], // No plugins at this point. All environment authz is happening inline in 'environment-service' using the 'environment-authz-service'
+ 'project-authz': [], // No plugins at this point. All project authz is happening inline in 'project-service'
+ 'index-authz': [], // No plugins at this point. All index authz is happening inline in 'index-service'
+ 'account-authz': [], // No plugins at this point. All account authz is happening inline in 'account-service'
+ 'aws-account-authz': [], // No plugins at this point. All aws-account authz is happening inline in 'aws-account-service'
+ 'cost-authz': [], // No plugins at this point. All cost authz is happening inline in 'costs-service'
+};
+
+async function getPlugins(extensionPoint) {
+ return extensionPoints[extensionPoint];
+}
+
+const registry = {
+ getPlugins,
+};
+
+module.exports = registry;
diff --git a/main/solution/backend/src/lambdas/workflow-loop-runner/plugins/services-plugin.js b/main/solution/backend/src/lambdas/workflow-loop-runner/plugins/services-plugin.js
new file mode 100644
index 0000000000..e8fb76b4d1
--- /dev/null
+++ b/main/solution/backend/src/lambdas/workflow-loop-runner/plugins/services-plugin.js
@@ -0,0 +1,219 @@
+/*
+ * 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.
+ */
+
+/**
+ * Function to register solution specific services to the services container
+ * @param container An instance of ServicesContainer to register services to
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerServices(container, pluginRegistry) {
+ // This is where you can register your services
+ // Example:
+ // container.register('service1',new Service1());
+ // container.register('service2',new Service2());
+ // TODO: Register additional services as per your solution requirements
+}
+
+/**
+ * Function to register solution specific static settings. "static settings" is a plain JavaScript object containing
+ * settings as key/value. In Lambda environment, the settings are provided by environment variables.
+ * There is 4K limit to the env variables that can be passed to a Lambda. The default settings service impl provided by the
+ * "@aws-ee/base-services" package reads settings from env variables.
+ * In addition to those, any other settings that be derived via convention should be passed as "static settings" to
+ * avoid occupying space in env variables space.
+ *
+ * @param existingStaticSettings An existing static settings plain javascript object containing settings as key/value contributed by other plugins
+ * @param settingsService Default instance of settings service that resolves settings from environment variables
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to static settings object
+ */
+// eslint-disable-next-line no-unused-vars
+function getStaticSettings(existingStaticSettings, settingsService, pluginRegistry) {
+ // This is where you can
+ // 1. register your static settings, to register your static settings
+ //
+ // const staticSettings = {
+ // ...existingStaticSettings,
+ // // add other static settings here as follows
+ // 'staticSetting1':'static-setting-1-value',
+ // 'staticSetting2':'static-setting-2-value',
+ // }
+ // return staticSettings;
+ //
+ // 2. modify any static settings
+ // existingStaticSettings['the-existing-static-setting-you-want-to-replace'] = 'new-value';
+ // return existingStaticSettings;
+ //
+ // 3. delete any existing static setting, to delete existing static setting
+ //
+ // existingStaticSettings.delete('the-existing-static-setting-you-want-to-delete');
+ //
+
+ // TODO: Register additional static settings as per your solution requirements here
+ const staticSettings = {
+ ...existingStaticSettings,
+ };
+ // DO NOT forget to return staticSettings here. If you do not return here no static settings will be configured
+ return staticSettings;
+}
+
+/**
+ * Function to register solution specific logging context. "logging context" is a plain JavaScript object containing
+ * key/values. These key/values are automatically added to logs by the loggingService.
+ * These additional context items can be useful for debugging and monitoring especially when aggregating logs from
+ * multiple lambdas, across multiple environments into a single dashboard or log analytics environment such as ELK stack.
+ *
+ * @param existingLoggingContext An existing logging context plain javascript object containing logging context items as key/value(s)
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to logging context object
+ */
+// eslint-disable-next-line no-unused-vars
+async function getLoggingContext(existingLoggingContext, pluginRegistry) {
+ // This is where you can
+ // 1. register your logging context items, to register your logging context items
+ //
+ // const loggingContext = {
+ // ...existingLoggingContext,
+ //
+ // // add other items here as follows, for example,
+ // 'someKey':'someValue',
+ // }
+ // return loggingContext;
+ //
+ // 2. modify any logging context items
+ // existingLoggingContext['the-existing-logging-context-item-you-want-to-replace'] = 'new-value';
+ // return existingLoggingContext;
+ //
+ // 3. delete any existing logging context, to delete existing logging context item
+ //
+ // existingLoggingContext.delete('the-existing-logging-context-item-you-want-to-delete');
+ //
+
+ // TODO: Register additional logging context items as per your solution requirements here
+ const loggingContext = {
+ ...existingLoggingContext,
+ };
+
+ // DO NOT forget to return loggingContext here. If you do not return here no logging context will be configured
+ return loggingContext;
+}
+
+/**
+ * Function to add solution specific fields for masking in logs.
+ * "fields to mask" is an array containing field names to mask in logs. The logingService will mask these fields as
+ * '****' in logs. The service will look for these fields in deeply nested objects too. Note that the masking only works
+ * when logging JavaScript objects.
+ *
+ * For example, let's say you want to mask "ssn" numbers from the logs so the fields to mask is ['ssn'].
+ *
+ * const objToLog = { key1: 'value1', ssn:'some-ssn'}
+ * this.log.info(objToLog); // The ssn will be masked here
+ *
+ * const objWithNestedSsn = { key1: 'value1', nested: { deepNested: {'ssn':'some-ssn-value'}}}
+ * this.log.info(objWithNestedSsn); // The ssn will be masked here as well
+ *
+ * but
+ *
+ * this.log.info(`value of ssn is ${someSssn}`); // The ssn will NOT be masked here
+ *
+ * These additional context items can be useful for debugging and monitoring especially when aggregating logs from
+ * multiple lambdas, across multiple environments into a single dashboard or log analytics environment such as ELK stack.
+ *
+ * @param existingFieldsToMask An existing array of field names to mask
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to an array of field names to mask when logging
+ */
+// eslint-disable-next-line no-unused-vars
+async function getFieldsToMask(existingFieldsToMask, pluginRegistry) {
+ // This is where you can
+ // 1. add your additional fields to mask here
+ //
+ // const fieldsToMask = [
+ // ...existingFieldsToMask,
+ //
+ // // add other fields to mask
+ // 'someField1',
+ // 'someField2'
+ // ]
+ // return fieldsToMask;
+ //
+ // 3. remove any existing field(s) from masking by returning an array without that field(s).
+ // This field will be removed from masking list (i.e., it will be logged as is)
+ //
+ // return _.filter(fieldsToMask,_.negate(fieldName => fieldName === fieldNameToNotMask));
+ //
+
+ // TODO: Register additional fieldsToMask as per your solution requirements here
+ const fieldsToMask = {
+ ...existingFieldsToMask,
+ };
+ // DO NOT forget to return fieldsToMask here. If you do not return here no fields will be masked
+ return fieldsToMask;
+}
+
+/**
+ * Function to register solution specific implementation for settings service. This is an optional function.
+ * By default, an implementation of settings service (i.e., "@aws-ee/base-services/lib/settings/env-settings-service")
+ * that resolves settings from environment variables is already registered in the "container"
+ *
+ * @param container Services container
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerSettingsService(container, pluginRegistry) {
+ // The container has default settings service already registered
+ // If you want to register your own settings service implementation then
+ // register it with the key "settings" as follows
+ //
+ // container.register('settings', yourSettingsServiceImpl);
+}
+
+/**
+ * Function to register solution specific implementation for logger service. This is an optional function.
+ * By default, an implementation of logger service (i.e., "@aws-ee/base-services/lib/logger/logger-service") is
+ * already registered in the "container"
+ *
+ * @param container Services container
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerLoggerService(container, pluginRegistry) {
+ // The container has default logger service already registered
+ // If you want to register your own logger service implementation then
+ // register it with the key "log" as follows
+ //
+ // container.register('log', yourLoggerServiceImpl);
+}
+
+const plugin = {
+ registerServices,
+ getStaticSettings,
+ getLoggingContext,
+ getFieldsToMaskInLog: getFieldsToMask,
+ registerSettingsService,
+ registerLoggerService,
+};
+
+module.exports = plugin;
diff --git a/main/solution/edge-lambda/.gitignore b/main/solution/edge-lambda/.gitignore
new file mode 100644
index 0000000000..3659bd3a89
--- /dev/null
+++ b/main/solution/edge-lambda/.gitignore
@@ -0,0 +1,15 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/main/solution/edge-lambda/README.md b/main/solution/edge-lambda/README.md
new file mode 100644
index 0000000000..a45cf00cd2
--- /dev/null
+++ b/main/solution/edge-lambda/README.md
@@ -0,0 +1,12 @@
+
+# Lambda@Edge
+This component deploys a Lambda@Edge function that intercepts website Amazon CloudFront `origin-response` and adds various
+security related HTTP headers in the response before serving the website content from S3.
+
+## Packaging and deploying
+
+To deploy:
+
+```bash
+$ pnpx sls deploy --stage
+```
diff --git a/main/solution/edge-lambda/config/infra/cloudformation.yml b/main/solution/edge-lambda/config/infra/cloudformation.yml
new file mode 100644
index 0000000000..24e1c967af
--- /dev/null
+++ b/main/solution/edge-lambda/config/infra/cloudformation.yml
@@ -0,0 +1,95 @@
+Resources:
+ # =============================================================================================
+ # IAM Role for CloudFront Interceptor Lambda (Lambda@Edge)
+ # =============================================================================================
+ RoleCloudFrontInterceptor:
+ Type: "AWS::IAM::Role"
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service:
+ - lambda.amazonaws.com
+ - edgelambda.amazonaws.com
+ Action: "sts:AssumeRole"
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/AWSLambdaExecute
+ - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
+ Policies:
+ - PolicyName: logs-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - logs:CreateLogGroup
+ - logs:CreateLogStream
+ - logs:PutLogEvents
+ Resource: "arn:aws:logs:*:*:*"
+
+ # =============================================================================================
+ # CloudFront Interceptor Lambda (Lambda@Edge)
+ # =============================================================================================
+
+ # Lambda@Edge that intercepts CloudFront response and adds various security headers in the response
+ EdgeLambda:
+ Type: "AWS::Lambda::Function"
+ Properties:
+ Description: Lambda@Edge function to set security headers in CloudFront responses
+ Runtime: nodejs12.x
+ Handler: index.handler
+ Role: !GetAtt RoleCloudFrontInterceptor.Arn
+ # Declaring Lambda Function Code inline because the code requires API Gateway URL for the backend APIs (to set "connect-src" part of the "content-security-policy" header)
+ # Lambda@Edge currently does not support passing environment variables (See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html)
+ # To get around this limitation, we are accessing API URL inline in the code below using "${self:custom.settings.apiUrl}"
+ Code:
+ ZipFile: |
+ "use strict";
+ const url = require("url");
+
+ const handler = async event => {
+ //Get contents of cloudfront response
+ const response = event.Records[0].cf.response;
+ const headers = response.headers;
+
+ //Set new headers
+ headers["strict-transport-security"] = [
+ {
+ key: "Strict-Transport-Security",
+ value: "max-age=63072000; includeSubdomains"
+ }
+ ];
+
+ const q = url.parse('${self:custom.settings.apiUrl}');
+ // q.host includes port number
+ const backendApi = `${q.protocol}//${q.host}`;
+ const otherConnectSrc = '${self:custom.settings.otherConnectSrc}';
+ const connectSrc = `${backendApi} ${otherConnectSrc}`;
+ headers["content-security-policy"] = [
+ {
+ key: "Content-Security-Policy",
+ value: `default-src 'none'; connect-src ${connectSrc}; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:`
+ }
+ ];
+
+ headers["x-content-type-options"] = [
+ { key: "X-Content-Type-Options", value: "nosniff" }
+ ];
+ headers["x-frame-options"] = [{ key: "X-Frame-Options", value: "DENY" }];
+ headers["x-xss-protection"] = [
+ { key: "X-XSS-Protection", value: "1; mode=block" }
+ ];
+ headers["referrer-policy"] = [
+ { key: "Referrer-Policy", value: "same-origin" }
+ ];
+ //Return modified response
+ return response;
+ };
+
+ module.exports.handler = handler;
+
+Outputs:
+ EdgeLambdaArn:
+ Description: The ARN of the Lambda@Edge function
+ Value: !GetAtt [EdgeLambda, Arn]
diff --git a/main/solution/edge-lambda/config/settings/.defaults.yml b/main/solution/edge-lambda/config/settings/.defaults.yml
new file mode 100644
index 0000000000..b35efa7c7f
--- /dev/null
+++ b/main/solution/edge-lambda/config/settings/.defaults.yml
@@ -0,0 +1,45 @@
+# The stack name of the 'backend' serverless service
+backendStackName: ${self:custom.settings.namespace}-backend
+
+# The Gateway API endpoint
+apiUrl: ${cf:${self:custom.settings.backendStackName}.ServiceEndpoint}
+
+# The lambda@edge that is deployed by this component intercepts Amazon CloudFront origin-response and adds various security
+# related headers including "content-security-policy" header. The "connect-src" in that header lists the hosts the
+# browser should allow communication to for AJAX requests. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src
+#
+# The lambda whitelists the backend api calls. In addition, it also needs to whitelist the following endpoints
+#
+# 1. Cloud Formation Endpoints
+# ------------------------------
+# This is required to support use-case of launching analytics env directly in external researchers account using CFN.
+#
+# The connect-src supports wildcards BUT only for sub-domain or port so wildcard for region name below does not work
+# (for example, https://cloudformation.*.amazonaws.com and https://cloudformation-fips.*.amazonaws.com do NOT work)
+#
+# This setting specifies the other connect-src values to be added to the "content-security-policy" header
+#
+# Currently, whitelisting all AWS service endpoints across all regions using wildcard as follows
+# This also takes care of whitelisting all S3 endpoints mentioned in #2 below
+# otherConnectSrc: *.amazonaws.com
+#
+# Alternatively you can whitelist all AWS CloudFormation service endpoints across all currently supported regions.
+# See https://docs.aws.amazon.com/general/latest/gr/cfn.html.
+#
+# 2. S3 Endpoints
+# ----------------
+# This is required to support uploading data files to studies.
+#
+# -------------------
+# This is required to auto-populate user's IP address when launching EC2 based workspace from the UI
+#
+#otherConnectSrc: cloudformation.us-east-2.amazonaws.com cloudformation-fips.us-east-2.amazonaws.com cloudformation.us-east-1.amazonaws.com cloudformation-fips.us-east-1.amazonaws.com cloudformation.us-west-1.amazonaws.com cloudformation-fips.us-west-1.amazonaws.com cloudformation.us-west-2.amazonaws.com cloudformation-fips.us-west-2.amazonaws.com cloudformation.ap-south-1.amazonaws.com cloudformation.ap-northeast-3.amazonaws.com cloudformation.ap-northeast-2.amazonaws.com cloudformation.ap-southeast-1.amazonaws.com cloudformation.ap-southeast-2.amazonaws.com cloudformation.ap-northeast-1.amazonaws.com cloudformation.ca-central-1.amazonaws.com cloudformation.cn-north-1.amazonaws.com.cn cloudformation.cn-northwest-1.amazonaws.com.cn cloudformation.eu-central-1.amazonaws.com cloudformation.eu-west-1.amazonaws.com cloudformation.eu-west-2.amazonaws.com cloudformation.eu-west-3.amazonaws.com cloudformation.sa-east-1.amazonaws.com cloudformation.sa-east-1.amazonaws.com cloudformation.us-gov-west-1.amazonaws.com
+otherConnectSrc: '*.amazonaws.com'
+
+# The region to deploy to
+# DO NOT change the region here. This needs to be "us-east-1" irrespective of the AWS region you are deploying the rest
+# of the solution to.
+# The Lambda@Edge needs to be deployed only in US East Virginia (us-east-1) region and
+# it gets replicated to CloudFront edge locations when we associate it with CloudFront.
+# See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html
+awsRegion: us-east-1
diff --git a/main/solution/edge-lambda/config/settings/.settings.js b/main/solution/edge-lambda/config/settings/.settings.js
new file mode 100644
index 0000000000..a797b2215e
--- /dev/null
+++ b/main/solution/edge-lambda/config/settings/.settings.js
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+module.exports.merged = require('@aws-ee/base-serverless-settings-helper').mergeSettings(
+ __dirname,
+ [
+ '../../../../config/settings/.defaults.yml',
+ './.defaults.yml',
+ '../../../../config/settings/${stage}.yml',
+ './${stage}.yml',
+ ],
+ {
+ crossRegionCloudFormation: {
+ backendStackName: [
+ {
+ settingName: 'apiUrl',
+ outputKey: 'ServiceEndpoint',
+ },
+ ],
+ },
+ },
+);
diff --git a/main/solution/edge-lambda/jsconfig.json b/main/solution/edge-lambda/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/main/solution/edge-lambda/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/main/solution/edge-lambda/package.json b/main/solution/edge-lambda/package.json
new file mode 100644
index 0000000000..4deae75602
--- /dev/null
+++ b/main/solution/edge-lambda/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@aws-ee/edge-lambda",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Package for Lambda@Edge that intercepts CloudFront response and adds various security headers in the response",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "@aws-ee/base-serverless-settings-helper": "workspace:*",
+ "serverless": "^1.63.0",
+ "serverless-deployment-bucket": "^1.1.0"
+ }
+}
diff --git a/main/solution/edge-lambda/serverless.yml b/main/solution/edge-lambda/serverless.yml
new file mode 100644
index 0000000000..062687ba05
--- /dev/null
+++ b/main/solution/edge-lambda/serverless.yml
@@ -0,0 +1,30 @@
+# For full config options, see docs.serverless.com
+# Note that most settings in here come from config/settings/*.yaml
+service: ${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}-edgeLambda
+
+provider:
+ name: aws
+ region: ${self:custom.settings.awsRegion}
+ profile: ${self:custom.settings.awsProfile}
+ stackName: ${self:custom.settings.envName}-${self:service}
+ deploymentBucket:
+ name: ${self:custom.settings.deploymentBucketName}
+ stackTags: ${self:custom.tags}
+ versionFunctions: false
+ # All references beginning with ${self:*, ${opt:*, ${file:*, ${deep:*, and ${cf:* will be resolved by Serverless
+ # All other ${* references will be resolved by CloudFormation
+ # See https://forum.serverless.com/t/getting-handle-accountid-in-serverless-config/946/11 and
+ # See https://github.com/serverless/serverless/issues/5011
+ variableSyntax: '\$\{((((self|opt|deep|cf):)|file)((?!\$\{).)+?)}'
+
+custom:
+ settings: ${file(./config/settings/.settings.js):merged}
+ tags:
+ Name: ${self:custom.settings.envName}-${self:service}
+
+resources:
+ - Description: Galileo-Gateway ${self:custom.settings.version} ${self:custom.settings.solutionName} ${self:custom.settings.envName} Edge-Lambda
+ - ${file(./config/infra/cloudformation.yml)}
+
+plugins:
+ - serverless-deployment-bucket
diff --git a/main/solution/infrastructure/.gitignore b/main/solution/infrastructure/.gitignore
new file mode 100644
index 0000000000..ed7c568396
--- /dev/null
+++ b/main/solution/infrastructure/.gitignore
@@ -0,0 +1,19 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# Webpack generated directories
+.webpack
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/main/solution/infrastructure/.prettierrc.json b/main/solution/infrastructure/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/main/solution/infrastructure/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/main/solution/infrastructure/README.md b/main/solution/infrastructure/README.md
new file mode 100644
index 0000000000..96416c55ca
--- /dev/null
+++ b/main/solution/infrastructure/README.md
@@ -0,0 +1,9 @@
+# Infrastructure
+
+## Packaging and deploying
+
+To deploy:
+
+```bash
+$ pnpx sls deploy --stage
+```
diff --git a/main/solution/infrastructure/config/infra/cloudformation.yml b/main/solution/infrastructure/config/infra/cloudformation.yml
new file mode 100644
index 0000000000..31f0c980bf
--- /dev/null
+++ b/main/solution/infrastructure/config/infra/cloudformation.yml
@@ -0,0 +1,170 @@
+Conditions:
+ IsDev: !Equals ['${self:custom.settings.envType}', 'dev']
+
+Resources:
+ # =============================================================================================
+ # S3 Buckets
+ # =============================================================================================
+
+ # S3 Bucket for S3 access logs
+ LoggingBucket:
+ Type: AWS::S3::Bucket
+ Properties:
+ BucketName: ${self:custom.settings.loggingBucketName}
+ AccessControl: LogDeliveryWrite
+ BucketEncryption:
+ ServerSideEncryptionConfiguration:
+ - ServerSideEncryptionByDefault:
+ SSEAlgorithm: AES256
+ PublicAccessBlockConfiguration:
+ BlockPublicAcls: true
+ BlockPublicPolicy: true
+ IgnorePublicAcls: true
+ RestrictPublicBuckets: true
+
+ LoggingBucketPolicy:
+ Type: AWS::S3::BucketPolicy
+ Properties:
+ Bucket: !Ref LoggingBucket
+ PolicyDocument:
+ Statement:
+ - Sid: Deny requests that do not use TLS
+ Effect: Deny
+ Principal: '*'
+ Action: s3:*
+ Resource: !Join ['/', [!GetAtt LoggingBucket.Arn, '*']]
+ Condition:
+ Bool:
+ aws:SecureTransport: false
+ - Sid: Deny requests that do not use SigV4
+ Effect: Deny
+ Principal: '*'
+ Action: s3:*
+ Resource: !Join ['/', [!GetAtt LoggingBucket.Arn, '*']]
+ Condition:
+ StringNotEquals:
+ s3:signatureversion: 'AWS4-HMAC-SHA256'
+
+ # S3 Bucket for the static website
+ WebsiteBucket:
+ Type: AWS::S3::Bucket
+ Properties:
+ BucketName: ${self:custom.settings.websiteBucketName}
+ BucketEncryption:
+ ServerSideEncryptionConfiguration:
+ - ServerSideEncryptionByDefault:
+ SSEAlgorithm: AES256
+ WebsiteConfiguration:
+ IndexDocument: index.html
+ ErrorDocument: index.html
+ PublicAccessBlockConfiguration:
+ BlockPublicAcls: true
+ BlockPublicPolicy: true
+ IgnorePublicAcls: true
+ RestrictPublicBuckets: true
+
+ WebsiteBucketPolicy:
+ Type: AWS::S3::BucketPolicy
+ Properties:
+ Bucket: !Ref WebsiteBucket
+ PolicyDocument:
+ Statement:
+ - Sid: Allow CloudFront Origin Access Identity
+ Action:
+ - 's3:GetObject'
+ Effect: Allow
+ Principal:
+ AWS: !Join ['', ['arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ', !Ref 'CloudFrontOAI']]
+ Resource:
+ - !Join ['', ['arn:aws:s3:::', !Ref 'WebsiteBucket', '/*']]
+ - Sid: Deny requests that do not use TLS
+ Effect: Deny
+ Principal: '*'
+ Action: s3:*
+ Resource: !Join ['/', [!GetAtt WebsiteBucket.Arn, '*']]
+ Condition:
+ Bool:
+ aws:SecureTransport: false
+ - Sid: Deny requests that do not use SigV4
+ Effect: Deny
+ Principal: '*'
+ Action: s3:*
+ Resource: !Join ['/', [!GetAtt WebsiteBucket.Arn, '*']]
+ Condition:
+ StringNotEquals:
+ s3:signatureversion: 'AWS4-HMAC-SHA256'
+
+ # =============================================================================================
+ # CloudFront
+ # =============================================================================================
+
+ WebsiteCloudFront:
+ Type: AWS::CloudFront::Distribution
+ DependsOn:
+ - WebsiteBucket
+ Properties:
+ DistributionConfig:
+ Comment: 'CloudFront Distribution pointing to ${self:custom.settings.websiteBucketName}'
+ Origins:
+ - DomainName: !GetAtt WebsiteBucket.RegionalDomainName
+ Id: S3Origin
+ S3OriginConfig:
+ OriginAccessIdentity: !Join ['', ['origin-access-identity/cloudfront/', !Ref 'CloudFrontOAI']]
+ Enabled: true
+ HttpVersion: 'http2'
+ DefaultRootObject: index.html
+ CustomErrorResponses:
+ - ErrorCachingMinTTL: 300
+ ErrorCode: 404
+ ResponseCode: 200
+ ResponsePagePath: /index.html
+ - ErrorCachingMinTTL: 300
+ ErrorCode: 403
+ ResponseCode: 200
+ ResponsePagePath: /index.html
+ DefaultCacheBehavior:
+ DefaultTTL: 0
+ MinTTL: 0
+ MaxTTL: 0
+ AllowedMethods:
+ - GET
+ - HEAD
+ - OPTIONS
+ Compress: true
+ TargetOriginId: S3Origin
+ ForwardedValues:
+ QueryString: true
+ Cookies:
+ Forward: none
+ ViewerProtocolPolicy: redirect-to-https
+ PriceClass: PriceClass_100
+ Logging:
+ Bucket: ${self:custom.settings.loggingBucketName}.s3.amazonaws.com
+ Prefix: cloudfront/
+
+ CloudFrontOAI:
+ Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
+ Properties:
+ CloudFrontOriginAccessIdentityConfig:
+ Comment: 'OAI for ${self:custom.settings.websiteBucketName}'
+
+Outputs:
+ WebsiteUrl:
+ Description: URL for the main website hosted on S3 via CloudFront
+ Value: !Join ['', ['https://', !GetAtt [WebsiteCloudFront, DomainName]]]
+
+ WebsiteBucket:
+ Description: The bucket name of the static website
+ Value: !Ref WebsiteBucket
+
+ LoggingBucket:
+ Description: The bucket name for S3 access logging
+ Value: !Ref LoggingBucket
+
+ CloudFrontEndpoint:
+ Value: !GetAtt [WebsiteCloudFront, DomainName]
+ Description: Endpoint for CloudFront distribution for the website
+
+ CloudFrontId:
+ Value: !Ref WebsiteCloudFront
+ Description: Id of the CloudFront distribution
diff --git a/main/solution/infrastructure/config/settings/.defaults.yml b/main/solution/infrastructure/config/settings/.defaults.yml
new file mode 100644
index 0000000000..e42416c2cc
--- /dev/null
+++ b/main/solution/infrastructure/config/settings/.defaults.yml
@@ -0,0 +1,5 @@
+# The S3 bucket name to be used to host the static website
+websiteBucketName: ${self:custom.settings.globalNamespace}-website
+
+# The S3 bucket name to be used for S3 access logging
+loggingBucketName: ${self:custom.settings.globalNamespace}-logging
diff --git a/main/solution/infrastructure/config/settings/.settings.js b/main/solution/infrastructure/config/settings/.settings.js
new file mode 100644
index 0000000000..0f0a5acf3b
--- /dev/null
+++ b/main/solution/infrastructure/config/settings/.settings.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports.merged = require('@aws-ee/base-serverless-settings-helper').mergeSettings(__dirname, [
+ '../../../../config/settings/.defaults.yml',
+ './.defaults.yml',
+ '../../../../config/settings/${stage}.yml',
+ './${stage}.yml',
+]);
diff --git a/main/solution/infrastructure/jsconfig.json b/main/solution/infrastructure/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/main/solution/infrastructure/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/main/solution/infrastructure/package.json b/main/solution/infrastructure/package.json
new file mode 100644
index 0000000000..ef786b1525
--- /dev/null
+++ b/main/solution/infrastructure/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@aws-ee/infrastructure",
+ "version": "1.0.0",
+ "private": true,
+ "description": "The website infrastructure",
+ "author": "Amazon Web Services",
+ "license": "Apache 2.0",
+ "devDependencies": {
+ "@aws-ee/base-serverless-settings-helper": "workspace:*",
+ "serverless": "^1.63.0",
+ "serverless-deployment-bucket": "^1.1.0"
+ }
+}
diff --git a/main/solution/infrastructure/serverless.yml b/main/solution/infrastructure/serverless.yml
new file mode 100644
index 0000000000..9d1170a3e9
--- /dev/null
+++ b/main/solution/infrastructure/serverless.yml
@@ -0,0 +1,32 @@
+# For full config options, see docs.serverless.com
+# Note that most settings in here come from config/settings/*.yaml
+service: ${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}-infrastructure
+
+provider:
+ name: aws
+ region: ${self:custom.settings.awsRegion}
+ profile: ${self:custom.settings.awsProfile}
+ stackName: ${self:custom.settings.envName}-${self:service}
+ deploymentBucket:
+ name: ${self:custom.settings.deploymentBucketName}
+ serverSideEncryption: AES256
+ stackTags: ${self:custom.tags}
+ # All references beginning with ${self:*, ${opt:*, ${file:*, ${deep:*, and ${cf:* will be resolved by Serverless
+ # All other ${* references will be resolved by CloudFormation
+ # See https://forum.serverless.com/t/getting-handle-accountid-in-serverless-config/946/11 and
+ # See https://github.com/serverless/serverless/issues/5011
+ variableSyntax: '\$\{((((self|opt|deep|cf):)|file)((?!\$\{).)+?)}'
+
+custom:
+ settings: ${file(./config/settings/.settings.js):merged}
+ tags:
+ Name: ${self:custom.settings.envName}-${self:service}
+ deploymentBucket:
+ policy: ${self:custom.settings.deploymentBucketPolicy}
+
+resources:
+ - Description: Galileo-Gateway ${self:custom.settings.version} ${self:custom.settings.solutionName} ${self:custom.settings.envName} Infrastructure
+ - ${file(./config/infra/cloudformation.yml)}
+
+plugins:
+ - serverless-deployment-bucket
diff --git a/main/solution/machine-images/.gitignore b/main/solution/machine-images/.gitignore
new file mode 100644
index 0000000000..3659bd3a89
--- /dev/null
+++ b/main/solution/machine-images/.gitignore
@@ -0,0 +1,15 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/main/solution/machine-images/README.md b/main/solution/machine-images/README.md
new file mode 100644
index 0000000000..058269fc59
--- /dev/null
+++ b/main/solution/machine-images/README.md
@@ -0,0 +1,15 @@
+# Building Machine Images
+
+## Packer
+
+This solution uses [Packer](https://www.packer.io/) to create an Amazon Machine Image (AMI). This AMI forms the basis for EC2/EMR environments that investigators use for their research.
+
+To install Packer, please see their installation [instructions](https://www.packer.io/intro/getting-started/install.html).
+
+## Package and Deploy
+
+To build Amazon Machine Images:
+
+```bash
+$ pnpx sls build-image -s $STAGE
+```
diff --git a/main/solution/machine-images/config/infra/packer-ec2-linux-workspace.json b/main/solution/machine-images/config/infra/packer-ec2-linux-workspace.json
new file mode 100644
index 0000000000..48a06bfb86
--- /dev/null
+++ b/main/solution/machine-images/config/infra/packer-ec2-linux-workspace.json
@@ -0,0 +1,31 @@
+{
+ "variables": {
+ "aws_access_key": "",
+ "aws_secret_key": "",
+ "vpc-id": "",
+ "awsProfile": "",
+ "awsRegion": "",
+ "amiName": ""
+ },
+ "builders": [
+ {
+ "type": "amazon-ebs",
+ "profile": "{{user `awsProfile`}}",
+ "access_key": "{{user `aws_access_key`}}",
+ "secret_key": "{{user `aws_secret_key`}}",
+ "region": "{{user `awsRegion`}}",
+ "source_ami_filter": {
+ "filters": {
+ "virtualization-type": "hvm",
+ "name": "amzn-ami-hvm-*-x86_64-ebs",
+ "root-device-type": "ebs"
+ },
+ "owners": ["amazon"],
+ "most_recent": true
+ },
+ "instance_type": "t2.medium",
+ "ssh_username": "ec2-user",
+ "ami_name": "{{user `ec2LinuxAmiPrefix`}}-{{timestamp}}"
+ }
+ ]
+}
diff --git a/main/solution/machine-images/config/infra/packer-ec2-windows-workspace.json b/main/solution/machine-images/config/infra/packer-ec2-windows-workspace.json
new file mode 100644
index 0000000000..ee3e8398e6
--- /dev/null
+++ b/main/solution/machine-images/config/infra/packer-ec2-windows-workspace.json
@@ -0,0 +1,31 @@
+{
+ "variables": {
+ "aws_access_key": "",
+ "aws_secret_key": "",
+ "vpc-id": "",
+ "awsProfile": "",
+ "awsRegion": "",
+ "amiName": ""
+ },
+ "builders": [
+ {
+ "type": "amazon-ebs",
+ "profile": "{{user `awsProfile`}}",
+ "access_key": "{{user `aws_access_key`}}",
+ "secret_key": "{{user `aws_secret_key`}}",
+ "region": "{{user `awsRegion`}}",
+ "source_ami_filter": {
+ "filters": {
+ "virtualization-type": "hvm",
+ "name": "Windows_Server-*-English-Full-Base-*",
+ "root-device-type": "ebs"
+ },
+ "owners": ["amazon"],
+ "most_recent": true
+ },
+ "instance_type": "t2.medium",
+ "communicator": "none",
+ "ami_name": "{{user `ec2WindowsAmiPrefix`}}-{{timestamp}}"
+ }
+ ]
+}
diff --git a/main/solution/machine-images/config/infra/packer-emr-workspace.json b/main/solution/machine-images/config/infra/packer-emr-workspace.json
new file mode 100644
index 0000000000..3369f9ea15
--- /dev/null
+++ b/main/solution/machine-images/config/infra/packer-emr-workspace.json
@@ -0,0 +1,37 @@
+{
+ "variables": {
+ "aws_access_key": "",
+ "aws_secret_key": "",
+ "vpc-id": "",
+ "awsProfile": "",
+ "awsRegion": "",
+ "amiName": ""
+ },
+ "builders": [
+ {
+ "type": "amazon-ebs",
+ "profile": "{{user `awsProfile`}}",
+ "access_key": "{{user `aws_access_key`}}",
+ "secret_key": "{{user `aws_secret_key`}}",
+ "region": "{{user `awsRegion`}}",
+ "source_ami_filter": {
+ "filters": {
+ "virtualization-type": "hvm",
+ "name": "amzn-ami-hvm-*-x86_64-ebs",
+ "root-device-type": "ebs"
+ },
+ "owners": ["amazon"],
+ "most_recent": true
+ },
+ "instance_type": "t2.medium",
+ "ssh_username": "ec2-user",
+ "ami_name": "{{user `emrAmiPrefix`}}-{{timestamp}}"
+ }
+ ],
+ "provisioners": [
+ {
+ "type": "shell",
+ "script": "./config/infra/provisioners/provision-hail.sh"
+ }
+ ]
+}
diff --git a/main/solution/machine-images/config/infra/provisioners/provision-hail.sh b/main/solution/machine-images/config/infra/provisioners/provision-hail.sh
new file mode 100644
index 0000000000..35edf6ece4
--- /dev/null
+++ b/main/solution/machine-images/config/infra/provisioners/provision-hail.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+
+sudo yum remove java-1.7.0-openjdk -y
+sudo yum install -y git java-1.8.0-devel
+# Add /usr/local/bin to path for future shells
+sudo sed -i /etc/profile -e "s#Path manipulation#Path manipulation\npathmunge /usr/local/bin#"
+PATH=$PATH:/usr/local/bin
+
+sudo chmod 777 /opt/
+git clone https://github.com/hms-dbmi/hail-on-AWS-spot-instances.git /opt/hail-on-AWS-spot-instances
+
+# Fake that this is a master to start with
+sudo mkdir -p /mnt/var/lib/info
+echo "isMaster true" | sudo tee -a /mnt/var/lib/info/instance.json
+
+# Some jre cleanup
+sudo rm /etc/alternatives/jre/include/include
+
+# install things
+cd /opt/hail-on-AWS-spot-instances/src
+sudo ./bootstrap_python36.sh
+
+# Update packages to make hail work as of hail 0.2.34
+PACKAGES="humanize==1.0.0
+ aiohttp
+ aiohttp-session==2.7
+ asyncinit
+ gcsfs==0.2.1
+ hurry.filesize
+ nest-asyncio
+ PyJWT
+ pyspark==2.4.1
+ python-json-logger
+ tabulate==0.8.3
+ tqdm==4.42.1
+ bokeh==1.2.0
+ pandas==0.25.3
+ requests==2.21.0
+ scipy==1.3"
+
+sudo python3 -m pip install $PACKAGES
+
+export HASH="current"
+./hail_build.sh -v $HASH
+
+# TODO: Consider adding jupyterlab-git plugin to enable notebook persistance via git
+
+# Change the password of the notebook to 'go-research-on-aws'
+sed -i 's/sha1:45f7d7ac038c:c36b98f22eac5921c435095af65a9a00b0e1eeb9/sha1:5fcaf700d85d:cd628e3a07b1db1f2cabd1beb11d3c32f4bde928/' ./jupyter_notebook_config.py
+
+# Change the owner of the notebook directory to 'hadoop' (user UID will be 501 but doesn't exist yet)
+sudo chown -R 501:501 /opt/hail-on-AWS-spot-instances/notebook
+
+# clean up
+sudo rm -rf /mnt/var
diff --git a/main/solution/machine-images/config/settings/.defaults.yml b/main/solution/machine-images/config/settings/.defaults.yml
new file mode 100644
index 0000000000..350a085a34
--- /dev/null
+++ b/main/solution/machine-images/config/settings/.defaults.yml
@@ -0,0 +1,3 @@
+ec2LinuxAmiPrefix: ${self:custom.settings.namespace}-EC2-LINUX-AMI
+ec2WindowsAmiPrefix: ${self:custom.settings.namespace}-EC2-WINDOWS-AMI
+emrAmiPrefix: ${self:custom.settings.namespace}-EMR-AMI
diff --git a/main/solution/machine-images/config/settings/.settings.js b/main/solution/machine-images/config/settings/.settings.js
new file mode 100644
index 0000000000..0f0a5acf3b
--- /dev/null
+++ b/main/solution/machine-images/config/settings/.settings.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports.merged = require('@aws-ee/base-serverless-settings-helper').mergeSettings(__dirname, [
+ '../../../../config/settings/.defaults.yml',
+ './.defaults.yml',
+ '../../../../config/settings/${stage}.yml',
+ './${stage}.yml',
+]);
diff --git a/main/solution/machine-images/jsconfig.json b/main/solution/machine-images/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/main/solution/machine-images/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/main/solution/machine-images/package.json b/main/solution/machine-images/package.json
new file mode 100644
index 0000000000..898b7dff52
--- /dev/null
+++ b/main/solution/machine-images/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@aws-ee/machine-images",
+ "version": "0.1.0",
+ "private": true,
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 11",
+ "not op_mini all"
+ ],
+ "devDependencies": {
+ "@aws-ee/serverless-packer": "workspace:*",
+ "@aws-ee/base-serverless-settings-helper": "workspace:*",
+ "serverless": "^1.63.0"
+ }
+}
diff --git a/main/solution/machine-images/serverless.yml b/main/solution/machine-images/serverless.yml
new file mode 100644
index 0000000000..78724511b0
--- /dev/null
+++ b/main/solution/machine-images/serverless.yml
@@ -0,0 +1,22 @@
+# For full config options, check the docs:
+# docs.serverless.com
+
+# NOTE: most of the values here are coming from the appropriate settings.yaml file
+# if you want to update the values then do so from the settings.yaml file instead
+service: ${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}-machine-images
+
+package:
+ individually: true
+ excludeDevDependencies: true
+
+provider:
+ name: aws
+ region: ${self:custom.settings.awsRegion}
+ profile: ${self:custom.settings.awsProfile}
+
+custom:
+ default.aws.profile: '' # Keep default profile to use as empty string to force people to specify profile or use env
+ settings: ${file(./config/settings/.settings.js):merged}
+
+plugins:
+ - '@aws-ee/serverless-packer'
diff --git a/main/solution/post-deployment/.eslintrc.json b/main/solution/post-deployment/.eslintrc.json
new file mode 100644
index 0000000000..a9e56eda24
--- /dev/null
+++ b/main/solution/post-deployment/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "extends": ["plugin:jest/recommended", "airbnb-base", "prettier"],
+ "plugins": ["jest", "prettier"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0
+ },
+ "env": {
+ "jest/globals": true
+ }
+}
diff --git a/main/solution/post-deployment/.gitignore b/main/solution/post-deployment/.gitignore
new file mode 100644
index 0000000000..0729b8f08e
--- /dev/null
+++ b/main/solution/post-deployment/.gitignore
@@ -0,0 +1,24 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+
+# Serverless directories
+.serverless
+
+# Webpack generated directories
+.webpack
+
+config/saml-metadata/exclude-*
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# IntelliJ Idea Module Files
+*.iml
diff --git a/main/solution/post-deployment/.prettierrc.json b/main/solution/post-deployment/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/main/solution/post-deployment/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/main/solution/post-deployment/README.md b/main/solution/post-deployment/README.md
new file mode 100644
index 0000000000..17eb42998a
--- /dev/null
+++ b/main/solution/post-deployment/README.md
@@ -0,0 +1,23 @@
+# Post-Deployment
+
+## To invoke post deployment locally
+
+After you run:
+
+```bash
+$ pnpx sls deploy -s
+```
+
+You can invoke lambda locally:
+
+```bash
+$ pnpx sls invoke local -f postDeployment -s
+```
+
+## Overview of Lambda Functions
+
+We customize each deployment using a Lambda function. This function runs a list of deployment steps.
+
+- Post-deployment Lambda
+
+ There are certain actions that need to take place after the solution is deployed: add authentication providers (such as ADFS), add workflows and workflow templates, auto-generate JWT-signing key and store it in AWS Parameter Store, create a Root user, etc. This Lambda executes a list of pre-configured steps after deployment is complete. It's possible to register additional custom steps, if needed.
diff --git a/main/solution/post-deployment/config/build/webpack.config.js b/main/solution/post-deployment/config/build/webpack.config.js
new file mode 100644
index 0000000000..8586fae781
--- /dev/null
+++ b/main/solution/post-deployment/config/build/webpack.config.js
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+const slsw = require('serverless-webpack');
+const CopyPlugin = require('copy-webpack-plugin'); // see https://github.com/boazdejong/webpack-plugin-copy
+
+const plugins = [new CopyPlugin([])];
+
+module.exports = {
+ entry: slsw.lib.entries,
+ target: 'node',
+ mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
+ optimization: {
+ minimize: false,
+ },
+ performance: {
+ hints: false,
+ },
+ devtool: 'nosources-source-map',
+ externals: [
+ /aws-sdk/, // Available on AWS Lambda
+ // slsw.lib.webpack.isLocal && nodeExternals(), // Do NOT uncomment this line
+ ], // .filter(x => !!x),
+ plugins,
+ node: {
+ __dirname: false,
+ },
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: [
+ [
+ '@babel/preset-env',
+ {
+ targets: { node: '10' }, // Node version on AWS Lambda
+ modules: 'commonjs',
+ },
+ ],
+ ],
+ plugins: ['source-map-support'],
+ },
+ },
+ },
+ {
+ test: /\.ya?ml$/,
+ use: 'js-yaml-loader',
+ },
+ ],
+ },
+};
diff --git a/main/solution/post-deployment/config/environment-files/bin/mount_s3.sh b/main/solution/post-deployment/config/environment-files/bin/mount_s3.sh
new file mode 100644
index 0000000000..9c23885383
--- /dev/null
+++ b/main/solution/post-deployment/config/environment-files/bin/mount_s3.sh
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+
+# This script mounts S3 buckets/prefixes onto the local filesystem using fuse and
+# goofys. It also attempts to create a sym link to the mounted data if the instance
+# is an EMR or SageMaker instance so that it can be easily accessed by Jupyter notebooks.
+#
+# /usr/local/s3-mounts.json should contain S3 study data metadata of the form
+# [{
+# "id": "STUDY_ID",
+# "bucket": "BUCKET_NAME",
+# "prefix": "BUCKET_PREFIX"
+# }, ...]
+CONFIG="/usr/local/etc/s3-mounts.json"
+MOUNT_DIR="${HOME}/studies"
+
+# Exit if CONFIG doesn't exist or is 0 bytes
+[ ! -s "$CONFIG" ] && exit 0
+
+# Define a function to determine what type of environment this is (EMR, SageMaker, or EC2 Linux)
+env_type() {
+ if [ -d "/usr/share/aws/emr" ]
+ then
+ printf "emr"
+ elif [ -d "/home/ec2-user/SageMaker" ]
+ then
+ printf "sagemaker"
+ else
+ printf "ec2-linux"
+ fi
+}
+
+# Mount S3 buckets
+mounts="$(cat "$CONFIG")"
+num_mounts=$(printf "%s" "$mounts" | jq ". | length" -)
+for ((study_idx=0; study_idx<$num_mounts; study_idx++))
+do
+ # Parse bucket/key info
+ study_id="$(printf "%s" "$mounts" | jq -r ".[$study_idx].id" -)"
+ s3_bucket="$(printf "%s" "$mounts" | jq -r ".[$study_idx].bucket" -)"
+ s3_prefix="$(printf "%s" "$mounts" | jq -r ".[$study_idx].prefix" -)"
+
+ # Mount S3 location if not already mounted
+ study_dir="${MOUNT_DIR}/${study_id}"
+ ps -U "$LOGNAME" -o "command" | egrep -q "goofys .* ${study_dir}$"
+ if [ $? -ne 0 ]
+ then
+ printf 'Mounting study "%s" at "%s"\n' "$study_id" "$study_dir"
+ mkdir -p "$study_dir"
+ goofys "${s3_bucket}:${s3_prefix}" "$study_dir"
+ fi
+done
+
+# Define where the Jupyter notebook (if any) should be running
+notebook_dir=""
+case "$(env_type)" in
+ "emr")
+ notebook_dir="/opt/hail-on-AWS-spot-instances/notebook"
+ ;;
+ "sagemaker")
+ notebook_dir="/home/ec2-user/SageMaker"
+ ;;
+esac
+
+# Add a link to the mount in the notebook directory.
+# (The user gets easy access, but it won't check the bucket into a git repo.)
+# Only create a link if Jupyter is running, there are studies mounted, and the link
+# doesn't already exist.
+if [ -n "$notebook_dir" -a $num_mounts -ne 0 ]
+then
+ symlink_name="$notebook_dir/studies"
+ [ ! -L "$symlink_name" ] && sudo ln -s "$MOUNT_DIR" "$symlink_name"
+fi
diff --git a/main/solution/post-deployment/config/environment-files/bootstrap.sh b/main/solution/post-deployment/config/environment-files/bootstrap.sh
new file mode 100644
index 0000000000..9e5952fede
--- /dev/null
+++ b/main/solution/post-deployment/config/environment-files/bootstrap.sh
@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+
+# This script bootstraps a workspace instance by preparing S3 study data to be
+# mounted via the mount_s3.sh environment script.
+# Note that mounting cannot be performed during initial bootstrapping
+# because the instance's role will not yet have access to S3 study
+# data since the associated resource policies aren't updated until after the
+# CFN stack has been completed created.
+S3_MOUNTS="$1"
+
+# Exit if no S3 mounts were specified
+[ -z "$S3_MOUNTS" -o "$S3_MOUNTS" = "[]" ] && exit 0
+
+# Get directory in which this script is stored and define URL from which to download goofys
+FILES_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+GOOFYS_URL="https://github.com/kahing/goofys/releases/download/v0.21.0/goofys"
+
+# Define a function to determine what type of environment this is (EMR, SageMaker, or EC2 Linux)
+env_type() {
+ if [ -d "/usr/share/aws/emr" ]
+ then
+ printf "emr"
+ elif [ -d "/home/ec2-user/SageMaker" ]
+ then
+ printf "sagemaker"
+ else
+ printf "ec2-linux"
+ fi
+}
+
+# Define a function to update Jupyter configuration files
+update_jupyter_config() {
+ config_file="$1"
+
+ # HACK: Update the default SessionManager class used by Jupyter notebooks
+ # so that it runs the S3 mount script the first time sessions are listed
+ cat << EOF | cut -b5- >> "$config_file"
+
+ import subprocess
+ from notebook.services.sessions.sessionmanager import SessionManager as BaseSessionManager
+
+ class SessionManager(BaseSessionManager):
+ def list_sessions(self, *args, **kwargs):
+ """Override default list_sessions() method"""
+ self.mount_studies()
+ result = super(SessionManager, self).list_sessions(*args, **kwargs)
+ return result
+
+ def mount_studies(self):
+ """Execute mount_s3.sh if it hasn't already been run"""
+ if not hasattr(self, 'studies_mounted'):
+ mounting_result = subprocess.run(
+ "mount_s3.sh",
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+ )
+
+ # Log results
+ if mounting_result.stdout:
+ for line in mounting_result.stdout.decode("utf-8").split("\n"):
+ if line: # Skip empty lines
+ self.log.info(line)
+
+ self.studies_mounted = True
+
+ c.NotebookApp.session_manager_class = SessionManager
+EOF
+}
+
+# Install dependencies
+yum install -y fuse jq
+curl -LSs -o "/usr/local/bin/goofys" "$GOOFYS_URL"
+chmod +x "/usr/local/bin/goofys"
+
+# Create S3 mount script and config file
+chmod +x "${FILES_DIR}/bin/mount_s3.sh"
+ln -s "${FILES_DIR}/bin/mount_s3.sh" "/usr/local/bin/mount_s3.sh"
+printf "%s" "$S3_MOUNTS" > "/usr/local/etc/s3-mounts.json"
+
+# Apply updates to environments based on environment type
+case "$(env_type)" in
+ "emr") # Update config and restart Jupyter
+ update_jupyter_config "/opt/hail-on-AWS-spot-instances/src/jupyter_notebook_config.py"
+ sudo -u hadoop PATH=$PATH:/usr/local/bin /opt/hail-on-AWS-spot-instances/src/jupyter_run.sh
+ ;;
+ "sagemaker") # Update config and restart Jupyter
+ update_jupyter_config "/home/ec2-user/.jupyter/jupyter_notebook_config.py"
+ initctl restart jupyter-server --no-wait
+ ;;
+ "ec2-linux") # Add mount script to bash profile
+ printf "\n# Mount S3 study data\nmount_s3.sh\n\n" >> "/home/ec2-user/.bash_profile"
+ ;;
+esac
+
+exit 0
diff --git a/main/solution/post-deployment/config/environment-files/get_bootstrap.sh b/main/solution/post-deployment/config/environment-files/get_bootstrap.sh
new file mode 100644
index 0000000000..4868e9a70a
--- /dev/null
+++ b/main/solution/post-deployment/config/environment-files/get_bootstrap.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+bootstrap_s3_location="$1"
+s3_mounts="$2"
+
+INSTALL_DIR="/usr/local/share/workspace-environment"
+
+# Download instance files and execute bootstrap script
+sudo mkdir "$INSTALL_DIR"
+sudo aws s3 sync "$bootstrap_s3_location" "$INSTALL_DIR"
+
+bootstrap_script="$INSTALL_DIR/bootstrap.sh"
+if [ -s "$bootstrap_script" ]
+then
+ sudo chmod 500 "$bootstrap_script"
+ sudo "$bootstrap_script" "$s3_mounts"
+fi
+
+exit 0
diff --git a/main/solution/post-deployment/config/infra/cloudformation.yml b/main/solution/post-deployment/config/infra/cloudformation.yml
new file mode 100644
index 0000000000..e8f8941bf1
--- /dev/null
+++ b/main/solution/post-deployment/config/infra/cloudformation.yml
@@ -0,0 +1,139 @@
+Resources:
+ # =============================================================================================
+ # IAM Roles
+ # =============================================================================================
+
+ # IAM Role for the postDeployment Function
+ RolePostDeploymentLambda:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: lambda.amazonaws.com
+ Action: 'sts:AssumeRole'
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+ - arn:aws:iam::aws:policy/AmazonCognitoPowerUser
+ Policies:
+ - PolicyName: iam-access
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ Effect: Allow
+ Action:
+ # required for associating lambda@edge to cf distro
+ # see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-permissions.html#lambda-edge-permissions-required
+ - iam:CreateServiceLinkedRole
+ Resource:
+ - !Sub 'arn:aws:iam::${AWS::AccountId}:role/aws-service-role/replicator.lambda.amazonaws.com/AWSServiceRoleForLambdaReplicator'
+ - !Sub 'arn:aws:iam::${AWS::AccountId}:role/aws-service-role/logger.cloudfront.amazonaws.com/AWSServiceRoleForCloudFrontLogger'
+ Condition:
+ ForAllValues:StringLike:
+ iam:AWSServiceName:
+ - replicator.lambda.amazonaws.com
+ - logger.cloudfront.amazonaws.com
+ - PolicyName: s3-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - s3:GetObject
+ Resource:
+ - arn:aws:s3:::${self:custom.settings.deploymentBucketName}/saml-metadata/*
+ - PolicyName: db-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - dynamodb:GetItem
+ - dynamodb:DeleteItem
+ - dynamodb:PutItem
+ - dynamodb:UpdateItem
+ - dynamodb:Query
+ - dynamodb:Scan
+ Resource:
+ - !GetAtt [DbDeploymentStore, Arn]
+ - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${self:custom.settings.dbTablePasswords}'
+ - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${self:custom.settings.dbTableUsers}'
+ - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${self:custom.settings.dbTableAuthenticationProviderTypes}'
+ - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${self:custom.settings.dbTableAuthenticationProviderConfigs}'
+ - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${self:custom.settings.dbTableStepTemplates}'
+ - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${self:custom.settings.dbTableWorkflowTemplates}'
+ - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${self:custom.settings.dbTableWorkflows}'
+ - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${self:custom.settings.dbTableWfAssignments}'
+ - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${self:custom.settings.dbTableWfAssignments}/index/*'
+ - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${self:custom.settings.dbTableUserRoles}'
+ - PolicyName: param-store-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - ssm:GetParameter
+ - ssm:PutParameter
+ Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${self:custom.settings.paramStoreRoot}/*'
+ - PolicyName: kms-access
+ PolicyDocument:
+ Statement:
+ - Effect: Allow
+ Action:
+ - kms:DescribeKey
+ Resource: '*'
+ - PolicyName: cfn-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - cloudformation:DescribeStacks
+ Resource:
+ - !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${self:custom.settings.backendStackName}/*
+ - PolicyName: cloudfront-access
+ PolicyDocument:
+ Statement:
+ Effect: Allow
+ Action:
+ - cloudfront:GetDistributionConfig
+ - cloudfront:UpdateDistribution
+ Resource:
+ - !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${self:custom.settings.cloudFrontId}
+ - PolicyName: lambda-access
+ PolicyDocument:
+ Statement:
+ - Effect: Allow
+ Action:
+ - lambda:GetFunction
+ - lambda:UpdateFunctionConfiguration
+ Resource:
+ - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:${self:custom.settings.workflowLambdaName}
+ - Effect: Allow
+ Action:
+ - lambda:GetFunction
+ - lambda:publishVersion
+ - lambda:EnableReplication
+ Resource:
+ - ${self:custom.settings.edgeLambdaArn}
+ - ${self:custom.settings.edgeLambdaArn}:* # appending ':*' to allow actions on specific versions of the lambda
+
+ # =============================================================================================
+ # Dynamo db
+ # =============================================================================================
+
+ DbDeploymentStore:
+ Type: AWS::DynamoDB::Table
+ Properties:
+ TableName: ${self:custom.settings.dbTableDeploymentStore}
+ AttributeDefinitions:
+ - AttributeName: 'type'
+ AttributeType: 'S'
+ - AttributeName: 'id'
+ AttributeType: 'S'
+ KeySchema:
+ - AttributeName: 'type'
+ KeyType: 'HASH'
+ - AttributeName: 'id'
+ KeyType: 'RANGE'
+ ProvisionedThroughput:
+ ReadCapacityUnits: '10'
+ WriteCapacityUnits: '5'
diff --git a/main/solution/post-deployment/config/infra/functions.yml b/main/solution/post-deployment/config/infra/functions.yml
new file mode 100644
index 0000000000..499885b796
--- /dev/null
+++ b/main/solution/post-deployment/config/infra/functions.yml
@@ -0,0 +1,28 @@
+postDeployment:
+ handler: src/lambdas/post-deployment/handler.handler
+ role: RolePostDeploymentLambda
+ tags: ${self:custom.tags}
+ description: The Post Deployment handler that executes post deployment tasks
+ timeout: 900 # 15 min timeout
+ environment:
+ APP_DB_TABLE_DEPLOYMENT_STORE: ${self:custom.settings.dbTableDeploymentStore}
+ APP_ROOT_USER_NAME: ${self:custom.settings.rootUserName}
+ APP_ROOT_USER_FIRST_NAME: ${self:custom.settings.rootUserFirstName}
+ APP_ROOT_USER_LAST_NAME: ${self:custom.settings.rootUserLastName}
+ APP_ROOT_USER_EMAIL: ${self:custom.settings.rootUserEmail}
+ APP_ROOT_USER_PASSWORD_PARAM_NAME: ${self:custom.settings.rootUserPasswordParamName}
+ APP_PARAM_STORE_JWT_SECRET: ${self:custom.settings.paramStoreJwtSecret}
+ APP_JWT_OPTIONS: ${self:custom.settings.jwtOptions}
+ APP_ENABLE_NATIVE_USER_POOL_USERS: ${self:custom.settings.enableNativeUserPoolUsers}
+ APP_FED_IDP_IDS: ${self:custom.settings.fedIdpIds}
+ APP_FED_IDP_NAMES: ${self:custom.settings.fedIdpNames}
+ APP_FED_IDP_DISPLAY_NAMES: ${self:custom.settings.fedIdpDisplayNames}
+ APP_FED_IDP_METADATAS: ${self:custom.settings.fedIdpMetadatas}
+ APP_WEBSITE_URL: ${self:custom.settings.websiteUrl}
+ APP_DEFAULT_AUTH_N_PROVIDER_TITLE: ${self:custom.settings.defaultAuthNProviderTitle}
+ APP_COGNITO_AUTH_N_PROVIDER_TITLE: ${self:custom.settings.cognitoAuthNProviderTitle}
+ APP_BACKEND_STACK_NAME: ${self:custom.settings.backendStackName}
+ APP_WORKFLOW_LAMBDA_NAME: ${self:custom.settings.workflowLambdaName}
+ APP_EDGE_LAMBDA_ARN: ${self:custom.settings.edgeLambdaArn}
+ APP_CLOUD_FRONT_ID: ${self:custom.settings.cloudFrontId}
+ APP_ENABLE_EXTERNAL_RESEARCHERS: ${self:custom.settings.enableExternalResearchers}
\ No newline at end of file
diff --git a/main/solution/post-deployment/config/saml-metadata/sample-idp-metadata.xml b/main/solution/post-deployment/config/saml-metadata/sample-idp-metadata.xml
new file mode 100644
index 0000000000..bebdd56c16
--- /dev/null
+++ b/main/solution/post-deployment/config/saml-metadata/sample-idp-metadata.xml
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/main/solution/post-deployment/config/settings/.defaults.yml b/main/solution/post-deployment/config/settings/.defaults.yml
new file mode 100644
index 0000000000..ee503f3725
--- /dev/null
+++ b/main/solution/post-deployment/config/settings/.defaults.yml
@@ -0,0 +1,134 @@
+# Options used when issuing JWT token such as which algorithm to use for hashing and how long to keep the tokens alive etc
+jwtOptions: '{"algorithm":"HS256","expiresIn":"2 days"}'
+
+# Name of the parameter in the parameter store to save the generated password of the root user
+rootUserPasswordParamName: /${self:custom.settings.envName}/${self:custom.settings.solutionName}/user/root/password
+
+# Name of the parameter in parameter store containing secret key for JWT. This key is used for signing and validating JWT tokens
+# issued by data lake authentication providers
+paramStoreJwtSecret: '/${self:custom.settings.paramStoreRoot}/jwt/secret'
+
+# The stack name of the 'infrastructure' serverless service
+infrastructureStackName: ${self:custom.settings.namespace}-infrastructure
+
+# The URL of the website as defined by the infrastructure stack
+# Used when provisioning a Cognito user pool so that Cognito can direct a user back to the site after auth
+websiteUrl: ${cf:${self:custom.settings.infrastructureStackName}.WebsiteUrl}
+
+# If a Cognito user pool is setup for the solution, this setting indicates whether native
+# Cognito users should be used or if the Cognito user pool will only be used to federate to
+# other identity providers
+enableNativeUserPoolUsers: false
+
+# Title of the default (internal) authentication provider. This shows up in the login dropdown.
+defaultAuthNProviderTitle: 'Default Login'
+
+# Title of the Cognito authentication provider that federates with Active Directory identity provider.
+cognitoAuthNProviderTitle: 'Login using Active Directory'
+
+# The stack name of the 'backend' serverless service
+backendStackName: ${self:custom.settings.namespace}-backend
+
+# The name of the workflowLoopRunner lambda
+workflowLambdaName: ${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}-backend-${self:custom.settings.envName}-workflowLoopRunner
+
+# The S3 bucket name to be used to the data
+dataBucketName: ${self:custom.settings.namespace}-data
+
+# The stack name of the 'edge-lambda' serverless service
+edgeLambdaStackName: ${self:custom.settings.namespace}-edgeLambda
+edgeLambdaArn: ${cf:${self:custom.settings.edgeLambdaStackName}.EdgeLambdaArn}
+cloudFrontId: ${cf:${self:custom.settings.infrastructureStackName}.CloudFrontId}
+
+# ================================ settings for Cognito with SAML2.0 Federation ===========================================
+
+# Array of identity provider ids.
+# The usual practice is to keep this same as the domain name of the idp.
+# For example, when connecting with an IdP that has users "user1@domain1.com", "user2@domain1.com" etc then
+# the id should be set to "domain1.com"
+#
+# If you do not want to connect to Active Directory then leave this setting as empty array as follows.
+#
+# fedIdpIds: '[]'
+#
+fedIdpIds: '[]'
+
+# Array of identity provider names. This array should be in same order as the "fedIdpIds"
+# Some name for the IdPs. (such as 'com.ee', 'EEAD' etc)
+#
+# If you do not want to connect to Active Directory then leave this setting as empty array as follows.
+#
+# fedIdpNames: '[]'
+#
+fedIdpNames: '[]'
+
+# Array of identity provider display names. This array should be in same order as the "fedIdpIds"
+# Display name (such as 'Employee Login', 'AD Login' etc). This is not used in the system.
+# This can be used in UI to show login buttons that redirect users to IdP specific login pages.
+# Currently, even UI is not using this as it directs the user to Cognito hosted UI login page.
+#
+# If you do not want to connect to Active Directory then leave this setting as empty array as follows.
+#
+# fedIdpDisplayNames: '[]'
+#
+fedIdpDisplayNames: '[]'
+
+# Array of identity provider SAML metadata. This array should be in same order as the "fedIdpIds".
+# The array should contain either
+# 1. S3 or http(s) url pointing to the IdP metadata.
+# If S3 URL then it must be accessible by the post-deployment lambda
+# (i.e., the lambda must have getObject permission to read the specified metadata file location from S3)
+# The GetObject permission is given by the "RolePostDeploymentLambda" in "post-deployment/config/infra/cloudformation.yml"
+# If it's http(s) URL then it must be reachable over the public internet.
+# (TODO: Add support for metadata URLs accessible only in private network.)
+# OR
+# 2. the metadata content XML blob as string
+# The current implementation looks for the file in "solution/post-deployment/config/saml-metadata" directory and
+# uploads the file to the S3 directory (prefix) "${self:custom.settings.deploymentBucketName}/saml-metadata/".
+# The uploading of the file is done using serverless s3-sync plugin. See "post-deployment/serverless.yml"
+# file and search for "s3Sync" to see which files are uploaded to the "${self:custom.settings.deploymentBucketName}" S3
+# bucket.
+#
+# If you do not want to connect to Active Directory then leave this setting as empty array as follows.
+#
+# fedIdpMetadatas: '[]'
+fedIdpMetadatas: '[]'
+
+# ================================ DB Settings ===========================================
+
+# DynamoDB table name for the deployment store
+dbTableDeploymentStore: ${self:custom.settings.dbTablePrefix}-DbDeploymentStore
+
+# DynamoDB table name for supported authentication provider types
+dbTableAuthenticationProviderTypes: ${self:custom.settings.dbTablePrefix}-DbAuthenticationProviderTypes
+
+# DynamoDB table name for authentication provider configurations
+dbTableAuthenticationProviderConfigs: ${self:custom.settings.dbTablePrefix}-DbAuthenticationProviderConfigs
+
+# DynamoDB table name for users
+dbTableUsers: ${self:custom.settings.dbTablePrefix}-DbUsers
+
+# DynamoDB table name for passwords for the internal users
+# (applicable only to the users authenticated by internal authentication provider)
+dbTablePasswords: ${self:custom.settings.dbTablePrefix}-DbPasswords
+
+# DynamoDB table name for Step Templates
+dbTableStepTemplates: ${self:custom.settings.dbTablePrefix}-DbStepTemplates
+
+# DynamoDB table name for Workflow Templates
+dbTableWorkflowTemplates: ${self:custom.settings.dbTablePrefix}-DbWorkflowTemplates
+
+# DynamoDB table name for Workflows
+dbTableWorkflows: ${self:custom.settings.dbTablePrefix}-DbWorkflows
+
+# DynamoDB table name for WfAssignments
+dbTableWfAssignments: ${self:custom.settings.dbTablePrefix}-DbWfAssignments
+
+# DynamoDB table name for Workflow Templates Drafts
+dbTableWorkflowTemplateDrafts: ${self:custom.settings.dbTablePrefix}-DbWorkflowTemplateDrafts
+
+# DynamoDB table name for Workflow Drafts
+dbTableWorkflowDrafts: ${self:custom.settings.dbTablePrefix}-DbWorkflowDrafts
+
+# DynamoDB table name for UserRoles
+dbTableUserRoles: ${self:custom.settings.dbTablePrefix}-DbUserRoles
diff --git a/main/solution/post-deployment/config/settings/.settings.js b/main/solution/post-deployment/config/settings/.settings.js
new file mode 100644
index 0000000000..1fb020c3de
--- /dev/null
+++ b/main/solution/post-deployment/config/settings/.settings.js
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+module.exports.merged = require('@aws-ee/base-serverless-settings-helper').mergeSettings(
+ __dirname,
+ [
+ '../../../../config/settings/.defaults.yml',
+ './.defaults.yml',
+ '../../../../config/settings/${stage}.yml',
+ './${stage}.yml',
+ ],
+ {
+ crossRegionCloudFormation: {
+ edgeLambdaStackName: [
+ {
+ settingName: 'edgeLambdaArn',
+ outputKey: 'EdgeLambdaArn',
+ },
+ ],
+ },
+ },
+);
diff --git a/main/solution/post-deployment/jest.config.js b/main/solution/post-deployment/jest.config.js
new file mode 100644
index 0000000000..3f7ffc8068
--- /dev/null
+++ b/main/solution/post-deployment/jest.config.js
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/main/solution/post-deployment/jsconfig.json b/main/solution/post-deployment/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/main/solution/post-deployment/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/main/solution/post-deployment/package.json b/main/solution/post-deployment/package.json
new file mode 100644
index 0000000000..2e5109efe9
--- /dev/null
+++ b/main/solution/post-deployment/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "@aws-ee/post-deployment",
+ "version": "1.0.0",
+ "private": true,
+ "description": "The post-deployment Service that executes some setup/configuration steps after main application deployment",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-ee/base-post-deployment": "workspace:*",
+ "@aws-ee/base-services": "workspace:*",
+ "@aws-ee/base-services-container": "workspace:*",
+ "@aws-ee/base-workflow-core": "workspace:*",
+ "@aws-ee/base-workflow-steps": "workspace:*",
+ "@aws-ee/base-workflow-templates": "workspace:*",
+ "@aws-ee/base-raas-rest-api": "workspace:*",
+ "@aws-ee/base-raas-services": "workspace:*",
+ "@aws-ee/base-raas-workflow-steps": "workspace:*",
+ "@aws-ee/base-raas-workflows": "workspace:*",
+ "@aws-ee/base-raas-post-deployment": "workspace:*",
+ "aws-sdk": "^2.647.0",
+ "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+ "@aws-ee/base-serverless-backend-tools": "workspace:*",
+ "@aws-ee/base-serverless-settings-helper": "workspace:*",
+ "@babel/core": "^7.8.4",
+ "@babel/preset-env": "^7.8.4",
+ "babel-loader": "^8.0.6",
+ "babel-plugin-source-map-support": "^2.1.1",
+ "copy-webpack-plugin": "^5.1.1",
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb-base": "^14.0.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "js-yaml-loader": "^1.2.2",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "serverless": "^1.63.0",
+ "serverless-deployment-bucket": "^1.1.0",
+ "serverless-s3-sync": "^1.10.2",
+ "serverless-webpack": "^5.3.1",
+ "source-map-support": "^0.5.16",
+ "webpack": "^4.41.5"
+ },
+ "scripts": {
+ "test": "NODE_ENV=test jest --config jest.config.js --passWithNoTests",
+ "test:watch": "NODE_ENV=test jest --config jest.config.js --passWithNoTests --watchAll",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/main/solution/post-deployment/serverless.yml b/main/solution/post-deployment/serverless.yml
new file mode 100644
index 0000000000..b579f2f4aa
--- /dev/null
+++ b/main/solution/post-deployment/serverless.yml
@@ -0,0 +1,68 @@
+# For full config options, see docs.serverless.com
+# Note that most settings in here come from config/settings/*.yaml
+service: ${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}-postDeployment
+
+package:
+ individually: true
+ excludeDevDependencies: true
+
+provider:
+ name: aws
+ runtime: nodejs12.x
+ region: ${self:custom.settings.awsRegion}
+ profile: ${self:custom.settings.awsProfile}
+ stackName: ${self:custom.settings.envName}-${self:service}
+ deploymentBucket:
+ name: ${self:custom.settings.deploymentBucketName}
+ serverSideEncryption: AES256
+ stackTags: ${self:custom.tags}
+ versionFunctions: false # see https://medium.com/@mayconbordin/lessons-learned-building-a-large-serverless-project-on-aws-74d40f5b0b46
+ # All references beginning with ${self:*, ${opt:*, ${file:*, ${deep:*, and ${cf:* will be resolved by Serverless
+ # All other ${* references will be resolved by CloudFormation
+ # See https://forum.serverless.com/t/getting-handle-accountid-in-serverless-config/946/11 and
+ # See https://github.com/serverless/serverless/issues/5011
+ variableSyntax: '\$\{((((self|opt|deep|cf):)|file)((?!\$\{).)+?)}'
+ environment:
+ APP_ENV_TYPE: ${self:custom.settings.envType}
+ APP_ENV_NAME: ${self:custom.settings.envName}
+ APP_AWS_REGION: ${self:custom.settings.awsRegion}
+ APP_SOLUTION_NAME: ${self:custom.settings.solutionName}
+ APP_DB_TABLE_PREFIX: ${self:custom.settings.dbTablePrefix}
+
+custom:
+ settings: ${file(./config/settings/.settings.js):merged}
+ tags:
+ Name: ${self:custom.settings.envName}-${self:service}
+ deploymentBucket:
+ policy: ${self:custom.settings.deploymentBucketPolicy}
+ webpack:
+ webpackConfig: ./config/build/webpack.config.js
+ packager: pnpm
+ keepOutputDirectory: true
+ excludeFiles: src/**/*.test.js
+ backendTools:
+ environmentOverrides: # when running locally
+ provider:
+ APP_AWS_REGION: ${self:custom.settings.awsRegion} # this is needed for local development
+ APP_AWS_PROFILE: ${self:custom.settings.awsProfile} # this is needed for local development
+ APP_USE_AWS_PROFILE: ${self:custom.settings.useAwsProfile}
+ IS_OFFLINE: true
+ s3Sync:
+ - bucketName: ${self:custom.settings.deploymentBucketName}
+ bucketPrefix: saml-metadata/
+ localDir: config/saml-metadata
+ - bucketName: ${self:custom.settings.environmentsBootstrapBucketName}
+ bucketPrefix: environment-files/
+ localDir: config/environment-files
+
+functions: ${file(./config/infra/functions.yml)}
+
+resources:
+ - Description: Galileo-Gateway ${self:custom.settings.version} ${self:custom.settings.solutionName} ${self:custom.settings.envName} Post-Deployment
+ - ${file(./config/infra/cloudformation.yml)}
+
+plugins:
+ - serverless-webpack
+ - serverless-deployment-bucket
+ - serverless-s3-sync
+ - '@aws-ee/base-serverless-backend-tools'
diff --git a/main/solution/post-deployment/src/lambdas/post-deployment/handler.js b/main/solution/post-deployment/src/lambdas/post-deployment/handler.js
new file mode 100644
index 0000000000..127985bceb
--- /dev/null
+++ b/main/solution/post-deployment/src/lambdas/post-deployment/handler.js
@@ -0,0 +1,56 @@
+/*
+ * 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-console */
+const ServicesContainer = require('@aws-ee/base-services-container/lib/services-container');
+const { registerServices } = require('@aws-ee/base-services/lib/utils/services-registration-util');
+const { registerSteps } = require('@aws-ee/base-post-deployment/lib/steps-registration-util');
+
+const pluginRegistry = require('./plugins/plugin-registry');
+
+async function handler(_event, _context) {
+ // eslint-disable-line no-unused-vars
+ // register services
+ const container = new ServicesContainer(['settings', 'log']);
+ // registerServices - Registers services by calling each service registration plugin in order.
+ await registerServices(container, pluginRegistry);
+
+ // registerSteps - Registers post deployment steps by calling each step registration plugin in order.
+ const stepsMap = await registerSteps(container, pluginRegistry);
+ await container.initServices();
+
+ const log = await container.find('log');
+
+ try {
+ log.info('Post deployment -- STARTED');
+ const entries = Array.from(stepsMap);
+ // We need to await execution of steps in the strict sequence so awaiting in loop
+ for (let i = 0; i < entries.length; i += 1) {
+ const entry = entries[i];
+ const name = entry[0]; // entry is [stepServiceName, serviceImpl]
+ const service = await container.find(name); // eslint-disable-line no-await-in-loop
+ log.info(`====> Running ${name}.execute()`);
+ await service.execute(); // eslint-disable-line no-await-in-loop
+ }
+ /* eslint-enable no-restricted-syntax, no-await-in-loop */
+ log.info('Post deployment -- ENDED');
+ } catch (error) {
+ console.log('================= Error ==================');
+ console.log(JSON.stringify(error, null, 2)); // so we can print the payload of the error object (if any)
+ throw error;
+ }
+}
+
+module.exports.handler = handler;
diff --git a/main/solution/post-deployment/src/lambdas/post-deployment/plugins/plugin-registry.js b/main/solution/post-deployment/src/lambdas/post-deployment/plugins/plugin-registry.js
new file mode 100644
index 0000000000..d99aeadcff
--- /dev/null
+++ b/main/solution/post-deployment/src/lambdas/post-deployment/plugins/plugin-registry.js
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+const baseAuditPlugin = require('@aws-ee/base-services/lib/plugins/audit-plugin');
+const baseServicesPlugin = require('@aws-ee/base-post-deployment/lib/plugins/services-plugin');
+const baseStepsPlugin = require('@aws-ee/base-post-deployment/lib/plugins/steps-plugin');
+const workflowServicesPlugin = require('@aws-ee/base-workflow-core/lib/runner/plugins/services-plugin');
+const workflowPostDeploymentStepsPlugin = require('@aws-ee/base-workflow-core/lib/post-deployment/plugins/steps-plugin');
+const baseWfStepsPlugin = require('@aws-ee/base-workflow-steps/steps/workflow-steps-plugin');
+const baseWfTemplatesPlugin = require('@aws-ee/base-workflow-templates/templates/workflow-templates-plugin');
+const baseRaasServicesPlugin = require('@aws-ee/base-raas-rest-api/lib/plugins/services-plugin');
+const baseRaasPostDeploymentStepsPlugin = require('@aws-ee/base-raas-post-deployment/lib/plugins/steps-plugin');
+const baseRaasWfStepsPlugin = require('@aws-ee/base-raas-workflow-steps/lib/plugins/workflow-steps-plugin');
+const baseRaasWorkflowsPlugin = require('@aws-ee/base-raas-workflows/lib/plugins/workflows-plugin');
+const baseRaasUserAuthzPlugin = require('@aws-ee/base-raas-services/lib/user/user-authz-plugin');
+
+const servicesPlugin = require('./services-plugin');
+const stepsPlugin = require('./steps-plugin');
+
+const extensionPoints = {
+ 'service': [baseServicesPlugin, workflowServicesPlugin, baseRaasServicesPlugin, servicesPlugin],
+ 'postDeploymentStep': [
+ baseStepsPlugin,
+ workflowPostDeploymentStepsPlugin,
+ baseRaasPostDeploymentStepsPlugin,
+ stepsPlugin,
+ ],
+ 'authentication-provider-type': [], // No plugins at this point. The built in authentication provider types are registered by "addon-base-rest-api/packages/services/lib/authentication-providers/authentication-provider-type-service.js" service
+ 'workflow-steps': [baseWfStepsPlugin, baseRaasWfStepsPlugin],
+ 'workflow-templates': [baseWfTemplatesPlugin],
+ 'workflows': [baseRaasWorkflowsPlugin],
+ 'workflow-assignments': [],
+ 'audit': [baseAuditPlugin],
+ 'user-authz': [baseRaasUserAuthzPlugin],
+ 'user-role-management-authz': [], // No plugins at this point. All user-role-management authz is happening inline in 'user-roles-service'
+ 'environment-authz': [], // No plugins at this point. All environment authz is happening inline in 'environment-service' using the 'environment-authz-service'
+ 'project-authz': [], // No plugins at this point. All project authz is happening inline in 'project-service'
+ 'index-authz': [], // No plugins at this point. All index authz is happening inline in 'index-service'
+ 'account-authz': [], // No plugins at this point. All account authz is happening inline in 'account-service'
+ 'aws-account-authz': [], // No plugins at this point. All aws-account authz is happening inline in 'aws-account-service'
+ 'cost-authz': [], // No plugins at this point. All cost authz is happening inline in 'costs-service'
+};
+
+async function getPlugins(extensionPoint) {
+ return extensionPoints[extensionPoint];
+}
+
+const registry = {
+ getPlugins,
+};
+
+module.exports = registry;
diff --git a/main/solution/post-deployment/src/lambdas/post-deployment/plugins/services-plugin.js b/main/solution/post-deployment/src/lambdas/post-deployment/plugins/services-plugin.js
new file mode 100644
index 0000000000..e8fb76b4d1
--- /dev/null
+++ b/main/solution/post-deployment/src/lambdas/post-deployment/plugins/services-plugin.js
@@ -0,0 +1,219 @@
+/*
+ * 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.
+ */
+
+/**
+ * Function to register solution specific services to the services container
+ * @param container An instance of ServicesContainer to register services to
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerServices(container, pluginRegistry) {
+ // This is where you can register your services
+ // Example:
+ // container.register('service1',new Service1());
+ // container.register('service2',new Service2());
+ // TODO: Register additional services as per your solution requirements
+}
+
+/**
+ * Function to register solution specific static settings. "static settings" is a plain JavaScript object containing
+ * settings as key/value. In Lambda environment, the settings are provided by environment variables.
+ * There is 4K limit to the env variables that can be passed to a Lambda. The default settings service impl provided by the
+ * "@aws-ee/base-services" package reads settings from env variables.
+ * In addition to those, any other settings that be derived via convention should be passed as "static settings" to
+ * avoid occupying space in env variables space.
+ *
+ * @param existingStaticSettings An existing static settings plain javascript object containing settings as key/value contributed by other plugins
+ * @param settingsService Default instance of settings service that resolves settings from environment variables
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to static settings object
+ */
+// eslint-disable-next-line no-unused-vars
+function getStaticSettings(existingStaticSettings, settingsService, pluginRegistry) {
+ // This is where you can
+ // 1. register your static settings, to register your static settings
+ //
+ // const staticSettings = {
+ // ...existingStaticSettings,
+ // // add other static settings here as follows
+ // 'staticSetting1':'static-setting-1-value',
+ // 'staticSetting2':'static-setting-2-value',
+ // }
+ // return staticSettings;
+ //
+ // 2. modify any static settings
+ // existingStaticSettings['the-existing-static-setting-you-want-to-replace'] = 'new-value';
+ // return existingStaticSettings;
+ //
+ // 3. delete any existing static setting, to delete existing static setting
+ //
+ // existingStaticSettings.delete('the-existing-static-setting-you-want-to-delete');
+ //
+
+ // TODO: Register additional static settings as per your solution requirements here
+ const staticSettings = {
+ ...existingStaticSettings,
+ };
+ // DO NOT forget to return staticSettings here. If you do not return here no static settings will be configured
+ return staticSettings;
+}
+
+/**
+ * Function to register solution specific logging context. "logging context" is a plain JavaScript object containing
+ * key/values. These key/values are automatically added to logs by the loggingService.
+ * These additional context items can be useful for debugging and monitoring especially when aggregating logs from
+ * multiple lambdas, across multiple environments into a single dashboard or log analytics environment such as ELK stack.
+ *
+ * @param existingLoggingContext An existing logging context plain javascript object containing logging context items as key/value(s)
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to logging context object
+ */
+// eslint-disable-next-line no-unused-vars
+async function getLoggingContext(existingLoggingContext, pluginRegistry) {
+ // This is where you can
+ // 1. register your logging context items, to register your logging context items
+ //
+ // const loggingContext = {
+ // ...existingLoggingContext,
+ //
+ // // add other items here as follows, for example,
+ // 'someKey':'someValue',
+ // }
+ // return loggingContext;
+ //
+ // 2. modify any logging context items
+ // existingLoggingContext['the-existing-logging-context-item-you-want-to-replace'] = 'new-value';
+ // return existingLoggingContext;
+ //
+ // 3. delete any existing logging context, to delete existing logging context item
+ //
+ // existingLoggingContext.delete('the-existing-logging-context-item-you-want-to-delete');
+ //
+
+ // TODO: Register additional logging context items as per your solution requirements here
+ const loggingContext = {
+ ...existingLoggingContext,
+ };
+
+ // DO NOT forget to return loggingContext here. If you do not return here no logging context will be configured
+ return loggingContext;
+}
+
+/**
+ * Function to add solution specific fields for masking in logs.
+ * "fields to mask" is an array containing field names to mask in logs. The logingService will mask these fields as
+ * '****' in logs. The service will look for these fields in deeply nested objects too. Note that the masking only works
+ * when logging JavaScript objects.
+ *
+ * For example, let's say you want to mask "ssn" numbers from the logs so the fields to mask is ['ssn'].
+ *
+ * const objToLog = { key1: 'value1', ssn:'some-ssn'}
+ * this.log.info(objToLog); // The ssn will be masked here
+ *
+ * const objWithNestedSsn = { key1: 'value1', nested: { deepNested: {'ssn':'some-ssn-value'}}}
+ * this.log.info(objWithNestedSsn); // The ssn will be masked here as well
+ *
+ * but
+ *
+ * this.log.info(`value of ssn is ${someSssn}`); // The ssn will NOT be masked here
+ *
+ * These additional context items can be useful for debugging and monitoring especially when aggregating logs from
+ * multiple lambdas, across multiple environments into a single dashboard or log analytics environment such as ELK stack.
+ *
+ * @param existingFieldsToMask An existing array of field names to mask
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise<*>} A promise that resolves to an array of field names to mask when logging
+ */
+// eslint-disable-next-line no-unused-vars
+async function getFieldsToMask(existingFieldsToMask, pluginRegistry) {
+ // This is where you can
+ // 1. add your additional fields to mask here
+ //
+ // const fieldsToMask = [
+ // ...existingFieldsToMask,
+ //
+ // // add other fields to mask
+ // 'someField1',
+ // 'someField2'
+ // ]
+ // return fieldsToMask;
+ //
+ // 3. remove any existing field(s) from masking by returning an array without that field(s).
+ // This field will be removed from masking list (i.e., it will be logged as is)
+ //
+ // return _.filter(fieldsToMask,_.negate(fieldName => fieldName === fieldNameToNotMask));
+ //
+
+ // TODO: Register additional fieldsToMask as per your solution requirements here
+ const fieldsToMask = {
+ ...existingFieldsToMask,
+ };
+ // DO NOT forget to return fieldsToMask here. If you do not return here no fields will be masked
+ return fieldsToMask;
+}
+
+/**
+ * Function to register solution specific implementation for settings service. This is an optional function.
+ * By default, an implementation of settings service (i.e., "@aws-ee/base-services/lib/settings/env-settings-service")
+ * that resolves settings from environment variables is already registered in the "container"
+ *
+ * @param container Services container
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerSettingsService(container, pluginRegistry) {
+ // The container has default settings service already registered
+ // If you want to register your own settings service implementation then
+ // register it with the key "settings" as follows
+ //
+ // container.register('settings', yourSettingsServiceImpl);
+}
+
+/**
+ * Function to register solution specific implementation for logger service. This is an optional function.
+ * By default, an implementation of logger service (i.e., "@aws-ee/base-services/lib/logger/logger-service") is
+ * already registered in the "container"
+ *
+ * @param container Services container
+ * @param pluginRegistry A registry that provides plugins registered by various addons for the specified extension point.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function registerLoggerService(container, pluginRegistry) {
+ // The container has default logger service already registered
+ // If you want to register your own logger service implementation then
+ // register it with the key "log" as follows
+ //
+ // container.register('log', yourLoggerServiceImpl);
+}
+
+const plugin = {
+ registerServices,
+ getStaticSettings,
+ getLoggingContext,
+ getFieldsToMaskInLog: getFieldsToMask,
+ registerSettingsService,
+ registerLoggerService,
+};
+
+module.exports = plugin;
diff --git a/main/solution/post-deployment/src/lambdas/post-deployment/plugins/steps-plugin.js b/main/solution/post-deployment/src/lambdas/post-deployment/plugins/steps-plugin.js
new file mode 100644
index 0000000000..7b66be6e2c
--- /dev/null
+++ b/main/solution/post-deployment/src/lambdas/post-deployment/plugins/steps-plugin.js
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+async function getSteps(existingStepsMap) {
+ const steps = new Map([
+ ...existingStepsMap,
+ // TODO: Add your other post deployment steps here
+ // Example: ['yourStepServiceName', new StepImplementationService()],
+ ]);
+ return steps;
+}
+
+const plugin = {
+ getSteps,
+};
+
+module.exports = plugin;
diff --git a/main/solution/prepare-master-acc/.gitignore b/main/solution/prepare-master-acc/.gitignore
new file mode 100644
index 0000000000..3659bd3a89
--- /dev/null
+++ b/main/solution/prepare-master-acc/.gitignore
@@ -0,0 +1,15 @@
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/main/solution/prepare-master-acc/README.md b/main/solution/prepare-master-acc/README.md
new file mode 100644
index 0000000000..49df9d9e0e
--- /dev/null
+++ b/main/solution/prepare-master-acc/README.md
@@ -0,0 +1,15 @@
+# A utility component to prepare RaaS master account containing main AWS Organization
+
+The component creates the master AWS IAM role in the RaaS master account and adds trust policy in the role
+to grant AssumeRole permissions to the main account. The solution deployed in the main account assumes this role
+to create member account under the AWS Organization in the master account when you use "Create AWS Account" feature.
+If you are not familiar with the terms `main account` vs `master account` vs the `member account` then please
+read ["documentation/aws-accounts-readme.md"](../../documentation/aws-accounts-readme.md) first.
+
+## Packaging and deploying
+
+To deploy:
+
+```bash
+$ pnpx sls deploy --stage
+```
diff --git a/main/solution/prepare-master-acc/config/infra/cloudformation.yml b/main/solution/prepare-master-acc/config/infra/cloudformation.yml
new file mode 100644
index 0000000000..93f19e9e6e
--- /dev/null
+++ b/main/solution/prepare-master-acc/config/infra/cloudformation.yml
@@ -0,0 +1,38 @@
+Resources:
+ MasterRole:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ AWS: arn:aws:iam::${self:custom.settings.mainAccountId}:root
+ Action: sts:AssumeRole
+ Condition:
+ StringEquals:
+ sts:ExternalId: ${self:custom.settings.externalId}
+ Policies:
+ - PolicyName: AllowOrganizationAccessInMasterAcc
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Sid: OrgAccess
+ Effect: Allow
+ Action:
+ - organizations:describeAccount
+ - organizations:describeCreateAccountStatus
+ - organizations:createAccount
+ Resource: "*"
+ - PolicyName: AllowAssumeRoleInMemberAcc
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Sid: AssumeMemberAccOrgRole
+ Effect: Allow
+ Action:
+ - sts:AssumeRole
+ Resource: "arn:aws:iam::*:role/OrganizationAccountAccessRole"
+Outputs:
+ MasterRoleArn:
+ Value: !GetAtt MasterRole.Arn
diff --git a/main/solution/prepare-master-acc/config/settings/.defaults.yml b/main/solution/prepare-master-acc/config/settings/.defaults.yml
new file mode 100644
index 0000000000..98a82aec85
--- /dev/null
+++ b/main/solution/prepare-master-acc/config/settings/.defaults.yml
@@ -0,0 +1 @@
+# No default settings for this component at the moment
\ No newline at end of file
diff --git a/main/solution/prepare-master-acc/config/settings/.settings.js b/main/solution/prepare-master-acc/config/settings/.settings.js
new file mode 100644
index 0000000000..0f0a5acf3b
--- /dev/null
+++ b/main/solution/prepare-master-acc/config/settings/.settings.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports.merged = require('@aws-ee/base-serverless-settings-helper').mergeSettings(__dirname, [
+ '../../../../config/settings/.defaults.yml',
+ './.defaults.yml',
+ '../../../../config/settings/${stage}.yml',
+ './${stage}.yml',
+]);
diff --git a/main/solution/prepare-master-acc/jsconfig.json b/main/solution/prepare-master-acc/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/main/solution/prepare-master-acc/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/main/solution/prepare-master-acc/package.json b/main/solution/prepare-master-acc/package.json
new file mode 100644
index 0000000000..8beaa9e1bc
--- /dev/null
+++ b/main/solution/prepare-master-acc/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@aws-ee/prepare-master-acc",
+ "version": "1.0.0",
+ "private": true,
+ "description": "A utility component to prepare RaaS master account containing main AWS Organization",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "@aws-ee/base-serverless-settings-helper": "workspace:*",
+ "serverless": "^1.63.0",
+ "serverless-deployment-bucket": "^1.1.0"
+ }
+}
diff --git a/main/solution/prepare-master-acc/serverless.yml b/main/solution/prepare-master-acc/serverless.yml
new file mode 100644
index 0000000000..ed6f80f310
--- /dev/null
+++ b/main/solution/prepare-master-acc/serverless.yml
@@ -0,0 +1,30 @@
+# For full config options, see docs.serverless.com
+# Note that most settings in here come from config/settings/*.yaml
+service: ${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}-prep-raas-master
+
+provider:
+ name: aws
+ region: ${self:custom.settings.awsRegion}
+ profile: ${self:custom.settings.awsProfile}
+ stackName: ${self:custom.settings.envName}-${self:service}
+ deploymentBucket:
+ name: ${self:custom.settings.globalNamespace}-raas-master-artifacts
+ stackTags: ${self:custom.tags}
+ versionFunctions: false
+ # All references beginning with ${self:*, ${opt:*, ${file:*, ${deep:*, and ${cf:* will be resolved by Serverless
+ # All other ${* references will be resolved by CloudFormation
+ # See https://forum.serverless.com/t/getting-handle-accountid-in-serverless-config/946/11 and
+ # See https://github.com/serverless/serverless/issues/5011
+ variableSyntax: '\$\{((((self|opt|deep|cf):)|file)((?!\$\{).)+?)}'
+
+custom:
+ settings: ${file(./config/settings/.settings.js):merged}
+ tags:
+ Name: ${self:custom.settings.envName}-${self:service}
+
+resources:
+ - Description: Galileo-Gateway ${self:custom.settings.version} ${self:custom.settings.solutionName} ${self:custom.settings.envName} Master-Account
+ - ${file(./config/infra/cloudformation.yml)}
+
+plugins:
+ - serverless-deployment-bucket
diff --git a/main/solution/ui/.env b/main/solution/ui/.env
new file mode 100644
index 0000000000..99864e9906
--- /dev/null
+++ b/main/solution/ui/.env
@@ -0,0 +1,5 @@
+# The webpack version hoisted at top is higher than the one required by Create React App
+# The hoisting is done by pnpm. Ignore this version conflict by adding the SKIP_PREFLIGHT_CHECK=true in .env
+# Alternative is to downgrade all sibling packages to use same version of webpack or not let pnpm hoist
+# any packages managed by Create React App.
+SKIP_PREFLIGHT_CHECK=true
diff --git a/main/solution/ui/.eslintrc.json b/main/solution/ui/.eslintrc.json
new file mode 100644
index 0000000000..90ac399dcf
--- /dev/null
+++ b/main/solution/ui/.eslintrc.json
@@ -0,0 +1,42 @@
+{
+ "extends": ["airbnb", "prettier", "prettier/react", "plugin:jest/recommended", "eslint-config-prettier"],
+ "parser": "babel-eslint",
+ "plugins": ["jest", "prettier", "react"],
+ "rules": {
+ "prettier/prettier": ["error"],
+ "no-unused-vars": [
+ "error",
+ {
+ "varsIgnorePattern": "^_.+",
+ "argsIgnorePattern": "^_",
+ "caughtErrorsIgnorePattern": "^_",
+ "args": "after-used",
+ "ignoreRestSiblings": true
+ }
+ ],
+ "prefer-destructuring": 0,
+ "no-underscore-dangle": 0,
+ "no-param-reassign": 0,
+ "class-methods-use-this": 0,
+ "no-use-before-define": 0,
+ "no-console": 0,
+ "no-plusplus": 0,
+ "no-nested-ternary": 0,
+ "import/no-named-as-default": 0,
+ "import/no-named-as-default-member": 0,
+ "react/jsx-props-no-spreading": 0,
+ "react/jsx-one-expression-per-line": 0,
+ "react/jsx-filename-extension": 0,
+ "react/destructuring-assignment": 0,
+ "react/sort-comp": 0,
+ "react/prop-types": 0,
+ "jsx-a11y/no-static-element-interactions": 0,
+ "jsx-a11y/click-events-have-key-events": 0
+ },
+ "env": {
+ "browser": true,
+ "node": true,
+ "es6": true,
+ "jest/globals": true
+ }
+}
diff --git a/main/solution/ui/.gitignore b/main/solution/ui/.gitignore
new file mode 100644
index 0000000000..819ec19e55
--- /dev/null
+++ b/main/solution/ui/.gitignore
@@ -0,0 +1,42 @@
+# See https://help.github.com/ignore-files/ for more about ignoring files.
+
+**/.class
+**/.DS_Store
+**/node_modules
+
+**/npm-debug.log
+**/pnpm-debug.log
+**/npm-debug.log*
+
+# Serverless directories
+.serverless
+
+# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# dependencies
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.env.local
+.env.development
+.env.development.local
+.env.test.local
+.env.production
+.env.production.local
+
+yarn-debug.log*
+yarn-error.log*
+
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
diff --git a/main/solution/ui/.prettierrc.json b/main/solution/ui/.prettierrc.json
new file mode 100644
index 0000000000..a333103711
--- /dev/null
+++ b/main/solution/ui/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "tabWidth": 2,
+ "printWidth": 120,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "all"
+}
diff --git a/main/solution/ui/README.md b/main/solution/ui/README.md
new file mode 100644
index 0000000000..8a2e713107
--- /dev/null
+++ b/main/solution/ui/README.md
@@ -0,0 +1,33 @@
+## Packaging and deploying
+
+To package locally (to populate .env.local only)
+
+```
+$ pnpx sls package-ui --stage --local=true
+```
+
+To package for deployment (to populate .env.production and create a build via "npm build")
+
+```
+$ pnpx sls package-ui --stage
+```
+
+To run locally
+
+```
+$ pnpx sls start-ui --stage
+```
+
+To deploy to S3
+
+```
+$ pnpx sls deploy-ui --stage --invalidate-cache=true
+```
+
+## Useful commands
+
+To list all resolved variables
+
+```
+$ pnpx sls print --stage
+```
diff --git a/main/solution/ui/config/environment/env-template.yml b/main/solution/ui/config/environment/env-template.yml
new file mode 100644
index 0000000000..71d4ff5f5c
--- /dev/null
+++ b/main/solution/ui/config/environment/env-template.yml
@@ -0,0 +1,26 @@
+# ========================================================================
+# Variables shared between .env.local and .env.production
+# ========================================================================
+
+SKIP_PREFLIGHT_CHECK: true
+REACT_APP_LOCAL_DEV: false
+REACT_APP_AWS_REGION: ${self:custom.settings.awsRegion}
+REACT_APP_API_URL: ${self:custom.settings.apiUrl}
+REACT_APP_WEBSITE_URL: ${self:custom.settings.websiteUrl}
+REACT_APP_STAGE: ${self:custom.settings.envName}
+REACT_APP_REGION: ${self:custom.settings.awsRegion}
+REACT_APP_BRAND_PAGE_TITLE: ${self:custom.settings.brandPageTitle}
+REACT_APP_BRAND_MAIN_TITLE: ${self:custom.settings.brandMainTitle}
+REACT_APP_BRAND_LOGIN_TITLE: ${self:custom.settings.brandLoginTitle}
+REACT_APP_BRAND_LOGIN_SUBTITLE: ${self:custom.settings.brandLoginSubtitle}
+REACT_APP_AUTO_LOGOUT_TIMEOUT_IN_MINUTES: ${self:custom.settings.autoLogoutTimeoutInMinutes}
+
+# ========================================================================
+# Overrides for .env.local
+# ========================================================================
+
+localOverrides:
+ REACT_APP_LOCAL_DEV: true
+ REACT_APP_API_URL: 'http://localhost:4000'
+ REACT_APP_WEBSITE_URL: 'http://localhost:3000'
+ REACT_APP_BRAND_PAGE_TITLE: LOCAL ${self:custom.settings.brandPageTitle}
diff --git a/main/solution/ui/config/settings/.defaults.yml b/main/solution/ui/config/settings/.defaults.yml
new file mode 100644
index 0000000000..f9f7156252
--- /dev/null
+++ b/main/solution/ui/config/settings/.defaults.yml
@@ -0,0 +1,27 @@
+# The Gateway API endpoint
+apiUrl: ${cf:${self:custom.settings.backendStackName}.ServiceEndpoint}
+
+# The stack name of the 'backend' serverless service
+backendStackName: ${self:custom.settings.namespace}-backend
+
+# The stack name of the 'cloudfront' serverless service
+infrastructureStackName: ${self:custom.settings.namespace}-infrastructure
+
+# The S3 bucket name used to host the static website
+websiteBucketName: ${cf:${self:custom.settings.infrastructureStackName}.WebsiteBucket}
+
+# The id of the CloudFront distribution for the static website
+websiteCloudFrontId: ${cf:${self:custom.settings.infrastructureStackName}.CloudFrontId}
+
+# URL of the website
+websiteUrl: ${cf:${self:custom.settings.infrastructureStackName}.WebsiteUrl}
+
+# Branding
+brandPageTitle: 'Research as a Service'
+brandMainTitle: 'Research as a Service (${self:custom.settings.envName}/${self:custom.settings.awsRegion})'
+brandLoginTitle: 'RaaS'
+brandLoginSubtitle: 'Research as a Service Portal (${self:custom.settings.envName}/${self:custom.settings.awsRegion})'
+
+# After how many minutes should the auto logout dialog be displayed? once displayed the user has 1 minute to dismiss
+# the dialog, otherwise they will be automatically logged out
+autoLogoutTimeoutInMinutes: 5
diff --git a/main/solution/ui/config/settings/.settings.js b/main/solution/ui/config/settings/.settings.js
new file mode 100644
index 0000000000..0f0a5acf3b
--- /dev/null
+++ b/main/solution/ui/config/settings/.settings.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports.merged = require('@aws-ee/base-serverless-settings-helper').mergeSettings(__dirname, [
+ '../../../../config/settings/.defaults.yml',
+ './.defaults.yml',
+ '../../../../config/settings/${stage}.yml',
+ './${stage}.yml',
+]);
diff --git a/main/solution/ui/jest.config.js b/main/solution/ui/jest.config.js
new file mode 100644
index 0000000000..60c221fc1d
--- /dev/null
+++ b/main/solution/ui/jest.config.js
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+// jest.config.js
+module.exports = {
+ // verbose: true,
+ notify: false,
+ testEnvironment: 'node',
+ // testPathIgnorePatterns: ['service.test.js'],
+ // Configure JUnit reporter as CodeBuild currently only supports JUnit or Cucumber reports
+ // See https://docs.aws.amazon.com/codebuild/latest/userguide/test-reporting.html
+ reporters: ['default', ['jest-junit', { suiteName: 'jest tests', outputDirectory: './.build/test' }]],
+};
diff --git a/main/solution/ui/jsconfig.json b/main/solution/ui/jsconfig.json
new file mode 100644
index 0000000000..780d3afae6
--- /dev/null
+++ b/main/solution/ui/jsconfig.json
@@ -0,0 +1,6 @@
+{
+ "exclude": [
+ "node_modules",
+ "**/node_modules/*"
+ ]
+}
\ No newline at end of file
diff --git a/main/solution/ui/package.json b/main/solution/ui/package.json
new file mode 100644
index 0000000000..e23f4d1c0e
--- /dev/null
+++ b/main/solution/ui/package.json
@@ -0,0 +1,78 @@
+{
+ "name": "@aws-ee/ui",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Client-side UI application",
+ "author": "Amazon Web Services",
+ "license": "Apache-2.0",
+ "homepage": "/",
+ "proxy": "http://localhost:4000",
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 11",
+ "not op_mini all"
+ ],
+ "dependencies": {
+ "@aws-ee/base-ui": "workspace:*",
+ "@aws-ee/base-workflow-ui": "workspace:*",
+ "@aws-ee/base-raas-ui": "workspace:*",
+ "aws-sdk": "^2.647.0",
+ "prop-types": "^15.7.2",
+ "lodash": "^4.17.15",
+ "mobx": "^5.15.4",
+ "mobx-react": "^6.1.7",
+ "mobx-react-form": "^2.0.8",
+ "mobx-state-tree": "^3.15.0",
+ "react": "^16.12.0",
+ "react-avatar": "^3.9.0",
+ "react-dom": "^16.12.0",
+ "react-router-dom": "^5.1.2",
+ "react-table": "^6.11.5",
+ "toastr": "^2.1.4",
+ "semantic-ui-react": "^0.88.2",
+ "typeface-lato": "0.0.75",
+ "uuid": "^3.3.3"
+ },
+ "devDependencies": {
+ "@aws-ee/base-serverless-settings-helper": "workspace:*",
+ "@aws-ee/base-serverless-ui-tools": "workspace:*",
+ "babel-eslint": "^10.0.3",
+ "eslint": "^6.8.0",
+ "eslint-config-airbnb": "^18.0.1",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-import-resolver-node": "^0.3.3",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jest": "^22.21.0",
+ "eslint-plugin-jsx-a11y": "^6.2.3",
+ "eslint-plugin-prettier": "^3.1.2",
+ "eslint-plugin-react": "^7.18.3",
+ "eslint-plugin-react-hooks": "^2.0.1",
+ "husky": "^3.1.0",
+ "jest": "^24.9.0",
+ "jest-junit": "^10.0.0",
+ "prettier": "^1.19.1",
+ "pretty-quick": "^1.11.1",
+ "react-scripts": "^3.3.1",
+ "serverless": "^1.63.0",
+ "serverless-deployment-bucket": "^1.1.0"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "INLINE_RUNTIME_CHUNK=false react-scripts build",
+ "test": "react-scripts test --watchAll=false --passWithNoTests",
+ "test:watch": "react-scripts test --passWithNoTests",
+ "eject": "react-scripts eject",
+ "lint": "pnpm run lint:eslint && pnpm run lint:prettier",
+ "lint:eslint": "eslint --ignore-path .gitignore . ",
+ "lint:prettier": "prettier --check --ignore-path .gitignore '**/*.{js,jsx}' ",
+ "format": "pnpm run format:eslint && pnpm run format:prettier",
+ "format:eslint": "eslint --fix --ignore-path .gitignore . ",
+ "format:prettier": "prettier --write --ignore-path .gitignore '**/*.{js,jsx}' "
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx)'"
+ }
+ }
+}
diff --git a/main/solution/ui/public/favicon-32x32.png b/main/solution/ui/public/favicon-32x32.png
new file mode 100644
index 0000000000..d4944a98af
Binary files /dev/null and b/main/solution/ui/public/favicon-32x32.png differ
diff --git a/main/solution/ui/public/favicon.ico b/main/solution/ui/public/favicon.ico
new file mode 100644
index 0000000000..18196ffd79
Binary files /dev/null and b/main/solution/ui/public/favicon.ico differ
diff --git a/main/solution/ui/public/index.html b/main/solution/ui/public/index.html
new file mode 100644
index 0000000000..453c827498
--- /dev/null
+++ b/main/solution/ui/public/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/main/solution/ui/serverless.yml b/main/solution/ui/serverless.yml
new file mode 100644
index 0000000000..b522254480
--- /dev/null
+++ b/main/solution/ui/serverless.yml
@@ -0,0 +1,31 @@
+# For full config options, see docs.serverless.com
+# Note that most settings in here come from config/settings/*.yaml
+service: ${self:custom.settings.awsRegionShortName}-${self:custom.settings.solutionName}-ui
+
+provider:
+ name: aws
+ region: ${self:custom.settings.awsRegion}
+ profile: ${self:custom.settings.awsProfile}
+ stackName: ${self:custom.settings.envName}-${self:service}
+ deploymentBucket:
+ name: ${self:custom.settings.deploymentBucketName}
+ serverSideEncryption: AES256
+ # All references beginning with ${self:*, ${opt:*, ${file:*, ${deep:*, and ${cf:* will be resolved by Serverless
+ # All other ${* references will be resolved by CloudFormation
+ # See https://forum.serverless.com/t/getting-handle-accountid-in-serverless-config/946/11 and
+ # See https://github.com/serverless/serverless/issues/5011
+ variableSyntax: '\$\{((((self|opt|deep|cf):)|file)((?!\$\{).)+?)}'
+
+
+custom:
+ settings: ${file(./config/settings/.settings.js):merged}
+ envTemplate: ${file(./config/environment/env-template.yml)}
+ deploymentBucket:
+ policy: ${self:custom.settings.deploymentBucketPolicy}
+
+resources:
+ - Description: Galileo-Gateway ${self:custom.settings.version} ${self:custom.settings.solutionName} ${self:custom.settings.envName} UI
+
+plugins:
+ - serverless-deployment-bucket
+ - '@aws-ee/base-serverless-ui-tools'
diff --git a/main/solution/ui/src/App.js b/main/solution/ui/src/App.js
new file mode 100644
index 0000000000..e862e45618
--- /dev/null
+++ b/main/solution/ui/src/App.js
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+// Are you looking for the main App react component that is used to render the app?
+// You can find the default App react component in
+// addons/addon-base-ui/packages/base-ui/src/App.js
+// Keep in mind that plugins might choose to provide a different App react component as
+// part of the extension point 'app-component'
diff --git a/main/solution/ui/src/css/animate.css b/main/solution/ui/src/css/animate.css
new file mode 100644
index 0000000000..dac48f174f
--- /dev/null
+++ b/main/solution/ui/src/css/animate.css
@@ -0,0 +1,3623 @@
+@charset "UTF-8";
+
+/*!
+ * animate.css -http://daneden.me/animate
+ * Version - 3.7.0
+ * Licensed under the MIT license - http://opensource.org/licenses/MIT
+ *
+ * Copyright (c) 2018 Daniel Eden
+ */
+
+@-webkit-keyframes bounce {
+ from,
+ 20%,
+ 53%,
+ 80%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 40%,
+ 43% {
+ -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
+ -webkit-transform: translate3d(0, -30px, 0);
+ transform: translate3d(0, -30px, 0);
+ }
+
+ 70% {
+ -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
+ -webkit-transform: translate3d(0, -15px, 0);
+ transform: translate3d(0, -15px, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(0, -4px, 0);
+ transform: translate3d(0, -4px, 0);
+ }
+}
+
+@keyframes bounce {
+ from,
+ 20%,
+ 53%,
+ 80%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 40%,
+ 43% {
+ -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
+ -webkit-transform: translate3d(0, -30px, 0);
+ transform: translate3d(0, -30px, 0);
+ }
+
+ 70% {
+ -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
+ -webkit-transform: translate3d(0, -15px, 0);
+ transform: translate3d(0, -15px, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(0, -4px, 0);
+ transform: translate3d(0, -4px, 0);
+ }
+}
+
+.bounce {
+ -webkit-animation-name: bounce;
+ animation-name: bounce;
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+}
+
+@-webkit-keyframes flash {
+ from,
+ 50%,
+ to {
+ opacity: 1;
+ }
+
+ 25%,
+ 75% {
+ opacity: 0;
+ }
+}
+
+@keyframes flash {
+ from,
+ 50%,
+ to {
+ opacity: 1;
+ }
+
+ 25%,
+ 75% {
+ opacity: 0;
+ }
+}
+
+.flash {
+ -webkit-animation-name: flash;
+ animation-name: flash;
+}
+
+/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */
+
+@-webkit-keyframes pulse {
+ from {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 50% {
+ -webkit-transform: scale3d(1.05, 1.05, 1.05);
+ transform: scale3d(1.05, 1.05, 1.05);
+ }
+
+ to {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+@keyframes pulse {
+ from {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 50% {
+ -webkit-transform: scale3d(1.05, 1.05, 1.05);
+ transform: scale3d(1.05, 1.05, 1.05);
+ }
+
+ to {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+.pulse {
+ -webkit-animation-name: pulse;
+ animation-name: pulse;
+}
+
+@-webkit-keyframes rubberBand {
+ from {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 30% {
+ -webkit-transform: scale3d(1.25, 0.75, 1);
+ transform: scale3d(1.25, 0.75, 1);
+ }
+
+ 40% {
+ -webkit-transform: scale3d(0.75, 1.25, 1);
+ transform: scale3d(0.75, 1.25, 1);
+ }
+
+ 50% {
+ -webkit-transform: scale3d(1.15, 0.85, 1);
+ transform: scale3d(1.15, 0.85, 1);
+ }
+
+ 65% {
+ -webkit-transform: scale3d(0.95, 1.05, 1);
+ transform: scale3d(0.95, 1.05, 1);
+ }
+
+ 75% {
+ -webkit-transform: scale3d(1.05, 0.95, 1);
+ transform: scale3d(1.05, 0.95, 1);
+ }
+
+ to {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+@keyframes rubberBand {
+ from {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 30% {
+ -webkit-transform: scale3d(1.25, 0.75, 1);
+ transform: scale3d(1.25, 0.75, 1);
+ }
+
+ 40% {
+ -webkit-transform: scale3d(0.75, 1.25, 1);
+ transform: scale3d(0.75, 1.25, 1);
+ }
+
+ 50% {
+ -webkit-transform: scale3d(1.15, 0.85, 1);
+ transform: scale3d(1.15, 0.85, 1);
+ }
+
+ 65% {
+ -webkit-transform: scale3d(0.95, 1.05, 1);
+ transform: scale3d(0.95, 1.05, 1);
+ }
+
+ 75% {
+ -webkit-transform: scale3d(1.05, 0.95, 1);
+ transform: scale3d(1.05, 0.95, 1);
+ }
+
+ to {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+.rubberBand {
+ -webkit-animation-name: rubberBand;
+ animation-name: rubberBand;
+}
+
+@-webkit-keyframes shake {
+ from,
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 10%,
+ 30%,
+ 50%,
+ 70%,
+ 90% {
+ -webkit-transform: translate3d(-10px, 0, 0);
+ transform: translate3d(-10px, 0, 0);
+ }
+
+ 20%,
+ 40%,
+ 60%,
+ 80% {
+ -webkit-transform: translate3d(10px, 0, 0);
+ transform: translate3d(10px, 0, 0);
+ }
+}
+
+@keyframes shake {
+ from,
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 10%,
+ 30%,
+ 50%,
+ 70%,
+ 90% {
+ -webkit-transform: translate3d(-10px, 0, 0);
+ transform: translate3d(-10px, 0, 0);
+ }
+
+ 20%,
+ 40%,
+ 60%,
+ 80% {
+ -webkit-transform: translate3d(10px, 0, 0);
+ transform: translate3d(10px, 0, 0);
+ }
+}
+
+.shake {
+ -webkit-animation-name: shake;
+ animation-name: shake;
+}
+
+@-webkit-keyframes headShake {
+ 0% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+
+ 6.5% {
+ -webkit-transform: translateX(-6px) rotateY(-9deg);
+ transform: translateX(-6px) rotateY(-9deg);
+ }
+
+ 18.5% {
+ -webkit-transform: translateX(5px) rotateY(7deg);
+ transform: translateX(5px) rotateY(7deg);
+ }
+
+ 31.5% {
+ -webkit-transform: translateX(-3px) rotateY(-5deg);
+ transform: translateX(-3px) rotateY(-5deg);
+ }
+
+ 43.5% {
+ -webkit-transform: translateX(2px) rotateY(3deg);
+ transform: translateX(2px) rotateY(3deg);
+ }
+
+ 50% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+}
+
+@keyframes headShake {
+ 0% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+
+ 6.5% {
+ -webkit-transform: translateX(-6px) rotateY(-9deg);
+ transform: translateX(-6px) rotateY(-9deg);
+ }
+
+ 18.5% {
+ -webkit-transform: translateX(5px) rotateY(7deg);
+ transform: translateX(5px) rotateY(7deg);
+ }
+
+ 31.5% {
+ -webkit-transform: translateX(-3px) rotateY(-5deg);
+ transform: translateX(-3px) rotateY(-5deg);
+ }
+
+ 43.5% {
+ -webkit-transform: translateX(2px) rotateY(3deg);
+ transform: translateX(2px) rotateY(3deg);
+ }
+
+ 50% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+}
+
+.headShake {
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ -webkit-animation-name: headShake;
+ animation-name: headShake;
+}
+
+@-webkit-keyframes swing {
+ 20% {
+ -webkit-transform: rotate3d(0, 0, 1, 15deg);
+ transform: rotate3d(0, 0, 1, 15deg);
+ }
+
+ 40% {
+ -webkit-transform: rotate3d(0, 0, 1, -10deg);
+ transform: rotate3d(0, 0, 1, -10deg);
+ }
+
+ 60% {
+ -webkit-transform: rotate3d(0, 0, 1, 5deg);
+ transform: rotate3d(0, 0, 1, 5deg);
+ }
+
+ 80% {
+ -webkit-transform: rotate3d(0, 0, 1, -5deg);
+ transform: rotate3d(0, 0, 1, -5deg);
+ }
+
+ to {
+ -webkit-transform: rotate3d(0, 0, 1, 0deg);
+ transform: rotate3d(0, 0, 1, 0deg);
+ }
+}
+
+@keyframes swing {
+ 20% {
+ -webkit-transform: rotate3d(0, 0, 1, 15deg);
+ transform: rotate3d(0, 0, 1, 15deg);
+ }
+
+ 40% {
+ -webkit-transform: rotate3d(0, 0, 1, -10deg);
+ transform: rotate3d(0, 0, 1, -10deg);
+ }
+
+ 60% {
+ -webkit-transform: rotate3d(0, 0, 1, 5deg);
+ transform: rotate3d(0, 0, 1, 5deg);
+ }
+
+ 80% {
+ -webkit-transform: rotate3d(0, 0, 1, -5deg);
+ transform: rotate3d(0, 0, 1, -5deg);
+ }
+
+ to {
+ -webkit-transform: rotate3d(0, 0, 1, 0deg);
+ transform: rotate3d(0, 0, 1, 0deg);
+ }
+}
+
+.swing {
+ -webkit-transform-origin: top center;
+ transform-origin: top center;
+ -webkit-animation-name: swing;
+ animation-name: swing;
+}
+
+@-webkit-keyframes tada {
+ from {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 10%,
+ 20% {
+ -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
+ transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
+ }
+
+ 30%,
+ 50%,
+ 70%,
+ 90% {
+ -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
+ transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
+ }
+
+ 40%,
+ 60%,
+ 80% {
+ -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
+ transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
+ }
+
+ to {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+@keyframes tada {
+ from {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+
+ 10%,
+ 20% {
+ -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
+ transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
+ }
+
+ 30%,
+ 50%,
+ 70%,
+ 90% {
+ -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
+ transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
+ }
+
+ 40%,
+ 60%,
+ 80% {
+ -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
+ transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
+ }
+
+ to {
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+.tada {
+ -webkit-animation-name: tada;
+ animation-name: tada;
+}
+
+/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */
+
+@-webkit-keyframes wobble {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 15% {
+ -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
+ transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
+ }
+
+ 30% {
+ -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
+ transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
+ }
+
+ 45% {
+ -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
+ transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
+ }
+
+ 60% {
+ -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
+ transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
+ transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes wobble {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 15% {
+ -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
+ transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
+ }
+
+ 30% {
+ -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
+ transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
+ }
+
+ 45% {
+ -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
+ transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
+ }
+
+ 60% {
+ -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
+ transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
+ transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.wobble {
+ -webkit-animation-name: wobble;
+ animation-name: wobble;
+}
+
+@-webkit-keyframes jello {
+ from,
+ 11.1%,
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 22.2% {
+ -webkit-transform: skewX(-12.5deg) skewY(-12.5deg);
+ transform: skewX(-12.5deg) skewY(-12.5deg);
+ }
+
+ 33.3% {
+ -webkit-transform: skewX(6.25deg) skewY(6.25deg);
+ transform: skewX(6.25deg) skewY(6.25deg);
+ }
+
+ 44.4% {
+ -webkit-transform: skewX(-3.125deg) skewY(-3.125deg);
+ transform: skewX(-3.125deg) skewY(-3.125deg);
+ }
+
+ 55.5% {
+ -webkit-transform: skewX(1.5625deg) skewY(1.5625deg);
+ transform: skewX(1.5625deg) skewY(1.5625deg);
+ }
+
+ 66.6% {
+ -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg);
+ transform: skewX(-0.78125deg) skewY(-0.78125deg);
+ }
+
+ 77.7% {
+ -webkit-transform: skewX(0.390625deg) skewY(0.390625deg);
+ transform: skewX(0.390625deg) skewY(0.390625deg);
+ }
+
+ 88.8% {
+ -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg);
+ transform: skewX(-0.1953125deg) skewY(-0.1953125deg);
+ }
+}
+
+@keyframes jello {
+ from,
+ 11.1%,
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 22.2% {
+ -webkit-transform: skewX(-12.5deg) skewY(-12.5deg);
+ transform: skewX(-12.5deg) skewY(-12.5deg);
+ }
+
+ 33.3% {
+ -webkit-transform: skewX(6.25deg) skewY(6.25deg);
+ transform: skewX(6.25deg) skewY(6.25deg);
+ }
+
+ 44.4% {
+ -webkit-transform: skewX(-3.125deg) skewY(-3.125deg);
+ transform: skewX(-3.125deg) skewY(-3.125deg);
+ }
+
+ 55.5% {
+ -webkit-transform: skewX(1.5625deg) skewY(1.5625deg);
+ transform: skewX(1.5625deg) skewY(1.5625deg);
+ }
+
+ 66.6% {
+ -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg);
+ transform: skewX(-0.78125deg) skewY(-0.78125deg);
+ }
+
+ 77.7% {
+ -webkit-transform: skewX(0.390625deg) skewY(0.390625deg);
+ transform: skewX(0.390625deg) skewY(0.390625deg);
+ }
+
+ 88.8% {
+ -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg);
+ transform: skewX(-0.1953125deg) skewY(-0.1953125deg);
+ }
+}
+
+.jello {
+ -webkit-animation-name: jello;
+ animation-name: jello;
+ -webkit-transform-origin: center;
+ transform-origin: center;
+}
+
+@-webkit-keyframes heartBeat {
+ 0% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+
+ 14% {
+ -webkit-transform: scale(1.3);
+ transform: scale(1.3);
+ }
+
+ 28% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+
+ 42% {
+ -webkit-transform: scale(1.3);
+ transform: scale(1.3);
+ }
+
+ 70% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+}
+
+@keyframes heartBeat {
+ 0% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+
+ 14% {
+ -webkit-transform: scale(1.3);
+ transform: scale(1.3);
+ }
+
+ 28% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+
+ 42% {
+ -webkit-transform: scale(1.3);
+ transform: scale(1.3);
+ }
+
+ 70% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+}
+
+.heartBeat {
+ -webkit-animation-name: heartBeat;
+ animation-name: heartBeat;
+ -webkit-animation-duration: 1.3s;
+ animation-duration: 1.3s;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+}
+
+@-webkit-keyframes bounceIn {
+ from,
+ 20%,
+ 40%,
+ 60%,
+ 80%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(0.3, 0.3, 0.3);
+ transform: scale3d(0.3, 0.3, 0.3);
+ }
+
+ 20% {
+ -webkit-transform: scale3d(1.1, 1.1, 1.1);
+ transform: scale3d(1.1, 1.1, 1.1);
+ }
+
+ 40% {
+ -webkit-transform: scale3d(0.9, 0.9, 0.9);
+ transform: scale3d(0.9, 0.9, 0.9);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(1.03, 1.03, 1.03);
+ transform: scale3d(1.03, 1.03, 1.03);
+ }
+
+ 80% {
+ -webkit-transform: scale3d(0.97, 0.97, 0.97);
+ transform: scale3d(0.97, 0.97, 0.97);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+@keyframes bounceIn {
+ from,
+ 20%,
+ 40%,
+ 60%,
+ 80%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: scale3d(0.3, 0.3, 0.3);
+ transform: scale3d(0.3, 0.3, 0.3);
+ }
+
+ 20% {
+ -webkit-transform: scale3d(1.1, 1.1, 1.1);
+ transform: scale3d(1.1, 1.1, 1.1);
+ }
+
+ 40% {
+ -webkit-transform: scale3d(0.9, 0.9, 0.9);
+ transform: scale3d(0.9, 0.9, 0.9);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(1.03, 1.03, 1.03);
+ transform: scale3d(1.03, 1.03, 1.03);
+ }
+
+ 80% {
+ -webkit-transform: scale3d(0.97, 0.97, 0.97);
+ transform: scale3d(0.97, 0.97, 0.97);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: scale3d(1, 1, 1);
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+.bounceIn {
+ -webkit-animation-duration: 0.75s;
+ animation-duration: 0.75s;
+ -webkit-animation-name: bounceIn;
+ animation-name: bounceIn;
+}
+
+@-webkit-keyframes bounceInDown {
+ from,
+ 60%,
+ 75%,
+ 90%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -3000px, 0);
+ transform: translate3d(0, -3000px, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 25px, 0);
+ transform: translate3d(0, 25px, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(0, -10px, 0);
+ transform: translate3d(0, -10px, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(0, 5px, 0);
+ transform: translate3d(0, 5px, 0);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes bounceInDown {
+ from,
+ 60%,
+ 75%,
+ 90%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -3000px, 0);
+ transform: translate3d(0, -3000px, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 25px, 0);
+ transform: translate3d(0, 25px, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(0, -10px, 0);
+ transform: translate3d(0, -10px, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(0, 5px, 0);
+ transform: translate3d(0, 5px, 0);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.bounceInDown {
+ -webkit-animation-name: bounceInDown;
+ animation-name: bounceInDown;
+}
+
+@-webkit-keyframes bounceInLeft {
+ from,
+ 60%,
+ 75%,
+ 90%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(-3000px, 0, 0);
+ transform: translate3d(-3000px, 0, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(25px, 0, 0);
+ transform: translate3d(25px, 0, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(-10px, 0, 0);
+ transform: translate3d(-10px, 0, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(5px, 0, 0);
+ transform: translate3d(5px, 0, 0);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes bounceInLeft {
+ from,
+ 60%,
+ 75%,
+ 90%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ 0% {
+ opacity: 0;
+ -webkit-transform: translate3d(-3000px, 0, 0);
+ transform: translate3d(-3000px, 0, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(25px, 0, 0);
+ transform: translate3d(25px, 0, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(-10px, 0, 0);
+ transform: translate3d(-10px, 0, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(5px, 0, 0);
+ transform: translate3d(5px, 0, 0);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.bounceInLeft {
+ -webkit-animation-name: bounceInLeft;
+ animation-name: bounceInLeft;
+}
+
+@-webkit-keyframes bounceInRight {
+ from,
+ 60%,
+ 75%,
+ 90%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(3000px, 0, 0);
+ transform: translate3d(3000px, 0, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(-25px, 0, 0);
+ transform: translate3d(-25px, 0, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(10px, 0, 0);
+ transform: translate3d(10px, 0, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(-5px, 0, 0);
+ transform: translate3d(-5px, 0, 0);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes bounceInRight {
+ from,
+ 60%,
+ 75%,
+ 90%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(3000px, 0, 0);
+ transform: translate3d(3000px, 0, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(-25px, 0, 0);
+ transform: translate3d(-25px, 0, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(10px, 0, 0);
+ transform: translate3d(10px, 0, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(-5px, 0, 0);
+ transform: translate3d(-5px, 0, 0);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.bounceInRight {
+ -webkit-animation-name: bounceInRight;
+ animation-name: bounceInRight;
+}
+
+@-webkit-keyframes bounceInUp {
+ from,
+ 60%,
+ 75%,
+ 90%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 3000px, 0);
+ transform: translate3d(0, 3000px, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, -20px, 0);
+ transform: translate3d(0, -20px, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(0, 10px, 0);
+ transform: translate3d(0, 10px, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(0, -5px, 0);
+ transform: translate3d(0, -5px, 0);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes bounceInUp {
+ from,
+ 60%,
+ 75%,
+ 90%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 3000px, 0);
+ transform: translate3d(0, 3000px, 0);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, -20px, 0);
+ transform: translate3d(0, -20px, 0);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(0, 10px, 0);
+ transform: translate3d(0, 10px, 0);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(0, -5px, 0);
+ transform: translate3d(0, -5px, 0);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.bounceInUp {
+ -webkit-animation-name: bounceInUp;
+ animation-name: bounceInUp;
+}
+
+@-webkit-keyframes bounceOut {
+ 20% {
+ -webkit-transform: scale3d(0.9, 0.9, 0.9);
+ transform: scale3d(0.9, 0.9, 0.9);
+ }
+
+ 50%,
+ 55% {
+ opacity: 1;
+ -webkit-transform: scale3d(1.1, 1.1, 1.1);
+ transform: scale3d(1.1, 1.1, 1.1);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: scale3d(0.3, 0.3, 0.3);
+ transform: scale3d(0.3, 0.3, 0.3);
+ }
+}
+
+@keyframes bounceOut {
+ 20% {
+ -webkit-transform: scale3d(0.9, 0.9, 0.9);
+ transform: scale3d(0.9, 0.9, 0.9);
+ }
+
+ 50%,
+ 55% {
+ opacity: 1;
+ -webkit-transform: scale3d(1.1, 1.1, 1.1);
+ transform: scale3d(1.1, 1.1, 1.1);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: scale3d(0.3, 0.3, 0.3);
+ transform: scale3d(0.3, 0.3, 0.3);
+ }
+}
+
+.bounceOut {
+ -webkit-animation-duration: 0.75s;
+ animation-duration: 0.75s;
+ -webkit-animation-name: bounceOut;
+ animation-name: bounceOut;
+}
+
+@-webkit-keyframes bounceOutDown {
+ 20% {
+ -webkit-transform: translate3d(0, 10px, 0);
+ transform: translate3d(0, 10px, 0);
+ }
+
+ 40%,
+ 45% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, -20px, 0);
+ transform: translate3d(0, -20px, 0);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 2000px, 0);
+ transform: translate3d(0, 2000px, 0);
+ }
+}
+
+@keyframes bounceOutDown {
+ 20% {
+ -webkit-transform: translate3d(0, 10px, 0);
+ transform: translate3d(0, 10px, 0);
+ }
+
+ 40%,
+ 45% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, -20px, 0);
+ transform: translate3d(0, -20px, 0);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 2000px, 0);
+ transform: translate3d(0, 2000px, 0);
+ }
+}
+
+.bounceOutDown {
+ -webkit-animation-name: bounceOutDown;
+ animation-name: bounceOutDown;
+}
+
+@-webkit-keyframes bounceOutLeft {
+ 20% {
+ opacity: 1;
+ -webkit-transform: translate3d(20px, 0, 0);
+ transform: translate3d(20px, 0, 0);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(-2000px, 0, 0);
+ transform: translate3d(-2000px, 0, 0);
+ }
+}
+
+@keyframes bounceOutLeft {
+ 20% {
+ opacity: 1;
+ -webkit-transform: translate3d(20px, 0, 0);
+ transform: translate3d(20px, 0, 0);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(-2000px, 0, 0);
+ transform: translate3d(-2000px, 0, 0);
+ }
+}
+
+.bounceOutLeft {
+ -webkit-animation-name: bounceOutLeft;
+ animation-name: bounceOutLeft;
+}
+
+@-webkit-keyframes bounceOutRight {
+ 20% {
+ opacity: 1;
+ -webkit-transform: translate3d(-20px, 0, 0);
+ transform: translate3d(-20px, 0, 0);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(2000px, 0, 0);
+ transform: translate3d(2000px, 0, 0);
+ }
+}
+
+@keyframes bounceOutRight {
+ 20% {
+ opacity: 1;
+ -webkit-transform: translate3d(-20px, 0, 0);
+ transform: translate3d(-20px, 0, 0);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(2000px, 0, 0);
+ transform: translate3d(2000px, 0, 0);
+ }
+}
+
+.bounceOutRight {
+ -webkit-animation-name: bounceOutRight;
+ animation-name: bounceOutRight;
+}
+
+@-webkit-keyframes bounceOutUp {
+ 20% {
+ -webkit-transform: translate3d(0, -10px, 0);
+ transform: translate3d(0, -10px, 0);
+ }
+
+ 40%,
+ 45% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 20px, 0);
+ transform: translate3d(0, 20px, 0);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -2000px, 0);
+ transform: translate3d(0, -2000px, 0);
+ }
+}
+
+@keyframes bounceOutUp {
+ 20% {
+ -webkit-transform: translate3d(0, -10px, 0);
+ transform: translate3d(0, -10px, 0);
+ }
+
+ 40%,
+ 45% {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 20px, 0);
+ transform: translate3d(0, 20px, 0);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -2000px, 0);
+ transform: translate3d(0, -2000px, 0);
+ }
+}
+
+.bounceOutUp {
+ -webkit-animation-name: bounceOutUp;
+ animation-name: bounceOutUp;
+}
+
+@-webkit-keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+.fadeIn {
+ -webkit-animation-name: fadeIn;
+ animation-name: fadeIn;
+}
+
+@-webkit-keyframes fadeInDown {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes fadeInDown {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.fadeInDown {
+ -webkit-animation-name: fadeInDown;
+ animation-name: fadeInDown;
+}
+
+@-webkit-keyframes fadeInDownBig {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -2000px, 0);
+ transform: translate3d(0, -2000px, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes fadeInDownBig {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -2000px, 0);
+ transform: translate3d(0, -2000px, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.fadeInDownBig {
+ -webkit-animation-name: fadeInDownBig;
+ animation-name: fadeInDownBig;
+}
+
+@-webkit-keyframes fadeInLeft {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes fadeInLeft {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.fadeInLeft {
+ -webkit-animation-name: fadeInLeft;
+ animation-name: fadeInLeft;
+}
+
+@-webkit-keyframes fadeInLeftBig {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(-2000px, 0, 0);
+ transform: translate3d(-2000px, 0, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes fadeInLeftBig {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(-2000px, 0, 0);
+ transform: translate3d(-2000px, 0, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.fadeInLeftBig {
+ -webkit-animation-name: fadeInLeftBig;
+ animation-name: fadeInLeftBig;
+}
+
+@-webkit-keyframes fadeInRight {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes fadeInRight {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.fadeInRight {
+ -webkit-animation-name: fadeInRight;
+ animation-name: fadeInRight;
+}
+
+@-webkit-keyframes fadeInRightBig {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(2000px, 0, 0);
+ transform: translate3d(2000px, 0, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes fadeInRightBig {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(2000px, 0, 0);
+ transform: translate3d(2000px, 0, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.fadeInRightBig {
+ -webkit-animation-name: fadeInRightBig;
+ animation-name: fadeInRightBig;
+}
+
+@-webkit-keyframes fadeInUp {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.fadeInUp {
+ -webkit-animation-name: fadeInUp;
+ animation-name: fadeInUp;
+}
+
+@-webkit-keyframes fadeInUpBig {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 2000px, 0);
+ transform: translate3d(0, 2000px, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes fadeInUpBig {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 2000px, 0);
+ transform: translate3d(0, 2000px, 0);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.fadeInUpBig {
+ -webkit-animation-name: fadeInUpBig;
+ animation-name: fadeInUpBig;
+}
+
+@-webkit-keyframes fadeOut {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ }
+}
+
+@keyframes fadeOut {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ }
+}
+
+.fadeOut {
+ -webkit-animation-name: fadeOut;
+ animation-name: fadeOut;
+}
+
+@-webkit-keyframes fadeOutDown {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0);
+ }
+}
+
+@keyframes fadeOutDown {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0);
+ }
+}
+
+.fadeOutDown {
+ -webkit-animation-name: fadeOutDown;
+ animation-name: fadeOutDown;
+}
+
+@-webkit-keyframes fadeOutDownBig {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 2000px, 0);
+ transform: translate3d(0, 2000px, 0);
+ }
+}
+
+@keyframes fadeOutDownBig {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(0, 2000px, 0);
+ transform: translate3d(0, 2000px, 0);
+ }
+}
+
+.fadeOutDownBig {
+ -webkit-animation-name: fadeOutDownBig;
+ animation-name: fadeOutDownBig;
+}
+
+@-webkit-keyframes fadeOutLeft {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ }
+}
+
+@keyframes fadeOutLeft {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ }
+}
+
+.fadeOutLeft {
+ -webkit-animation-name: fadeOutLeft;
+ animation-name: fadeOutLeft;
+}
+
+@-webkit-keyframes fadeOutLeftBig {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(-2000px, 0, 0);
+ transform: translate3d(-2000px, 0, 0);
+ }
+}
+
+@keyframes fadeOutLeftBig {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(-2000px, 0, 0);
+ transform: translate3d(-2000px, 0, 0);
+ }
+}
+
+.fadeOutLeftBig {
+ -webkit-animation-name: fadeOutLeftBig;
+ animation-name: fadeOutLeftBig;
+}
+
+@-webkit-keyframes fadeOutRight {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ }
+}
+
+@keyframes fadeOutRight {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ }
+}
+
+.fadeOutRight {
+ -webkit-animation-name: fadeOutRight;
+ animation-name: fadeOutRight;
+}
+
+@-webkit-keyframes fadeOutRightBig {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(2000px, 0, 0);
+ transform: translate3d(2000px, 0, 0);
+ }
+}
+
+@keyframes fadeOutRightBig {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(2000px, 0, 0);
+ transform: translate3d(2000px, 0, 0);
+ }
+}
+
+.fadeOutRightBig {
+ -webkit-animation-name: fadeOutRightBig;
+ animation-name: fadeOutRightBig;
+}
+
+@-webkit-keyframes fadeOutUp {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0);
+ }
+}
+
+@keyframes fadeOutUp {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0);
+ }
+}
+
+.fadeOutUp {
+ -webkit-animation-name: fadeOutUp;
+ animation-name: fadeOutUp;
+}
+
+@-webkit-keyframes fadeOutUpBig {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -2000px, 0);
+ transform: translate3d(0, -2000px, 0);
+ }
+}
+
+@keyframes fadeOutUpBig {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(0, -2000px, 0);
+ transform: translate3d(0, -2000px, 0);
+ }
+}
+
+.fadeOutUpBig {
+ -webkit-animation-name: fadeOutUpBig;
+ animation-name: fadeOutUpBig;
+}
+
+@-webkit-keyframes flip {
+ from {
+ -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0)
+ rotate3d(0, 1, 0, -360deg);
+ transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, -360deg);
+ -webkit-animation-timing-function: ease-out;
+ animation-timing-function: ease-out;
+ }
+
+ 40% {
+ -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px)
+ rotate3d(0, 1, 0, -190deg);
+ transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px)
+ rotate3d(0, 1, 0, -190deg);
+ -webkit-animation-timing-function: ease-out;
+ animation-timing-function: ease-out;
+ }
+
+ 50% {
+ -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px)
+ rotate3d(0, 1, 0, -170deg);
+ transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px)
+ rotate3d(0, 1, 0, -170deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+
+ 80% {
+ -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95) translate3d(0, 0, 0)
+ rotate3d(0, 1, 0, 0deg);
+ transform: perspective(400px) scale3d(0.95, 0.95, 0.95) translate3d(0, 0, 0)
+ rotate3d(0, 1, 0, 0deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+
+ to {
+ -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0)
+ rotate3d(0, 1, 0, 0deg);
+ transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, 0deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+}
+
+@keyframes flip {
+ from {
+ -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0)
+ rotate3d(0, 1, 0, -360deg);
+ transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, -360deg);
+ -webkit-animation-timing-function: ease-out;
+ animation-timing-function: ease-out;
+ }
+
+ 40% {
+ -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px)
+ rotate3d(0, 1, 0, -190deg);
+ transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px)
+ rotate3d(0, 1, 0, -190deg);
+ -webkit-animation-timing-function: ease-out;
+ animation-timing-function: ease-out;
+ }
+
+ 50% {
+ -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px)
+ rotate3d(0, 1, 0, -170deg);
+ transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 150px)
+ rotate3d(0, 1, 0, -170deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+
+ 80% {
+ -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95) translate3d(0, 0, 0)
+ rotate3d(0, 1, 0, 0deg);
+ transform: perspective(400px) scale3d(0.95, 0.95, 0.95) translate3d(0, 0, 0)
+ rotate3d(0, 1, 0, 0deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+
+ to {
+ -webkit-transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0)
+ rotate3d(0, 1, 0, 0deg);
+ transform: perspective(400px) scale3d(1, 1, 1) translate3d(0, 0, 0) rotate3d(0, 1, 0, 0deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+}
+
+.animated.flip {
+ -webkit-backface-visibility: visible;
+ backface-visibility: visible;
+ -webkit-animation-name: flip;
+ animation-name: flip;
+}
+
+@-webkit-keyframes flipInX {
+ from {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ opacity: 0;
+ }
+
+ 40% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+
+ 60% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
+ opacity: 1;
+ }
+
+ 80% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
+ }
+
+ to {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+}
+
+@keyframes flipInX {
+ from {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ opacity: 0;
+ }
+
+ 40% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+
+ 60% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
+ opacity: 1;
+ }
+
+ 80% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
+ }
+
+ to {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+}
+
+.flipInX {
+ -webkit-backface-visibility: visible !important;
+ backface-visibility: visible !important;
+ -webkit-animation-name: flipInX;
+ animation-name: flipInX;
+}
+
+@-webkit-keyframes flipInY {
+ from {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ opacity: 0;
+ }
+
+ 40% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -20deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+
+ 60% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, 10deg);
+ opacity: 1;
+ }
+
+ 80% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -5deg);
+ }
+
+ to {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+}
+
+@keyframes flipInY {
+ from {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ opacity: 0;
+ }
+
+ 40% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -20deg);
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+ }
+
+ 60% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, 10deg);
+ opacity: 1;
+ }
+
+ 80% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -5deg);
+ }
+
+ to {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+}
+
+.flipInY {
+ -webkit-backface-visibility: visible !important;
+ backface-visibility: visible !important;
+ -webkit-animation-name: flipInY;
+ animation-name: flipInY;
+}
+
+@-webkit-keyframes flipOutX {
+ from {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+
+ 30% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ opacity: 0;
+ }
+}
+
+@keyframes flipOutX {
+ from {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+
+ 30% {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ opacity: 0;
+ }
+}
+
+.flipOutX {
+ -webkit-animation-duration: 0.75s;
+ animation-duration: 0.75s;
+ -webkit-animation-name: flipOutX;
+ animation-name: flipOutX;
+ -webkit-backface-visibility: visible !important;
+ backface-visibility: visible !important;
+}
+
+@-webkit-keyframes flipOutY {
+ from {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+
+ 30% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -15deg);
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ opacity: 0;
+ }
+}
+
+@keyframes flipOutY {
+ from {
+ -webkit-transform: perspective(400px);
+ transform: perspective(400px);
+ }
+
+ 30% {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, -15deg);
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ transform: perspective(400px) rotate3d(0, 1, 0, 90deg);
+ opacity: 0;
+ }
+}
+
+.flipOutY {
+ -webkit-animation-duration: 0.75s;
+ animation-duration: 0.75s;
+ -webkit-backface-visibility: visible !important;
+ backface-visibility: visible !important;
+ -webkit-animation-name: flipOutY;
+ animation-name: flipOutY;
+}
+
+@-webkit-keyframes lightSpeedIn {
+ from {
+ -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg);
+ transform: translate3d(100%, 0, 0) skewX(-30deg);
+ opacity: 0;
+ }
+
+ 60% {
+ -webkit-transform: skewX(20deg);
+ transform: skewX(20deg);
+ opacity: 1;
+ }
+
+ 80% {
+ -webkit-transform: skewX(-5deg);
+ transform: skewX(-5deg);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes lightSpeedIn {
+ from {
+ -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg);
+ transform: translate3d(100%, 0, 0) skewX(-30deg);
+ opacity: 0;
+ }
+
+ 60% {
+ -webkit-transform: skewX(20deg);
+ transform: skewX(20deg);
+ opacity: 1;
+ }
+
+ 80% {
+ -webkit-transform: skewX(-5deg);
+ transform: skewX(-5deg);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.lightSpeedIn {
+ -webkit-animation-name: lightSpeedIn;
+ animation-name: lightSpeedIn;
+ -webkit-animation-timing-function: ease-out;
+ animation-timing-function: ease-out;
+}
+
+@-webkit-keyframes lightSpeedOut {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform: translate3d(100%, 0, 0) skewX(30deg);
+ transform: translate3d(100%, 0, 0) skewX(30deg);
+ opacity: 0;
+ }
+}
+
+@keyframes lightSpeedOut {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform: translate3d(100%, 0, 0) skewX(30deg);
+ transform: translate3d(100%, 0, 0) skewX(30deg);
+ opacity: 0;
+ }
+}
+
+.lightSpeedOut {
+ -webkit-animation-name: lightSpeedOut;
+ animation-name: lightSpeedOut;
+ -webkit-animation-timing-function: ease-in;
+ animation-timing-function: ease-in;
+}
+
+@-webkit-keyframes rotateIn {
+ from {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ -webkit-transform: rotate3d(0, 0, 1, -200deg);
+ transform: rotate3d(0, 0, 1, -200deg);
+ opacity: 0;
+ }
+
+ to {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+@keyframes rotateIn {
+ from {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ -webkit-transform: rotate3d(0, 0, 1, -200deg);
+ transform: rotate3d(0, 0, 1, -200deg);
+ opacity: 0;
+ }
+
+ to {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+.rotateIn {
+ -webkit-animation-name: rotateIn;
+ animation-name: rotateIn;
+}
+
+@-webkit-keyframes rotateInDownLeft {
+ from {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -45deg);
+ transform: rotate3d(0, 0, 1, -45deg);
+ opacity: 0;
+ }
+
+ to {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+@keyframes rotateInDownLeft {
+ from {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -45deg);
+ transform: rotate3d(0, 0, 1, -45deg);
+ opacity: 0;
+ }
+
+ to {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+.rotateInDownLeft {
+ -webkit-animation-name: rotateInDownLeft;
+ animation-name: rotateInDownLeft;
+}
+
+@-webkit-keyframes rotateInDownRight {
+ from {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 45deg);
+ transform: rotate3d(0, 0, 1, 45deg);
+ opacity: 0;
+ }
+
+ to {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+@keyframes rotateInDownRight {
+ from {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 45deg);
+ transform: rotate3d(0, 0, 1, 45deg);
+ opacity: 0;
+ }
+
+ to {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+.rotateInDownRight {
+ -webkit-animation-name: rotateInDownRight;
+ animation-name: rotateInDownRight;
+}
+
+@-webkit-keyframes rotateInUpLeft {
+ from {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 45deg);
+ transform: rotate3d(0, 0, 1, 45deg);
+ opacity: 0;
+ }
+
+ to {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+@keyframes rotateInUpLeft {
+ from {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 45deg);
+ transform: rotate3d(0, 0, 1, 45deg);
+ opacity: 0;
+ }
+
+ to {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+.rotateInUpLeft {
+ -webkit-animation-name: rotateInUpLeft;
+ animation-name: rotateInUpLeft;
+}
+
+@-webkit-keyframes rotateInUpRight {
+ from {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -90deg);
+ transform: rotate3d(0, 0, 1, -90deg);
+ opacity: 0;
+ }
+
+ to {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+@keyframes rotateInUpRight {
+ from {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -90deg);
+ transform: rotate3d(0, 0, 1, -90deg);
+ opacity: 0;
+ }
+
+ to {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+}
+
+.rotateInUpRight {
+ -webkit-animation-name: rotateInUpRight;
+ animation-name: rotateInUpRight;
+}
+
+@-webkit-keyframes rotateOut {
+ from {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ -webkit-transform: rotate3d(0, 0, 1, 200deg);
+ transform: rotate3d(0, 0, 1, 200deg);
+ opacity: 0;
+ }
+}
+
+@keyframes rotateOut {
+ from {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform-origin: center;
+ transform-origin: center;
+ -webkit-transform: rotate3d(0, 0, 1, 200deg);
+ transform: rotate3d(0, 0, 1, 200deg);
+ opacity: 0;
+ }
+}
+
+.rotateOut {
+ -webkit-animation-name: rotateOut;
+ animation-name: rotateOut;
+}
+
+@-webkit-keyframes rotateOutDownLeft {
+ from {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 45deg);
+ transform: rotate3d(0, 0, 1, 45deg);
+ opacity: 0;
+ }
+}
+
+@keyframes rotateOutDownLeft {
+ from {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 45deg);
+ transform: rotate3d(0, 0, 1, 45deg);
+ opacity: 0;
+ }
+}
+
+.rotateOutDownLeft {
+ -webkit-animation-name: rotateOutDownLeft;
+ animation-name: rotateOutDownLeft;
+}
+
+@-webkit-keyframes rotateOutDownRight {
+ from {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -45deg);
+ transform: rotate3d(0, 0, 1, -45deg);
+ opacity: 0;
+ }
+}
+
+@keyframes rotateOutDownRight {
+ from {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -45deg);
+ transform: rotate3d(0, 0, 1, -45deg);
+ opacity: 0;
+ }
+}
+
+.rotateOutDownRight {
+ -webkit-animation-name: rotateOutDownRight;
+ animation-name: rotateOutDownRight;
+}
+
+@-webkit-keyframes rotateOutUpLeft {
+ from {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -45deg);
+ transform: rotate3d(0, 0, 1, -45deg);
+ opacity: 0;
+ }
+}
+
+@keyframes rotateOutUpLeft {
+ from {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform-origin: left bottom;
+ transform-origin: left bottom;
+ -webkit-transform: rotate3d(0, 0, 1, -45deg);
+ transform: rotate3d(0, 0, 1, -45deg);
+ opacity: 0;
+ }
+}
+
+.rotateOutUpLeft {
+ -webkit-animation-name: rotateOutUpLeft;
+ animation-name: rotateOutUpLeft;
+}
+
+@-webkit-keyframes rotateOutUpRight {
+ from {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 90deg);
+ transform: rotate3d(0, 0, 1, 90deg);
+ opacity: 0;
+ }
+}
+
+@keyframes rotateOutUpRight {
+ from {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform-origin: right bottom;
+ transform-origin: right bottom;
+ -webkit-transform: rotate3d(0, 0, 1, 90deg);
+ transform: rotate3d(0, 0, 1, 90deg);
+ opacity: 0;
+ }
+}
+
+.rotateOutUpRight {
+ -webkit-animation-name: rotateOutUpRight;
+ animation-name: rotateOutUpRight;
+}
+
+@-webkit-keyframes hinge {
+ 0% {
+ -webkit-transform-origin: top left;
+ transform-origin: top left;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ }
+
+ 20%,
+ 60% {
+ -webkit-transform: rotate3d(0, 0, 1, 80deg);
+ transform: rotate3d(0, 0, 1, 80deg);
+ -webkit-transform-origin: top left;
+ transform-origin: top left;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ }
+
+ 40%,
+ 80% {
+ -webkit-transform: rotate3d(0, 0, 1, 60deg);
+ transform: rotate3d(0, 0, 1, 60deg);
+ -webkit-transform-origin: top left;
+ transform-origin: top left;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 700px, 0);
+ transform: translate3d(0, 700px, 0);
+ opacity: 0;
+ }
+}
+
+@keyframes hinge {
+ 0% {
+ -webkit-transform-origin: top left;
+ transform-origin: top left;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ }
+
+ 20%,
+ 60% {
+ -webkit-transform: rotate3d(0, 0, 1, 80deg);
+ transform: rotate3d(0, 0, 1, 80deg);
+ -webkit-transform-origin: top left;
+ transform-origin: top left;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ }
+
+ 40%,
+ 80% {
+ -webkit-transform: rotate3d(0, 0, 1, 60deg);
+ transform: rotate3d(0, 0, 1, 60deg);
+ -webkit-transform-origin: top left;
+ transform-origin: top left;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ opacity: 1;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 700px, 0);
+ transform: translate3d(0, 700px, 0);
+ opacity: 0;
+ }
+}
+
+.hinge {
+ -webkit-animation-duration: 2s;
+ animation-duration: 2s;
+ -webkit-animation-name: hinge;
+ animation-name: hinge;
+}
+
+@-webkit-keyframes jackInTheBox {
+ from {
+ opacity: 0;
+ -webkit-transform: scale(0.1) rotate(30deg);
+ transform: scale(0.1) rotate(30deg);
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+ }
+
+ 50% {
+ -webkit-transform: rotate(-10deg);
+ transform: rotate(-10deg);
+ }
+
+ 70% {
+ -webkit-transform: rotate(3deg);
+ transform: rotate(3deg);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+}
+
+@keyframes jackInTheBox {
+ from {
+ opacity: 0;
+ -webkit-transform: scale(0.1) rotate(30deg);
+ transform: scale(0.1) rotate(30deg);
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+ }
+
+ 50% {
+ -webkit-transform: rotate(-10deg);
+ transform: rotate(-10deg);
+ }
+
+ 70% {
+ -webkit-transform: rotate(3deg);
+ transform: rotate(3deg);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+}
+
+.jackInTheBox {
+ -webkit-animation-name: jackInTheBox;
+ animation-name: jackInTheBox;
+}
+
+/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */
+
+@-webkit-keyframes rollIn {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);
+ transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes rollIn {
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);
+ transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);
+ }
+
+ to {
+ opacity: 1;
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.rollIn {
+ -webkit-animation-name: rollIn;
+ animation-name: rollIn;
+}
+
+/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */
+
+@-webkit-keyframes rollOut {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);
+ transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);
+ }
+}
+
+@keyframes rollOut {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);
+ transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);
+ }
+}
+
+.rollOut {
+ -webkit-animation-name: rollOut;
+ animation-name: rollOut;
+}
+
+@-webkit-keyframes zoomIn {
+ from {
+ opacity: 0;
+ -webkit-transform: scale3d(0.3, 0.3, 0.3);
+ transform: scale3d(0.3, 0.3, 0.3);
+ }
+
+ 50% {
+ opacity: 1;
+ }
+}
+
+@keyframes zoomIn {
+ from {
+ opacity: 0;
+ -webkit-transform: scale3d(0.3, 0.3, 0.3);
+ transform: scale3d(0.3, 0.3, 0.3);
+ }
+
+ 50% {
+ opacity: 1;
+ }
+}
+
+.zoomIn {
+ -webkit-animation-name: zoomIn;
+ animation-name: zoomIn;
+}
+
+@-webkit-keyframes zoomInDown {
+ from {
+ opacity: 0;
+ -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0);
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ }
+}
+
+@keyframes zoomInDown {
+ from {
+ opacity: 0;
+ -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0);
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ }
+}
+
+.zoomInDown {
+ -webkit-animation-name: zoomInDown;
+ animation-name: zoomInDown;
+}
+
+@-webkit-keyframes zoomInLeft {
+ from {
+ opacity: 0;
+ -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0);
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ }
+}
+
+@keyframes zoomInLeft {
+ from {
+ opacity: 0;
+ -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0);
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ }
+}
+
+.zoomInLeft {
+ -webkit-animation-name: zoomInLeft;
+ animation-name: zoomInLeft;
+}
+
+@-webkit-keyframes zoomInRight {
+ from {
+ opacity: 0;
+ -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0);
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ }
+}
+
+@keyframes zoomInRight {
+ from {
+ opacity: 0;
+ -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0);
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ }
+}
+
+.zoomInRight {
+ -webkit-animation-name: zoomInRight;
+ animation-name: zoomInRight;
+}
+
+@-webkit-keyframes zoomInUp {
+ from {
+ opacity: 0;
+ -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ }
+}
+
+@keyframes zoomInUp {
+ from {
+ opacity: 0;
+ -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ }
+}
+
+.zoomInUp {
+ -webkit-animation-name: zoomInUp;
+ animation-name: zoomInUp;
+}
+
+@-webkit-keyframes zoomOut {
+ from {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0;
+ -webkit-transform: scale3d(0.3, 0.3, 0.3);
+ transform: scale3d(0.3, 0.3, 0.3);
+ }
+
+ to {
+ opacity: 0;
+ }
+}
+
+@keyframes zoomOut {
+ from {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0;
+ -webkit-transform: scale3d(0.3, 0.3, 0.3);
+ transform: scale3d(0.3, 0.3, 0.3);
+ }
+
+ to {
+ opacity: 0;
+ }
+}
+
+.zoomOut {
+ -webkit-animation-name: zoomOut;
+ animation-name: zoomOut;
+}
+
+@-webkit-keyframes zoomOutDown {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0);
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0);
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ }
+}
+
+@keyframes zoomOutDown {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0);
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0);
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ }
+}
+
+.zoomOutDown {
+ -webkit-animation-name: zoomOutDown;
+ animation-name: zoomOutDown;
+}
+
+@-webkit-keyframes zoomOutLeft {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0);
+ transform: scale(0.1) translate3d(-2000px, 0, 0);
+ -webkit-transform-origin: left center;
+ transform-origin: left center;
+ }
+}
+
+@keyframes zoomOutLeft {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0);
+ transform: scale(0.1) translate3d(-2000px, 0, 0);
+ -webkit-transform-origin: left center;
+ transform-origin: left center;
+ }
+}
+
+.zoomOutLeft {
+ -webkit-animation-name: zoomOutLeft;
+ animation-name: zoomOutLeft;
+}
+
+@-webkit-keyframes zoomOutRight {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: scale(0.1) translate3d(2000px, 0, 0);
+ transform: scale(0.1) translate3d(2000px, 0, 0);
+ -webkit-transform-origin: right center;
+ transform-origin: right center;
+ }
+}
+
+@keyframes zoomOutRight {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: scale(0.1) translate3d(2000px, 0, 0);
+ transform: scale(0.1) translate3d(2000px, 0, 0);
+ -webkit-transform-origin: right center;
+ transform-origin: right center;
+ }
+}
+
+.zoomOutRight {
+ -webkit-animation-name: zoomOutRight;
+ animation-name: zoomOutRight;
+}
+
+@-webkit-keyframes zoomOutUp {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0);
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0);
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ }
+}
+
+@keyframes zoomOutUp {
+ 40% {
+ opacity: 1;
+ -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);
+ -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ }
+
+ to {
+ opacity: 0;
+ -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0);
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0);
+ -webkit-transform-origin: center bottom;
+ transform-origin: center bottom;
+ -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
+ }
+}
+
+.zoomOutUp {
+ -webkit-animation-name: zoomOutUp;
+ animation-name: zoomOutUp;
+}
+
+@-webkit-keyframes slideInDown {
+ from {
+ -webkit-transform: translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0);
+ visibility: visible;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes slideInDown {
+ from {
+ -webkit-transform: translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0);
+ visibility: visible;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.slideInDown {
+ -webkit-animation-name: slideInDown;
+ animation-name: slideInDown;
+}
+
+@-webkit-keyframes slideInLeft {
+ from {
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ visibility: visible;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes slideInLeft {
+ from {
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ visibility: visible;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.slideInLeft {
+ -webkit-animation-name: slideInLeft;
+ animation-name: slideInLeft;
+}
+
+@-webkit-keyframes slideInRight {
+ from {
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ visibility: visible;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes slideInRight {
+ from {
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ visibility: visible;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.slideInRight {
+ -webkit-animation-name: slideInRight;
+ animation-name: slideInRight;
+}
+
+@-webkit-keyframes slideInUp {
+ from {
+ -webkit-transform: translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0);
+ visibility: visible;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes slideInUp {
+ from {
+ -webkit-transform: translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0);
+ visibility: visible;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.slideInUp {
+ -webkit-animation-name: slideInUp;
+ animation-name: slideInUp;
+}
+
+@-webkit-keyframes slideOutDown {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ -webkit-transform: translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0);
+ }
+}
+
+@keyframes slideOutDown {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ -webkit-transform: translate3d(0, 100%, 0);
+ transform: translate3d(0, 100%, 0);
+ }
+}
+
+.slideOutDown {
+ -webkit-animation-name: slideOutDown;
+ animation-name: slideOutDown;
+}
+
+@-webkit-keyframes slideOutLeft {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ }
+}
+
+@keyframes slideOutLeft {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ }
+}
+
+.slideOutLeft {
+ -webkit-animation-name: slideOutLeft;
+ animation-name: slideOutLeft;
+}
+
+@-webkit-keyframes slideOutRight {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ }
+}
+
+@keyframes slideOutRight {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ }
+}
+
+.slideOutRight {
+ -webkit-animation-name: slideOutRight;
+ animation-name: slideOutRight;
+}
+
+@-webkit-keyframes slideOutUp {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ -webkit-transform: translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0);
+ }
+}
+
+@keyframes slideOutUp {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ -webkit-transform: translate3d(0, -100%, 0);
+ transform: translate3d(0, -100%, 0);
+ }
+}
+
+.slideOutUp {
+ -webkit-animation-name: slideOutUp;
+ animation-name: slideOutUp;
+}
+
+.animated {
+ -webkit-animation-duration: 1s;
+ animation-duration: 1s;
+ -webkit-animation-fill-mode: both;
+ animation-fill-mode: both;
+}
+
+.animated.infinite {
+ -webkit-animation-iteration-count: infinite;
+ animation-iteration-count: infinite;
+}
+
+.animated.delay-1s {
+ -webkit-animation-delay: 1s;
+ animation-delay: 1s;
+}
+
+.animated.delay-2s {
+ -webkit-animation-delay: 2s;
+ animation-delay: 2s;
+}
+
+.animated.delay-3s {
+ -webkit-animation-delay: 3s;
+ animation-delay: 3s;
+}
+
+.animated.delay-4s {
+ -webkit-animation-delay: 4s;
+ animation-delay: 4s;
+}
+
+.animated.delay-5s {
+ -webkit-animation-delay: 5s;
+ animation-delay: 5s;
+}
+
+.animated.fast {
+ -webkit-animation-duration: 800ms;
+ animation-duration: 800ms;
+}
+
+.animated.faster {
+ -webkit-animation-duration: 500ms;
+ animation-duration: 500ms;
+}
+
+.animated.slow {
+ -webkit-animation-duration: 2s;
+ animation-duration: 2s;
+}
+
+.animated.slower {
+ -webkit-animation-duration: 3s;
+ animation-duration: 3s;
+}
+
+@media (print), (prefers-reduced-motion) {
+ .animated {
+ -webkit-animation: unset !important;
+ animation: unset !important;
+ -webkit-transition: none !important;
+ transition: none !important;
+ }
+}
diff --git a/main/solution/ui/src/css/basscss-important.css b/main/solution/ui/src/css/basscss-important.css
new file mode 100644
index 0000000000..518a3b6960
--- /dev/null
+++ b/main/solution/ui/src/css/basscss-important.css
@@ -0,0 +1,553 @@
+/*! Basscss | http://basscss.com | MIT License */
+/*
+ IMPORTANT: I get this from https://github.com/basscss/basscss version 8.0.4.
+ I didn't install the npm module, just the basscss-important.css file. Remember to use
+ basscss-important.css and not the basscss.css.
+ I had to comment out the .left and .right classes otherwise they were messing up with
+ semantic ui styles.
+*/
+
+.h1{ font-size: 2rem!important }
+.h2{ font-size: 1.5rem!important }
+.h3{ font-size: 1.25rem!important }
+.h4{ font-size: 1rem!important }
+.h5{ font-size: .875rem!important }
+.h6{ font-size: .75rem!important }
+
+.font-family-inherit{ font-family:inherit!important }
+.font-size-inherit{ font-size:inherit!important }
+.text-decoration-none{ text-decoration:none!important }
+
+.bold{ font-weight: bold!important; font-weight: bold!important }
+.regular{ font-weight:normal!important }
+.italic{ font-style:italic!important }
+.caps{ text-transform:uppercase!important; letter-spacing: .2em!important; }
+
+.left-align{ text-align:left!important }
+.center{ text-align:center!important }
+.right-align{ text-align:right!important }
+.justify{ text-align:justify!important }
+
+.nowrap{ white-space:nowrap!important }
+.break-word{ word-wrap:break-word!important }
+
+.line-height-1{ line-height: 1!important }
+.line-height-2{ line-height: 1.125!important }
+.line-height-3{ line-height: 1.25!important }
+.line-height-4{ line-height: 1.5!important }
+
+.list-style-none{ list-style:none!important }
+.underline{ text-decoration:underline!important }
+
+.truncate{
+ max-width:100%!important;
+ overflow:hidden!important;
+ text-overflow:ellipsis!important;
+ white-space:nowrap!important;
+}
+
+.list-reset{
+ list-style:none!important;
+ padding-left:0!important;
+}
+
+.inline{ display:inline!important }
+.block{ display:block!important }
+.inline-block{ display:inline-block!important }
+.table{ display:table!important }
+.table-cell{ display:table-cell!important }
+
+.overflow-hidden{ overflow:hidden!important }
+.overflow-scroll{ overflow:scroll!important }
+.overflow-auto{ overflow:auto!important }
+
+.clearfix:before,
+.clearfix:after{
+ content:" "!important;
+ display:table!important
+}
+.clearfix:after{ clear:both!important }
+
+/* .left{ float:left!important }
+.right{ float:right!important } */
+
+.fit{ max-width:100%!important }
+
+.max-width-1{ max-width: 24rem!important }
+.max-width-2{ max-width: 32rem!important }
+.max-width-3{ max-width: 48rem!important }
+.max-width-4{ max-width: 64rem!important }
+
+.border-box{ box-sizing:border-box!important }
+
+.align-baseline{ vertical-align:baseline!important }
+.align-top{ vertical-align:top!important }
+.align-middle{ vertical-align:middle!important }
+.align-bottom{ vertical-align:bottom!important }
+
+.m0{ margin:0!important }
+.mt0{ margin-top:0!important }
+.mr0{ margin-right:0!important }
+.mb0{ margin-bottom:0!important }
+.ml0{ margin-left:0!important }
+.mx0{ margin-left:0!important; margin-right:0!important }
+.my0{ margin-top:0!important; margin-bottom:0!important }
+
+.m1{ margin: .5rem!important }
+.mt1{ margin-top: .5rem!important }
+.mr1{ margin-right: .5rem!important }
+.mb1{ margin-bottom: .5rem!important }
+.ml1{ margin-left: .5rem!important }
+.mx1{ margin-left: .5rem!important; margin-right: .5rem!important }
+.my1{ margin-top: .5rem!important; margin-bottom: .5rem!important }
+
+.m2{ margin: 1rem!important }
+.mt2{ margin-top: 1rem!important }
+.mr2{ margin-right: 1rem!important }
+.mb2{ margin-bottom: 1rem!important }
+.ml2{ margin-left: 1rem!important }
+.mx2{ margin-left: 1rem!important; margin-right: 1rem!important }
+.my2{ margin-top: 1rem!important; margin-bottom: 1rem!important }
+
+.m3{ margin: 2rem!important }
+.mt3{ margin-top: 2rem!important }
+.mr3{ margin-right: 2rem!important }
+.mb3{ margin-bottom: 2rem!important }
+.ml3{ margin-left: 2rem!important }
+.mx3{ margin-left: 2rem!important; margin-right: 2rem!important }
+.my3{ margin-top: 2rem!important; margin-bottom: 2rem!important }
+
+.m4{ margin: 4rem!important }
+.mt4{ margin-top: 4rem!important }
+.mr4{ margin-right: 4rem!important }
+.mb4{ margin-bottom: 4rem!important }
+.ml4{ margin-left: 4rem!important }
+.mx4{ margin-left: 4rem!important; margin-right: 4rem!important }
+.my4{ margin-top: 4rem!important; margin-bottom: 4rem!important }
+
+.mxn1{ margin-left: -.5rem!important; margin-right: -.5rem!important; }
+.mxn2{ margin-left: -1rem!important; margin-right: -1rem!important; }
+.mxn3{ margin-left: -2rem!important; margin-right: -2rem!important; }
+.mxn4{ margin-left: -4rem!important; margin-right: -4rem!important; }
+
+.m-auto{ margin:auto!important; }
+.mt-auto{ margin-top:auto!important }
+.mr-auto{ margin-right:auto!important }
+.mb-auto{ margin-bottom:auto!important }
+.ml-auto{ margin-left:auto!important }
+.mx-auto{ margin-left:auto!important; margin-right:auto!important; }
+.my-auto{ margin-top:auto!important; margin-bottom:auto!important; }
+
+.p0{ padding:0!important }
+.pt0{ padding-top:0!important }
+.pr0{ padding-right:0!important }
+.pb0{ padding-bottom:0!important }
+.pl0{ padding-left:0!important }
+.px0{ padding-left:0!important; padding-right:0!important }
+.py0{ padding-top:0!important; padding-bottom:0!important }
+
+.p1{ padding: .5rem!important }
+.pt1{ padding-top: .5rem!important }
+.pr1{ padding-right: .5rem!important }
+.pb1{ padding-bottom: .5rem!important }
+.pl1{ padding-left: .5rem!important }
+.py1{ padding-top: .5rem!important; padding-bottom: .5rem!important }
+.px1{ padding-left: .5rem!important; padding-right: .5rem!important }
+
+.p2{ padding: 1rem!important }
+.pt2{ padding-top: 1rem!important }
+.pr2{ padding-right: 1rem!important }
+.pb2{ padding-bottom: 1rem!important }
+.pl2{ padding-left: 1rem!important }
+.py2{ padding-top: 1rem!important; padding-bottom: 1rem!important }
+.px2{ padding-left: 1rem!important; padding-right: 1rem!important }
+
+.p3{ padding: 2rem!important }
+.pt3{ padding-top: 2rem!important }
+.pr3{ padding-right: 2rem!important }
+.pb3{ padding-bottom: 2rem!important }
+.pl3{ padding-left: 2rem!important }
+.py3{ padding-top: 2rem!important; padding-bottom: 2rem!important }
+.px3{ padding-left: 2rem!important; padding-right: 2rem!important }
+
+.p4{ padding: 4rem!important }
+.pt4{ padding-top: 4rem!important }
+.pr4{ padding-right: 4rem!important }
+.pb4{ padding-bottom: 4rem!important }
+.pl4{ padding-left: 4rem!important }
+.py4{ padding-top: 4rem!important; padding-bottom: 4rem!important }
+.px4{ padding-left: 4rem!important; padding-right: 4rem!important }
+
+.col{
+ float:left!important;
+ box-sizing:border-box!important;
+}
+
+.col-right{
+ float:right!important;
+ box-sizing:border-box!important;
+}
+
+.col-1{
+ width:8.33333%!important;
+}
+
+.col-2{
+ width:16.66667%!important;
+}
+
+.col-3{
+ width:25%!important;
+}
+
+.col-4{
+ width:33.33333%!important;
+}
+
+.col-5{
+ width:41.66667%!important;
+}
+
+.col-6{
+ width:50%!important;
+}
+
+.col-7{
+ width:58.33333%!important;
+}
+
+.col-8{
+ width:66.66667%!important;
+}
+
+.col-9{
+ width:75%!important;
+}
+
+.col-10{
+ width:83.33333%!important;
+}
+
+.col-11{
+ width:91.66667%!important;
+}
+
+.col-12{
+ width:100%!important;
+}
+@media (min-width: 40em){
+
+ .sm-col{
+ float:left!important;
+ box-sizing:border-box!important;
+ }
+
+ .sm-col-right{
+ float:right!important;
+ box-sizing:border-box!important;
+ }
+
+ .sm-col-1{
+ width:8.33333%!important;
+ }
+
+ .sm-col-2{
+ width:16.66667%!important;
+ }
+
+ .sm-col-3{
+ width:25%!important;
+ }
+
+ .sm-col-4{
+ width:33.33333%!important;
+ }
+
+ .sm-col-5{
+ width:41.66667%!important;
+ }
+
+ .sm-col-6{
+ width:50%!important;
+ }
+
+ .sm-col-7{
+ width:58.33333%!important;
+ }
+
+ .sm-col-8{
+ width:66.66667%!important;
+ }
+
+ .sm-col-9{
+ width:75%!important;
+ }
+
+ .sm-col-10{
+ width:83.33333%!important;
+ }
+
+ .sm-col-11{
+ width:91.66667%!important;
+ }
+
+ .sm-col-12{
+ width:100%!important;
+ }
+
+}
+@media (min-width: 52em){
+
+ .md-col{
+ float:left!important;
+ box-sizing:border-box!important;
+ }
+
+ .md-col-right{
+ float:right!important;
+ box-sizing:border-box!important;
+ }
+
+ .md-col-1{
+ width:8.33333%!important;
+ }
+
+ .md-col-2{
+ width:16.66667%!important;
+ }
+
+ .md-col-3{
+ width:25%!important;
+ }
+
+ .md-col-4{
+ width:33.33333%!important;
+ }
+
+ .md-col-5{
+ width:41.66667%!important;
+ }
+
+ .md-col-6{
+ width:50%!important;
+ }
+
+ .md-col-7{
+ width:58.33333%!important;
+ }
+
+ .md-col-8{
+ width:66.66667%!important;
+ }
+
+ .md-col-9{
+ width:75%!important;
+ }
+
+ .md-col-10{
+ width:83.33333%!important;
+ }
+
+ .md-col-11{
+ width:91.66667%!important;
+ }
+
+ .md-col-12{
+ width:100%!important;
+ }
+
+}
+@media (min-width: 64em){
+
+ .lg-col{
+ float:left!important;
+ box-sizing:border-box!important;
+ }
+
+ .lg-col-right{
+ float:right!important;
+ box-sizing:border-box!important;
+ }
+
+ .lg-col-1{
+ width:8.33333%!important;
+ }
+
+ .lg-col-2{
+ width:16.66667%!important;
+ }
+
+ .lg-col-3{
+ width:25%!important;
+ }
+
+ .lg-col-4{
+ width:33.33333%!important;
+ }
+
+ .lg-col-5{
+ width:41.66667%!important;
+ }
+
+ .lg-col-6{
+ width:50%!important;
+ }
+
+ .lg-col-7{
+ width:58.33333%!important;
+ }
+
+ .lg-col-8{
+ width:66.66667%!important;
+ }
+
+ .lg-col-9{
+ width:75%!important;
+ }
+
+ .lg-col-10{
+ width:83.33333%!important;
+ }
+
+ .lg-col-11{
+ width:91.66667%!important;
+ }
+
+ .lg-col-12{
+ width:100%!important;
+ }
+
+}
+.flex{ display:-ms-flexbox!important; display:flex!important }
+
+@media (min-width: 40em){
+ .sm-flex{ display:-ms-flexbox!important; display:flex!important }
+}
+
+@media (min-width: 52em){
+ .md-flex{ display:-ms-flexbox!important; display:flex!important }
+}
+
+@media (min-width: 64em){
+ .lg-flex{ display:-ms-flexbox!important; display:flex!important }
+}
+
+.flex-column{ -ms-flex-direction:column!important; flex-direction:column!important }
+.flex-wrap{ -ms-flex-wrap:wrap!important; flex-wrap:wrap!important }
+
+.items-start{ -ms-flex-align:start!important; align-items:flex-start!important }
+.items-end{ -ms-flex-align:end!important; align-items:flex-end!important }
+.items-center{ -ms-flex-align:center!important; align-items:center!important }
+.items-baseline{ -ms-flex-align:baseline!important; align-items:baseline!important }
+.items-stretch{ -ms-flex-align:stretch!important; align-items:stretch!important }
+
+.self-start{ -ms-flex-item-align:start!important; align-self:flex-start!important }
+.self-end{ -ms-flex-item-align:end!important; align-self:flex-end!important }
+.self-center{ -ms-flex-item-align:center!important; -ms-grid-row-align:center!important; align-self:center!important }
+.self-baseline{ -ms-flex-item-align:baseline!important; align-self:baseline!important }
+.self-stretch{ -ms-flex-item-align:stretch!important; -ms-grid-row-align:stretch!important; align-self:stretch!important }
+
+.justify-start{ -ms-flex-pack:start!important; justify-content:flex-start!important }
+.justify-end{ -ms-flex-pack:end!important; justify-content:flex-end!important }
+.justify-center{ -ms-flex-pack:center!important; justify-content:center!important }
+.justify-between{ -ms-flex-pack:justify!important; justify-content:space-between!important }
+.justify-around{ -ms-flex-pack:distribute!important; justify-content:space-around!important }
+.justify-evenly{ -ms-flex-pack:space-evenly!important; justify-content:space-evenly!important }
+
+.content-start{ -ms-flex-line-pack:start!important; align-content:flex-start!important }
+.content-end{ -ms-flex-line-pack:end!important; align-content:flex-end!important }
+.content-center{ -ms-flex-line-pack:center!important; align-content:center!important }
+.content-between{ -ms-flex-line-pack:justify!important; align-content:space-between!important }
+.content-around{ -ms-flex-line-pack:distribute!important; align-content:space-around!important }
+.content-stretch{ -ms-flex-line-pack:stretch!important; align-content:stretch!important }
+.flex-auto{
+ -ms-flex:1 1 auto!important;
+ flex:1 1 auto!important;
+ min-width:0!important;
+ min-height:0!important;
+}
+.flex-none{ -ms-flex:none!important; flex:none!important }
+
+.order-0{ -ms-flex-order:0!important; order:0!important }
+.order-1{ -ms-flex-order:1!important; order:1!important }
+.order-2{ -ms-flex-order:2!important; order:2!important }
+.order-3{ -ms-flex-order:3!important; order:3!important }
+.order-last{ -ms-flex-order:99999!important; order:99999!important }
+
+.relative{ position:relative!important }
+.absolute{ position:absolute!important }
+.fixed{ position:fixed!important }
+
+.top-0{ top:0!important }
+.right-0{ right:0!important }
+.bottom-0{ bottom:0!important }
+.left-0{ left:0!important }
+
+.z1{ z-index: 1!important }
+.z2{ z-index: 2!important }
+.z3{ z-index: 3!important }
+.z4{ z-index: 4!important }
+
+.border{
+ border-style:solid!important;
+ border-width: 1px!important;
+}
+
+.border-top{
+ border-top-style:solid!important;
+ border-top-width: 1px!important;
+}
+
+.border-right{
+ border-right-style:solid!important;
+ border-right-width: 1px!important;
+}
+
+.border-bottom{
+ border-bottom-style:solid!important;
+ border-bottom-width: 1px!important;
+}
+
+.border-left{
+ border-left-style:solid!important;
+ border-left-width: 1px!important;
+}
+
+.border-none{ border:0!important }
+
+.rounded{ border-radius: 3px!important }
+.circle{ border-radius:50%!important }
+
+.rounded-top{ border-radius: 3px 3px 0 0!important }
+.rounded-right{ border-radius: 0 3px 3px 0!important }
+.rounded-bottom{ border-radius: 0 0 3px 3px!important }
+.rounded-left{ border-radius: 3px 0 0 3px!important }
+
+.not-rounded{ border-radius:0!important }
+
+.hide{
+ position:absolute !important;
+ height:1px!important;
+ width:1px!important;
+ overflow:hidden!important;
+ clip:rect(1px, 1px, 1px, 1px)!important;
+}
+
+@media (max-width: 40em){
+ .xs-hide{ display:none !important }
+}
+
+@media (min-width: 40em) and (max-width: 52em){
+ .sm-hide{ display:none !important }
+}
+
+@media (min-width: 52em) and (max-width: 64em){
+ .md-hide{ display:none !important }
+}
+
+@media (min-width: 64em){
+ .lg-hide{ display:none !important }
+}
+
+.display-none{ display:none !important }
+
diff --git a/main/solution/ui/src/css/index.css b/main/solution/ui/src/css/index.css
new file mode 100644
index 0000000000..1fbb046e27
--- /dev/null
+++ b/main/solution/ui/src/css/index.css
@@ -0,0 +1,258 @@
+/* ==================================== Utility classes ==================================== */
+
+.cursor-grab {
+ cursor: grab;
+}
+
+.cursor-pointer {
+ cursor: pointer;
+}
+
+.cursor-default {
+ cursor: default;
+}
+
+.fs-9 {
+ font-size: 0.9rem !important;
+}
+
+.fs-8 {
+ font-size: 0.8rem !important;
+}
+
+.fs-7 {
+ font-size: 0.7rem !important;
+}
+
+
+.color-orange {
+ color: #f2711c!important;
+}
+
+.color-brown {
+ color: #a5673f!important;
+}
+
+.color-grey {
+ color: #767676 !important;
+}
+
+.color-green {
+ color: #21ba45! important;
+}
+
+.color-red {
+ color: #9f3a38 !important; /* #db2828 !important; */
+}
+
+.color-blue {
+ color: #2185d0!important;
+}
+
+.border-grey {
+ border-color: #d4d4d5 !important;
+}
+
+.op-3 {
+ opacity: 0.3 !important;
+}
+
+.op-45 {
+ opacity: 0.45 !important;
+}
+
+.op-65 {
+ opacity: 0.65 !important;
+}
+
+.mt75 {
+ margin-top: 0.75rem !important;
+}
+
+.box-shadow {
+ box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12) !important;
+}
+
+.bg-lightgreen {
+ background-color: #f7fcfa !important;
+}
+
+.undo-line-height {
+ line-height: inherit !important;
+}
+
+.zindex-1500 {
+ z-index: 1500 !important;
+}
+
+.w-50 {
+ width: 50% !important;
+}
+
+.w-100 {
+ width: 100% !important;
+}
+
+.min-height-55-px {
+ min-height: 55px !important;
+}
+
+.min-width-735-px {
+ min-width: 735px !important;
+}
+
+.min-width-350-px {
+ min-width: 350px !important;
+}
+
+.width-580-px {
+ width: 580px !important;
+}
+
+.width-500-px {
+ width: 500px !important;
+}
+
+.width-350-px {
+ width: 350px !important;
+}
+
+.width-300-px {
+ width: 300px !important;
+}
+
+.width-280-px {
+ width: 280px !important;
+}
+
+.width-260-px {
+ width: 260px !important;
+}
+
+.width-230-px {
+ width: 230px !important;
+}
+
+.width-200-px {
+ width: 200px !important;
+}
+
+.width-140-px {
+ width: 140px !important;
+}
+
+.width-110-px {
+ width: 110px !important;
+}
+
+.width-100-px {
+ width: 100px !important;
+}
+
+.width-90-px {
+ width: 90px !important;
+}
+
+.width-80-px {
+ width: 80px !important;
+}
+
+.width-70-px {
+ width: 70px !important;
+}
+
+.width-60-px {
+ width: 60px !important;
+}
+
+.width-40-px {
+ width: 40px !important;
+}
+
+.width-24-px {
+ width: 24px !important;
+}
+
+.height-22-px {
+ height: 22px !important;
+}
+
+.height-60-px {
+ height: 60px !important;
+}
+
+.height-50-px {
+ height: 50px !important;
+}
+
+.line-height-20-px {
+ line-height: 20px !important;
+}
+
+.ellipsis {
+ /* height: 50px; */
+ text-overflow: ellipsis;
+
+ /* required for text-overflow to do anything */
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.breakout {
+ /* see https://css-tricks.com/snippets/css/prevent-long-urls-from-breaking-out-of-container/ */
+ /* These are technically the same, but use both */
+ overflow-wrap: break-word !important;
+ word-wrap: break-word !important;
+
+ -ms-word-break: break-all !important;
+ /* This is the dangerous one in WebKit, as it breaks things wherever */
+ word-break: break-all !important;
+}
+
+/* ==================================== Semantic UI Overrides ==================================== */
+
+@media only screen and (max-width: 991px) and (min-width: 768px) {
+ .ui.container {
+ width: 639px; /* 723 - 84 (the width of the left menu bar) */
+ margin-left: auto!important;
+ margin-right: auto!important;
+ }
+}
+
+@media only screen and (max-width: 1199px) and (min-width: 992px) {
+ .ui.container {
+ width: 849px; /* 933 - 84 (the width of the left menu bar) */
+ margin-left: auto!important;
+ margin-right: auto!important;
+ }
+}
+
+@media only screen and (min-width: 1200px) {
+ .ui.container {
+ width: 1043px; /* 1127 - 84 (the width of the left menu bar) */
+ margin-left: auto!important;
+ margin-right: auto!important;
+ }
+}
+
+.error .field, .error.field, .error .label, .error .field.label, .ui.form .error .field>label,
+ .ui.header .error, .ui.header.error, .error .text, .error.text, .error .ui.basic.label {
+ color: #9f3a38 !important;
+}
+
+.error .ui.basic.label {
+ background: none #fff!important;
+ border-color:#9f3a38!important;
+}
+
+.ui.form .error textarea {
+ background: #fff6f6;
+ border-color: #e0b4b4;
+}
+
+.disabled .ui.basic.label, .disabled .ui.header, .disabled .field, .disabled textarea {
+ pointer-events: none;
+ opacity: 0.45 !important;
+}
+
+/* ====================================== Others ========================================= */
diff --git a/main/solution/ui/src/css/semantic.min.css b/main/solution/ui/src/css/semantic.min.css
new file mode 100644
index 0000000000..f4b249b54d
--- /dev/null
+++ b/main/solution/ui/src/css/semantic.min.css
@@ -0,0 +1,372 @@
+ /*
+ * # Semantic UI - 2.4.2
+ * https://github.com/Semantic-Org/Semantic-UI
+ * http://www.semantic-ui.com/
+ *
+ * Copyright 2014 Contributors
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+/*!
+ * # Semantic UI 2.4.2 - Reset
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}html{-webkit-box-sizing:border-box;box-sizing:border-box}input[type=email],input[type=password],input[type=search],input[type=text]{-webkit-appearance:none;-moz-appearance:none}/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}/*!
+ * # Semantic UI 2.4.2 - Site
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */body,html{height:100%}html{font-size:14px}body{margin:0;padding:0;overflow-x:hidden;min-width:320px;background:#fff;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-size:14px;line-height:1.4285em;color:rgba(0,0,0,.87);font-smoothing:antialiased}h1,h2,h3,h4,h5{font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;line-height:1.28571429em;margin:calc(2rem - .14285714em) 0 1rem;font-weight:700;padding:0}h1{min-height:1rem;font-size:2rem}h2{font-size:1.71428571rem}h3{font-size:1.28571429rem}h4{font-size:1.07142857rem}h5{font-size:1rem}h1:first-child,h2:first-child,h3:first-child,h4:first-child,h5:first-child{margin-top:0}h1:last-child,h2:last-child,h3:last-child,h4:last-child,h5:last-child{margin-bottom:0}p{margin:0 0 1em;line-height:1.4285em}p:first-child{margin-top:0}p:last-child{margin-bottom:0}a{color:#4183c4;text-decoration:none}a:hover{color:#1e70bf;text-decoration:none}::-webkit-selection{background-color:#cce2ff;color:rgba(0,0,0,.87)}::-moz-selection{background-color:#cce2ff;color:rgba(0,0,0,.87)}::selection{background-color:#cce2ff;color:rgba(0,0,0,.87)}input::-webkit-selection,textarea::-webkit-selection{background-color:rgba(100,100,100,.4);color:rgba(0,0,0,.87)}input::-moz-selection,textarea::-moz-selection{background-color:rgba(100,100,100,.4);color:rgba(0,0,0,.87)}input::selection,textarea::selection{background-color:rgba(100,100,100,.4);color:rgba(0,0,0,.87)}body ::-webkit-scrollbar{-webkit-appearance:none;width:10px;height:10px}body ::-webkit-scrollbar-track{background:rgba(0,0,0,.1);border-radius:0}body ::-webkit-scrollbar-thumb{cursor:pointer;border-radius:5px;background:rgba(0,0,0,.25);-webkit-transition:color .2s ease;transition:color .2s ease}body ::-webkit-scrollbar-thumb:window-inactive{background:rgba(0,0,0,.15)}body ::-webkit-scrollbar-thumb:hover{background:rgba(128,135,139,.8)}body .ui.inverted::-webkit-scrollbar-track{background:rgba(255,255,255,.1)}body .ui.inverted::-webkit-scrollbar-thumb{background:rgba(255,255,255,.25)}body .ui.inverted::-webkit-scrollbar-thumb:window-inactive{background:rgba(255,255,255,.15)}body .ui.inverted::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.35)}/*!
+ * # Semantic UI 2.4.2 - Button
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.button{cursor:pointer;display:inline-block;min-height:1em;outline:0;border:none;vertical-align:baseline;background:#e0e1e2 none;color:rgba(0,0,0,.6);font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;margin:0 .25em 0 0;padding:.78571429em 1.5em .78571429em;text-transform:none;text-shadow:none;font-weight:700;line-height:1em;font-style:normal;text-align:center;text-decoration:none;border-radius:.28571429rem;-webkit-box-shadow:0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,-webkit-box-shadow .1s ease;transition:opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,-webkit-box-shadow .1s ease;transition:opacity .1s ease,background-color .1s ease,color .1s ease,box-shadow .1s ease,background .1s ease;transition:opacity .1s ease,background-color .1s ease,color .1s ease,box-shadow .1s ease,background .1s ease,-webkit-box-shadow .1s ease;will-change:'';-webkit-tap-highlight-color:transparent}.ui.button:hover{background-color:#cacbcd;background-image:none;-webkit-box-shadow:0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;color:rgba(0,0,0,.8)}.ui.button:hover .icon{opacity:.85}.ui.button:focus{background-color:#cacbcd;color:rgba(0,0,0,.8);background-image:''!important;-webkit-box-shadow:''!important;box-shadow:''!important}.ui.button:focus .icon{opacity:.85}.ui.active.button:active,.ui.button:active{background-color:#babbbc;background-image:'';color:rgba(0,0,0,.9);-webkit-box-shadow:0 0 0 1px transparent inset,none;box-shadow:0 0 0 1px transparent inset,none}.ui.active.button{background-color:#c0c1c2;background-image:none;-webkit-box-shadow:0 0 0 1px transparent inset;box-shadow:0 0 0 1px transparent inset;color:rgba(0,0,0,.95)}.ui.active.button:hover{background-color:#c0c1c2;background-image:none;color:rgba(0,0,0,.95)}.ui.active.button:active{background-color:#c0c1c2;background-image:none}.ui.loading.loading.loading.loading.loading.loading.button{position:relative;cursor:default;text-shadow:none!important;color:transparent!important;opacity:1;pointer-events:auto;-webkit-transition:all 0s linear,opacity .1s ease;transition:all 0s linear,opacity .1s ease}.ui.loading.button:before{position:absolute;content:'';top:50%;left:50%;margin:-.64285714em 0 0 -.64285714em;width:1.28571429em;height:1.28571429em;border-radius:500rem;border:.2em solid rgba(0,0,0,.15)}.ui.loading.button:after{position:absolute;content:'';top:50%;left:50%;margin:-.64285714em 0 0 -.64285714em;width:1.28571429em;height:1.28571429em;-webkit-animation:button-spin .6s linear;animation:button-spin .6s linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;border-radius:500rem;border-color:#fff transparent transparent;border-style:solid;border-width:.2em;-webkit-box-shadow:0 0 0 1px transparent;box-shadow:0 0 0 1px transparent}.ui.labeled.icon.loading.button .icon{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}@-webkit-keyframes button-spin{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes button-spin{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.ui.basic.loading.button:not(.inverted):before{border-color:rgba(0,0,0,.1)}.ui.basic.loading.button:not(.inverted):after{border-top-color:#767676}.ui.button:disabled,.ui.buttons .disabled.button,.ui.disabled.active.button,.ui.disabled.button,.ui.disabled.button:hover{cursor:default;opacity:.45!important;background-image:none!important;-webkit-box-shadow:none!important;box-shadow:none!important;pointer-events:none!important}.ui.basic.buttons .ui.disabled.button{border-color:rgba(34,36,38,.5)}.ui.animated.button{position:relative;overflow:hidden;padding-right:0!important;vertical-align:middle;z-index:1}.ui.animated.button .content{will-change:transform,opacity}.ui.animated.button .visible.content{position:relative;margin-right:1.5em}.ui.animated.button .hidden.content{position:absolute;width:100%}.ui.animated.button .hidden.content,.ui.animated.button .visible.content{-webkit-transition:right .3s ease 0s;transition:right .3s ease 0s}.ui.animated.button .visible.content{left:auto;right:0}.ui.animated.button .hidden.content{top:50%;left:auto;right:-100%;margin-top:-.5em}.ui.animated.button:focus .visible.content,.ui.animated.button:hover .visible.content{left:auto;right:200%}.ui.animated.button:focus .hidden.content,.ui.animated.button:hover .hidden.content{left:auto;right:0}.ui.vertical.animated.button .hidden.content,.ui.vertical.animated.button .visible.content{-webkit-transition:top .3s ease,-webkit-transform .3s ease;transition:top .3s ease,-webkit-transform .3s ease;transition:top .3s ease,transform .3s ease;transition:top .3s ease,transform .3s ease,-webkit-transform .3s ease}.ui.vertical.animated.button .visible.content{-webkit-transform:translateY(0);transform:translateY(0);right:auto}.ui.vertical.animated.button .hidden.content{top:-50%;left:0;right:auto}.ui.vertical.animated.button:focus .visible.content,.ui.vertical.animated.button:hover .visible.content{-webkit-transform:translateY(200%);transform:translateY(200%);right:auto}.ui.vertical.animated.button:focus .hidden.content,.ui.vertical.animated.button:hover .hidden.content{top:50%;right:auto}.ui.fade.animated.button .hidden.content,.ui.fade.animated.button .visible.content{-webkit-transition:opacity .3s ease,-webkit-transform .3s ease;transition:opacity .3s ease,-webkit-transform .3s ease;transition:opacity .3s ease,transform .3s ease;transition:opacity .3s ease,transform .3s ease,-webkit-transform .3s ease}.ui.fade.animated.button .visible.content{left:auto;right:auto;opacity:1;-webkit-transform:scale(1);transform:scale(1)}.ui.fade.animated.button .hidden.content{opacity:0;left:0;right:auto;-webkit-transform:scale(1.5);transform:scale(1.5)}.ui.fade.animated.button:focus .visible.content,.ui.fade.animated.button:hover .visible.content{left:auto;right:auto;opacity:0;-webkit-transform:scale(.75);transform:scale(.75)}.ui.fade.animated.button:focus .hidden.content,.ui.fade.animated.button:hover .hidden.content{left:0;right:auto;opacity:1;-webkit-transform:scale(1);transform:scale(1)}.ui.inverted.button{-webkit-box-shadow:0 0 0 2px #fff inset!important;box-shadow:0 0 0 2px #fff inset!important;background:transparent none;color:#fff;text-shadow:none!important}.ui.inverted.buttons .button{margin:0 0 0 -2px}.ui.inverted.buttons .button:first-child{margin-left:0}.ui.inverted.vertical.buttons .button{margin:0 0 -2px 0}.ui.inverted.vertical.buttons .button:first-child{margin-top:0}.ui.inverted.button:hover{background:#fff;-webkit-box-shadow:0 0 0 2px #fff inset!important;box-shadow:0 0 0 2px #fff inset!important;color:rgba(0,0,0,.8)}.ui.inverted.button.active,.ui.inverted.button:focus{background:#fff;-webkit-box-shadow:0 0 0 2px #fff inset!important;box-shadow:0 0 0 2px #fff inset!important;color:rgba(0,0,0,.8)}.ui.inverted.button.active:focus{background:#dcddde;-webkit-box-shadow:0 0 0 2px #dcddde inset!important;box-shadow:0 0 0 2px #dcddde inset!important;color:rgba(0,0,0,.8)}.ui.labeled.button:not(.icon){display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;background:0 0!important;padding:0!important;border:none!important;-webkit-box-shadow:none!important;box-shadow:none!important}.ui.labeled.button>.button{margin:0}.ui.labeled.button>.label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0 0 0 -1px!important;padding:'';font-size:1em;border-color:rgba(34,36,38,.15)}.ui.labeled.button>.tag.label:before{width:1.85em;height:1.85em}.ui.labeled.button:not([class*="left labeled"])>.button{border-top-right-radius:0;border-bottom-right-radius:0}.ui.labeled.button:not([class*="left labeled"])>.label{border-top-left-radius:0;border-bottom-left-radius:0}.ui[class*="left labeled"].button>.button{border-top-left-radius:0;border-bottom-left-radius:0}.ui[class*="left labeled"].button>.label{border-top-right-radius:0;border-bottom-right-radius:0}.ui.facebook.button{background-color:#3b5998;color:#fff;text-shadow:none;background-image:none;-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.facebook.button:hover{background-color:#304d8a;color:#fff;text-shadow:none}.ui.facebook.button:active{background-color:#2d4373;color:#fff;text-shadow:none}.ui.twitter.button{background-color:#55acee;color:#fff;text-shadow:none;background-image:none;-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.twitter.button:hover{background-color:#35a2f4;color:#fff;text-shadow:none}.ui.twitter.button:active{background-color:#2795e9;color:#fff;text-shadow:none}.ui.google.plus.button{background-color:#dd4b39;color:#fff;text-shadow:none;background-image:none;-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.google.plus.button:hover{background-color:#e0321c;color:#fff;text-shadow:none}.ui.google.plus.button:active{background-color:#c23321;color:#fff;text-shadow:none}.ui.linkedin.button{background-color:#1f88be;color:#fff;text-shadow:none}.ui.linkedin.button:hover{background-color:#147baf;color:#fff;text-shadow:none}.ui.linkedin.button:active{background-color:#186992;color:#fff;text-shadow:none}.ui.youtube.button{background-color:red;color:#fff;text-shadow:none;background-image:none;-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.youtube.button:hover{background-color:#e60000;color:#fff;text-shadow:none}.ui.youtube.button:active{background-color:#c00;color:#fff;text-shadow:none}.ui.instagram.button{background-color:#49769c;color:#fff;text-shadow:none;background-image:none;-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.instagram.button:hover{background-color:#3d698e;color:#fff;text-shadow:none}.ui.instagram.button:active{background-color:#395c79;color:#fff;text-shadow:none}.ui.pinterest.button{background-color:#bd081c;color:#fff;text-shadow:none;background-image:none;-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.pinterest.button:hover{background-color:#ac0013;color:#fff;text-shadow:none}.ui.pinterest.button:active{background-color:#8c0615;color:#fff;text-shadow:none}.ui.vk.button{background-color:#4d7198;color:#fff;background-image:none;-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.vk.button:hover{background-color:#41648a;color:#fff}.ui.vk.button:active{background-color:#3c5876;color:#fff}.ui.button>.icon:not(.button){height:.85714286em;opacity:.8;margin:0 .42857143em 0 -.21428571em;-webkit-transition:opacity .1s ease;transition:opacity .1s ease;vertical-align:'';color:''}.ui.button:not(.icon)>.icon:not(.button):not(.dropdown){margin:0 .42857143em 0 -.21428571em}.ui.button:not(.icon)>.right.icon:not(.button):not(.dropdown){margin:0 -.21428571em 0 .42857143em}.ui[class*="left floated"].button,.ui[class*="left floated"].buttons{float:left;margin-left:0;margin-right:.25em}.ui[class*="right floated"].button,.ui[class*="right floated"].buttons{float:right;margin-right:0;margin-left:.25em}.ui.compact.button,.ui.compact.buttons .button{padding:.58928571em 1.125em .58928571em}.ui.compact.icon.button,.ui.compact.icon.buttons .button{padding:.58928571em .58928571em .58928571em}.ui.compact.labeled.icon.button,.ui.compact.labeled.icon.buttons .button{padding:.58928571em 3.69642857em .58928571em}.ui.mini.button,.ui.mini.buttons .button,.ui.mini.buttons .or{font-size:.78571429rem}.ui.tiny.button,.ui.tiny.buttons .button,.ui.tiny.buttons .or{font-size:.85714286rem}.ui.small.button,.ui.small.buttons .button,.ui.small.buttons .or{font-size:.92857143rem}.ui.button,.ui.buttons .button,.ui.buttons .or{font-size:1rem}.ui.large.button,.ui.large.buttons .button,.ui.large.buttons .or{font-size:1.14285714rem}.ui.big.button,.ui.big.buttons .button,.ui.big.buttons .or{font-size:1.28571429rem}.ui.huge.button,.ui.huge.buttons .button,.ui.huge.buttons .or{font-size:1.42857143rem}.ui.massive.button,.ui.massive.buttons .button,.ui.massive.buttons .or{font-size:1.71428571rem}.ui.icon.button,.ui.icon.buttons .button{padding:.78571429em .78571429em .78571429em}.ui.icon.button>.icon,.ui.icon.buttons .button>.icon{opacity:.9;margin:0!important;vertical-align:top}.ui.basic.button,.ui.basic.buttons .button{background:transparent none!important;color:rgba(0,0,0,.6)!important;font-weight:400;border-radius:.28571429rem;text-transform:none;text-shadow:none!important;-webkit-box-shadow:0 0 0 1px rgba(34,36,38,.15) inset;box-shadow:0 0 0 1px rgba(34,36,38,.15) inset}.ui.basic.buttons{-webkit-box-shadow:none;box-shadow:none;border:1px solid rgba(34,36,38,.15);border-radius:.28571429rem}.ui.basic.buttons .button{border-radius:0}.ui.basic.button:hover,.ui.basic.buttons .button:hover{background:#fff!important;color:rgba(0,0,0,.8)!important;-webkit-box-shadow:0 0 0 1px rgba(34,36,38,.35) inset,0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 1px rgba(34,36,38,.35) inset,0 0 0 0 rgba(34,36,38,.15) inset}.ui.basic.button:focus,.ui.basic.buttons .button:focus{background:#fff!important;color:rgba(0,0,0,.8)!important;-webkit-box-shadow:0 0 0 1px rgba(34,36,38,.35) inset,0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 1px rgba(34,36,38,.35) inset,0 0 0 0 rgba(34,36,38,.15) inset}.ui.basic.button:active,.ui.basic.buttons .button:active{background:#f8f8f8!important;color:rgba(0,0,0,.9)!important;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 1px 4px 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 1px 4px 0 rgba(34,36,38,.15) inset}.ui.basic.active.button,.ui.basic.buttons .active.button{background:rgba(0,0,0,.05)!important;-webkit-box-shadow:''!important;box-shadow:''!important;color:rgba(0,0,0,.95)!important}.ui.basic.active.button:hover,.ui.basic.buttons .active.button:hover{background-color:rgba(0,0,0,.05)}.ui.basic.buttons .button:hover{-webkit-box-shadow:0 0 0 1px rgba(34,36,38,.35) inset,0 0 0 0 rgba(34,36,38,.15) inset inset;box-shadow:0 0 0 1px rgba(34,36,38,.35) inset,0 0 0 0 rgba(34,36,38,.15) inset inset}.ui.basic.buttons .button:active{-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 1px 4px 0 rgba(34,36,38,.15) inset inset;box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 1px 4px 0 rgba(34,36,38,.15) inset inset}.ui.basic.buttons .active.button{-webkit-box-shadow:''!important;box-shadow:''!important}.ui.basic.inverted.button,.ui.basic.inverted.buttons .button{background-color:transparent!important;color:#f9fafb!important;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important}.ui.basic.inverted.button:hover,.ui.basic.inverted.buttons .button:hover{color:#fff!important;-webkit-box-shadow:0 0 0 2px #fff inset!important;box-shadow:0 0 0 2px #fff inset!important}.ui.basic.inverted.button:focus,.ui.basic.inverted.buttons .button:focus{color:#fff!important;-webkit-box-shadow:0 0 0 2px #fff inset!important;box-shadow:0 0 0 2px #fff inset!important}.ui.basic.inverted.button:active,.ui.basic.inverted.buttons .button:active{background-color:rgba(255,255,255,.08)!important;color:#fff!important;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.9) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.9) inset!important}.ui.basic.inverted.active.button,.ui.basic.inverted.buttons .active.button{background-color:rgba(255,255,255,.08);color:#fff;text-shadow:none;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.7) inset;box-shadow:0 0 0 2px rgba(255,255,255,.7) inset}.ui.basic.inverted.active.button:hover,.ui.basic.inverted.buttons .active.button:hover{background-color:rgba(255,255,255,.15);-webkit-box-shadow:0 0 0 2px #fff inset!important;box-shadow:0 0 0 2px #fff inset!important}.ui.basic.buttons .button{border-left:1px solid rgba(34,36,38,.15);-webkit-box-shadow:none;box-shadow:none}.ui.basic.vertical.buttons .button{border-left:none}.ui.basic.vertical.buttons .button{border-left-width:0;border-top:1px solid rgba(34,36,38,.15)}.ui.basic.vertical.buttons .button:first-child{border-top-width:0}.ui.labeled.icon.button,.ui.labeled.icon.buttons .button{position:relative;padding-left:4.07142857em!important;padding-right:1.5em!important}.ui.labeled.icon.button>.icon,.ui.labeled.icon.buttons>.button>.icon{position:absolute;height:100%;line-height:1;border-radius:0;border-top-left-radius:inherit;border-bottom-left-radius:inherit;text-align:center;margin:0;width:2.57142857em;background-color:rgba(0,0,0,.05);color:'';-webkit-box-shadow:-1px 0 0 0 transparent inset;box-shadow:-1px 0 0 0 transparent inset}.ui.labeled.icon.button>.icon,.ui.labeled.icon.buttons>.button>.icon{top:0;left:0}.ui[class*="right labeled"].icon.button{padding-right:4.07142857em!important;padding-left:1.5em!important}.ui[class*="right labeled"].icon.button>.icon{left:auto;right:0;border-radius:0;border-top-right-radius:inherit;border-bottom-right-radius:inherit;-webkit-box-shadow:1px 0 0 0 transparent inset;box-shadow:1px 0 0 0 transparent inset}.ui.labeled.icon.button>.icon:after,.ui.labeled.icon.button>.icon:before,.ui.labeled.icon.buttons>.button>.icon:after,.ui.labeled.icon.buttons>.button>.icon:before{display:block;position:absolute;width:100%;top:50%;text-align:center;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.ui.labeled.icon.buttons .button>.icon{border-radius:0}.ui.labeled.icon.buttons .button:first-child>.icon{border-top-left-radius:.28571429rem;border-bottom-left-radius:.28571429rem}.ui.labeled.icon.buttons .button:last-child>.icon{border-top-right-radius:.28571429rem;border-bottom-right-radius:.28571429rem}.ui.vertical.labeled.icon.buttons .button:first-child>.icon{border-radius:0;border-top-left-radius:.28571429rem}.ui.vertical.labeled.icon.buttons .button:last-child>.icon{border-radius:0;border-bottom-left-radius:.28571429rem}.ui.fluid[class*="left labeled"].icon.button,.ui.fluid[class*="right labeled"].icon.button{padding-left:1.5em!important;padding-right:1.5em!important}.ui.button.toggle.active,.ui.buttons .button.toggle.active,.ui.toggle.buttons .active.button{background-color:#21ba45!important;-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none;color:#fff!important}.ui.button.toggle.active:hover{background-color:#16ab39!important;text-shadow:none;color:#fff!important}.ui.circular.button{border-radius:10em}.ui.circular.button>.icon{width:1em;vertical-align:baseline}.ui.buttons .or{position:relative;width:.3em;height:2.57142857em;z-index:3}.ui.buttons .or:before{position:absolute;text-align:center;border-radius:500rem;content:'or';top:50%;left:50%;background-color:#fff;text-shadow:none;margin-top:-.89285714em;margin-left:-.89285714em;width:1.78571429em;height:1.78571429em;line-height:1.78571429em;color:rgba(0,0,0,.4);font-style:normal;font-weight:700;-webkit-box-shadow:0 0 0 1px transparent inset;box-shadow:0 0 0 1px transparent inset}.ui.buttons .or[data-text]:before{content:attr(data-text)}.ui.fluid.buttons .or{width:0!important}.ui.fluid.buttons .or:after{display:none}.ui.attached.button{position:relative;display:block;margin:0;border-radius:0;-webkit-box-shadow:0 0 0 1px rgba(34,36,38,.15)!important;box-shadow:0 0 0 1px rgba(34,36,38,.15)!important}.ui.attached.top.button{border-radius:.28571429rem .28571429rem 0 0}.ui.attached.bottom.button{border-radius:0 0 .28571429rem .28571429rem}.ui.left.attached.button{display:inline-block;border-left:none;text-align:right;padding-right:.75em;border-radius:.28571429rem 0 0 .28571429rem}.ui.right.attached.button{display:inline-block;text-align:left;padding-left:.75em;border-radius:0 .28571429rem .28571429rem 0}.ui.attached.buttons{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;border-radius:0;width:auto!important;z-index:2;margin-left:-1px;margin-right:-1px}.ui.attached.buttons .button{margin:0}.ui.attached.buttons .button:first-child{border-radius:0}.ui.attached.buttons .button:last-child{border-radius:0}.ui[class*="top attached"].buttons{margin-bottom:-1px;border-radius:.28571429rem .28571429rem 0 0}.ui[class*="top attached"].buttons .button:first-child{border-radius:.28571429rem 0 0 0}.ui[class*="top attached"].buttons .button:last-child{border-radius:0 .28571429rem 0 0}.ui[class*="bottom attached"].buttons{margin-top:-1px;border-radius:0 0 .28571429rem .28571429rem}.ui[class*="bottom attached"].buttons .button:first-child{border-radius:0 0 0 .28571429rem}.ui[class*="bottom attached"].buttons .button:last-child{border-radius:0 0 .28571429rem 0}.ui[class*="left attached"].buttons{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-right:0;margin-left:-1px;border-radius:0 .28571429rem .28571429rem 0}.ui[class*="left attached"].buttons .button:first-child{margin-left:-1px;border-radius:0 .28571429rem 0 0}.ui[class*="left attached"].buttons .button:last-child{margin-left:-1px;border-radius:0 0 .28571429rem 0}.ui[class*="right attached"].buttons{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-left:0;margin-right:-1px;border-radius:.28571429rem 0 0 .28571429rem}.ui[class*="right attached"].buttons .button:first-child{margin-left:-1px;border-radius:.28571429rem 0 0 0}.ui[class*="right attached"].buttons .button:last-child{margin-left:-1px;border-radius:0 0 0 .28571429rem}.ui.fluid.button,.ui.fluid.buttons{width:100%}.ui.fluid.button{display:block}.ui.two.buttons{width:100%}.ui.two.buttons>.button{width:50%}.ui.three.buttons{width:100%}.ui.three.buttons>.button{width:33.333%}.ui.four.buttons{width:100%}.ui.four.buttons>.button{width:25%}.ui.five.buttons{width:100%}.ui.five.buttons>.button{width:20%}.ui.six.buttons{width:100%}.ui.six.buttons>.button{width:16.666%}.ui.seven.buttons{width:100%}.ui.seven.buttons>.button{width:14.285%}.ui.eight.buttons{width:100%}.ui.eight.buttons>.button{width:12.5%}.ui.nine.buttons{width:100%}.ui.nine.buttons>.button{width:11.11%}.ui.ten.buttons{width:100%}.ui.ten.buttons>.button{width:10%}.ui.eleven.buttons{width:100%}.ui.eleven.buttons>.button{width:9.09%}.ui.twelve.buttons{width:100%}.ui.twelve.buttons>.button{width:8.3333%}.ui.fluid.vertical.buttons,.ui.fluid.vertical.buttons>.button{display:-webkit-box;display:-ms-flexbox;display:flex;width:auto}.ui.two.vertical.buttons>.button{height:50%}.ui.three.vertical.buttons>.button{height:33.333%}.ui.four.vertical.buttons>.button{height:25%}.ui.five.vertical.buttons>.button{height:20%}.ui.six.vertical.buttons>.button{height:16.666%}.ui.seven.vertical.buttons>.button{height:14.285%}.ui.eight.vertical.buttons>.button{height:12.5%}.ui.nine.vertical.buttons>.button{height:11.11%}.ui.ten.vertical.buttons>.button{height:10%}.ui.eleven.vertical.buttons>.button{height:9.09%}.ui.twelve.vertical.buttons>.button{height:8.3333%}.ui.black.button,.ui.black.buttons .button{background-color:#1b1c1d;color:#fff;text-shadow:none;background-image:none}.ui.black.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.black.button:hover,.ui.black.buttons .button:hover{background-color:#27292a;color:#fff;text-shadow:none}.ui.black.button:focus,.ui.black.buttons .button:focus{background-color:#2f3032;color:#fff;text-shadow:none}.ui.black.button:active,.ui.black.buttons .button:active{background-color:#343637;color:#fff;text-shadow:none}.ui.black.active.button,.ui.black.button .active.button:active,.ui.black.buttons .active.button,.ui.black.buttons .active.button:active{background-color:#0f0f10;color:#fff;text-shadow:none}.ui.basic.black.button,.ui.basic.black.buttons .button{-webkit-box-shadow:0 0 0 1px #1b1c1d inset!important;box-shadow:0 0 0 1px #1b1c1d inset!important;color:#1b1c1d!important}.ui.basic.black.button:hover,.ui.basic.black.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #27292a inset!important;box-shadow:0 0 0 1px #27292a inset!important;color:#27292a!important}.ui.basic.black.button:focus,.ui.basic.black.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #2f3032 inset!important;box-shadow:0 0 0 1px #2f3032 inset!important;color:#27292a!important}.ui.basic.black.active.button,.ui.basic.black.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #0f0f10 inset!important;box-shadow:0 0 0 1px #0f0f10 inset!important;color:#343637!important}.ui.basic.black.button:active,.ui.basic.black.buttons .button:active{-webkit-box-shadow:0 0 0 1px #343637 inset!important;box-shadow:0 0 0 1px #343637 inset!important;color:#343637!important}.ui.buttons:not(.vertical)>.basic.black.button:not(:first-child){margin-left:-1px}.ui.inverted.black.button,.ui.inverted.black.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #d4d4d5 inset!important;box-shadow:0 0 0 2px #d4d4d5 inset!important;color:#fff}.ui.inverted.black.button.active,.ui.inverted.black.button:active,.ui.inverted.black.button:focus,.ui.inverted.black.button:hover,.ui.inverted.black.buttons .button.active,.ui.inverted.black.buttons .button:active,.ui.inverted.black.buttons .button:focus,.ui.inverted.black.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:#fff}.ui.inverted.black.button:hover,.ui.inverted.black.buttons .button:hover{background-color:#000}.ui.inverted.black.button:focus,.ui.inverted.black.buttons .button:focus{background-color:#000}.ui.inverted.black.active.button,.ui.inverted.black.buttons .active.button{background-color:#000}.ui.inverted.black.button:active,.ui.inverted.black.buttons .button:active{background-color:#000}.ui.inverted.black.basic.button,.ui.inverted.black.basic.buttons .button,.ui.inverted.black.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.black.basic.button:hover,.ui.inverted.black.basic.buttons .button:hover,.ui.inverted.black.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #000 inset!important;box-shadow:0 0 0 2px #000 inset!important;color:#fff!important}.ui.inverted.black.basic.button:focus,.ui.inverted.black.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #000 inset!important;box-shadow:0 0 0 2px #000 inset!important;color:#545454!important}.ui.inverted.black.basic.active.button,.ui.inverted.black.basic.buttons .active.button,.ui.inverted.black.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #000 inset!important;box-shadow:0 0 0 2px #000 inset!important;color:#fff!important}.ui.inverted.black.basic.button:active,.ui.inverted.black.basic.buttons .button:active,.ui.inverted.black.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #000 inset!important;box-shadow:0 0 0 2px #000 inset!important;color:#fff!important}.ui.grey.button,.ui.grey.buttons .button{background-color:#767676;color:#fff;text-shadow:none;background-image:none}.ui.grey.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.grey.button:hover,.ui.grey.buttons .button:hover{background-color:#838383;color:#fff;text-shadow:none}.ui.grey.button:focus,.ui.grey.buttons .button:focus{background-color:#8a8a8a;color:#fff;text-shadow:none}.ui.grey.button:active,.ui.grey.buttons .button:active{background-color:#909090;color:#fff;text-shadow:none}.ui.grey.active.button,.ui.grey.button .active.button:active,.ui.grey.buttons .active.button,.ui.grey.buttons .active.button:active{background-color:#696969;color:#fff;text-shadow:none}.ui.basic.grey.button,.ui.basic.grey.buttons .button{-webkit-box-shadow:0 0 0 1px #767676 inset!important;box-shadow:0 0 0 1px #767676 inset!important;color:#767676!important}.ui.basic.grey.button:hover,.ui.basic.grey.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #838383 inset!important;box-shadow:0 0 0 1px #838383 inset!important;color:#838383!important}.ui.basic.grey.button:focus,.ui.basic.grey.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #8a8a8a inset!important;box-shadow:0 0 0 1px #8a8a8a inset!important;color:#838383!important}.ui.basic.grey.active.button,.ui.basic.grey.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #696969 inset!important;box-shadow:0 0 0 1px #696969 inset!important;color:#909090!important}.ui.basic.grey.button:active,.ui.basic.grey.buttons .button:active{-webkit-box-shadow:0 0 0 1px #909090 inset!important;box-shadow:0 0 0 1px #909090 inset!important;color:#909090!important}.ui.buttons:not(.vertical)>.basic.grey.button:not(:first-child){margin-left:-1px}.ui.inverted.grey.button,.ui.inverted.grey.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #d4d4d5 inset!important;box-shadow:0 0 0 2px #d4d4d5 inset!important;color:#fff}.ui.inverted.grey.button.active,.ui.inverted.grey.button:active,.ui.inverted.grey.button:focus,.ui.inverted.grey.button:hover,.ui.inverted.grey.buttons .button.active,.ui.inverted.grey.buttons .button:active,.ui.inverted.grey.buttons .button:focus,.ui.inverted.grey.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:rgba(0,0,0,.6)}.ui.inverted.grey.button:hover,.ui.inverted.grey.buttons .button:hover{background-color:#cfd0d2}.ui.inverted.grey.button:focus,.ui.inverted.grey.buttons .button:focus{background-color:#c7c9cb}.ui.inverted.grey.active.button,.ui.inverted.grey.buttons .active.button{background-color:#cfd0d2}.ui.inverted.grey.button:active,.ui.inverted.grey.buttons .button:active{background-color:#c2c4c5}.ui.inverted.grey.basic.button,.ui.inverted.grey.basic.buttons .button,.ui.inverted.grey.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.grey.basic.button:hover,.ui.inverted.grey.basic.buttons .button:hover,.ui.inverted.grey.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #cfd0d2 inset!important;box-shadow:0 0 0 2px #cfd0d2 inset!important;color:#fff!important}.ui.inverted.grey.basic.button:focus,.ui.inverted.grey.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #c7c9cb inset!important;box-shadow:0 0 0 2px #c7c9cb inset!important;color:#dcddde!important}.ui.inverted.grey.basic.active.button,.ui.inverted.grey.basic.buttons .active.button,.ui.inverted.grey.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #cfd0d2 inset!important;box-shadow:0 0 0 2px #cfd0d2 inset!important;color:#fff!important}.ui.inverted.grey.basic.button:active,.ui.inverted.grey.basic.buttons .button:active,.ui.inverted.grey.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #c2c4c5 inset!important;box-shadow:0 0 0 2px #c2c4c5 inset!important;color:#fff!important}.ui.brown.button,.ui.brown.buttons .button{background-color:#a5673f;color:#fff;text-shadow:none;background-image:none}.ui.brown.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.brown.button:hover,.ui.brown.buttons .button:hover{background-color:#975b33;color:#fff;text-shadow:none}.ui.brown.button:focus,.ui.brown.buttons .button:focus{background-color:#90532b;color:#fff;text-shadow:none}.ui.brown.button:active,.ui.brown.buttons .button:active{background-color:#805031;color:#fff;text-shadow:none}.ui.brown.active.button,.ui.brown.button .active.button:active,.ui.brown.buttons .active.button,.ui.brown.buttons .active.button:active{background-color:#995a31;color:#fff;text-shadow:none}.ui.basic.brown.button,.ui.basic.brown.buttons .button{-webkit-box-shadow:0 0 0 1px #a5673f inset!important;box-shadow:0 0 0 1px #a5673f inset!important;color:#a5673f!important}.ui.basic.brown.button:hover,.ui.basic.brown.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #975b33 inset!important;box-shadow:0 0 0 1px #975b33 inset!important;color:#975b33!important}.ui.basic.brown.button:focus,.ui.basic.brown.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #90532b inset!important;box-shadow:0 0 0 1px #90532b inset!important;color:#975b33!important}.ui.basic.brown.active.button,.ui.basic.brown.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #995a31 inset!important;box-shadow:0 0 0 1px #995a31 inset!important;color:#805031!important}.ui.basic.brown.button:active,.ui.basic.brown.buttons .button:active{-webkit-box-shadow:0 0 0 1px #805031 inset!important;box-shadow:0 0 0 1px #805031 inset!important;color:#805031!important}.ui.buttons:not(.vertical)>.basic.brown.button:not(:first-child){margin-left:-1px}.ui.inverted.brown.button,.ui.inverted.brown.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #d67c1c inset!important;box-shadow:0 0 0 2px #d67c1c inset!important;color:#d67c1c}.ui.inverted.brown.button.active,.ui.inverted.brown.button:active,.ui.inverted.brown.button:focus,.ui.inverted.brown.button:hover,.ui.inverted.brown.buttons .button.active,.ui.inverted.brown.buttons .button:active,.ui.inverted.brown.buttons .button:focus,.ui.inverted.brown.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:#fff}.ui.inverted.brown.button:hover,.ui.inverted.brown.buttons .button:hover{background-color:#c86f11}.ui.inverted.brown.button:focus,.ui.inverted.brown.buttons .button:focus{background-color:#c16808}.ui.inverted.brown.active.button,.ui.inverted.brown.buttons .active.button{background-color:#cc6f0d}.ui.inverted.brown.button:active,.ui.inverted.brown.buttons .button:active{background-color:#a96216}.ui.inverted.brown.basic.button,.ui.inverted.brown.basic.buttons .button,.ui.inverted.brown.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.brown.basic.button:hover,.ui.inverted.brown.basic.buttons .button:hover,.ui.inverted.brown.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #c86f11 inset!important;box-shadow:0 0 0 2px #c86f11 inset!important;color:#d67c1c!important}.ui.inverted.brown.basic.button:focus,.ui.inverted.brown.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #c16808 inset!important;box-shadow:0 0 0 2px #c16808 inset!important;color:#d67c1c!important}.ui.inverted.brown.basic.active.button,.ui.inverted.brown.basic.buttons .active.button,.ui.inverted.brown.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #cc6f0d inset!important;box-shadow:0 0 0 2px #cc6f0d inset!important;color:#d67c1c!important}.ui.inverted.brown.basic.button:active,.ui.inverted.brown.basic.buttons .button:active,.ui.inverted.brown.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #a96216 inset!important;box-shadow:0 0 0 2px #a96216 inset!important;color:#d67c1c!important}.ui.blue.button,.ui.blue.buttons .button{background-color:#2185d0;color:#fff;text-shadow:none;background-image:none}.ui.blue.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.blue.button:hover,.ui.blue.buttons .button:hover{background-color:#1678c2;color:#fff;text-shadow:none}.ui.blue.button:focus,.ui.blue.buttons .button:focus{background-color:#0d71bb;color:#fff;text-shadow:none}.ui.blue.button:active,.ui.blue.buttons .button:active{background-color:#1a69a4;color:#fff;text-shadow:none}.ui.blue.active.button,.ui.blue.button .active.button:active,.ui.blue.buttons .active.button,.ui.blue.buttons .active.button:active{background-color:#1279c6;color:#fff;text-shadow:none}.ui.basic.blue.button,.ui.basic.blue.buttons .button{-webkit-box-shadow:0 0 0 1px #2185d0 inset!important;box-shadow:0 0 0 1px #2185d0 inset!important;color:#2185d0!important}.ui.basic.blue.button:hover,.ui.basic.blue.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #1678c2 inset!important;box-shadow:0 0 0 1px #1678c2 inset!important;color:#1678c2!important}.ui.basic.blue.button:focus,.ui.basic.blue.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #0d71bb inset!important;box-shadow:0 0 0 1px #0d71bb inset!important;color:#1678c2!important}.ui.basic.blue.active.button,.ui.basic.blue.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #1279c6 inset!important;box-shadow:0 0 0 1px #1279c6 inset!important;color:#1a69a4!important}.ui.basic.blue.button:active,.ui.basic.blue.buttons .button:active{-webkit-box-shadow:0 0 0 1px #1a69a4 inset!important;box-shadow:0 0 0 1px #1a69a4 inset!important;color:#1a69a4!important}.ui.buttons:not(.vertical)>.basic.blue.button:not(:first-child){margin-left:-1px}.ui.inverted.blue.button,.ui.inverted.blue.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #54c8ff inset!important;box-shadow:0 0 0 2px #54c8ff inset!important;color:#54c8ff}.ui.inverted.blue.button.active,.ui.inverted.blue.button:active,.ui.inverted.blue.button:focus,.ui.inverted.blue.button:hover,.ui.inverted.blue.buttons .button.active,.ui.inverted.blue.buttons .button:active,.ui.inverted.blue.buttons .button:focus,.ui.inverted.blue.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:#fff}.ui.inverted.blue.button:hover,.ui.inverted.blue.buttons .button:hover{background-color:#3ac0ff}.ui.inverted.blue.button:focus,.ui.inverted.blue.buttons .button:focus{background-color:#2bbbff}.ui.inverted.blue.active.button,.ui.inverted.blue.buttons .active.button{background-color:#3ac0ff}.ui.inverted.blue.button:active,.ui.inverted.blue.buttons .button:active{background-color:#21b8ff}.ui.inverted.blue.basic.button,.ui.inverted.blue.basic.buttons .button,.ui.inverted.blue.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.blue.basic.button:hover,.ui.inverted.blue.basic.buttons .button:hover,.ui.inverted.blue.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #3ac0ff inset!important;box-shadow:0 0 0 2px #3ac0ff inset!important;color:#54c8ff!important}.ui.inverted.blue.basic.button:focus,.ui.inverted.blue.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #2bbbff inset!important;box-shadow:0 0 0 2px #2bbbff inset!important;color:#54c8ff!important}.ui.inverted.blue.basic.active.button,.ui.inverted.blue.basic.buttons .active.button,.ui.inverted.blue.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #3ac0ff inset!important;box-shadow:0 0 0 2px #3ac0ff inset!important;color:#54c8ff!important}.ui.inverted.blue.basic.button:active,.ui.inverted.blue.basic.buttons .button:active,.ui.inverted.blue.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #21b8ff inset!important;box-shadow:0 0 0 2px #21b8ff inset!important;color:#54c8ff!important}.ui.green.button,.ui.green.buttons .button{background-color:#21ba45;color:#fff;text-shadow:none;background-image:none}.ui.green.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.green.button:hover,.ui.green.buttons .button:hover{background-color:#16ab39;color:#fff;text-shadow:none}.ui.green.button:focus,.ui.green.buttons .button:focus{background-color:#0ea432;color:#fff;text-shadow:none}.ui.green.button:active,.ui.green.buttons .button:active{background-color:#198f35;color:#fff;text-shadow:none}.ui.green.active.button,.ui.green.button .active.button:active,.ui.green.buttons .active.button,.ui.green.buttons .active.button:active{background-color:#13ae38;color:#fff;text-shadow:none}.ui.basic.green.button,.ui.basic.green.buttons .button{-webkit-box-shadow:0 0 0 1px #21ba45 inset!important;box-shadow:0 0 0 1px #21ba45 inset!important;color:#21ba45!important}.ui.basic.green.button:hover,.ui.basic.green.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #16ab39 inset!important;box-shadow:0 0 0 1px #16ab39 inset!important;color:#16ab39!important}.ui.basic.green.button:focus,.ui.basic.green.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #0ea432 inset!important;box-shadow:0 0 0 1px #0ea432 inset!important;color:#16ab39!important}.ui.basic.green.active.button,.ui.basic.green.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #13ae38 inset!important;box-shadow:0 0 0 1px #13ae38 inset!important;color:#198f35!important}.ui.basic.green.button:active,.ui.basic.green.buttons .button:active{-webkit-box-shadow:0 0 0 1px #198f35 inset!important;box-shadow:0 0 0 1px #198f35 inset!important;color:#198f35!important}.ui.buttons:not(.vertical)>.basic.green.button:not(:first-child){margin-left:-1px}.ui.inverted.green.button,.ui.inverted.green.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #2ecc40 inset!important;box-shadow:0 0 0 2px #2ecc40 inset!important;color:#2ecc40}.ui.inverted.green.button.active,.ui.inverted.green.button:active,.ui.inverted.green.button:focus,.ui.inverted.green.button:hover,.ui.inverted.green.buttons .button.active,.ui.inverted.green.buttons .button:active,.ui.inverted.green.buttons .button:focus,.ui.inverted.green.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:#fff}.ui.inverted.green.button:hover,.ui.inverted.green.buttons .button:hover{background-color:#22be34}.ui.inverted.green.button:focus,.ui.inverted.green.buttons .button:focus{background-color:#19b82b}.ui.inverted.green.active.button,.ui.inverted.green.buttons .active.button{background-color:#1fc231}.ui.inverted.green.button:active,.ui.inverted.green.buttons .button:active{background-color:#25a233}.ui.inverted.green.basic.button,.ui.inverted.green.basic.buttons .button,.ui.inverted.green.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.green.basic.button:hover,.ui.inverted.green.basic.buttons .button:hover,.ui.inverted.green.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #22be34 inset!important;box-shadow:0 0 0 2px #22be34 inset!important;color:#2ecc40!important}.ui.inverted.green.basic.button:focus,.ui.inverted.green.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #19b82b inset!important;box-shadow:0 0 0 2px #19b82b inset!important;color:#2ecc40!important}.ui.inverted.green.basic.active.button,.ui.inverted.green.basic.buttons .active.button,.ui.inverted.green.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #1fc231 inset!important;box-shadow:0 0 0 2px #1fc231 inset!important;color:#2ecc40!important}.ui.inverted.green.basic.button:active,.ui.inverted.green.basic.buttons .button:active,.ui.inverted.green.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #25a233 inset!important;box-shadow:0 0 0 2px #25a233 inset!important;color:#2ecc40!important}.ui.orange.button,.ui.orange.buttons .button{background-color:#f2711c;color:#fff;text-shadow:none;background-image:none}.ui.orange.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.orange.button:hover,.ui.orange.buttons .button:hover{background-color:#f26202;color:#fff;text-shadow:none}.ui.orange.button:focus,.ui.orange.buttons .button:focus{background-color:#e55b00;color:#fff;text-shadow:none}.ui.orange.button:active,.ui.orange.buttons .button:active{background-color:#cf590c;color:#fff;text-shadow:none}.ui.orange.active.button,.ui.orange.button .active.button:active,.ui.orange.buttons .active.button,.ui.orange.buttons .active.button:active{background-color:#f56100;color:#fff;text-shadow:none}.ui.basic.orange.button,.ui.basic.orange.buttons .button{-webkit-box-shadow:0 0 0 1px #f2711c inset!important;box-shadow:0 0 0 1px #f2711c inset!important;color:#f2711c!important}.ui.basic.orange.button:hover,.ui.basic.orange.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #f26202 inset!important;box-shadow:0 0 0 1px #f26202 inset!important;color:#f26202!important}.ui.basic.orange.button:focus,.ui.basic.orange.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #e55b00 inset!important;box-shadow:0 0 0 1px #e55b00 inset!important;color:#f26202!important}.ui.basic.orange.active.button,.ui.basic.orange.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #f56100 inset!important;box-shadow:0 0 0 1px #f56100 inset!important;color:#cf590c!important}.ui.basic.orange.button:active,.ui.basic.orange.buttons .button:active{-webkit-box-shadow:0 0 0 1px #cf590c inset!important;box-shadow:0 0 0 1px #cf590c inset!important;color:#cf590c!important}.ui.buttons:not(.vertical)>.basic.orange.button:not(:first-child){margin-left:-1px}.ui.inverted.orange.button,.ui.inverted.orange.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #ff851b inset!important;box-shadow:0 0 0 2px #ff851b inset!important;color:#ff851b}.ui.inverted.orange.button.active,.ui.inverted.orange.button:active,.ui.inverted.orange.button:focus,.ui.inverted.orange.button:hover,.ui.inverted.orange.buttons .button.active,.ui.inverted.orange.buttons .button:active,.ui.inverted.orange.buttons .button:focus,.ui.inverted.orange.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:#fff}.ui.inverted.orange.button:hover,.ui.inverted.orange.buttons .button:hover{background-color:#ff7701}.ui.inverted.orange.button:focus,.ui.inverted.orange.buttons .button:focus{background-color:#f17000}.ui.inverted.orange.active.button,.ui.inverted.orange.buttons .active.button{background-color:#ff7701}.ui.inverted.orange.button:active,.ui.inverted.orange.buttons .button:active{background-color:#e76b00}.ui.inverted.orange.basic.button,.ui.inverted.orange.basic.buttons .button,.ui.inverted.orange.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.orange.basic.button:hover,.ui.inverted.orange.basic.buttons .button:hover,.ui.inverted.orange.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #ff7701 inset!important;box-shadow:0 0 0 2px #ff7701 inset!important;color:#ff851b!important}.ui.inverted.orange.basic.button:focus,.ui.inverted.orange.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #f17000 inset!important;box-shadow:0 0 0 2px #f17000 inset!important;color:#ff851b!important}.ui.inverted.orange.basic.active.button,.ui.inverted.orange.basic.buttons .active.button,.ui.inverted.orange.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #ff7701 inset!important;box-shadow:0 0 0 2px #ff7701 inset!important;color:#ff851b!important}.ui.inverted.orange.basic.button:active,.ui.inverted.orange.basic.buttons .button:active,.ui.inverted.orange.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #e76b00 inset!important;box-shadow:0 0 0 2px #e76b00 inset!important;color:#ff851b!important}.ui.pink.button,.ui.pink.buttons .button{background-color:#e03997;color:#fff;text-shadow:none;background-image:none}.ui.pink.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.pink.button:hover,.ui.pink.buttons .button:hover{background-color:#e61a8d;color:#fff;text-shadow:none}.ui.pink.button:focus,.ui.pink.buttons .button:focus{background-color:#e10f85;color:#fff;text-shadow:none}.ui.pink.button:active,.ui.pink.buttons .button:active{background-color:#c71f7e;color:#fff;text-shadow:none}.ui.pink.active.button,.ui.pink.button .active.button:active,.ui.pink.buttons .active.button,.ui.pink.buttons .active.button:active{background-color:#ea158d;color:#fff;text-shadow:none}.ui.basic.pink.button,.ui.basic.pink.buttons .button{-webkit-box-shadow:0 0 0 1px #e03997 inset!important;box-shadow:0 0 0 1px #e03997 inset!important;color:#e03997!important}.ui.basic.pink.button:hover,.ui.basic.pink.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #e61a8d inset!important;box-shadow:0 0 0 1px #e61a8d inset!important;color:#e61a8d!important}.ui.basic.pink.button:focus,.ui.basic.pink.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #e10f85 inset!important;box-shadow:0 0 0 1px #e10f85 inset!important;color:#e61a8d!important}.ui.basic.pink.active.button,.ui.basic.pink.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #ea158d inset!important;box-shadow:0 0 0 1px #ea158d inset!important;color:#c71f7e!important}.ui.basic.pink.button:active,.ui.basic.pink.buttons .button:active{-webkit-box-shadow:0 0 0 1px #c71f7e inset!important;box-shadow:0 0 0 1px #c71f7e inset!important;color:#c71f7e!important}.ui.buttons:not(.vertical)>.basic.pink.button:not(:first-child){margin-left:-1px}.ui.inverted.pink.button,.ui.inverted.pink.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #ff8edf inset!important;box-shadow:0 0 0 2px #ff8edf inset!important;color:#ff8edf}.ui.inverted.pink.button.active,.ui.inverted.pink.button:active,.ui.inverted.pink.button:focus,.ui.inverted.pink.button:hover,.ui.inverted.pink.buttons .button.active,.ui.inverted.pink.buttons .button:active,.ui.inverted.pink.buttons .button:focus,.ui.inverted.pink.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:#fff}.ui.inverted.pink.button:hover,.ui.inverted.pink.buttons .button:hover{background-color:#ff74d8}.ui.inverted.pink.button:focus,.ui.inverted.pink.buttons .button:focus{background-color:#ff65d3}.ui.inverted.pink.active.button,.ui.inverted.pink.buttons .active.button{background-color:#ff74d8}.ui.inverted.pink.button:active,.ui.inverted.pink.buttons .button:active{background-color:#ff5bd1}.ui.inverted.pink.basic.button,.ui.inverted.pink.basic.buttons .button,.ui.inverted.pink.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.pink.basic.button:hover,.ui.inverted.pink.basic.buttons .button:hover,.ui.inverted.pink.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #ff74d8 inset!important;box-shadow:0 0 0 2px #ff74d8 inset!important;color:#ff8edf!important}.ui.inverted.pink.basic.button:focus,.ui.inverted.pink.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #ff65d3 inset!important;box-shadow:0 0 0 2px #ff65d3 inset!important;color:#ff8edf!important}.ui.inverted.pink.basic.active.button,.ui.inverted.pink.basic.buttons .active.button,.ui.inverted.pink.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #ff74d8 inset!important;box-shadow:0 0 0 2px #ff74d8 inset!important;color:#ff8edf!important}.ui.inverted.pink.basic.button:active,.ui.inverted.pink.basic.buttons .button:active,.ui.inverted.pink.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #ff5bd1 inset!important;box-shadow:0 0 0 2px #ff5bd1 inset!important;color:#ff8edf!important}.ui.violet.button,.ui.violet.buttons .button{background-color:#6435c9;color:#fff;text-shadow:none;background-image:none}.ui.violet.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.violet.button:hover,.ui.violet.buttons .button:hover{background-color:#5829bb;color:#fff;text-shadow:none}.ui.violet.button:focus,.ui.violet.buttons .button:focus{background-color:#4f20b5;color:#fff;text-shadow:none}.ui.violet.button:active,.ui.violet.buttons .button:active{background-color:#502aa1;color:#fff;text-shadow:none}.ui.violet.active.button,.ui.violet.button .active.button:active,.ui.violet.buttons .active.button,.ui.violet.buttons .active.button:active{background-color:#5626bf;color:#fff;text-shadow:none}.ui.basic.violet.button,.ui.basic.violet.buttons .button{-webkit-box-shadow:0 0 0 1px #6435c9 inset!important;box-shadow:0 0 0 1px #6435c9 inset!important;color:#6435c9!important}.ui.basic.violet.button:hover,.ui.basic.violet.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #5829bb inset!important;box-shadow:0 0 0 1px #5829bb inset!important;color:#5829bb!important}.ui.basic.violet.button:focus,.ui.basic.violet.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #4f20b5 inset!important;box-shadow:0 0 0 1px #4f20b5 inset!important;color:#5829bb!important}.ui.basic.violet.active.button,.ui.basic.violet.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #5626bf inset!important;box-shadow:0 0 0 1px #5626bf inset!important;color:#502aa1!important}.ui.basic.violet.button:active,.ui.basic.violet.buttons .button:active{-webkit-box-shadow:0 0 0 1px #502aa1 inset!important;box-shadow:0 0 0 1px #502aa1 inset!important;color:#502aa1!important}.ui.buttons:not(.vertical)>.basic.violet.button:not(:first-child){margin-left:-1px}.ui.inverted.violet.button,.ui.inverted.violet.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #a291fb inset!important;box-shadow:0 0 0 2px #a291fb inset!important;color:#a291fb}.ui.inverted.violet.button.active,.ui.inverted.violet.button:active,.ui.inverted.violet.button:focus,.ui.inverted.violet.button:hover,.ui.inverted.violet.buttons .button.active,.ui.inverted.violet.buttons .button:active,.ui.inverted.violet.buttons .button:focus,.ui.inverted.violet.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:#fff}.ui.inverted.violet.button:hover,.ui.inverted.violet.buttons .button:hover{background-color:#8a73ff}.ui.inverted.violet.button:focus,.ui.inverted.violet.buttons .button:focus{background-color:#7d64ff}.ui.inverted.violet.active.button,.ui.inverted.violet.buttons .active.button{background-color:#8a73ff}.ui.inverted.violet.button:active,.ui.inverted.violet.buttons .button:active{background-color:#7860f9}.ui.inverted.violet.basic.button,.ui.inverted.violet.basic.buttons .button,.ui.inverted.violet.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.violet.basic.button:hover,.ui.inverted.violet.basic.buttons .button:hover,.ui.inverted.violet.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #8a73ff inset!important;box-shadow:0 0 0 2px #8a73ff inset!important;color:#a291fb!important}.ui.inverted.violet.basic.button:focus,.ui.inverted.violet.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #7d64ff inset!important;box-shadow:0 0 0 2px #7d64ff inset!important;color:#a291fb!important}.ui.inverted.violet.basic.active.button,.ui.inverted.violet.basic.buttons .active.button,.ui.inverted.violet.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #8a73ff inset!important;box-shadow:0 0 0 2px #8a73ff inset!important;color:#a291fb!important}.ui.inverted.violet.basic.button:active,.ui.inverted.violet.basic.buttons .button:active,.ui.inverted.violet.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #7860f9 inset!important;box-shadow:0 0 0 2px #7860f9 inset!important;color:#a291fb!important}.ui.purple.button,.ui.purple.buttons .button{background-color:#a333c8;color:#fff;text-shadow:none;background-image:none}.ui.purple.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.purple.button:hover,.ui.purple.buttons .button:hover{background-color:#9627ba;color:#fff;text-shadow:none}.ui.purple.button:focus,.ui.purple.buttons .button:focus{background-color:#8f1eb4;color:#fff;text-shadow:none}.ui.purple.button:active,.ui.purple.buttons .button:active{background-color:#82299f;color:#fff;text-shadow:none}.ui.purple.active.button,.ui.purple.button .active.button:active,.ui.purple.buttons .active.button,.ui.purple.buttons .active.button:active{background-color:#9724be;color:#fff;text-shadow:none}.ui.basic.purple.button,.ui.basic.purple.buttons .button{-webkit-box-shadow:0 0 0 1px #a333c8 inset!important;box-shadow:0 0 0 1px #a333c8 inset!important;color:#a333c8!important}.ui.basic.purple.button:hover,.ui.basic.purple.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #9627ba inset!important;box-shadow:0 0 0 1px #9627ba inset!important;color:#9627ba!important}.ui.basic.purple.button:focus,.ui.basic.purple.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #8f1eb4 inset!important;box-shadow:0 0 0 1px #8f1eb4 inset!important;color:#9627ba!important}.ui.basic.purple.active.button,.ui.basic.purple.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #9724be inset!important;box-shadow:0 0 0 1px #9724be inset!important;color:#82299f!important}.ui.basic.purple.button:active,.ui.basic.purple.buttons .button:active{-webkit-box-shadow:0 0 0 1px #82299f inset!important;box-shadow:0 0 0 1px #82299f inset!important;color:#82299f!important}.ui.buttons:not(.vertical)>.basic.purple.button:not(:first-child){margin-left:-1px}.ui.inverted.purple.button,.ui.inverted.purple.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #dc73ff inset!important;box-shadow:0 0 0 2px #dc73ff inset!important;color:#dc73ff}.ui.inverted.purple.button.active,.ui.inverted.purple.button:active,.ui.inverted.purple.button:focus,.ui.inverted.purple.button:hover,.ui.inverted.purple.buttons .button.active,.ui.inverted.purple.buttons .button:active,.ui.inverted.purple.buttons .button:focus,.ui.inverted.purple.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:#fff}.ui.inverted.purple.button:hover,.ui.inverted.purple.buttons .button:hover{background-color:#d65aff}.ui.inverted.purple.button:focus,.ui.inverted.purple.buttons .button:focus{background-color:#d24aff}.ui.inverted.purple.active.button,.ui.inverted.purple.buttons .active.button{background-color:#d65aff}.ui.inverted.purple.button:active,.ui.inverted.purple.buttons .button:active{background-color:#cf40ff}.ui.inverted.purple.basic.button,.ui.inverted.purple.basic.buttons .button,.ui.inverted.purple.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.purple.basic.button:hover,.ui.inverted.purple.basic.buttons .button:hover,.ui.inverted.purple.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #d65aff inset!important;box-shadow:0 0 0 2px #d65aff inset!important;color:#dc73ff!important}.ui.inverted.purple.basic.button:focus,.ui.inverted.purple.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #d24aff inset!important;box-shadow:0 0 0 2px #d24aff inset!important;color:#dc73ff!important}.ui.inverted.purple.basic.active.button,.ui.inverted.purple.basic.buttons .active.button,.ui.inverted.purple.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #d65aff inset!important;box-shadow:0 0 0 2px #d65aff inset!important;color:#dc73ff!important}.ui.inverted.purple.basic.button:active,.ui.inverted.purple.basic.buttons .button:active,.ui.inverted.purple.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #cf40ff inset!important;box-shadow:0 0 0 2px #cf40ff inset!important;color:#dc73ff!important}.ui.red.button,.ui.red.buttons .button{background-color:#db2828;color:#fff;text-shadow:none;background-image:none}.ui.red.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.red.button:hover,.ui.red.buttons .button:hover{background-color:#d01919;color:#fff;text-shadow:none}.ui.red.button:focus,.ui.red.buttons .button:focus{background-color:#ca1010;color:#fff;text-shadow:none}.ui.red.button:active,.ui.red.buttons .button:active{background-color:#b21e1e;color:#fff;text-shadow:none}.ui.red.active.button,.ui.red.button .active.button:active,.ui.red.buttons .active.button,.ui.red.buttons .active.button:active{background-color:#d41515;color:#fff;text-shadow:none}.ui.basic.red.button,.ui.basic.red.buttons .button{-webkit-box-shadow:0 0 0 1px #db2828 inset!important;box-shadow:0 0 0 1px #db2828 inset!important;color:#db2828!important}.ui.basic.red.button:hover,.ui.basic.red.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #d01919 inset!important;box-shadow:0 0 0 1px #d01919 inset!important;color:#d01919!important}.ui.basic.red.button:focus,.ui.basic.red.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #ca1010 inset!important;box-shadow:0 0 0 1px #ca1010 inset!important;color:#d01919!important}.ui.basic.red.active.button,.ui.basic.red.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #d41515 inset!important;box-shadow:0 0 0 1px #d41515 inset!important;color:#b21e1e!important}.ui.basic.red.button:active,.ui.basic.red.buttons .button:active{-webkit-box-shadow:0 0 0 1px #b21e1e inset!important;box-shadow:0 0 0 1px #b21e1e inset!important;color:#b21e1e!important}.ui.buttons:not(.vertical)>.basic.red.button:not(:first-child){margin-left:-1px}.ui.inverted.red.button,.ui.inverted.red.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #ff695e inset!important;box-shadow:0 0 0 2px #ff695e inset!important;color:#ff695e}.ui.inverted.red.button.active,.ui.inverted.red.button:active,.ui.inverted.red.button:focus,.ui.inverted.red.button:hover,.ui.inverted.red.buttons .button.active,.ui.inverted.red.buttons .button:active,.ui.inverted.red.buttons .button:focus,.ui.inverted.red.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:#fff}.ui.inverted.red.button:hover,.ui.inverted.red.buttons .button:hover{background-color:#ff5144}.ui.inverted.red.button:focus,.ui.inverted.red.buttons .button:focus{background-color:#ff4335}.ui.inverted.red.active.button,.ui.inverted.red.buttons .active.button{background-color:#ff5144}.ui.inverted.red.button:active,.ui.inverted.red.buttons .button:active{background-color:#ff392b}.ui.inverted.red.basic.button,.ui.inverted.red.basic.buttons .button,.ui.inverted.red.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.red.basic.button:hover,.ui.inverted.red.basic.buttons .button:hover,.ui.inverted.red.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #ff5144 inset!important;box-shadow:0 0 0 2px #ff5144 inset!important;color:#ff695e!important}.ui.inverted.red.basic.button:focus,.ui.inverted.red.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #ff4335 inset!important;box-shadow:0 0 0 2px #ff4335 inset!important;color:#ff695e!important}.ui.inverted.red.basic.active.button,.ui.inverted.red.basic.buttons .active.button,.ui.inverted.red.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #ff5144 inset!important;box-shadow:0 0 0 2px #ff5144 inset!important;color:#ff695e!important}.ui.inverted.red.basic.button:active,.ui.inverted.red.basic.buttons .button:active,.ui.inverted.red.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #ff392b inset!important;box-shadow:0 0 0 2px #ff392b inset!important;color:#ff695e!important}.ui.teal.button,.ui.teal.buttons .button{background-color:#00b5ad;color:#fff;text-shadow:none;background-image:none}.ui.teal.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.teal.button:hover,.ui.teal.buttons .button:hover{background-color:#009c95;color:#fff;text-shadow:none}.ui.teal.button:focus,.ui.teal.buttons .button:focus{background-color:#008c86;color:#fff;text-shadow:none}.ui.teal.button:active,.ui.teal.buttons .button:active{background-color:#00827c;color:#fff;text-shadow:none}.ui.teal.active.button,.ui.teal.button .active.button:active,.ui.teal.buttons .active.button,.ui.teal.buttons .active.button:active{background-color:#009c95;color:#fff;text-shadow:none}.ui.basic.teal.button,.ui.basic.teal.buttons .button{-webkit-box-shadow:0 0 0 1px #00b5ad inset!important;box-shadow:0 0 0 1px #00b5ad inset!important;color:#00b5ad!important}.ui.basic.teal.button:hover,.ui.basic.teal.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #009c95 inset!important;box-shadow:0 0 0 1px #009c95 inset!important;color:#009c95!important}.ui.basic.teal.button:focus,.ui.basic.teal.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #008c86 inset!important;box-shadow:0 0 0 1px #008c86 inset!important;color:#009c95!important}.ui.basic.teal.active.button,.ui.basic.teal.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #009c95 inset!important;box-shadow:0 0 0 1px #009c95 inset!important;color:#00827c!important}.ui.basic.teal.button:active,.ui.basic.teal.buttons .button:active{-webkit-box-shadow:0 0 0 1px #00827c inset!important;box-shadow:0 0 0 1px #00827c inset!important;color:#00827c!important}.ui.buttons:not(.vertical)>.basic.teal.button:not(:first-child){margin-left:-1px}.ui.inverted.teal.button,.ui.inverted.teal.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #6dffff inset!important;box-shadow:0 0 0 2px #6dffff inset!important;color:#6dffff}.ui.inverted.teal.button.active,.ui.inverted.teal.button:active,.ui.inverted.teal.button:focus,.ui.inverted.teal.button:hover,.ui.inverted.teal.buttons .button.active,.ui.inverted.teal.buttons .button:active,.ui.inverted.teal.buttons .button:focus,.ui.inverted.teal.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:rgba(0,0,0,.6)}.ui.inverted.teal.button:hover,.ui.inverted.teal.buttons .button:hover{background-color:#54ffff}.ui.inverted.teal.button:focus,.ui.inverted.teal.buttons .button:focus{background-color:#4ff}.ui.inverted.teal.active.button,.ui.inverted.teal.buttons .active.button{background-color:#54ffff}.ui.inverted.teal.button:active,.ui.inverted.teal.buttons .button:active{background-color:#3affff}.ui.inverted.teal.basic.button,.ui.inverted.teal.basic.buttons .button,.ui.inverted.teal.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.teal.basic.button:hover,.ui.inverted.teal.basic.buttons .button:hover,.ui.inverted.teal.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #54ffff inset!important;box-shadow:0 0 0 2px #54ffff inset!important;color:#6dffff!important}.ui.inverted.teal.basic.button:focus,.ui.inverted.teal.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #4ff inset!important;box-shadow:0 0 0 2px #4ff inset!important;color:#6dffff!important}.ui.inverted.teal.basic.active.button,.ui.inverted.teal.basic.buttons .active.button,.ui.inverted.teal.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #54ffff inset!important;box-shadow:0 0 0 2px #54ffff inset!important;color:#6dffff!important}.ui.inverted.teal.basic.button:active,.ui.inverted.teal.basic.buttons .button:active,.ui.inverted.teal.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #3affff inset!important;box-shadow:0 0 0 2px #3affff inset!important;color:#6dffff!important}.ui.olive.button,.ui.olive.buttons .button{background-color:#b5cc18;color:#fff;text-shadow:none;background-image:none}.ui.olive.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.olive.button:hover,.ui.olive.buttons .button:hover{background-color:#a7bd0d;color:#fff;text-shadow:none}.ui.olive.button:focus,.ui.olive.buttons .button:focus{background-color:#a0b605;color:#fff;text-shadow:none}.ui.olive.button:active,.ui.olive.buttons .button:active{background-color:#8d9e13;color:#fff;text-shadow:none}.ui.olive.active.button,.ui.olive.button .active.button:active,.ui.olive.buttons .active.button,.ui.olive.buttons .active.button:active{background-color:#aac109;color:#fff;text-shadow:none}.ui.basic.olive.button,.ui.basic.olive.buttons .button{-webkit-box-shadow:0 0 0 1px #b5cc18 inset!important;box-shadow:0 0 0 1px #b5cc18 inset!important;color:#b5cc18!important}.ui.basic.olive.button:hover,.ui.basic.olive.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #a7bd0d inset!important;box-shadow:0 0 0 1px #a7bd0d inset!important;color:#a7bd0d!important}.ui.basic.olive.button:focus,.ui.basic.olive.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #a0b605 inset!important;box-shadow:0 0 0 1px #a0b605 inset!important;color:#a7bd0d!important}.ui.basic.olive.active.button,.ui.basic.olive.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #aac109 inset!important;box-shadow:0 0 0 1px #aac109 inset!important;color:#8d9e13!important}.ui.basic.olive.button:active,.ui.basic.olive.buttons .button:active{-webkit-box-shadow:0 0 0 1px #8d9e13 inset!important;box-shadow:0 0 0 1px #8d9e13 inset!important;color:#8d9e13!important}.ui.buttons:not(.vertical)>.basic.olive.button:not(:first-child){margin-left:-1px}.ui.inverted.olive.button,.ui.inverted.olive.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #d9e778 inset!important;box-shadow:0 0 0 2px #d9e778 inset!important;color:#d9e778}.ui.inverted.olive.button.active,.ui.inverted.olive.button:active,.ui.inverted.olive.button:focus,.ui.inverted.olive.button:hover,.ui.inverted.olive.buttons .button.active,.ui.inverted.olive.buttons .button:active,.ui.inverted.olive.buttons .button:focus,.ui.inverted.olive.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:rgba(0,0,0,.6)}.ui.inverted.olive.button:hover,.ui.inverted.olive.buttons .button:hover{background-color:#d8ea5c}.ui.inverted.olive.button:focus,.ui.inverted.olive.buttons .button:focus{background-color:#daef47}.ui.inverted.olive.active.button,.ui.inverted.olive.buttons .active.button{background-color:#daed59}.ui.inverted.olive.button:active,.ui.inverted.olive.buttons .button:active{background-color:#cddf4d}.ui.inverted.olive.basic.button,.ui.inverted.olive.basic.buttons .button,.ui.inverted.olive.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.olive.basic.button:hover,.ui.inverted.olive.basic.buttons .button:hover,.ui.inverted.olive.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #d8ea5c inset!important;box-shadow:0 0 0 2px #d8ea5c inset!important;color:#d9e778!important}.ui.inverted.olive.basic.button:focus,.ui.inverted.olive.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #daef47 inset!important;box-shadow:0 0 0 2px #daef47 inset!important;color:#d9e778!important}.ui.inverted.olive.basic.active.button,.ui.inverted.olive.basic.buttons .active.button,.ui.inverted.olive.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #daed59 inset!important;box-shadow:0 0 0 2px #daed59 inset!important;color:#d9e778!important}.ui.inverted.olive.basic.button:active,.ui.inverted.olive.basic.buttons .button:active,.ui.inverted.olive.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #cddf4d inset!important;box-shadow:0 0 0 2px #cddf4d inset!important;color:#d9e778!important}.ui.yellow.button,.ui.yellow.buttons .button{background-color:#fbbd08;color:#fff;text-shadow:none;background-image:none}.ui.yellow.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.yellow.button:hover,.ui.yellow.buttons .button:hover{background-color:#eaae00;color:#fff;text-shadow:none}.ui.yellow.button:focus,.ui.yellow.buttons .button:focus{background-color:#daa300;color:#fff;text-shadow:none}.ui.yellow.button:active,.ui.yellow.buttons .button:active{background-color:#cd9903;color:#fff;text-shadow:none}.ui.yellow.active.button,.ui.yellow.button .active.button:active,.ui.yellow.buttons .active.button,.ui.yellow.buttons .active.button:active{background-color:#eaae00;color:#fff;text-shadow:none}.ui.basic.yellow.button,.ui.basic.yellow.buttons .button{-webkit-box-shadow:0 0 0 1px #fbbd08 inset!important;box-shadow:0 0 0 1px #fbbd08 inset!important;color:#fbbd08!important}.ui.basic.yellow.button:hover,.ui.basic.yellow.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #eaae00 inset!important;box-shadow:0 0 0 1px #eaae00 inset!important;color:#eaae00!important}.ui.basic.yellow.button:focus,.ui.basic.yellow.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #daa300 inset!important;box-shadow:0 0 0 1px #daa300 inset!important;color:#eaae00!important}.ui.basic.yellow.active.button,.ui.basic.yellow.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #eaae00 inset!important;box-shadow:0 0 0 1px #eaae00 inset!important;color:#cd9903!important}.ui.basic.yellow.button:active,.ui.basic.yellow.buttons .button:active{-webkit-box-shadow:0 0 0 1px #cd9903 inset!important;box-shadow:0 0 0 1px #cd9903 inset!important;color:#cd9903!important}.ui.buttons:not(.vertical)>.basic.yellow.button:not(:first-child){margin-left:-1px}.ui.inverted.yellow.button,.ui.inverted.yellow.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #ffe21f inset!important;box-shadow:0 0 0 2px #ffe21f inset!important;color:#ffe21f}.ui.inverted.yellow.button.active,.ui.inverted.yellow.button:active,.ui.inverted.yellow.button:focus,.ui.inverted.yellow.button:hover,.ui.inverted.yellow.buttons .button.active,.ui.inverted.yellow.buttons .button:active,.ui.inverted.yellow.buttons .button:focus,.ui.inverted.yellow.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:rgba(0,0,0,.6)}.ui.inverted.yellow.button:hover,.ui.inverted.yellow.buttons .button:hover{background-color:#ffdf05}.ui.inverted.yellow.button:focus,.ui.inverted.yellow.buttons .button:focus{background-color:#f5d500}.ui.inverted.yellow.active.button,.ui.inverted.yellow.buttons .active.button{background-color:#ffdf05}.ui.inverted.yellow.button:active,.ui.inverted.yellow.buttons .button:active{background-color:#ebcd00}.ui.inverted.yellow.basic.button,.ui.inverted.yellow.basic.buttons .button,.ui.inverted.yellow.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.yellow.basic.button:hover,.ui.inverted.yellow.basic.buttons .button:hover,.ui.inverted.yellow.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #ffdf05 inset!important;box-shadow:0 0 0 2px #ffdf05 inset!important;color:#ffe21f!important}.ui.inverted.yellow.basic.button:focus,.ui.inverted.yellow.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #f5d500 inset!important;box-shadow:0 0 0 2px #f5d500 inset!important;color:#ffe21f!important}.ui.inverted.yellow.basic.active.button,.ui.inverted.yellow.basic.buttons .active.button,.ui.inverted.yellow.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #ffdf05 inset!important;box-shadow:0 0 0 2px #ffdf05 inset!important;color:#ffe21f!important}.ui.inverted.yellow.basic.button:active,.ui.inverted.yellow.basic.buttons .button:active,.ui.inverted.yellow.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #ebcd00 inset!important;box-shadow:0 0 0 2px #ebcd00 inset!important;color:#ffe21f!important}.ui.primary.button,.ui.primary.buttons .button{background-color:#2185d0;color:#fff;text-shadow:none;background-image:none}.ui.primary.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.primary.button:hover,.ui.primary.buttons .button:hover{background-color:#1678c2;color:#fff;text-shadow:none}.ui.primary.button:focus,.ui.primary.buttons .button:focus{background-color:#0d71bb;color:#fff;text-shadow:none}.ui.primary.button:active,.ui.primary.buttons .button:active{background-color:#1a69a4;color:#fff;text-shadow:none}.ui.primary.active.button,.ui.primary.button .active.button:active,.ui.primary.buttons .active.button,.ui.primary.buttons .active.button:active{background-color:#1279c6;color:#fff;text-shadow:none}.ui.basic.primary.button,.ui.basic.primary.buttons .button{-webkit-box-shadow:0 0 0 1px #2185d0 inset!important;box-shadow:0 0 0 1px #2185d0 inset!important;color:#2185d0!important}.ui.basic.primary.button:hover,.ui.basic.primary.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #1678c2 inset!important;box-shadow:0 0 0 1px #1678c2 inset!important;color:#1678c2!important}.ui.basic.primary.button:focus,.ui.basic.primary.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #0d71bb inset!important;box-shadow:0 0 0 1px #0d71bb inset!important;color:#1678c2!important}.ui.basic.primary.active.button,.ui.basic.primary.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #1279c6 inset!important;box-shadow:0 0 0 1px #1279c6 inset!important;color:#1a69a4!important}.ui.basic.primary.button:active,.ui.basic.primary.buttons .button:active{-webkit-box-shadow:0 0 0 1px #1a69a4 inset!important;box-shadow:0 0 0 1px #1a69a4 inset!important;color:#1a69a4!important}.ui.buttons:not(.vertical)>.basic.primary.button:not(:first-child){margin-left:-1px}.ui.inverted.primary.button,.ui.inverted.primary.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #54c8ff inset!important;box-shadow:0 0 0 2px #54c8ff inset!important;color:#54c8ff}.ui.inverted.primary.button.active,.ui.inverted.primary.button:active,.ui.inverted.primary.button:focus,.ui.inverted.primary.button:hover,.ui.inverted.primary.buttons .button.active,.ui.inverted.primary.buttons .button:active,.ui.inverted.primary.buttons .button:focus,.ui.inverted.primary.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:#fff}.ui.inverted.primary.button:hover,.ui.inverted.primary.buttons .button:hover{background-color:#3ac0ff}.ui.inverted.primary.button:focus,.ui.inverted.primary.buttons .button:focus{background-color:#2bbbff}.ui.inverted.primary.active.button,.ui.inverted.primary.buttons .active.button{background-color:#3ac0ff}.ui.inverted.primary.button:active,.ui.inverted.primary.buttons .button:active{background-color:#21b8ff}.ui.inverted.primary.basic.button,.ui.inverted.primary.basic.buttons .button,.ui.inverted.primary.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.primary.basic.button:hover,.ui.inverted.primary.basic.buttons .button:hover,.ui.inverted.primary.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #3ac0ff inset!important;box-shadow:0 0 0 2px #3ac0ff inset!important;color:#54c8ff!important}.ui.inverted.primary.basic.button:focus,.ui.inverted.primary.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #2bbbff inset!important;box-shadow:0 0 0 2px #2bbbff inset!important;color:#54c8ff!important}.ui.inverted.primary.basic.active.button,.ui.inverted.primary.basic.buttons .active.button,.ui.inverted.primary.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #3ac0ff inset!important;box-shadow:0 0 0 2px #3ac0ff inset!important;color:#54c8ff!important}.ui.inverted.primary.basic.button:active,.ui.inverted.primary.basic.buttons .button:active,.ui.inverted.primary.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #21b8ff inset!important;box-shadow:0 0 0 2px #21b8ff inset!important;color:#54c8ff!important}.ui.secondary.button,.ui.secondary.buttons .button{background-color:#1b1c1d;color:#fff;text-shadow:none;background-image:none}.ui.secondary.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.secondary.button:hover,.ui.secondary.buttons .button:hover{background-color:#27292a;color:#fff;text-shadow:none}.ui.secondary.button:focus,.ui.secondary.buttons .button:focus{background-color:#2e3032;color:#fff;text-shadow:none}.ui.secondary.button:active,.ui.secondary.buttons .button:active{background-color:#343637;color:#fff;text-shadow:none}.ui.secondary.active.button,.ui.secondary.button .active.button:active,.ui.secondary.buttons .active.button,.ui.secondary.buttons .active.button:active{background-color:#27292a;color:#fff;text-shadow:none}.ui.basic.secondary.button,.ui.basic.secondary.buttons .button{-webkit-box-shadow:0 0 0 1px #1b1c1d inset!important;box-shadow:0 0 0 1px #1b1c1d inset!important;color:#1b1c1d!important}.ui.basic.secondary.button:hover,.ui.basic.secondary.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #27292a inset!important;box-shadow:0 0 0 1px #27292a inset!important;color:#27292a!important}.ui.basic.secondary.button:focus,.ui.basic.secondary.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #2e3032 inset!important;box-shadow:0 0 0 1px #2e3032 inset!important;color:#27292a!important}.ui.basic.secondary.active.button,.ui.basic.secondary.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #27292a inset!important;box-shadow:0 0 0 1px #27292a inset!important;color:#343637!important}.ui.basic.secondary.button:active,.ui.basic.secondary.buttons .button:active{-webkit-box-shadow:0 0 0 1px #343637 inset!important;box-shadow:0 0 0 1px #343637 inset!important;color:#343637!important}.ui.buttons:not(.vertical)>.basic.primary.button:not(:first-child){margin-left:-1px}.ui.inverted.secondary.button,.ui.inverted.secondary.buttons .button{background-color:transparent;-webkit-box-shadow:0 0 0 2px #545454 inset!important;box-shadow:0 0 0 2px #545454 inset!important;color:#545454}.ui.inverted.secondary.button.active,.ui.inverted.secondary.button:active,.ui.inverted.secondary.button:focus,.ui.inverted.secondary.button:hover,.ui.inverted.secondary.buttons .button.active,.ui.inverted.secondary.buttons .button:active,.ui.inverted.secondary.buttons .button:focus,.ui.inverted.secondary.buttons .button:hover{-webkit-box-shadow:none!important;box-shadow:none!important;color:#fff}.ui.inverted.secondary.button:hover,.ui.inverted.secondary.buttons .button:hover{background-color:#616161}.ui.inverted.secondary.button:focus,.ui.inverted.secondary.buttons .button:focus{background-color:#686868}.ui.inverted.secondary.active.button,.ui.inverted.secondary.buttons .active.button{background-color:#616161}.ui.inverted.secondary.button:active,.ui.inverted.secondary.buttons .button:active{background-color:#6e6e6e}.ui.inverted.secondary.basic.button,.ui.inverted.secondary.basic.buttons .button,.ui.inverted.secondary.buttons .basic.button{background-color:transparent;-webkit-box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;box-shadow:0 0 0 2px rgba(255,255,255,.5) inset!important;color:#fff!important}.ui.inverted.secondary.basic.button:hover,.ui.inverted.secondary.basic.buttons .button:hover,.ui.inverted.secondary.buttons .basic.button:hover{-webkit-box-shadow:0 0 0 2px #616161 inset!important;box-shadow:0 0 0 2px #616161 inset!important;color:#545454!important}.ui.inverted.secondary.basic.button:focus,.ui.inverted.secondary.basic.buttons .button:focus{-webkit-box-shadow:0 0 0 2px #686868 inset!important;box-shadow:0 0 0 2px #686868 inset!important;color:#545454!important}.ui.inverted.secondary.basic.active.button,.ui.inverted.secondary.basic.buttons .active.button,.ui.inverted.secondary.buttons .basic.active.button{-webkit-box-shadow:0 0 0 2px #616161 inset!important;box-shadow:0 0 0 2px #616161 inset!important;color:#545454!important}.ui.inverted.secondary.basic.button:active,.ui.inverted.secondary.basic.buttons .button:active,.ui.inverted.secondary.buttons .basic.button:active{-webkit-box-shadow:0 0 0 2px #6e6e6e inset!important;box-shadow:0 0 0 2px #6e6e6e inset!important;color:#545454!important}.ui.positive.button,.ui.positive.buttons .button{background-color:#21ba45;color:#fff;text-shadow:none;background-image:none}.ui.positive.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.positive.button:hover,.ui.positive.buttons .button:hover{background-color:#16ab39;color:#fff;text-shadow:none}.ui.positive.button:focus,.ui.positive.buttons .button:focus{background-color:#0ea432;color:#fff;text-shadow:none}.ui.positive.button:active,.ui.positive.buttons .button:active{background-color:#198f35;color:#fff;text-shadow:none}.ui.positive.active.button,.ui.positive.button .active.button:active,.ui.positive.buttons .active.button,.ui.positive.buttons .active.button:active{background-color:#13ae38;color:#fff;text-shadow:none}.ui.basic.positive.button,.ui.basic.positive.buttons .button{-webkit-box-shadow:0 0 0 1px #21ba45 inset!important;box-shadow:0 0 0 1px #21ba45 inset!important;color:#21ba45!important}.ui.basic.positive.button:hover,.ui.basic.positive.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #16ab39 inset!important;box-shadow:0 0 0 1px #16ab39 inset!important;color:#16ab39!important}.ui.basic.positive.button:focus,.ui.basic.positive.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #0ea432 inset!important;box-shadow:0 0 0 1px #0ea432 inset!important;color:#16ab39!important}.ui.basic.positive.active.button,.ui.basic.positive.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #13ae38 inset!important;box-shadow:0 0 0 1px #13ae38 inset!important;color:#198f35!important}.ui.basic.positive.button:active,.ui.basic.positive.buttons .button:active{-webkit-box-shadow:0 0 0 1px #198f35 inset!important;box-shadow:0 0 0 1px #198f35 inset!important;color:#198f35!important}.ui.buttons:not(.vertical)>.basic.primary.button:not(:first-child){margin-left:-1px}.ui.negative.button,.ui.negative.buttons .button{background-color:#db2828;color:#fff;text-shadow:none;background-image:none}.ui.negative.button{-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 0 rgba(34,36,38,.15) inset}.ui.negative.button:hover,.ui.negative.buttons .button:hover{background-color:#d01919;color:#fff;text-shadow:none}.ui.negative.button:focus,.ui.negative.buttons .button:focus{background-color:#ca1010;color:#fff;text-shadow:none}.ui.negative.button:active,.ui.negative.buttons .button:active{background-color:#b21e1e;color:#fff;text-shadow:none}.ui.negative.active.button,.ui.negative.button .active.button:active,.ui.negative.buttons .active.button,.ui.negative.buttons .active.button:active{background-color:#d41515;color:#fff;text-shadow:none}.ui.basic.negative.button,.ui.basic.negative.buttons .button{-webkit-box-shadow:0 0 0 1px #db2828 inset!important;box-shadow:0 0 0 1px #db2828 inset!important;color:#db2828!important}.ui.basic.negative.button:hover,.ui.basic.negative.buttons .button:hover{background:0 0!important;-webkit-box-shadow:0 0 0 1px #d01919 inset!important;box-shadow:0 0 0 1px #d01919 inset!important;color:#d01919!important}.ui.basic.negative.button:focus,.ui.basic.negative.buttons .button:focus{background:0 0!important;-webkit-box-shadow:0 0 0 1px #ca1010 inset!important;box-shadow:0 0 0 1px #ca1010 inset!important;color:#d01919!important}.ui.basic.negative.active.button,.ui.basic.negative.buttons .active.button{background:0 0!important;-webkit-box-shadow:0 0 0 1px #d41515 inset!important;box-shadow:0 0 0 1px #d41515 inset!important;color:#b21e1e!important}.ui.basic.negative.button:active,.ui.basic.negative.buttons .button:active{-webkit-box-shadow:0 0 0 1px #b21e1e inset!important;box-shadow:0 0 0 1px #b21e1e inset!important;color:#b21e1e!important}.ui.buttons:not(.vertical)>.basic.primary.button:not(:first-child){margin-left:-1px}.ui.buttons{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;font-size:0;vertical-align:baseline;margin:0 .25em 0 0}.ui.buttons:not(.basic):not(.inverted){-webkit-box-shadow:none;box-shadow:none}.ui.buttons:after{content:".";display:block;height:0;clear:both;visibility:hidden}.ui.buttons .button{-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;margin:0;border-radius:0;margin:0}.ui.buttons:not(.basic):not(.inverted)>.button,.ui.buttons>.ui.button:not(.basic):not(.inverted){-webkit-box-shadow:0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;box-shadow:0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset}.ui.buttons .button:first-child{border-left:none;margin-left:0;border-top-left-radius:.28571429rem;border-bottom-left-radius:.28571429rem}.ui.buttons .button:last-child{border-top-right-radius:.28571429rem;border-bottom-right-radius:.28571429rem}.ui.vertical.buttons{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.ui.vertical.buttons .button{display:block;float:none;width:100%;margin:0;-webkit-box-shadow:none;box-shadow:none;border-radius:0}.ui.vertical.buttons .button:first-child{border-top-left-radius:.28571429rem;border-top-right-radius:.28571429rem}.ui.vertical.buttons .button:last-child{margin-bottom:0;border-bottom-left-radius:.28571429rem;border-bottom-right-radius:.28571429rem}.ui.vertical.buttons .button:only-child{border-radius:.28571429rem}/*!
+ * # Semantic UI 2.4.2 - Container
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.container{display:block;max-width:100%!important}@media only screen and (max-width:767px){.ui.container{width:auto!important;margin-left:1em!important;margin-right:1em!important}.ui.grid.container{width:auto!important}.ui.relaxed.grid.container{width:auto!important}.ui.very.relaxed.grid.container{width:auto!important}}@media only screen and (min-width:768px) and (max-width:991px){.ui.container{width:723px;margin-left:auto!important;margin-right:auto!important}.ui.grid.container{width:calc(723px + 2rem)!important}.ui.relaxed.grid.container{width:calc(723px + 3rem)!important}.ui.very.relaxed.grid.container{width:calc(723px + 5rem)!important}}@media only screen and (min-width:992px) and (max-width:1199px){.ui.container{width:933px;margin-left:auto!important;margin-right:auto!important}.ui.grid.container{width:calc(933px + 2rem)!important}.ui.relaxed.grid.container{width:calc(933px + 3rem)!important}.ui.very.relaxed.grid.container{width:calc(933px + 5rem)!important}}@media only screen and (min-width:1200px){.ui.container{width:1127px;margin-left:auto!important;margin-right:auto!important}.ui.grid.container{width:calc(1127px + 2rem)!important}.ui.relaxed.grid.container{width:calc(1127px + 3rem)!important}.ui.very.relaxed.grid.container{width:calc(1127px + 5rem)!important}}.ui.text.container{font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;max-width:700px!important;line-height:1.5}.ui.text.container{font-size:1.14285714rem}.ui.fluid.container{width:100%}.ui[class*="left aligned"].container{text-align:left}.ui[class*="center aligned"].container{text-align:center}.ui[class*="right aligned"].container{text-align:right}.ui.justified.container{text-align:justify;-webkit-hyphens:auto;-ms-hyphens:auto;hyphens:auto}/*!
+ * # Semantic UI 2.4.2 - Divider
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.divider{margin:1rem 0;line-height:1;height:0;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:rgba(0,0,0,.85);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.ui.divider:not(.vertical):not(.horizontal){border-top:1px solid rgba(34,36,38,.15);border-bottom:1px solid rgba(255,255,255,.1)}.ui.grid>.column+.divider,.ui.grid>.row>.column+.divider{left:auto}.ui.horizontal.divider{display:table;white-space:nowrap;height:auto;margin:'';line-height:1;text-align:center}.ui.horizontal.divider:after,.ui.horizontal.divider:before{content:'';display:table-cell;position:relative;top:50%;width:50%;background-repeat:no-repeat}.ui.horizontal.divider:before{background-position:right 1em top 50%}.ui.horizontal.divider:after{background-position:left 1em top 50%}.ui.vertical.divider{position:absolute;z-index:2;top:50%;left:50%;margin:0;padding:0;width:auto;height:50%;line-height:0;text-align:center;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.ui.vertical.divider:after,.ui.vertical.divider:before{position:absolute;left:50%;content:'';z-index:3;border-left:1px solid rgba(34,36,38,.15);border-right:1px solid rgba(255,255,255,.1);width:0%;height:calc(100% - 1rem)}.ui.vertical.divider:before{top:-100%}.ui.vertical.divider:after{top:auto;bottom:0}@media only screen and (max-width:767px){.ui.grid .stackable.row .ui.vertical.divider,.ui.stackable.grid .ui.vertical.divider{display:table;white-space:nowrap;height:auto;margin:'';overflow:hidden;line-height:1;text-align:center;position:static;top:0;left:0;-webkit-transform:none;transform:none}.ui.grid .stackable.row .ui.vertical.divider:after,.ui.grid .stackable.row .ui.vertical.divider:before,.ui.stackable.grid .ui.vertical.divider:after,.ui.stackable.grid .ui.vertical.divider:before{position:static;left:0;border-left:none;border-right:none;content:'';display:table-cell;position:relative;top:50%;width:50%;background-repeat:no-repeat}.ui.grid .stackable.row .ui.vertical.divider:before,.ui.stackable.grid .ui.vertical.divider:before{background-position:right 1em top 50%}.ui.grid .stackable.row .ui.vertical.divider:after,.ui.stackable.grid .ui.vertical.divider:after{background-position:left 1em top 50%}}.ui.divider>.icon{margin:0;font-size:1rem;height:1em;vertical-align:middle}.ui.hidden.divider{border-color:transparent!important}.ui.hidden.divider:after,.ui.hidden.divider:before{display:none}.ui.divider.inverted,.ui.horizontal.inverted.divider,.ui.vertical.inverted.divider{color:#fff}.ui.divider.inverted,.ui.divider.inverted:after,.ui.divider.inverted:before{border-top-color:rgba(34,36,38,.15)!important;border-left-color:rgba(34,36,38,.15)!important;border-bottom-color:rgba(255,255,255,.15)!important;border-right-color:rgba(255,255,255,.15)!important}.ui.fitted.divider{margin:0}.ui.clearing.divider{clear:both}.ui.section.divider{margin-top:2rem;margin-bottom:2rem}.ui.divider{font-size:1rem}.ui.horizontal.divider:after,.ui.horizontal.divider:before{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABaAAAAACCAYAAACuTHuKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyFpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1OThBRDY4OUNDMTYxMUU0OUE3NUVGOEJDMzMzMjE2NyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1OThBRDY4QUNDMTYxMUU0OUE3NUVGOEJDMzMzMjE2NyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjU5OEFENjg3Q0MxNjExRTQ5QTc1RUY4QkMzMzMyMTY3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjU5OEFENjg4Q0MxNjExRTQ5QTc1RUY4QkMzMzMyMTY3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+VU513gAAADVJREFUeNrs0DENACAQBDBIWLGBJQby/mUcJn5sJXQmOQMAAAAAAJqt+2prAAAAAACg2xdgANk6BEVuJgyMAAAAAElFTkSuQmCC)}@media only screen and (max-width:767px){.ui.grid .stackable.row .ui.vertical.divider:after,.ui.grid .stackable.row .ui.vertical.divider:before,.ui.stackable.grid .ui.vertical.divider:after,.ui.stackable.grid .ui.vertical.divider:before{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABaAAAAACCAYAAACuTHuKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyFpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1OThBRDY4OUNDMTYxMUU0OUE3NUVGOEJDMzMzMjE2NyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1OThBRDY4QUNDMTYxMUU0OUE3NUVGOEJDMzMzMjE2NyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjU5OEFENjg3Q0MxNjExRTQ5QTc1RUY4QkMzMzMyMTY3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjU5OEFENjg4Q0MxNjExRTQ5QTc1RUY4QkMzMzMyMTY3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+VU513gAAADVJREFUeNrs0DENACAQBDBIWLGBJQby/mUcJn5sJXQmOQMAAAAAAJqt+2prAAAAAACg2xdgANk6BEVuJgyMAAAAAElFTkSuQmCC)}}/*!
+ * # Semantic UI 2.4.2 - Flag
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */i.flag:not(.icon){display:inline-block;width:16px;height:11px;line-height:11px;vertical-align:baseline;margin:0 .5em 0 0;text-decoration:inherit;speak:none;font-smoothing:antialiased;-webkit-backface-visibility:hidden;backface-visibility:hidden}i.flag:not(.icon):before{display:inline-block;content:'';background:url(themes/default/assets/images/flags.png) no-repeat -108px -1976px;width:16px;height:11px}i.flag.ad:before,i.flag.andorra:before{background-position:0 0}i.flag.ae:before,i.flag.uae:before,i.flag.united.arab.emirates:before{background-position:0 -26px}i.flag.af:before,i.flag.afghanistan:before{background-position:0 -52px}i.flag.ag:before,i.flag.antigua:before{background-position:0 -78px}i.flag.ai:before,i.flag.anguilla:before{background-position:0 -104px}i.flag.al:before,i.flag.albania:before{background-position:0 -130px}i.flag.am:before,i.flag.armenia:before{background-position:0 -156px}i.flag.an:before,i.flag.netherlands.antilles:before{background-position:0 -182px}i.flag.angola:before,i.flag.ao:before{background-position:0 -208px}i.flag.ar:before,i.flag.argentina:before{background-position:0 -234px}i.flag.american.samoa:before,i.flag.as:before{background-position:0 -260px}i.flag.at:before,i.flag.austria:before{background-position:0 -286px}i.flag.au:before,i.flag.australia:before{background-position:0 -312px}i.flag.aruba:before,i.flag.aw:before{background-position:0 -338px}i.flag.aland.islands:before,i.flag.ax:before{background-position:0 -364px}i.flag.az:before,i.flag.azerbaijan:before{background-position:0 -390px}i.flag.ba:before,i.flag.bosnia:before{background-position:0 -416px}i.flag.barbados:before,i.flag.bb:before{background-position:0 -442px}i.flag.bangladesh:before,i.flag.bd:before{background-position:0 -468px}i.flag.be:before,i.flag.belgium:before{background-position:0 -494px}i.flag.bf:before,i.flag.burkina.faso:before{background-position:0 -520px}i.flag.bg:before,i.flag.bulgaria:before{background-position:0 -546px}i.flag.bahrain:before,i.flag.bh:before{background-position:0 -572px}i.flag.bi:before,i.flag.burundi:before{background-position:0 -598px}i.flag.benin:before,i.flag.bj:before{background-position:0 -624px}i.flag.bermuda:before,i.flag.bm:before{background-position:0 -650px}i.flag.bn:before,i.flag.brunei:before{background-position:0 -676px}i.flag.bo:before,i.flag.bolivia:before{background-position:0 -702px}i.flag.br:before,i.flag.brazil:before{background-position:0 -728px}i.flag.bahamas:before,i.flag.bs:before{background-position:0 -754px}i.flag.bhutan:before,i.flag.bt:before{background-position:0 -780px}i.flag.bouvet.island:before,i.flag.bv:before{background-position:0 -806px}i.flag.botswana:before,i.flag.bw:before{background-position:0 -832px}i.flag.belarus:before,i.flag.by:before{background-position:0 -858px}i.flag.belize:before,i.flag.bz:before{background-position:0 -884px}i.flag.ca:before,i.flag.canada:before{background-position:0 -910px}i.flag.cc:before,i.flag.cocos.islands:before{background-position:0 -962px}i.flag.cd:before,i.flag.congo:before{background-position:0 -988px}i.flag.central.african.republic:before,i.flag.cf:before{background-position:0 -1014px}i.flag.cg:before,i.flag.congo.brazzaville:before{background-position:0 -1040px}i.flag.ch:before,i.flag.switzerland:before{background-position:0 -1066px}i.flag.ci:before,i.flag.cote.divoire:before{background-position:0 -1092px}i.flag.ck:before,i.flag.cook.islands:before{background-position:0 -1118px}i.flag.chile:before,i.flag.cl:before{background-position:0 -1144px}i.flag.cameroon:before,i.flag.cm:before{background-position:0 -1170px}i.flag.china:before,i.flag.cn:before{background-position:0 -1196px}i.flag.co:before,i.flag.colombia:before{background-position:0 -1222px}i.flag.costa.rica:before,i.flag.cr:before{background-position:0 -1248px}i.flag.cs:before,i.flag.serbia:before{background-position:0 -1274px}i.flag.cu:before,i.flag.cuba:before{background-position:0 -1300px}i.flag.cape.verde:before,i.flag.cv:before{background-position:0 -1326px}i.flag.christmas.island:before,i.flag.cx:before{background-position:0 -1352px}i.flag.cy:before,i.flag.cyprus:before{background-position:0 -1378px}i.flag.cz:before,i.flag.czech.republic:before{background-position:0 -1404px}i.flag.de:before,i.flag.germany:before{background-position:0 -1430px}i.flag.dj:before,i.flag.djibouti:before{background-position:0 -1456px}i.flag.denmark:before,i.flag.dk:before{background-position:0 -1482px}i.flag.dm:before,i.flag.dominica:before{background-position:0 -1508px}i.flag.do:before,i.flag.dominican.republic:before{background-position:0 -1534px}i.flag.algeria:before,i.flag.dz:before{background-position:0 -1560px}i.flag.ec:before,i.flag.ecuador:before{background-position:0 -1586px}i.flag.ee:before,i.flag.estonia:before{background-position:0 -1612px}i.flag.eg:before,i.flag.egypt:before{background-position:0 -1638px}i.flag.eh:before,i.flag.western.sahara:before{background-position:0 -1664px}i.flag.england:before,i.flag.gb.eng:before{background-position:0 -1690px}i.flag.er:before,i.flag.eritrea:before{background-position:0 -1716px}i.flag.es:before,i.flag.spain:before{background-position:0 -1742px}i.flag.et:before,i.flag.ethiopia:before{background-position:0 -1768px}i.flag.eu:before,i.flag.european.union:before{background-position:0 -1794px}i.flag.fi:before,i.flag.finland:before{background-position:0 -1846px}i.flag.fiji:before,i.flag.fj:before{background-position:0 -1872px}i.flag.falkland.islands:before,i.flag.fk:before{background-position:0 -1898px}i.flag.fm:before,i.flag.micronesia:before{background-position:0 -1924px}i.flag.faroe.islands:before,i.flag.fo:before{background-position:0 -1950px}i.flag.fr:before,i.flag.france:before{background-position:0 -1976px}i.flag.ga:before,i.flag.gabon:before{background-position:-36px 0}i.flag.gb:before,i.flag.uk:before,i.flag.united.kingdom:before{background-position:-36px -26px}i.flag.gd:before,i.flag.grenada:before{background-position:-36px -52px}i.flag.ge:before,i.flag.georgia:before{background-position:-36px -78px}i.flag.french.guiana:before,i.flag.gf:before{background-position:-36px -104px}i.flag.gh:before,i.flag.ghana:before{background-position:-36px -130px}i.flag.gi:before,i.flag.gibraltar:before{background-position:-36px -156px}i.flag.gl:before,i.flag.greenland:before{background-position:-36px -182px}i.flag.gambia:before,i.flag.gm:before{background-position:-36px -208px}i.flag.gn:before,i.flag.guinea:before{background-position:-36px -234px}i.flag.gp:before,i.flag.guadeloupe:before{background-position:-36px -260px}i.flag.equatorial.guinea:before,i.flag.gq:before{background-position:-36px -286px}i.flag.gr:before,i.flag.greece:before{background-position:-36px -312px}i.flag.gs:before,i.flag.sandwich.islands:before{background-position:-36px -338px}i.flag.gt:before,i.flag.guatemala:before{background-position:-36px -364px}i.flag.gu:before,i.flag.guam:before{background-position:-36px -390px}i.flag.guinea-bissau:before,i.flag.gw:before{background-position:-36px -416px}i.flag.guyana:before,i.flag.gy:before{background-position:-36px -442px}i.flag.hk:before,i.flag.hong.kong:before{background-position:-36px -468px}i.flag.heard.island:before,i.flag.hm:before{background-position:-36px -494px}i.flag.hn:before,i.flag.honduras:before{background-position:-36px -520px}i.flag.croatia:before,i.flag.hr:before{background-position:-36px -546px}i.flag.haiti:before,i.flag.ht:before{background-position:-36px -572px}i.flag.hu:before,i.flag.hungary:before{background-position:-36px -598px}i.flag.id:before,i.flag.indonesia:before{background-position:-36px -624px}i.flag.ie:before,i.flag.ireland:before{background-position:-36px -650px}i.flag.il:before,i.flag.israel:before{background-position:-36px -676px}i.flag.in:before,i.flag.india:before{background-position:-36px -702px}i.flag.indian.ocean.territory:before,i.flag.io:before{background-position:-36px -728px}i.flag.iq:before,i.flag.iraq:before{background-position:-36px -754px}i.flag.ir:before,i.flag.iran:before{background-position:-36px -780px}i.flag.iceland:before,i.flag.is:before{background-position:-36px -806px}i.flag.it:before,i.flag.italy:before{background-position:-36px -832px}i.flag.jamaica:before,i.flag.jm:before{background-position:-36px -858px}i.flag.jo:before,i.flag.jordan:before{background-position:-36px -884px}i.flag.japan:before,i.flag.jp:before{background-position:-36px -910px}i.flag.ke:before,i.flag.kenya:before{background-position:-36px -936px}i.flag.kg:before,i.flag.kyrgyzstan:before{background-position:-36px -962px}i.flag.cambodia:before,i.flag.kh:before{background-position:-36px -988px}i.flag.ki:before,i.flag.kiribati:before{background-position:-36px -1014px}i.flag.comoros:before,i.flag.km:before{background-position:-36px -1040px}i.flag.kn:before,i.flag.saint.kitts.and.nevis:before{background-position:-36px -1066px}i.flag.kp:before,i.flag.north.korea:before{background-position:-36px -1092px}i.flag.kr:before,i.flag.south.korea:before{background-position:-36px -1118px}i.flag.kuwait:before,i.flag.kw:before{background-position:-36px -1144px}i.flag.cayman.islands:before,i.flag.ky:before{background-position:-36px -1170px}i.flag.kazakhstan:before,i.flag.kz:before{background-position:-36px -1196px}i.flag.la:before,i.flag.laos:before{background-position:-36px -1222px}i.flag.lb:before,i.flag.lebanon:before{background-position:-36px -1248px}i.flag.lc:before,i.flag.saint.lucia:before{background-position:-36px -1274px}i.flag.li:before,i.flag.liechtenstein:before{background-position:-36px -1300px}i.flag.lk:before,i.flag.sri.lanka:before{background-position:-36px -1326px}i.flag.liberia:before,i.flag.lr:before{background-position:-36px -1352px}i.flag.lesotho:before,i.flag.ls:before{background-position:-36px -1378px}i.flag.lithuania:before,i.flag.lt:before{background-position:-36px -1404px}i.flag.lu:before,i.flag.luxembourg:before{background-position:-36px -1430px}i.flag.latvia:before,i.flag.lv:before{background-position:-36px -1456px}i.flag.libya:before,i.flag.ly:before{background-position:-36px -1482px}i.flag.ma:before,i.flag.morocco:before{background-position:-36px -1508px}i.flag.mc:before,i.flag.monaco:before{background-position:-36px -1534px}i.flag.md:before,i.flag.moldova:before{background-position:-36px -1560px}i.flag.me:before,i.flag.montenegro:before{background-position:-36px -1586px}i.flag.madagascar:before,i.flag.mg:before{background-position:-36px -1613px}i.flag.marshall.islands:before,i.flag.mh:before{background-position:-36px -1639px}i.flag.macedonia:before,i.flag.mk:before{background-position:-36px -1665px}i.flag.mali:before,i.flag.ml:before{background-position:-36px -1691px}i.flag.burma:before,i.flag.mm:before,i.flag.myanmar:before{background-position:-73px -1821px}i.flag.mn:before,i.flag.mongolia:before{background-position:-36px -1743px}i.flag.macau:before,i.flag.mo:before{background-position:-36px -1769px}i.flag.mp:before,i.flag.northern.mariana.islands:before{background-position:-36px -1795px}i.flag.martinique:before,i.flag.mq:before{background-position:-36px -1821px}i.flag.mauritania:before,i.flag.mr:before{background-position:-36px -1847px}i.flag.montserrat:before,i.flag.ms:before{background-position:-36px -1873px}i.flag.malta:before,i.flag.mt:before{background-position:-36px -1899px}i.flag.mauritius:before,i.flag.mu:before{background-position:-36px -1925px}i.flag.maldives:before,i.flag.mv:before{background-position:-36px -1951px}i.flag.malawi:before,i.flag.mw:before{background-position:-36px -1977px}i.flag.mexico:before,i.flag.mx:before{background-position:-72px 0}i.flag.malaysia:before,i.flag.my:before{background-position:-72px -26px}i.flag.mozambique:before,i.flag.mz:before{background-position:-72px -52px}i.flag.na:before,i.flag.namibia:before{background-position:-72px -78px}i.flag.nc:before,i.flag.new.caledonia:before{background-position:-72px -104px}i.flag.ne:before,i.flag.niger:before{background-position:-72px -130px}i.flag.nf:before,i.flag.norfolk.island:before{background-position:-72px -156px}i.flag.ng:before,i.flag.nigeria:before{background-position:-72px -182px}i.flag.ni:before,i.flag.nicaragua:before{background-position:-72px -208px}i.flag.netherlands:before,i.flag.nl:before{background-position:-72px -234px}i.flag.no:before,i.flag.norway:before{background-position:-72px -260px}i.flag.nepal:before,i.flag.np:before{background-position:-72px -286px}i.flag.nauru:before,i.flag.nr:before{background-position:-72px -312px}i.flag.niue:before,i.flag.nu:before{background-position:-72px -338px}i.flag.new.zealand:before,i.flag.nz:before{background-position:-72px -364px}i.flag.om:before,i.flag.oman:before{background-position:-72px -390px}i.flag.pa:before,i.flag.panama:before{background-position:-72px -416px}i.flag.pe:before,i.flag.peru:before{background-position:-72px -442px}i.flag.french.polynesia:before,i.flag.pf:before{background-position:-72px -468px}i.flag.new.guinea:before,i.flag.pg:before{background-position:-72px -494px}i.flag.ph:before,i.flag.philippines:before{background-position:-72px -520px}i.flag.pakistan:before,i.flag.pk:before{background-position:-72px -546px}i.flag.pl:before,i.flag.poland:before{background-position:-72px -572px}i.flag.pm:before,i.flag.saint.pierre:before{background-position:-72px -598px}i.flag.pitcairn.islands:before,i.flag.pn:before{background-position:-72px -624px}i.flag.pr:before,i.flag.puerto.rico:before{background-position:-72px -650px}i.flag.palestine:before,i.flag.ps:before{background-position:-72px -676px}i.flag.portugal:before,i.flag.pt:before{background-position:-72px -702px}i.flag.palau:before,i.flag.pw:before{background-position:-72px -728px}i.flag.paraguay:before,i.flag.py:before{background-position:-72px -754px}i.flag.qa:before,i.flag.qatar:before{background-position:-72px -780px}i.flag.re:before,i.flag.reunion:before{background-position:-72px -806px}i.flag.ro:before,i.flag.romania:before{background-position:-72px -832px}i.flag.rs:before,i.flag.serbia:before{background-position:-72px -858px}i.flag.ru:before,i.flag.russia:before{background-position:-72px -884px}i.flag.rw:before,i.flag.rwanda:before{background-position:-72px -910px}i.flag.sa:before,i.flag.saudi.arabia:before{background-position:-72px -936px}i.flag.sb:before,i.flag.solomon.islands:before{background-position:-72px -962px}i.flag.sc:before,i.flag.seychelles:before{background-position:-72px -988px}i.flag.gb.sct:before,i.flag.scotland:before{background-position:-72px -1014px}i.flag.sd:before,i.flag.sudan:before{background-position:-72px -1040px}i.flag.se:before,i.flag.sweden:before{background-position:-72px -1066px}i.flag.sg:before,i.flag.singapore:before{background-position:-72px -1092px}i.flag.saint.helena:before,i.flag.sh:before{background-position:-72px -1118px}i.flag.si:before,i.flag.slovenia:before{background-position:-72px -1144px}i.flag.jan.mayen:before,i.flag.sj:before,i.flag.svalbard:before{background-position:-72px -1170px}i.flag.sk:before,i.flag.slovakia:before{background-position:-72px -1196px}i.flag.sierra.leone:before,i.flag.sl:before{background-position:-72px -1222px}i.flag.san.marino:before,i.flag.sm:before{background-position:-72px -1248px}i.flag.senegal:before,i.flag.sn:before{background-position:-72px -1274px}i.flag.so:before,i.flag.somalia:before{background-position:-72px -1300px}i.flag.sr:before,i.flag.suriname:before{background-position:-72px -1326px}i.flag.sao.tome:before,i.flag.st:before{background-position:-72px -1352px}i.flag.el.salvador:before,i.flag.sv:before{background-position:-72px -1378px}i.flag.sy:before,i.flag.syria:before{background-position:-72px -1404px}i.flag.swaziland:before,i.flag.sz:before{background-position:-72px -1430px}i.flag.caicos.islands:before,i.flag.tc:before{background-position:-72px -1456px}i.flag.chad:before,i.flag.td:before{background-position:-72px -1482px}i.flag.french.territories:before,i.flag.tf:before{background-position:-72px -1508px}i.flag.tg:before,i.flag.togo:before{background-position:-72px -1534px}i.flag.th:before,i.flag.thailand:before{background-position:-72px -1560px}i.flag.tajikistan:before,i.flag.tj:before{background-position:-72px -1586px}i.flag.tk:before,i.flag.tokelau:before{background-position:-72px -1612px}i.flag.timorleste:before,i.flag.tl:before{background-position:-72px -1638px}i.flag.tm:before,i.flag.turkmenistan:before{background-position:-72px -1664px}i.flag.tn:before,i.flag.tunisia:before{background-position:-72px -1690px}i.flag.to:before,i.flag.tonga:before{background-position:-72px -1716px}i.flag.tr:before,i.flag.turkey:before{background-position:-72px -1742px}i.flag.trinidad:before,i.flag.tt:before{background-position:-72px -1768px}i.flag.tuvalu:before,i.flag.tv:before{background-position:-72px -1794px}i.flag.taiwan:before,i.flag.tw:before{background-position:-72px -1820px}i.flag.tanzania:before,i.flag.tz:before{background-position:-72px -1846px}i.flag.ua:before,i.flag.ukraine:before{background-position:-72px -1872px}i.flag.ug:before,i.flag.uganda:before{background-position:-72px -1898px}i.flag.um:before,i.flag.us.minor.islands:before{background-position:-72px -1924px}i.flag.america:before,i.flag.united.states:before,i.flag.us:before{background-position:-72px -1950px}i.flag.uruguay:before,i.flag.uy:before{background-position:-72px -1976px}i.flag.uz:before,i.flag.uzbekistan:before{background-position:-108px 0}i.flag.va:before,i.flag.vatican.city:before{background-position:-108px -26px}i.flag.saint.vincent:before,i.flag.vc:before{background-position:-108px -52px}i.flag.ve:before,i.flag.venezuela:before{background-position:-108px -78px}i.flag.british.virgin.islands:before,i.flag.vg:before{background-position:-108px -104px}i.flag.us.virgin.islands:before,i.flag.vi:before{background-position:-108px -130px}i.flag.vietnam:before,i.flag.vn:before{background-position:-108px -156px}i.flag.vanuatu:before,i.flag.vu:before{background-position:-108px -182px}i.flag.gb.wls:before,i.flag.wales:before{background-position:-108px -208px}i.flag.wallis.and.futuna:before,i.flag.wf:before{background-position:-108px -234px}i.flag.samoa:before,i.flag.ws:before{background-position:-108px -260px}i.flag.ye:before,i.flag.yemen:before{background-position:-108px -286px}i.flag.mayotte:before,i.flag.yt:before{background-position:-108px -312px}i.flag.south.africa:before,i.flag.za:before{background-position:-108px -338px}i.flag.zambia:before,i.flag.zm:before{background-position:-108px -364px}i.flag.zimbabwe:before,i.flag.zw:before{background-position:-108px -390px}/*!
+ * # Semantic UI 2.4.2 - Header
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.header{border:none;margin:calc(2rem - .14285714em) 0 1rem;padding:0 0;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-weight:700;line-height:1.28571429em;text-transform:none;color:rgba(0,0,0,.87)}.ui.header:first-child{margin-top:-.14285714em}.ui.header:last-child{margin-bottom:0}.ui.header .sub.header{display:block;font-weight:400;padding:0;margin:0;font-size:1rem;line-height:1.2em;color:rgba(0,0,0,.6)}.ui.header>.icon{display:table-cell;opacity:1;font-size:1.5em;padding-top:0;vertical-align:middle}.ui.header .icon:only-child{display:inline-block;padding:0;margin-right:.75rem}.ui.header>.image:not(.icon),.ui.header>img{display:inline-block;margin-top:.14285714em;width:2.5em;height:auto;vertical-align:middle}.ui.header>.image:not(.icon):only-child,.ui.header>img:only-child{margin-right:.75rem}.ui.header .content{display:inline-block;vertical-align:top}.ui.header>.image+.content,.ui.header>img+.content{padding-left:.75rem;vertical-align:middle}.ui.header>.icon+.content{padding-left:.75rem;display:table-cell;vertical-align:middle}.ui.header .ui.label{font-size:'';margin-left:.5rem;vertical-align:middle}.ui.header+p{margin-top:0}h1.ui.header{font-size:2rem}h2.ui.header{font-size:1.71428571rem}h3.ui.header{font-size:1.28571429rem}h4.ui.header{font-size:1.07142857rem}h5.ui.header{font-size:1rem}h1.ui.header .sub.header{font-size:1.14285714rem}h2.ui.header .sub.header{font-size:1.14285714rem}h3.ui.header .sub.header{font-size:1rem}h4.ui.header .sub.header{font-size:1rem}h5.ui.header .sub.header{font-size:.92857143rem}.ui.huge.header{min-height:1em;font-size:2em}.ui.large.header{font-size:1.71428571em}.ui.medium.header{font-size:1.28571429em}.ui.small.header{font-size:1.07142857em}.ui.tiny.header{font-size:1em}.ui.huge.header .sub.header{font-size:1.14285714rem}.ui.large.header .sub.header{font-size:1.14285714rem}.ui.header .sub.header{font-size:1rem}.ui.small.header .sub.header{font-size:1rem}.ui.tiny.header .sub.header{font-size:.92857143rem}.ui.sub.header{padding:0;margin-bottom:.14285714rem;font-weight:700;font-size:.85714286em;text-transform:uppercase;color:''}.ui.small.sub.header{font-size:.78571429em}.ui.sub.header{font-size:.85714286em}.ui.large.sub.header{font-size:.92857143em}.ui.huge.sub.header{font-size:1em}.ui.icon.header{display:inline-block;text-align:center;margin:2rem 0 1rem}.ui.icon.header:after{content:'';display:block;height:0;clear:both;visibility:hidden}.ui.icon.header:first-child{margin-top:0}.ui.icon.header .icon{float:none;display:block;width:auto;height:auto;line-height:1;padding:0;font-size:3em;margin:0 auto .5rem;opacity:1}.ui.icon.header .content{display:block;padding:0}.ui.icon.header .circular.icon{font-size:2em}.ui.icon.header .square.icon{font-size:2em}.ui.block.icon.header .icon{margin-bottom:0}.ui.icon.header.aligned{margin-left:auto;margin-right:auto;display:block}.ui.disabled.header{opacity:.45}.ui.inverted.header{color:#fff}.ui.inverted.header .sub.header{color:rgba(255,255,255,.8)}.ui.inverted.attached.header{background:#545454 -webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.05)));background:#545454 -webkit-linear-gradient(transparent,rgba(0,0,0,.05));background:#545454 linear-gradient(transparent,rgba(0,0,0,.05));-webkit-box-shadow:none;box-shadow:none;border-color:transparent}.ui.inverted.block.header{background:#545454 -webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.05)));background:#545454 -webkit-linear-gradient(transparent,rgba(0,0,0,.05));background:#545454 linear-gradient(transparent,rgba(0,0,0,.05));-webkit-box-shadow:none;box-shadow:none}.ui.inverted.block.header{border-bottom:none}.ui.red.header{color:#db2828!important}a.ui.red.header:hover{color:#d01919!important}.ui.red.dividing.header{border-bottom:2px solid #db2828}.ui.inverted.red.header{color:#ff695e!important}a.ui.inverted.red.header:hover{color:#ff5144!important}.ui.orange.header{color:#f2711c!important}a.ui.orange.header:hover{color:#f26202!important}.ui.orange.dividing.header{border-bottom:2px solid #f2711c}.ui.inverted.orange.header{color:#ff851b!important}a.ui.inverted.orange.header:hover{color:#ff7701!important}.ui.olive.header{color:#b5cc18!important}a.ui.olive.header:hover{color:#a7bd0d!important}.ui.olive.dividing.header{border-bottom:2px solid #b5cc18}.ui.inverted.olive.header{color:#d9e778!important}a.ui.inverted.olive.header:hover{color:#d8ea5c!important}.ui.yellow.header{color:#fbbd08!important}a.ui.yellow.header:hover{color:#eaae00!important}.ui.yellow.dividing.header{border-bottom:2px solid #fbbd08}.ui.inverted.yellow.header{color:#ffe21f!important}a.ui.inverted.yellow.header:hover{color:#ffdf05!important}.ui.green.header{color:#21ba45!important}a.ui.green.header:hover{color:#16ab39!important}.ui.green.dividing.header{border-bottom:2px solid #21ba45}.ui.inverted.green.header{color:#2ecc40!important}a.ui.inverted.green.header:hover{color:#22be34!important}.ui.teal.header{color:#00b5ad!important}a.ui.teal.header:hover{color:#009c95!important}.ui.teal.dividing.header{border-bottom:2px solid #00b5ad}.ui.inverted.teal.header{color:#6dffff!important}a.ui.inverted.teal.header:hover{color:#54ffff!important}.ui.blue.header{color:#2185d0!important}a.ui.blue.header:hover{color:#1678c2!important}.ui.blue.dividing.header{border-bottom:2px solid #2185d0}.ui.inverted.blue.header{color:#54c8ff!important}a.ui.inverted.blue.header:hover{color:#3ac0ff!important}.ui.violet.header{color:#6435c9!important}a.ui.violet.header:hover{color:#5829bb!important}.ui.violet.dividing.header{border-bottom:2px solid #6435c9}.ui.inverted.violet.header{color:#a291fb!important}a.ui.inverted.violet.header:hover{color:#8a73ff!important}.ui.purple.header{color:#a333c8!important}a.ui.purple.header:hover{color:#9627ba!important}.ui.purple.dividing.header{border-bottom:2px solid #a333c8}.ui.inverted.purple.header{color:#dc73ff!important}a.ui.inverted.purple.header:hover{color:#d65aff!important}.ui.pink.header{color:#e03997!important}a.ui.pink.header:hover{color:#e61a8d!important}.ui.pink.dividing.header{border-bottom:2px solid #e03997}.ui.inverted.pink.header{color:#ff8edf!important}a.ui.inverted.pink.header:hover{color:#ff74d8!important}.ui.brown.header{color:#a5673f!important}a.ui.brown.header:hover{color:#975b33!important}.ui.brown.dividing.header{border-bottom:2px solid #a5673f}.ui.inverted.brown.header{color:#d67c1c!important}a.ui.inverted.brown.header:hover{color:#c86f11!important}.ui.grey.header{color:#767676!important}a.ui.grey.header:hover{color:#838383!important}.ui.grey.dividing.header{border-bottom:2px solid #767676}.ui.inverted.grey.header{color:#dcddde!important}a.ui.inverted.grey.header:hover{color:#cfd0d2!important}.ui.left.aligned.header{text-align:left}.ui.right.aligned.header{text-align:right}.ui.center.aligned.header,.ui.centered.header{text-align:center}.ui.justified.header{text-align:justify}.ui.justified.header:after{display:inline-block;content:'';width:100%}.ui.floated.header,.ui[class*="left floated"].header{float:left;margin-top:0;margin-right:.5em}.ui[class*="right floated"].header{float:right;margin-top:0;margin-left:.5em}.ui.fitted.header{padding:0}.ui.dividing.header{padding-bottom:.21428571rem;border-bottom:1px solid rgba(34,36,38,.15)}.ui.dividing.header .sub.header{padding-bottom:.21428571rem}.ui.dividing.header .icon{margin-bottom:0}.ui.inverted.dividing.header{border-bottom-color:rgba(255,255,255,.1)}.ui.block.header{background:#f3f4f5;padding:.78571429rem 1rem;-webkit-box-shadow:none;box-shadow:none;border:1px solid #d4d4d5;border-radius:.28571429rem}.ui.tiny.block.header{font-size:.85714286rem}.ui.small.block.header{font-size:.92857143rem}.ui.block.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6){font-size:1rem}.ui.large.block.header{font-size:1.14285714rem}.ui.huge.block.header{font-size:1.42857143rem}.ui.attached.header{background:#fff;padding:.78571429rem 1rem;margin-left:-1px;margin-right:-1px;-webkit-box-shadow:none;box-shadow:none;border:1px solid #d4d4d5}.ui.attached.block.header{background:#f3f4f5}.ui.attached:not(.top):not(.bottom).header{margin-top:0;margin-bottom:0;border-top:none;border-radius:0}.ui.top.attached.header{margin-bottom:0;border-radius:.28571429rem .28571429rem 0 0}.ui.bottom.attached.header{margin-top:0;border-top:none;border-radius:0 0 .28571429rem .28571429rem}.ui.tiny.attached.header{font-size:.85714286em}.ui.small.attached.header{font-size:.92857143em}.ui.attached.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6){font-size:1em}.ui.large.attached.header{font-size:1.14285714em}.ui.huge.attached.header{font-size:1.42857143em}.ui.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6){font-size:1.28571429em}/*!
+ * # Semantic UI 2.4.2 - Icon
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */@font-face{font-family:Icons;src:url(themes/default/assets/fonts/icons.eot);src:url(themes/default/assets/fonts/icons.eot?#iefix) format('embedded-opentype'),url(themes/default/assets/fonts/icons.woff2) format('woff2'),url(themes/default/assets/fonts/icons.woff) format('woff'),url(themes/default/assets/fonts/icons.ttf) format('truetype'),url(themes/default/assets/fonts/icons.svg#icons) format('svg');font-style:normal;font-weight:400;font-variant:normal;text-decoration:inherit;text-transform:none}i.icon{display:inline-block;opacity:1;margin:0 .25rem 0 0;width:1.18em;height:1em;font-family:Icons;font-style:normal;font-weight:400;text-decoration:inherit;text-align:center;speak:none;font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;-webkit-backface-visibility:hidden;backface-visibility:hidden}i.icon:before{background:0 0!important}i.icon.loading{height:1em;line-height:1;-webkit-animation:icon-loading 2s linear infinite;animation:icon-loading 2s linear infinite}@-webkit-keyframes icon-loading{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes icon-loading{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}i.icon.hover{opacity:1!important}i.icon.active{opacity:1!important}i.emphasized.icon{opacity:1!important}i.disabled.icon{opacity:.45!important}i.fitted.icon{width:auto;margin:0!important}i.link.icon,i.link.icons{cursor:pointer;opacity:.8;-webkit-transition:opacity .1s ease;transition:opacity .1s ease}i.link.icon:hover,i.link.icons:hover{opacity:1!important}i.circular.icon{border-radius:500em!important;line-height:1!important;padding:.5em 0!important;-webkit-box-shadow:0 0 0 .1em rgba(0,0,0,.1) inset;box-shadow:0 0 0 .1em rgba(0,0,0,.1) inset;width:2em!important;height:2em!important}i.circular.inverted.icon{border:none;-webkit-box-shadow:none;box-shadow:none}i.flipped.icon,i.horizontally.flipped.icon{-webkit-transform:scale(-1,1);transform:scale(-1,1)}i.vertically.flipped.icon{-webkit-transform:scale(1,-1);transform:scale(1,-1)}i.clockwise.rotated.icon,i.right.rotated.icon,i.rotated.icon{-webkit-transform:rotate(90deg);transform:rotate(90deg)}i.counterclockwise.rotated.icon,i.left.rotated.icon{-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}i.bordered.icon{line-height:1;vertical-align:baseline;width:2em;height:2em;padding:.5em 0!important;-webkit-box-shadow:0 0 0 .1em rgba(0,0,0,.1) inset;box-shadow:0 0 0 .1em rgba(0,0,0,.1) inset}i.bordered.inverted.icon{border:none;-webkit-box-shadow:none;box-shadow:none}i.inverted.bordered.icon,i.inverted.circular.icon{background-color:#1b1c1d!important;color:#fff!important}i.inverted.icon{color:#fff}i.red.icon{color:#db2828!important}i.inverted.red.icon{color:#ff695e!important}i.inverted.bordered.red.icon,i.inverted.circular.red.icon{background-color:#db2828!important;color:#fff!important}i.orange.icon{color:#f2711c!important}i.inverted.orange.icon{color:#ff851b!important}i.inverted.bordered.orange.icon,i.inverted.circular.orange.icon{background-color:#f2711c!important;color:#fff!important}i.yellow.icon{color:#fbbd08!important}i.inverted.yellow.icon{color:#ffe21f!important}i.inverted.bordered.yellow.icon,i.inverted.circular.yellow.icon{background-color:#fbbd08!important;color:#fff!important}i.olive.icon{color:#b5cc18!important}i.inverted.olive.icon{color:#d9e778!important}i.inverted.bordered.olive.icon,i.inverted.circular.olive.icon{background-color:#b5cc18!important;color:#fff!important}i.green.icon{color:#21ba45!important}i.inverted.green.icon{color:#2ecc40!important}i.inverted.bordered.green.icon,i.inverted.circular.green.icon{background-color:#21ba45!important;color:#fff!important}i.teal.icon{color:#00b5ad!important}i.inverted.teal.icon{color:#6dffff!important}i.inverted.bordered.teal.icon,i.inverted.circular.teal.icon{background-color:#00b5ad!important;color:#fff!important}i.blue.icon{color:#2185d0!important}i.inverted.blue.icon{color:#54c8ff!important}i.inverted.bordered.blue.icon,i.inverted.circular.blue.icon{background-color:#2185d0!important;color:#fff!important}i.violet.icon{color:#6435c9!important}i.inverted.violet.icon{color:#a291fb!important}i.inverted.bordered.violet.icon,i.inverted.circular.violet.icon{background-color:#6435c9!important;color:#fff!important}i.purple.icon{color:#a333c8!important}i.inverted.purple.icon{color:#dc73ff!important}i.inverted.bordered.purple.icon,i.inverted.circular.purple.icon{background-color:#a333c8!important;color:#fff!important}i.pink.icon{color:#e03997!important}i.inverted.pink.icon{color:#ff8edf!important}i.inverted.bordered.pink.icon,i.inverted.circular.pink.icon{background-color:#e03997!important;color:#fff!important}i.brown.icon{color:#a5673f!important}i.inverted.brown.icon{color:#d67c1c!important}i.inverted.bordered.brown.icon,i.inverted.circular.brown.icon{background-color:#a5673f!important;color:#fff!important}i.grey.icon{color:#767676!important}i.inverted.grey.icon{color:#dcddde!important}i.inverted.bordered.grey.icon,i.inverted.circular.grey.icon{background-color:#767676!important;color:#fff!important}i.black.icon{color:#1b1c1d!important}i.inverted.black.icon{color:#545454!important}i.inverted.bordered.black.icon,i.inverted.circular.black.icon{background-color:#1b1c1d!important;color:#fff!important}i.mini.icon,i.mini.icons{line-height:1;font-size:.4em}i.tiny.icon,i.tiny.icons{line-height:1;font-size:.5em}i.small.icon,i.small.icons{line-height:1;font-size:.75em}i.icon,i.icons{font-size:1em}i.large.icon,i.large.icons{line-height:1;vertical-align:middle;font-size:1.5em}i.big.icon,i.big.icons{line-height:1;vertical-align:middle;font-size:2em}i.huge.icon,i.huge.icons{line-height:1;vertical-align:middle;font-size:4em}i.massive.icon,i.massive.icons{line-height:1;vertical-align:middle;font-size:8em}i.icons{display:inline-block;position:relative;line-height:1}i.icons .icon{position:absolute;top:50%;left:50%;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%);margin:0;margin:0}i.icons .icon:first-child{position:static;width:auto;height:auto;vertical-align:top;-webkit-transform:none;transform:none;margin-right:.25rem}i.icons .corner.icon{top:auto;left:auto;right:0;bottom:0;-webkit-transform:none;transform:none;font-size:.45em;text-shadow:-1px -1px 0 #fff,1px -1px 0 #fff,-1px 1px 0 #fff,1px 1px 0 #fff}i.icons .top.right.corner.icon{top:0;left:auto;right:0;bottom:auto}i.icons .top.left.corner.icon{top:0;left:0;right:auto;bottom:auto}i.icons .bottom.left.corner.icon{top:auto;left:0;right:auto;bottom:0}i.icons .bottom.right.corner.icon{top:auto;left:auto;right:0;bottom:0}i.icons .inverted.corner.icon{text-shadow:-1px -1px 0 #1b1c1d,1px -1px 0 #1b1c1d,-1px 1px 0 #1b1c1d,1px 1px 0 #1b1c1d}i.icon.linkedin.in:before{content:"\f0e1"}i.icon.zoom.in:before{content:"\f00e"}i.icon.zoom.out:before{content:"\f010"}i.icon.sign.in:before{content:"\f2f6"}i.icon.in.cart:before{content:"\f218"}i.icon.log.out:before{content:"\f2f5"}i.icon.sign.out:before{content:"\f2f5"}i.icon.\35 00px:before{content:"\f26e"}i.icon.accessible.icon:before{content:"\f368"}i.icon.accusoft:before{content:"\f369"}i.icon.address.book:before{content:"\f2b9"}i.icon.address.card:before{content:"\f2bb"}i.icon.adjust:before{content:"\f042"}i.icon.adn:before{content:"\f170"}i.icon.adversal:before{content:"\f36a"}i.icon.affiliatetheme:before{content:"\f36b"}i.icon.algolia:before{content:"\f36c"}i.icon.align.center:before{content:"\f037"}i.icon.align.justify:before{content:"\f039"}i.icon.align.left:before{content:"\f036"}i.icon.align.right:before{content:"\f038"}i.icon.amazon:before{content:"\f270"}i.icon.amazon.pay:before{content:"\f42c"}i.icon.ambulance:before{content:"\f0f9"}i.icon.american.sign.language.interpreting:before{content:"\f2a3"}i.icon.amilia:before{content:"\f36d"}i.icon.anchor:before{content:"\f13d"}i.icon.android:before{content:"\f17b"}i.icon.angellist:before{content:"\f209"}i.icon.angle.double.down:before{content:"\f103"}i.icon.angle.double.left:before{content:"\f100"}i.icon.angle.double.right:before{content:"\f101"}i.icon.angle.double.up:before{content:"\f102"}i.icon.angle.down:before{content:"\f107"}i.icon.angle.left:before{content:"\f104"}i.icon.angle.right:before{content:"\f105"}i.icon.angle.up:before{content:"\f106"}i.icon.angrycreative:before{content:"\f36e"}i.icon.angular:before{content:"\f420"}i.icon.app.store:before{content:"\f36f"}i.icon.app.store.ios:before{content:"\f370"}i.icon.apper:before{content:"\f371"}i.icon.apple:before{content:"\f179"}i.icon.apple.pay:before{content:"\f415"}i.icon.archive:before{content:"\f187"}i.icon.arrow.alternate.circle.down:before{content:"\f358"}i.icon.arrow.alternate.circle.left:before{content:"\f359"}i.icon.arrow.alternate.circle.right:before{content:"\f35a"}i.icon.arrow.alternate.circle.up:before{content:"\f35b"}i.icon.arrow.circle.down:before{content:"\f0ab"}i.icon.arrow.circle.left:before{content:"\f0a8"}i.icon.arrow.circle.right:before{content:"\f0a9"}i.icon.arrow.circle.up:before{content:"\f0aa"}i.icon.arrow.down:before{content:"\f063"}i.icon.arrow.left:before{content:"\f060"}i.icon.arrow.right:before{content:"\f061"}i.icon.arrow.up:before{content:"\f062"}i.icon.arrows.alternate:before{content:"\f0b2"}i.icon.arrows.alternate.horizontal:before{content:"\f337"}i.icon.arrows.alternate.vertical:before{content:"\f338"}i.icon.assistive.listening.systems:before{content:"\f2a2"}i.icon.asterisk:before{content:"\f069"}i.icon.asymmetrik:before{content:"\f372"}i.icon.at:before{content:"\f1fa"}i.icon.audible:before{content:"\f373"}i.icon.audio.description:before{content:"\f29e"}i.icon.autoprefixer:before{content:"\f41c"}i.icon.avianex:before{content:"\f374"}i.icon.aviato:before{content:"\f421"}i.icon.aws:before{content:"\f375"}i.icon.backward:before{content:"\f04a"}i.icon.balance.scale:before{content:"\f24e"}i.icon.ban:before{content:"\f05e"}i.icon.band.aid:before{content:"\f462"}i.icon.bandcamp:before{content:"\f2d5"}i.icon.barcode:before{content:"\f02a"}i.icon.bars:before{content:"\f0c9"}i.icon.baseball.ball:before{content:"\f433"}i.icon.basketball.ball:before{content:"\f434"}i.icon.bath:before{content:"\f2cd"}i.icon.battery.empty:before{content:"\f244"}i.icon.battery.full:before{content:"\f240"}i.icon.battery.half:before{content:"\f242"}i.icon.battery.quarter:before{content:"\f243"}i.icon.battery.three.quarters:before{content:"\f241"}i.icon.bed:before{content:"\f236"}i.icon.beer:before{content:"\f0fc"}i.icon.behance:before{content:"\f1b4"}i.icon.behance.square:before{content:"\f1b5"}i.icon.bell:before{content:"\f0f3"}i.icon.bell.slash:before{content:"\f1f6"}i.icon.bicycle:before{content:"\f206"}i.icon.bimobject:before{content:"\f378"}i.icon.binoculars:before{content:"\f1e5"}i.icon.birthday.cake:before{content:"\f1fd"}i.icon.bitbucket:before{content:"\f171"}i.icon.bitcoin:before{content:"\f379"}i.icon.bity:before{content:"\f37a"}i.icon.black.tie:before{content:"\f27e"}i.icon.blackberry:before{content:"\f37b"}i.icon.blind:before{content:"\f29d"}i.icon.blogger:before{content:"\f37c"}i.icon.blogger.b:before{content:"\f37d"}i.icon.bluetooth:before{content:"\f293"}i.icon.bluetooth.b:before{content:"\f294"}i.icon.bold:before{content:"\f032"}i.icon.bolt:before{content:"\f0e7"}i.icon.bomb:before{content:"\f1e2"}i.icon.book:before{content:"\f02d"}i.icon.bookmark:before{content:"\f02e"}i.icon.bowling.ball:before{content:"\f436"}i.icon.box:before{content:"\f466"}i.icon.boxes:before{content:"\f468"}i.icon.braille:before{content:"\f2a1"}i.icon.briefcase:before{content:"\f0b1"}i.icon.btc:before{content:"\f15a"}i.icon.bug:before{content:"\f188"}i.icon.building:before{content:"\f1ad"}i.icon.bullhorn:before{content:"\f0a1"}i.icon.bullseye:before{content:"\f140"}i.icon.buromobelexperte:before{content:"\f37f"}i.icon.bus:before{content:"\f207"}i.icon.buysellads:before{content:"\f20d"}i.icon.calculator:before{content:"\f1ec"}i.icon.calendar:before{content:"\f133"}i.icon.calendar.alternate:before{content:"\f073"}i.icon.calendar.check:before{content:"\f274"}i.icon.calendar.minus:before{content:"\f272"}i.icon.calendar.plus:before{content:"\f271"}i.icon.calendar.times:before{content:"\f273"}i.icon.camera:before{content:"\f030"}i.icon.camera.retro:before{content:"\f083"}i.icon.car:before{content:"\f1b9"}i.icon.caret.down:before{content:"\f0d7"}i.icon.caret.left:before{content:"\f0d9"}i.icon.caret.right:before{content:"\f0da"}i.icon.caret.square.down:before{content:"\f150"}i.icon.caret.square.left:before{content:"\f191"}i.icon.caret.square.right:before{content:"\f152"}i.icon.caret.square.up:before{content:"\f151"}i.icon.caret.up:before{content:"\f0d8"}i.icon.cart.arrow.down:before{content:"\f218"}i.icon.cart.plus:before{content:"\f217"}i.icon.cc.amazon.pay:before{content:"\f42d"}i.icon.cc.amex:before{content:"\f1f3"}i.icon.cc.apple.pay:before{content:"\f416"}i.icon.cc.diners.club:before{content:"\f24c"}i.icon.cc.discover:before{content:"\f1f2"}i.icon.cc.jcb:before{content:"\f24b"}i.icon.cc.mastercard:before{content:"\f1f1"}i.icon.cc.paypal:before{content:"\f1f4"}i.icon.cc.stripe:before{content:"\f1f5"}i.icon.cc.visa:before{content:"\f1f0"}i.icon.centercode:before{content:"\f380"}i.icon.certificate:before{content:"\f0a3"}i.icon.chart.area:before{content:"\f1fe"}i.icon.chart.bar:before{content:"\f080"}i.icon.chart.line:before{content:"\f201"}i.icon.chart.pie:before{content:"\f200"}i.icon.check:before{content:"\f00c"}i.icon.check.circle:before{content:"\f058"}i.icon.check.square:before{content:"\f14a"}i.icon.chess:before{content:"\f439"}i.icon.chess.bishop:before{content:"\f43a"}i.icon.chess.board:before{content:"\f43c"}i.icon.chess.king:before{content:"\f43f"}i.icon.chess.knight:before{content:"\f441"}i.icon.chess.pawn:before{content:"\f443"}i.icon.chess.queen:before{content:"\f445"}i.icon.chess.rook:before{content:"\f447"}i.icon.chevron.circle.down:before{content:"\f13a"}i.icon.chevron.circle.left:before{content:"\f137"}i.icon.chevron.circle.right:before{content:"\f138"}i.icon.chevron.circle.up:before{content:"\f139"}i.icon.chevron.down:before{content:"\f078"}i.icon.chevron.left:before{content:"\f053"}i.icon.chevron.right:before{content:"\f054"}i.icon.chevron.up:before{content:"\f077"}i.icon.child:before{content:"\f1ae"}i.icon.chrome:before{content:"\f268"}i.icon.circle:before{content:"\f111"}i.icon.circle.notch:before{content:"\f1ce"}i.icon.clipboard:before{content:"\f328"}i.icon.clipboard.check:before{content:"\f46c"}i.icon.clipboard.list:before{content:"\f46d"}i.icon.clock:before{content:"\f017"}i.icon.clone:before{content:"\f24d"}i.icon.closed.captioning:before{content:"\f20a"}i.icon.cloud:before{content:"\f0c2"}i.icon.cloudscale:before{content:"\f383"}i.icon.cloudsmith:before{content:"\f384"}i.icon.cloudversify:before{content:"\f385"}i.icon.code:before{content:"\f121"}i.icon.code.branch:before{content:"\f126"}i.icon.codepen:before{content:"\f1cb"}i.icon.codiepie:before{content:"\f284"}i.icon.coffee:before{content:"\f0f4"}i.icon.cog:before{content:"\f013"}i.icon.cogs:before{content:"\f085"}i.icon.columns:before{content:"\f0db"}i.icon.comment:before{content:"\f075"}i.icon.comment.alternate:before{content:"\f27a"}i.icon.comments:before{content:"\f086"}i.icon.compass:before{content:"\f14e"}i.icon.compress:before{content:"\f066"}i.icon.connectdevelop:before{content:"\f20e"}i.icon.contao:before{content:"\f26d"}i.icon.copy:before{content:"\f0c5"}i.icon.copyright:before{content:"\f1f9"}i.icon.cpanel:before{content:"\f388"}i.icon.creative.commons:before{content:"\f25e"}i.icon.credit.card:before{content:"\f09d"}i.icon.crop:before{content:"\f125"}i.icon.crosshairs:before{content:"\f05b"}i.icon.css3:before{content:"\f13c"}i.icon.css3.alternate:before{content:"\f38b"}i.icon.cube:before{content:"\f1b2"}i.icon.cubes:before{content:"\f1b3"}i.icon.cut:before{content:"\f0c4"}i.icon.cuttlefish:before{content:"\f38c"}i.icon.d.and.d:before{content:"\f38d"}i.icon.dashcube:before{content:"\f210"}i.icon.database:before{content:"\f1c0"}i.icon.deaf:before{content:"\f2a4"}i.icon.delicious:before{content:"\f1a5"}i.icon.deploydog:before{content:"\f38e"}i.icon.deskpro:before{content:"\f38f"}i.icon.desktop:before{content:"\f108"}i.icon.deviantart:before{content:"\f1bd"}i.icon.digg:before{content:"\f1a6"}i.icon.digital.ocean:before{content:"\f391"}i.icon.discord:before{content:"\f392"}i.icon.discourse:before{content:"\f393"}i.icon.dna:before{content:"\f471"}i.icon.dochub:before{content:"\f394"}i.icon.docker:before{content:"\f395"}i.icon.dollar.sign:before{content:"\f155"}i.icon.dolly:before{content:"\f472"}i.icon.dolly.flatbed:before{content:"\f474"}i.icon.dot.circle:before{content:"\f192"}i.icon.download:before{content:"\f019"}i.icon.draft2digital:before{content:"\f396"}i.icon.dribbble:before{content:"\f17d"}i.icon.dribbble.square:before{content:"\f397"}i.icon.dropbox:before{content:"\f16b"}i.icon.drupal:before{content:"\f1a9"}i.icon.dyalog:before{content:"\f399"}i.icon.earlybirds:before{content:"\f39a"}i.icon.edge:before{content:"\f282"}i.icon.edit:before{content:"\f044"}i.icon.eject:before{content:"\f052"}i.icon.elementor:before{content:"\f430"}i.icon.ellipsis.horizontal:before{content:"\f141"}i.icon.ellipsis.vertical:before{content:"\f142"}i.icon.ember:before{content:"\f423"}i.icon.empire:before{content:"\f1d1"}i.icon.envelope:before{content:"\f0e0"}i.icon.envelope.open:before{content:"\f2b6"}i.icon.envelope.square:before{content:"\f199"}i.icon.envira:before{content:"\f299"}i.icon.eraser:before{content:"\f12d"}i.icon.erlang:before{content:"\f39d"}i.icon.ethereum:before{content:"\f42e"}i.icon.etsy:before{content:"\f2d7"}i.icon.euro.sign:before{content:"\f153"}i.icon.exchange.alternate:before{content:"\f362"}i.icon.exclamation:before{content:"\f12a"}i.icon.exclamation.circle:before{content:"\f06a"}i.icon.exclamation.triangle:before{content:"\f071"}i.icon.expand:before{content:"\f065"}i.icon.expand.arrows.alternate:before{content:"\f31e"}i.icon.expeditedssl:before{content:"\f23e"}i.icon.external.alternate:before{content:"\f35d"}i.icon.external.square.alternate:before{content:"\f360"}i.icon.eye:before{content:"\f06e"}i.icon.eye.dropper:before{content:"\f1fb"}i.icon.eye.slash:before{content:"\f070"}i.icon.facebook:before{content:"\f09a"}i.icon.facebook.f:before{content:"\f39e"}i.icon.facebook.messenger:before{content:"\f39f"}i.icon.facebook.square:before{content:"\f082"}i.icon.fast.backward:before{content:"\f049"}i.icon.fast.forward:before{content:"\f050"}i.icon.fax:before{content:"\f1ac"}i.icon.female:before{content:"\f182"}i.icon.fighter.jet:before{content:"\f0fb"}i.icon.file:before{content:"\f15b"}i.icon.file.alternate:before{content:"\f15c"}i.icon.file.archive:before{content:"\f1c6"}i.icon.file.audio:before{content:"\f1c7"}i.icon.file.code:before{content:"\f1c9"}i.icon.file.excel:before{content:"\f1c3"}i.icon.file.image:before{content:"\f1c5"}i.icon.file.pdf:before{content:"\f1c1"}i.icon.file.powerpoint:before{content:"\f1c4"}i.icon.file.video:before{content:"\f1c8"}i.icon.file.word:before{content:"\f1c2"}i.icon.film:before{content:"\f008"}i.icon.filter:before{content:"\f0b0"}i.icon.fire:before{content:"\f06d"}i.icon.fire.extinguisher:before{content:"\f134"}i.icon.firefox:before{content:"\f269"}i.icon.first.aid:before{content:"\f479"}i.icon.first.order:before{content:"\f2b0"}i.icon.firstdraft:before{content:"\f3a1"}i.icon.flag:before{content:"\f024"}i.icon.flag.checkered:before{content:"\f11e"}i.icon.flask:before{content:"\f0c3"}i.icon.flickr:before{content:"\f16e"}i.icon.flipboard:before{content:"\f44d"}i.icon.fly:before{content:"\f417"}i.icon.folder:before{content:"\f07b"}i.icon.folder.open:before{content:"\f07c"}i.icon.font:before{content:"\f031"}i.icon.font.awesome:before{content:"\f2b4"}i.icon.font.awesome.alternate:before{content:"\f35c"}i.icon.font.awesome.flag:before{content:"\f425"}i.icon.fonticons:before{content:"\f280"}i.icon.fonticons.fi:before{content:"\f3a2"}i.icon.football.ball:before{content:"\f44e"}i.icon.fort.awesome:before{content:"\f286"}i.icon.fort.awesome.alternate:before{content:"\f3a3"}i.icon.forumbee:before{content:"\f211"}i.icon.forward:before{content:"\f04e"}i.icon.foursquare:before{content:"\f180"}i.icon.free.code.camp:before{content:"\f2c5"}i.icon.freebsd:before{content:"\f3a4"}i.icon.frown:before{content:"\f119"}i.icon.futbol:before{content:"\f1e3"}i.icon.gamepad:before{content:"\f11b"}i.icon.gavel:before{content:"\f0e3"}i.icon.gem:before{content:"\f3a5"}i.icon.genderless:before{content:"\f22d"}i.icon.get.pocket:before{content:"\f265"}i.icon.gg:before{content:"\f260"}i.icon.gg.circle:before{content:"\f261"}i.icon.gift:before{content:"\f06b"}i.icon.git:before{content:"\f1d3"}i.icon.git.square:before{content:"\f1d2"}i.icon.github:before{content:"\f09b"}i.icon.github.alternate:before{content:"\f113"}i.icon.github.square:before{content:"\f092"}i.icon.gitkraken:before{content:"\f3a6"}i.icon.gitlab:before{content:"\f296"}i.icon.gitter:before{content:"\f426"}i.icon.glass.martini:before{content:"\f000"}i.icon.glide:before{content:"\f2a5"}i.icon.glide.g:before{content:"\f2a6"}i.icon.globe:before{content:"\f0ac"}i.icon.gofore:before{content:"\f3a7"}i.icon.golf.ball:before{content:"\f450"}i.icon.goodreads:before{content:"\f3a8"}i.icon.goodreads.g:before{content:"\f3a9"}i.icon.google:before{content:"\f1a0"}i.icon.google.drive:before{content:"\f3aa"}i.icon.google.play:before{content:"\f3ab"}i.icon.google.plus:before{content:"\f2b3"}i.icon.google.plus.g:before{content:"\f0d5"}i.icon.google.plus.square:before{content:"\f0d4"}i.icon.google.wallet:before{content:"\f1ee"}i.icon.graduation.cap:before{content:"\f19d"}i.icon.gratipay:before{content:"\f184"}i.icon.grav:before{content:"\f2d6"}i.icon.gripfire:before{content:"\f3ac"}i.icon.grunt:before{content:"\f3ad"}i.icon.gulp:before{content:"\f3ae"}i.icon.h.square:before{content:"\f0fd"}i.icon.hacker.news:before{content:"\f1d4"}i.icon.hacker.news.square:before{content:"\f3af"}i.icon.hand.lizard:before{content:"\f258"}i.icon.hand.paper:before{content:"\f256"}i.icon.hand.peace:before{content:"\f25b"}i.icon.hand.point.down:before{content:"\f0a7"}i.icon.hand.point.left:before{content:"\f0a5"}i.icon.hand.point.right:before{content:"\f0a4"}i.icon.hand.point.up:before{content:"\f0a6"}i.icon.hand.pointer:before{content:"\f25a"}i.icon.hand.rock:before{content:"\f255"}i.icon.hand.scissors:before{content:"\f257"}i.icon.hand.spock:before{content:"\f259"}i.icon.handshake:before{content:"\f2b5"}i.icon.hashtag:before{content:"\f292"}i.icon.hdd:before{content:"\f0a0"}i.icon.heading:before{content:"\f1dc"}i.icon.headphones:before{content:"\f025"}i.icon.heart:before{content:"\f004"}i.icon.heartbeat:before{content:"\f21e"}i.icon.hips:before{content:"\f452"}i.icon.hire.a.helper:before{content:"\f3b0"}i.icon.history:before{content:"\f1da"}i.icon.hockey.puck:before{content:"\f453"}i.icon.home:before{content:"\f015"}i.icon.hooli:before{content:"\f427"}i.icon.hospital:before{content:"\f0f8"}i.icon.hospital.symbol:before{content:"\f47e"}i.icon.hotjar:before{content:"\f3b1"}i.icon.hourglass:before{content:"\f254"}i.icon.hourglass.end:before{content:"\f253"}i.icon.hourglass.half:before{content:"\f252"}i.icon.hourglass.start:before{content:"\f251"}i.icon.houzz:before{content:"\f27c"}i.icon.html5:before{content:"\f13b"}i.icon.hubspot:before{content:"\f3b2"}i.icon.i.cursor:before{content:"\f246"}i.icon.id.badge:before{content:"\f2c1"}i.icon.id.card:before{content:"\f2c2"}i.icon.image:before{content:"\f03e"}i.icon.images:before{content:"\f302"}i.icon.imdb:before{content:"\f2d8"}i.icon.inbox:before{content:"\f01c"}i.icon.indent:before{content:"\f03c"}i.icon.industry:before{content:"\f275"}i.icon.info:before{content:"\f129"}i.icon.info.circle:before{content:"\f05a"}i.icon.instagram:before{content:"\f16d"}i.icon.internet.explorer:before{content:"\f26b"}i.icon.ioxhost:before{content:"\f208"}i.icon.italic:before{content:"\f033"}i.icon.itunes:before{content:"\f3b4"}i.icon.itunes.note:before{content:"\f3b5"}i.icon.jenkins:before{content:"\f3b6"}i.icon.joget:before{content:"\f3b7"}i.icon.joomla:before{content:"\f1aa"}i.icon.js:before{content:"\f3b8"}i.icon.js.square:before{content:"\f3b9"}i.icon.jsfiddle:before{content:"\f1cc"}i.icon.key:before{content:"\f084"}i.icon.keyboard:before{content:"\f11c"}i.icon.keycdn:before{content:"\f3ba"}i.icon.kickstarter:before{content:"\f3bb"}i.icon.kickstarter.k:before{content:"\f3bc"}i.icon.korvue:before{content:"\f42f"}i.icon.language:before{content:"\f1ab"}i.icon.laptop:before{content:"\f109"}i.icon.laravel:before{content:"\f3bd"}i.icon.lastfm:before{content:"\f202"}i.icon.lastfm.square:before{content:"\f203"}i.icon.leaf:before{content:"\f06c"}i.icon.leanpub:before{content:"\f212"}i.icon.lemon:before{content:"\f094"}i.icon.less:before{content:"\f41d"}i.icon.level.down.alternate:before{content:"\f3be"}i.icon.level.up.alternate:before{content:"\f3bf"}i.icon.life.ring:before{content:"\f1cd"}i.icon.lightbulb:before{content:"\f0eb"}i.icon.linechat:before{content:"\f3c0"}i.icon.linkify:before{content:"\f0c1"}i.icon.linkedin:before{content:"\f08c"}i.icon.linkedin.alt:before{content:"\f0e1"}i.icon.linode:before{content:"\f2b8"}i.icon.linux:before{content:"\f17c"}i.icon.lira.sign:before{content:"\f195"}i.icon.list:before{content:"\f03a"}i.icon.list.alternate:before{content:"\f022"}i.icon.list.ol:before{content:"\f0cb"}i.icon.list.ul:before{content:"\f0ca"}i.icon.location.arrow:before{content:"\f124"}i.icon.lock:before{content:"\f023"}i.icon.lock.open:before{content:"\f3c1"}i.icon.long.arrow.alternate.down:before{content:"\f309"}i.icon.long.arrow.alternate.left:before{content:"\f30a"}i.icon.long.arrow.alternate.right:before{content:"\f30b"}i.icon.long.arrow.alternate.up:before{content:"\f30c"}i.icon.low.vision:before{content:"\f2a8"}i.icon.lyft:before{content:"\f3c3"}i.icon.magento:before{content:"\f3c4"}i.icon.magic:before{content:"\f0d0"}i.icon.magnet:before{content:"\f076"}i.icon.male:before{content:"\f183"}i.icon.map:before{content:"\f279"}i.icon.map.marker:before{content:"\f041"}i.icon.map.marker.alternate:before{content:"\f3c5"}i.icon.map.pin:before{content:"\f276"}i.icon.map.signs:before{content:"\f277"}i.icon.mars:before{content:"\f222"}i.icon.mars.double:before{content:"\f227"}i.icon.mars.stroke:before{content:"\f229"}i.icon.mars.stroke.horizontal:before{content:"\f22b"}i.icon.mars.stroke.vertical:before{content:"\f22a"}i.icon.maxcdn:before{content:"\f136"}i.icon.medapps:before{content:"\f3c6"}i.icon.medium:before{content:"\f23a"}i.icon.medium.m:before{content:"\f3c7"}i.icon.medkit:before{content:"\f0fa"}i.icon.medrt:before{content:"\f3c8"}i.icon.meetup:before{content:"\f2e0"}i.icon.meh:before{content:"\f11a"}i.icon.mercury:before{content:"\f223"}i.icon.microchip:before{content:"\f2db"}i.icon.microphone:before{content:"\f130"}i.icon.microphone.slash:before{content:"\f131"}i.icon.microsoft:before{content:"\f3ca"}i.icon.minus:before{content:"\f068"}i.icon.minus.circle:before{content:"\f056"}i.icon.minus.square:before{content:"\f146"}i.icon.mix:before{content:"\f3cb"}i.icon.mixcloud:before{content:"\f289"}i.icon.mizuni:before{content:"\f3cc"}i.icon.mobile:before{content:"\f10b"}i.icon.mobile.alternate:before{content:"\f3cd"}i.icon.modx:before{content:"\f285"}i.icon.monero:before{content:"\f3d0"}i.icon.money.bill.alternate:before{content:"\f3d1"}i.icon.moon:before{content:"\f186"}i.icon.motorcycle:before{content:"\f21c"}i.icon.mouse.pointer:before{content:"\f245"}i.icon.music:before{content:"\f001"}i.icon.napster:before{content:"\f3d2"}i.icon.neuter:before{content:"\f22c"}i.icon.newspaper:before{content:"\f1ea"}i.icon.nintendo.switch:before{content:"\f418"}i.icon.node:before{content:"\f419"}i.icon.node.js:before{content:"\f3d3"}i.icon.npm:before{content:"\f3d4"}i.icon.ns8:before{content:"\f3d5"}i.icon.nutritionix:before{content:"\f3d6"}i.icon.object.group:before{content:"\f247"}i.icon.object.ungroup:before{content:"\f248"}i.icon.odnoklassniki:before{content:"\f263"}i.icon.odnoklassniki.square:before{content:"\f264"}i.icon.opencart:before{content:"\f23d"}i.icon.openid:before{content:"\f19b"}i.icon.opera:before{content:"\f26a"}i.icon.optin.monster:before{content:"\f23c"}i.icon.osi:before{content:"\f41a"}i.icon.outdent:before{content:"\f03b"}i.icon.page4:before{content:"\f3d7"}i.icon.pagelines:before{content:"\f18c"}i.icon.paint.brush:before{content:"\f1fc"}i.icon.palfed:before{content:"\f3d8"}i.icon.pallet:before{content:"\f482"}i.icon.paper.plane:before{content:"\f1d8"}i.icon.paperclip:before{content:"\f0c6"}i.icon.paragraph:before{content:"\f1dd"}i.icon.paste:before{content:"\f0ea"}i.icon.patreon:before{content:"\f3d9"}i.icon.pause:before{content:"\f04c"}i.icon.pause.circle:before{content:"\f28b"}i.icon.paw:before{content:"\f1b0"}i.icon.paypal:before{content:"\f1ed"}i.icon.pen.square:before{content:"\f14b"}i.icon.pencil.alternate:before{content:"\f303"}i.icon.percent:before{content:"\f295"}i.icon.periscope:before{content:"\f3da"}i.icon.phabricator:before{content:"\f3db"}i.icon.phoenix.framework:before{content:"\f3dc"}i.icon.phone:before{content:"\f095"}i.icon.phone.square:before{content:"\f098"}i.icon.phone.volume:before{content:"\f2a0"}i.icon.php:before{content:"\f457"}i.icon.pied.piper:before{content:"\f2ae"}i.icon.pied.piper.alternate:before{content:"\f1a8"}i.icon.pied.piper.pp:before{content:"\f1a7"}i.icon.pills:before{content:"\f484"}i.icon.pinterest:before{content:"\f0d2"}i.icon.pinterest.p:before{content:"\f231"}i.icon.pinterest.square:before{content:"\f0d3"}i.icon.plane:before{content:"\f072"}i.icon.play:before{content:"\f04b"}i.icon.play.circle:before{content:"\f144"}i.icon.playstation:before{content:"\f3df"}i.icon.plug:before{content:"\f1e6"}i.icon.plus:before{content:"\f067"}i.icon.plus.circle:before{content:"\f055"}i.icon.plus.square:before{content:"\f0fe"}i.icon.podcast:before{content:"\f2ce"}i.icon.pound.sign:before{content:"\f154"}i.icon.power.off:before{content:"\f011"}i.icon.print:before{content:"\f02f"}i.icon.product.hunt:before{content:"\f288"}i.icon.pushed:before{content:"\f3e1"}i.icon.puzzle.piece:before{content:"\f12e"}i.icon.python:before{content:"\f3e2"}i.icon.qq:before{content:"\f1d6"}i.icon.qrcode:before{content:"\f029"}i.icon.question:before{content:"\f128"}i.icon.question.circle:before{content:"\f059"}i.icon.quidditch:before{content:"\f458"}i.icon.quinscape:before{content:"\f459"}i.icon.quora:before{content:"\f2c4"}i.icon.quote.left:before{content:"\f10d"}i.icon.quote.right:before{content:"\f10e"}i.icon.random:before{content:"\f074"}i.icon.ravelry:before{content:"\f2d9"}i.icon.react:before{content:"\f41b"}i.icon.rebel:before{content:"\f1d0"}i.icon.recycle:before{content:"\f1b8"}i.icon.redriver:before{content:"\f3e3"}i.icon.reddit:before{content:"\f1a1"}i.icon.reddit.alien:before{content:"\f281"}i.icon.reddit.square:before{content:"\f1a2"}i.icon.redo:before{content:"\f01e"}i.icon.redo.alternate:before{content:"\f2f9"}i.icon.registered:before{content:"\f25d"}i.icon.rendact:before{content:"\f3e4"}i.icon.renren:before{content:"\f18b"}i.icon.reply:before{content:"\f3e5"}i.icon.reply.all:before{content:"\f122"}i.icon.replyd:before{content:"\f3e6"}i.icon.resolving:before{content:"\f3e7"}i.icon.retweet:before{content:"\f079"}i.icon.road:before{content:"\f018"}i.icon.rocket:before{content:"\f135"}i.icon.rocketchat:before{content:"\f3e8"}i.icon.rockrms:before{content:"\f3e9"}i.icon.rss:before{content:"\f09e"}i.icon.rss.square:before{content:"\f143"}i.icon.ruble.sign:before{content:"\f158"}i.icon.rupee.sign:before{content:"\f156"}i.icon.safari:before{content:"\f267"}i.icon.sass:before{content:"\f41e"}i.icon.save:before{content:"\f0c7"}i.icon.schlix:before{content:"\f3ea"}i.icon.scribd:before{content:"\f28a"}i.icon.search:before{content:"\f002"}i.icon.search.minus:before{content:"\f010"}i.icon.search.plus:before{content:"\f00e"}i.icon.searchengin:before{content:"\f3eb"}i.icon.sellcast:before{content:"\f2da"}i.icon.sellsy:before{content:"\f213"}i.icon.server:before{content:"\f233"}i.icon.servicestack:before{content:"\f3ec"}i.icon.share:before{content:"\f064"}i.icon.share.alternate:before{content:"\f1e0"}i.icon.share.alternate.square:before{content:"\f1e1"}i.icon.share.square:before{content:"\f14d"}i.icon.shekel.sign:before{content:"\f20b"}i.icon.shield.alternate:before{content:"\f3ed"}i.icon.ship:before{content:"\f21a"}i.icon.shipping.fast:before{content:"\f48b"}i.icon.shirtsinbulk:before{content:"\f214"}i.icon.shopping.bag:before{content:"\f290"}i.icon.shopping.basket:before{content:"\f291"}i.icon.shopping.cart:before{content:"\f07a"}i.icon.shower:before{content:"\f2cc"}i.icon.sign.language:before{content:"\f2a7"}i.icon.signal:before{content:"\f012"}i.icon.simplybuilt:before{content:"\f215"}i.icon.sistrix:before{content:"\f3ee"}i.icon.sitemap:before{content:"\f0e8"}i.icon.skyatlas:before{content:"\f216"}i.icon.skype:before{content:"\f17e"}i.icon.slack:before{content:"\f198"}i.icon.slack.hash:before{content:"\f3ef"}i.icon.sliders.horizontal:before{content:"\f1de"}i.icon.slideshare:before{content:"\f1e7"}i.icon.smile:before{content:"\f118"}i.icon.snapchat:before{content:"\f2ab"}i.icon.snapchat.ghost:before{content:"\f2ac"}i.icon.snapchat.square:before{content:"\f2ad"}i.icon.snowflake:before{content:"\f2dc"}i.icon.sort:before{content:"\f0dc"}i.icon.sort.alphabet.down:before{content:"\f15d"}i.icon.sort.alphabet.up:before{content:"\f15e"}i.icon.sort.amount.down:before{content:"\f160"}i.icon.sort.amount.up:before{content:"\f161"}i.icon.sort.down:before{content:"\f0dd"}i.icon.sort.numeric.down:before{content:"\f162"}i.icon.sort.numeric.up:before{content:"\f163"}i.icon.sort.up:before{content:"\f0de"}i.icon.soundcloud:before{content:"\f1be"}i.icon.space.shuttle:before{content:"\f197"}i.icon.speakap:before{content:"\f3f3"}i.icon.spinner:before{content:"\f110"}i.icon.spotify:before{content:"\f1bc"}i.icon.square:before{content:"\f0c8"}i.icon.square.full:before{content:"\f45c"}i.icon.stack.exchange:before{content:"\f18d"}i.icon.stack.overflow:before{content:"\f16c"}i.icon.star:before{content:"\f005"}i.icon.star.half:before{content:"\f089"}i.icon.staylinked:before{content:"\f3f5"}i.icon.steam:before{content:"\f1b6"}i.icon.steam.square:before{content:"\f1b7"}i.icon.steam.symbol:before{content:"\f3f6"}i.icon.step.backward:before{content:"\f048"}i.icon.step.forward:before{content:"\f051"}i.icon.stethoscope:before{content:"\f0f1"}i.icon.sticker.mule:before{content:"\f3f7"}i.icon.sticky.note:before{content:"\f249"}i.icon.stop:before{content:"\f04d"}i.icon.stop.circle:before{content:"\f28d"}i.icon.stopwatch:before{content:"\f2f2"}i.icon.strava:before{content:"\f428"}i.icon.street.view:before{content:"\f21d"}i.icon.strikethrough:before{content:"\f0cc"}i.icon.stripe:before{content:"\f429"}i.icon.stripe.s:before{content:"\f42a"}i.icon.studiovinari:before{content:"\f3f8"}i.icon.stumbleupon:before{content:"\f1a4"}i.icon.stumbleupon.circle:before{content:"\f1a3"}i.icon.subscript:before{content:"\f12c"}i.icon.subway:before{content:"\f239"}i.icon.suitcase:before{content:"\f0f2"}i.icon.sun:before{content:"\f185"}i.icon.superpowers:before{content:"\f2dd"}i.icon.superscript:before{content:"\f12b"}i.icon.supple:before{content:"\f3f9"}i.icon.sync:before{content:"\f021"}i.icon.sync.alternate:before{content:"\f2f1"}i.icon.syringe:before{content:"\f48e"}i.icon.table:before{content:"\f0ce"}i.icon.table.tennis:before{content:"\f45d"}i.icon.tablet:before{content:"\f10a"}i.icon.tablet.alternate:before{content:"\f3fa"}i.icon.tachometer.alternate:before{content:"\f3fd"}i.icon.tag:before{content:"\f02b"}i.icon.tags:before{content:"\f02c"}i.icon.tasks:before{content:"\f0ae"}i.icon.taxi:before{content:"\f1ba"}i.icon.telegram:before{content:"\f2c6"}i.icon.telegram.plane:before{content:"\f3fe"}i.icon.tencent.weibo:before{content:"\f1d5"}i.icon.terminal:before{content:"\f120"}i.icon.text.height:before{content:"\f034"}i.icon.text.width:before{content:"\f035"}i.icon.th:before{content:"\f00a"}i.icon.th.large:before{content:"\f009"}i.icon.th.list:before{content:"\f00b"}i.icon.themeisle:before{content:"\f2b2"}i.icon.thermometer:before{content:"\f491"}i.icon.thermometer.empty:before{content:"\f2cb"}i.icon.thermometer.full:before{content:"\f2c7"}i.icon.thermometer.half:before{content:"\f2c9"}i.icon.thermometer.quarter:before{content:"\f2ca"}i.icon.thermometer.three.quarters:before{content:"\f2c8"}i.icon.thumbs.down:before{content:"\f165"}i.icon.thumbs.up:before{content:"\f164"}i.icon.thumbtack:before{content:"\f08d"}i.icon.ticket.alternate:before{content:"\f3ff"}i.icon.times:before{content:"\f00d"}i.icon.times.circle:before{content:"\f057"}i.icon.tint:before{content:"\f043"}i.icon.toggle.off:before{content:"\f204"}i.icon.toggle.on:before{content:"\f205"}i.icon.trademark:before{content:"\f25c"}i.icon.train:before{content:"\f238"}i.icon.transgender:before{content:"\f224"}i.icon.transgender.alternate:before{content:"\f225"}i.icon.trash:before{content:"\f1f8"}i.icon.trash.alternate:before{content:"\f2ed"}i.icon.tree:before{content:"\f1bb"}i.icon.trello:before{content:"\f181"}i.icon.tripadvisor:before{content:"\f262"}i.icon.trophy:before{content:"\f091"}i.icon.truck:before{content:"\f0d1"}i.icon.tty:before{content:"\f1e4"}i.icon.tumblr:before{content:"\f173"}i.icon.tumblr.square:before{content:"\f174"}i.icon.tv:before{content:"\f26c"}i.icon.twitch:before{content:"\f1e8"}i.icon.twitter:before{content:"\f099"}i.icon.twitter.square:before{content:"\f081"}i.icon.typo3:before{content:"\f42b"}i.icon.uber:before{content:"\f402"}i.icon.uikit:before{content:"\f403"}i.icon.umbrella:before{content:"\f0e9"}i.icon.underline:before{content:"\f0cd"}i.icon.undo:before{content:"\f0e2"}i.icon.undo.alternate:before{content:"\f2ea"}i.icon.uniregistry:before{content:"\f404"}i.icon.universal.access:before{content:"\f29a"}i.icon.university:before{content:"\f19c"}i.icon.unlink:before{content:"\f127"}i.icon.unlock:before{content:"\f09c"}i.icon.unlock.alternate:before{content:"\f13e"}i.icon.untappd:before{content:"\f405"}i.icon.upload:before{content:"\f093"}i.icon.usb:before{content:"\f287"}i.icon.user:before{content:"\f007"}i.icon.user.circle:before{content:"\f2bd"}i.icon.user.md:before{content:"\f0f0"}i.icon.user.plus:before{content:"\f234"}i.icon.user.secret:before{content:"\f21b"}i.icon.user.times:before{content:"\f235"}i.icon.users:before{content:"\f0c0"}i.icon.ussunnah:before{content:"\f407"}i.icon.utensil.spoon:before{content:"\f2e5"}i.icon.utensils:before{content:"\f2e7"}i.icon.vaadin:before{content:"\f408"}i.icon.venus:before{content:"\f221"}i.icon.venus.double:before{content:"\f226"}i.icon.venus.mars:before{content:"\f228"}i.icon.viacoin:before{content:"\f237"}i.icon.viadeo:before{content:"\f2a9"}i.icon.viadeo.square:before{content:"\f2aa"}i.icon.viber:before{content:"\f409"}i.icon.video:before{content:"\f03d"}i.icon.vimeo:before{content:"\f40a"}i.icon.vimeo.square:before{content:"\f194"}i.icon.vimeo.v:before{content:"\f27d"}i.icon.vine:before{content:"\f1ca"}i.icon.vk:before{content:"\f189"}i.icon.vnv:before{content:"\f40b"}i.icon.volleyball.ball:before{content:"\f45f"}i.icon.volume.down:before{content:"\f027"}i.icon.volume.off:before{content:"\f026"}i.icon.volume.up:before{content:"\f028"}i.icon.vuejs:before{content:"\f41f"}i.icon.warehouse:before{content:"\f494"}i.icon.weibo:before{content:"\f18a"}i.icon.weight:before{content:"\f496"}i.icon.weixin:before{content:"\f1d7"}i.icon.whatsapp:before{content:"\f232"}i.icon.whatsapp.square:before{content:"\f40c"}i.icon.wheelchair:before{content:"\f193"}i.icon.whmcs:before{content:"\f40d"}i.icon.wifi:before{content:"\f1eb"}i.icon.wikipedia.w:before{content:"\f266"}i.icon.window.close:before{content:"\f410"}i.icon.window.maximize:before{content:"\f2d0"}i.icon.window.minimize:before{content:"\f2d1"}i.icon.window.restore:before{content:"\f2d2"}i.icon.windows:before{content:"\f17a"}i.icon.won.sign:before{content:"\f159"}i.icon.wordpress:before{content:"\f19a"}i.icon.wordpress.simple:before{content:"\f411"}i.icon.wpbeginner:before{content:"\f297"}i.icon.wpexplorer:before{content:"\f2de"}i.icon.wpforms:before{content:"\f298"}i.icon.wrench:before{content:"\f0ad"}i.icon.xbox:before{content:"\f412"}i.icon.xing:before{content:"\f168"}i.icon.xing.square:before{content:"\f169"}i.icon.y.combinator:before{content:"\f23b"}i.icon.yahoo:before{content:"\f19e"}i.icon.yandex:before{content:"\f413"}i.icon.yandex.international:before{content:"\f414"}i.icon.yelp:before{content:"\f1e9"}i.icon.yen.sign:before{content:"\f157"}i.icon.yoast:before{content:"\f2b1"}i.icon.youtube:before{content:"\f167"}i.icon.youtube.square:before{content:"\f431"}i.icon.chess.rock:before{content:"\f447"}i.icon.ordered.list:before{content:"\f0cb"}i.icon.unordered.list:before{content:"\f0ca"}i.icon.user.doctor:before{content:"\f0f0"}i.icon.shield:before{content:"\f3ed"}i.icon.puzzle:before{content:"\f12e"}i.icon.credit.card.amazon.pay:before{content:"\f42d"}i.icon.credit.card.american.express:before{content:"\f1f3"}i.icon.credit.card.diners.club:before{content:"\f24c"}i.icon.credit.card.discover:before{content:"\f1f2"}i.icon.credit.card.jcb:before{content:"\f24b"}i.icon.credit.card.mastercard:before{content:"\f1f1"}i.icon.credit.card.paypal:before{content:"\f1f4"}i.icon.credit.card.stripe:before{content:"\f1f5"}i.icon.credit.card.visa:before{content:"\f1f0"}i.icon.add.circle:before{content:"\f055"}i.icon.add.square:before{content:"\f0fe"}i.icon.add.to.calendar:before{content:"\f271"}i.icon.add.to.cart:before{content:"\f217"}i.icon.add.user:before{content:"\f234"}i.icon.add:before{content:"\f067"}i.icon.alarm.mute:before{content:"\f1f6"}i.icon.alarm:before{content:"\f0f3"}i.icon.ald:before{content:"\f2a2"}i.icon.als:before{content:"\f2a2"}i.icon.american.express.card:before{content:"\f1f3"}i.icon.american.express:before{content:"\f1f3"}i.icon.amex:before{content:"\f1f3"}i.icon.announcement:before{content:"\f0a1"}i.icon.area.chart:before{content:"\f1fe"}i.icon.area.graph:before{content:"\f1fe"}i.icon.arrow.down.cart:before{content:"\f218"}i.icon.asexual:before{content:"\f22d"}i.icon.asl.interpreting:before{content:"\f2a3"}i.icon.asl:before{content:"\f2a3"}i.icon.assistive.listening.devices:before{content:"\f2a2"}i.icon.attach:before{content:"\f0c6"}i.icon.attention:before{content:"\f06a"}i.icon.balance:before{content:"\f24e"}i.icon.bar:before{content:"\f0fc"}i.icon.bathtub:before{content:"\f2cd"}i.icon.battery.four:before{content:"\f240"}i.icon.battery.high:before{content:"\f241"}i.icon.battery.low:before{content:"\f243"}i.icon.battery.medium:before{content:"\f242"}i.icon.battery.one:before{content:"\f243"}i.icon.battery.three:before{content:"\f241"}i.icon.battery.two:before{content:"\f242"}i.icon.battery.zero:before{content:"\f244"}i.icon.birthday:before{content:"\f1fd"}i.icon.block.layout:before{content:"\f009"}i.icon.bluetooth.alternative:before{content:"\f294"}i.icon.broken.chain:before{content:"\f127"}i.icon.browser:before{content:"\f022"}i.icon.call.square:before{content:"\f098"}i.icon.call:before{content:"\f095"}i.icon.cancel:before{content:"\f00d"}i.icon.cart:before{content:"\f07a"}i.icon.cc:before{content:"\f20a"}i.icon.chain:before{content:"\f0c1"}i.icon.chat:before{content:"\f075"}i.icon.checked.calendar:before{content:"\f274"}i.icon.checkmark:before{content:"\f00c"}i.icon.circle.notched:before{content:"\f1ce"}i.icon.close:before{content:"\f00d"}i.icon.cny:before{content:"\f157"}i.icon.cocktail:before{content:"\f000"}i.icon.commenting:before{content:"\f27a"}i.icon.computer:before{content:"\f108"}i.icon.configure:before{content:"\f0ad"}i.icon.content:before{content:"\f0c9"}i.icon.deafness:before{content:"\f2a4"}i.icon.delete.calendar:before{content:"\f273"}i.icon.delete:before{content:"\f00d"}i.icon.detective:before{content:"\f21b"}i.icon.diners.club.card:before{content:"\f24c"}i.icon.diners.club:before{content:"\f24c"}i.icon.discover.card:before{content:"\f1f2"}i.icon.discover:before{content:"\f1f2"}i.icon.discussions:before{content:"\f086"}i.icon.doctor:before{content:"\f0f0"}i.icon.dollar:before{content:"\f155"}i.icon.dont:before{content:"\f05e"}i.icon.dribble:before{content:"\f17d"}i.icon.drivers.license:before{content:"\f2c2"}i.icon.dropdown:before{content:"\f0d7"}i.icon.eercast:before{content:"\f2da"}i.icon.emergency:before{content:"\f0f9"}i.icon.envira.gallery:before{content:"\f299"}i.icon.erase:before{content:"\f12d"}i.icon.eur:before{content:"\f153"}i.icon.euro:before{content:"\f153"}i.icon.eyedropper:before{content:"\f1fb"}i.icon.fa:before{content:"\f2b4"}i.icon.factory:before{content:"\f275"}i.icon.favorite:before{content:"\f005"}i.icon.feed:before{content:"\f09e"}i.icon.female.homosexual:before{content:"\f226"}i.icon.file.text:before{content:"\f15c"}i.icon.find:before{content:"\f1e5"}i.icon.first.aid:before{content:"\f0fa"}i.icon.five.hundred.pixels:before{content:"\f26e"}i.icon.fork:before{content:"\f126"}i.icon.game:before{content:"\f11b"}i.icon.gay:before{content:"\f227"}i.icon.gbp:before{content:"\f154"}i.icon.gittip:before{content:"\f184"}i.icon.google.plus.circle:before{content:"\f2b3"}i.icon.google.plus.official:before{content:"\f2b3"}i.icon.grab:before{content:"\f255"}i.icon.graduation:before{content:"\f19d"}i.icon.grid.layout:before{content:"\f00a"}i.icon.group:before{content:"\f0c0"}i.icon.h:before{content:"\f0fd"}i.icon.hand.victory:before{content:"\f25b"}i.icon.handicap:before{content:"\f193"}i.icon.hard.of.hearing:before{content:"\f2a4"}i.icon.header:before{content:"\f1dc"}i.icon.help.circle:before{content:"\f059"}i.icon.help:before{content:"\f128"}i.icon.heterosexual:before{content:"\f228"}i.icon.hide:before{content:"\f070"}i.icon.hotel:before{content:"\f236"}i.icon.hourglass.four:before{content:"\f254"}i.icon.hourglass.full:before{content:"\f254"}i.icon.hourglass.one:before{content:"\f251"}i.icon.hourglass.three:before{content:"\f253"}i.icon.hourglass.two:before{content:"\f252"}i.icon.idea:before{content:"\f0eb"}i.icon.ils:before{content:"\f20b"}i.icon.in-cart:before{content:"\f218"}i.icon.inr:before{content:"\f156"}i.icon.intergender:before{content:"\f224"}i.icon.intersex:before{content:"\f224"}i.icon.japan.credit.bureau.card:before{content:"\f24b"}i.icon.japan.credit.bureau:before{content:"\f24b"}i.icon.jcb:before{content:"\f24b"}i.icon.jpy:before{content:"\f157"}i.icon.krw:before{content:"\f159"}i.icon.lab:before{content:"\f0c3"}i.icon.law:before{content:"\f24e"}i.icon.legal:before{content:"\f0e3"}i.icon.lesbian:before{content:"\f226"}i.icon.lightning:before{content:"\f0e7"}i.icon.like:before{content:"\f004"}i.icon.line.graph:before{content:"\f201"}i.icon.linkedin.square:before{content:"\f08c"}i.icon.linkify:before{content:"\f0c1"}i.icon.lira:before{content:"\f195"}i.icon.list.layout:before{content:"\f00b"}i.icon.magnify:before{content:"\f00e"}i.icon.mail.forward:before{content:"\f064"}i.icon.mail.square:before{content:"\f199"}i.icon.mail:before{content:"\f0e0"}i.icon.male.homosexual:before{content:"\f227"}i.icon.man:before{content:"\f222"}i.icon.marker:before{content:"\f041"}i.icon.mars.alternate:before{content:"\f229"}i.icon.mars.horizontal:before{content:"\f22b"}i.icon.mars.vertical:before{content:"\f22a"}i.icon.mastercard.card:before{content:"\f1f1"}i.icon.mastercard:before{content:"\f1f1"}i.icon.microsoft.edge:before{content:"\f282"}i.icon.military:before{content:"\f0fb"}i.icon.ms.edge:before{content:"\f282"}i.icon.mute:before{content:"\f131"}i.icon.new.pied.piper:before{content:"\f2ae"}i.icon.non.binary.transgender:before{content:"\f223"}i.icon.numbered.list:before{content:"\f0cb"}i.icon.optinmonster:before{content:"\f23c"}i.icon.options:before{content:"\f1de"}i.icon.other.gender.horizontal:before{content:"\f22b"}i.icon.other.gender.vertical:before{content:"\f22a"}i.icon.other.gender:before{content:"\f229"}i.icon.payment:before{content:"\f09d"}i.icon.paypal.card:before{content:"\f1f4"}i.icon.pencil.square:before{content:"\f14b"}i.icon.photo:before{content:"\f030"}i.icon.picture:before{content:"\f03e"}i.icon.pie.chart:before{content:"\f200"}i.icon.pie.graph:before{content:"\f200"}i.icon.pied.piper.hat:before{content:"\f2ae"}i.icon.pin:before{content:"\f08d"}i.icon.plus.cart:before{content:"\f217"}i.icon.pocket:before{content:"\f265"}i.icon.point:before{content:"\f041"}i.icon.pointing.down:before{content:"\f0a7"}i.icon.pointing.left:before{content:"\f0a5"}i.icon.pointing.right:before{content:"\f0a4"}i.icon.pointing.up:before{content:"\f0a6"}i.icon.pound:before{content:"\f154"}i.icon.power.cord:before{content:"\f1e6"}i.icon.power:before{content:"\f011"}i.icon.privacy:before{content:"\f084"}i.icon.r.circle:before{content:"\f25d"}i.icon.rain:before{content:"\f0e9"}i.icon.record:before{content:"\f03d"}i.icon.refresh:before{content:"\f021"}i.icon.remove.circle:before{content:"\f057"}i.icon.remove.from.calendar:before{content:"\f272"}i.icon.remove.user:before{content:"\f235"}i.icon.remove:before{content:"\f00d"}i.icon.repeat:before{content:"\f01e"}i.icon.rmb:before{content:"\f157"}i.icon.rouble:before{content:"\f158"}i.icon.rub:before{content:"\f158"}i.icon.ruble:before{content:"\f158"}i.icon.rupee:before{content:"\f156"}i.icon.s15:before{content:"\f2cd"}i.icon.selected.radio:before{content:"\f192"}i.icon.send:before{content:"\f1d8"}i.icon.setting:before{content:"\f013"}i.icon.settings:before{content:"\f085"}i.icon.shekel:before{content:"\f20b"}i.icon.sheqel:before{content:"\f20b"}i.icon.shipping:before{content:"\f0d1"}i.icon.shop:before{content:"\f07a"}i.icon.shuffle:before{content:"\f074"}i.icon.shutdown:before{content:"\f011"}i.icon.sidebar:before{content:"\f0c9"}i.icon.signing:before{content:"\f2a7"}i.icon.signup:before{content:"\f044"}i.icon.sliders:before{content:"\f1de"}i.icon.soccer:before{content:"\f1e3"}i.icon.sort.alphabet.ascending:before{content:"\f15d"}i.icon.sort.alphabet.descending:before{content:"\f15e"}i.icon.sort.ascending:before{content:"\f0de"}i.icon.sort.content.ascending:before{content:"\f160"}i.icon.sort.content.descending:before{content:"\f161"}i.icon.sort.descending:before{content:"\f0dd"}i.icon.sort.numeric.ascending:before{content:"\f162"}i.icon.sort.numeric.descending:before{content:"\f163"}i.icon.sound:before{content:"\f025"}i.icon.spy:before{content:"\f21b"}i.icon.stripe.card:before{content:"\f1f5"}i.icon.student:before{content:"\f19d"}i.icon.talk:before{content:"\f27a"}i.icon.target:before{content:"\f140"}i.icon.teletype:before{content:"\f1e4"}i.icon.television:before{content:"\f26c"}i.icon.text.cursor:before{content:"\f246"}i.icon.text.telephone:before{content:"\f1e4"}i.icon.theme.isle:before{content:"\f2b2"}i.icon.theme:before{content:"\f043"}i.icon.thermometer:before{content:"\f2c7"}i.icon.thumb.tack:before{content:"\f08d"}i.icon.time:before{content:"\f017"}i.icon.tm:before{content:"\f25c"}i.icon.toggle.down:before{content:"\f150"}i.icon.toggle.left:before{content:"\f191"}i.icon.toggle.right:before{content:"\f152"}i.icon.toggle.up:before{content:"\f151"}i.icon.translate:before{content:"\f1ab"}i.icon.travel:before{content:"\f0b1"}i.icon.treatment:before{content:"\f0f1"}i.icon.triangle.down:before{content:"\f0d7"}i.icon.triangle.left:before{content:"\f0d9"}i.icon.triangle.right:before{content:"\f0da"}i.icon.triangle.up:before{content:"\f0d8"}i.icon.try:before{content:"\f195"}i.icon.unhide:before{content:"\f06e"}i.icon.unlinkify:before{content:"\f127"}i.icon.unmute:before{content:"\f130"}i.icon.usd:before{content:"\f155"}i.icon.user.cancel:before{content:"\f235"}i.icon.user.close:before{content:"\f235"}i.icon.user.delete:before{content:"\f235"}i.icon.user.x:before{content:"\f235"}i.icon.vcard:before{content:"\f2bb"}i.icon.video.camera:before{content:"\f03d"}i.icon.video.play:before{content:"\f144"}i.icon.visa.card:before{content:"\f1f0"}i.icon.visa:before{content:"\f1f0"}i.icon.volume.control.phone:before{content:"\f2a0"}i.icon.wait:before{content:"\f017"}i.icon.warning.circle:before{content:"\f06a"}i.icon.warning.sign:before{content:"\f071"}i.icon.warning:before{content:"\f12a"}i.icon.wechat:before{content:"\f1d7"}i.icon.wi-fi:before{content:"\f1eb"}i.icon.wikipedia:before{content:"\f266"}i.icon.winner:before{content:"\f091"}i.icon.wizard:before{content:"\f0d0"}i.icon.woman:before{content:"\f221"}i.icon.won:before{content:"\f159"}i.icon.wordpress.beginner:before{content:"\f297"}i.icon.wordpress.forms:before{content:"\f298"}i.icon.world:before{content:"\f0ac"}i.icon.write.square:before{content:"\f14b"}i.icon.x:before{content:"\f00d"}i.icon.yc:before{content:"\f23b"}i.icon.ycombinator:before{content:"\f23b"}i.icon.yen:before{content:"\f157"}i.icon.zip:before{content:"\f187"}i.icon.zoom-in:before{content:"\f00e"}i.icon.zoom-out:before{content:"\f010"}i.icon.zoom:before{content:"\f00e"}i.icon.bitbucket.square:before{content:"\f171"}i.icon.checkmark.box:before{content:"\f14a"}i.icon.circle.thin:before{content:"\f111"}i.icon.cloud.download:before{content:"\f381"}i.icon.cloud.upload:before{content:"\f382"}i.icon.compose:before{content:"\f303"}i.icon.conversation:before{content:"\f086"}i.icon.credit.card.alternative:before{content:"\f09d"}i.icon.currency:before{content:"\f3d1"}i.icon.dashboard:before{content:"\f3fd"}i.icon.diamond:before{content:"\f3a5"}i.icon.disk:before{content:"\f0a0"}i.icon.exchange:before{content:"\f362"}i.icon.external.share:before{content:"\f14d"}i.icon.external.square:before{content:"\f360"}i.icon.external:before{content:"\f35d"}i.icon.facebook.official:before{content:"\f082"}i.icon.food:before{content:"\f2e7"}i.icon.hourglass.zero:before{content:"\f253"}i.icon.level.down:before{content:"\f3be"}i.icon.level.up:before{content:"\f3bf"}i.icon.logout:before{content:"\f2f5"}i.icon.meanpath:before{content:"\f0c8"}i.icon.money:before{content:"\f3d1"}i.icon.move:before{content:"\f0b2"}i.icon.pencil:before{content:"\f303"}i.icon.protect:before{content:"\f023"}i.icon.radio:before{content:"\f192"}i.icon.remove.bookmark:before{content:"\f02e"}i.icon.resize.horizontal:before{content:"\f337"}i.icon.resize.vertical:before{content:"\f338"}i.icon.sign-in:before{content:"\f2f6"}i.icon.sign-out:before{content:"\f2f5"}i.icon.spoon:before{content:"\f2e5"}i.icon.star.half.empty:before{content:"\f089"}i.icon.star.half.full:before{content:"\f089"}i.icon.ticket:before{content:"\f3ff"}i.icon.times.rectangle:before{content:"\f410"}i.icon.write:before{content:"\f303"}i.icon.youtube.play:before{content:"\f167"}@font-face{font-family:outline-icons;src:url(themes/default/assets/fonts/outline-icons.eot);src:url(themes/default/assets/fonts/outline-icons.eot?#iefix) format('embedded-opentype'),url(themes/default/assets/fonts/outline-icons.woff2) format('woff2'),url(themes/default/assets/fonts/outline-icons.woff) format('woff'),url(themes/default/assets/fonts/outline-icons.ttf) format('truetype'),url(themes/default/assets/fonts/outline-icons.svg#icons) format('svg');font-style:normal;font-weight:400;font-variant:normal;text-decoration:inherit;text-transform:none}i.icon.outline{font-family:outline-icons}i.icon.address.book.outline:before{content:"\f2b9"}i.icon.address.card.outline:before{content:"\f2bb"}i.icon.arrow.alternate.circle.down.outline:before{content:"\f358"}i.icon.arrow.alternate.circle.left.outline:before{content:"\f359"}i.icon.arrow.alternate.circle.right.outline:before{content:"\f35a"}i.icon.arrow.alternate.circle.up.outline:before{content:"\f35b"}i.icon.bell.outline:before{content:"\f0f3"}i.icon.bell.slash.outline:before{content:"\f1f6"}i.icon.bookmark.outline:before{content:"\f02e"}i.icon.building.outline:before{content:"\f1ad"}i.icon.calendar.outline:before{content:"\f133"}i.icon.calendar.alternate.outline:before{content:"\f073"}i.icon.calendar.check.outline:before{content:"\f274"}i.icon.calendar.minus.outline:before{content:"\f272"}i.icon.calendar.plus.outline:before{content:"\f271"}i.icon.calendar.times.outline:before{content:"\f273"}i.icon.caret.square.down.outline:before{content:"\f150"}i.icon.caret.square.left.outline:before{content:"\f191"}i.icon.caret.square.right.outline:before{content:"\f152"}i.icon.caret.square.up.outline:before{content:"\f151"}i.icon.chart.bar.outline:before{content:"\f080"}i.icon.check.circle.outline:before{content:"\f058"}i.icon.check.square.outline:before{content:"\f14a"}i.icon.circle.outline:before{content:"\f111"}i.icon.clipboard.outline:before{content:"\f328"}i.icon.clock.outline:before{content:"\f017"}i.icon.clone.outline:before{content:"\f24d"}i.icon.closed.captioning.outline:before{content:"\f20a"}i.icon.comment.outline:before{content:"\f075"}i.icon.comment.alternate.outline:before{content:"\f27a"}i.icon.comments.outline:before{content:"\f086"}i.icon.compass.outline:before{content:"\f14e"}i.icon.copy.outline:before{content:"\f0c5"}i.icon.copyright.outline:before{content:"\f1f9"}i.icon.credit.card.outline:before{content:"\f09d"}i.icon.dot.circle.outline:before{content:"\f192"}i.icon.edit.outline:before{content:"\f044"}i.icon.envelope.outline:before{content:"\f0e0"}i.icon.envelope.open.outline:before{content:"\f2b6"}i.icon.eye.slash.outline:before{content:"\f070"}i.icon.file.outline:before{content:"\f15b"}i.icon.file.alternate.outline:before{content:"\f15c"}i.icon.file.archive.outline:before{content:"\f1c6"}i.icon.file.audio.outline:before{content:"\f1c7"}i.icon.file.code.outline:before{content:"\f1c9"}i.icon.file.excel.outline:before{content:"\f1c3"}i.icon.file.image.outline:before{content:"\f1c5"}i.icon.file.pdf.outline:before{content:"\f1c1"}i.icon.file.powerpoint.outline:before{content:"\f1c4"}i.icon.file.video.outline:before{content:"\f1c8"}i.icon.file.word.outline:before{content:"\f1c2"}i.icon.flag.outline:before{content:"\f024"}i.icon.folder.outline:before{content:"\f07b"}i.icon.folder.open.outline:before{content:"\f07c"}i.icon.frown.outline:before{content:"\f119"}i.icon.futbol.outline:before{content:"\f1e3"}i.icon.gem.outline:before{content:"\f3a5"}i.icon.hand.lizard.outline:before{content:"\f258"}i.icon.hand.paper.outline:before{content:"\f256"}i.icon.hand.peace.outline:before{content:"\f25b"}i.icon.hand.point.down.outline:before{content:"\f0a7"}i.icon.hand.point.left.outline:before{content:"\f0a5"}i.icon.hand.point.right.outline:before{content:"\f0a4"}i.icon.hand.point.up.outline:before{content:"\f0a6"}i.icon.hand.pointer.outline:before{content:"\f25a"}i.icon.hand.rock.outline:before{content:"\f255"}i.icon.hand.scissors.outline:before{content:"\f257"}i.icon.hand.spock.outline:before{content:"\f259"}i.icon.handshake.outline:before{content:"\f2b5"}i.icon.hdd.outline:before{content:"\f0a0"}i.icon.heart.outline:before{content:"\f004"}i.icon.hospital.outline:before{content:"\f0f8"}i.icon.hourglass.outline:before{content:"\f254"}i.icon.id.badge.outline:before{content:"\f2c1"}i.icon.id.card.outline:before{content:"\f2c2"}i.icon.image.outline:before{content:"\f03e"}i.icon.images.outline:before{content:"\f302"}i.icon.keyboard.outline:before{content:"\f11c"}i.icon.lemon.outline:before{content:"\f094"}i.icon.life.ring.outline:before{content:"\f1cd"}i.icon.lightbulb.outline:before{content:"\f0eb"}i.icon.list.alternate.outline:before{content:"\f022"}i.icon.map.outline:before{content:"\f279"}i.icon.meh.outline:before{content:"\f11a"}i.icon.minus.square.outline:before{content:"\f146"}i.icon.money.bill.alternate.outline:before{content:"\f3d1"}i.icon.moon.outline:before{content:"\f186"}i.icon.newspaper.outline:before{content:"\f1ea"}i.icon.object.group.outline:before{content:"\f247"}i.icon.object.ungroup.outline:before{content:"\f248"}i.icon.paper.plane.outline:before{content:"\f1d8"}i.icon.pause.circle.outline:before{content:"\f28b"}i.icon.play.circle.outline:before{content:"\f144"}i.icon.plus.square.outline:before{content:"\f0fe"}i.icon.question.circle.outline:before{content:"\f059"}i.icon.registered.outline:before{content:"\f25d"}i.icon.save.outline:before{content:"\f0c7"}i.icon.share.square.outline:before{content:"\f14d"}i.icon.smile.outline:before{content:"\f118"}i.icon.snowflake.outline:before{content:"\f2dc"}i.icon.square.outline:before{content:"\f0c8"}i.icon.star.outline:before{content:"\f005"}i.icon.star.half.outline:before{content:"\f089"}i.icon.sticky.note.outline:before{content:"\f249"}i.icon.stop.circle.outline:before{content:"\f28d"}i.icon.sun.outline:before{content:"\f185"}i.icon.thumbs.down.outline:before{content:"\f165"}i.icon.thumbs.up.outline:before{content:"\f164"}i.icon.times.circle.outline:before{content:"\f057"}i.icon.trash.alternate.outline:before{content:"\f2ed"}i.icon.user.outline:before{content:"\f007"}i.icon.user.circle.outline:before{content:"\f2bd"}i.icon.window.close.outline:before{content:"\f410"}i.icon.window.maximize.outline:before{content:"\f2d0"}i.icon.window.minimize.outline:before{content:"\f2d1"}i.icon.window.restore.outline:before{content:"\f2d2"}i.icon.disk.outline:before{content:"\f0a0"}i.icon.heart.empty,i.icon.star.empty{font-family:outline-icons}i.icon.heart.empty:before{content:"\f004"}i.icon.star.empty:before{content:"\f089"}@font-face{font-family:brand-icons;src:url(themes/default/assets/fonts/brand-icons.eot);src:url(themes/default/assets/fonts/brand-icons.eot?#iefix) format('embedded-opentype'),url(themes/default/assets/fonts/brand-icons.woff2) format('woff2'),url(themes/default/assets/fonts/brand-icons.woff) format('woff'),url(themes/default/assets/fonts/brand-icons.ttf) format('truetype'),url(themes/default/assets/fonts/brand-icons.svg#icons) format('svg');font-style:normal;font-weight:400;font-variant:normal;text-decoration:inherit;text-transform:none}i.icon.\35 00px,i.icon.accessible.icon,i.icon.accusoft,i.icon.adn,i.icon.adversal,i.icon.affiliatetheme,i.icon.algolia,i.icon.amazon,i.icon.amazon.pay,i.icon.amilia,i.icon.android,i.icon.angellist,i.icon.angrycreative,i.icon.angular,i.icon.app.store,i.icon.app.store.ios,i.icon.apper,i.icon.apple,i.icon.apple.pay,i.icon.asymmetrik,i.icon.audible,i.icon.autoprefixer,i.icon.avianex,i.icon.aviato,i.icon.aws,i.icon.bandcamp,i.icon.behance,i.icon.behance.square,i.icon.bimobject,i.icon.bitbucket,i.icon.bitcoin,i.icon.bity,i.icon.black.tie,i.icon.blackberry,i.icon.blogger,i.icon.blogger.b,i.icon.bluetooth,i.icon.bluetooth.b,i.icon.btc,i.icon.buromobelexperte,i.icon.buysellads,i.icon.cc.amazon.pay,i.icon.cc.amex,i.icon.cc.apple.pay,i.icon.cc.diners.club,i.icon.cc.discover,i.icon.cc.jcb,i.icon.cc.mastercard,i.icon.cc.paypal,i.icon.cc.stripe,i.icon.cc.visa,i.icon.centercode,i.icon.chrome,i.icon.cloudscale,i.icon.cloudsmith,i.icon.cloudversify,i.icon.codepen,i.icon.codiepie,i.icon.connectdevelop,i.icon.contao,i.icon.cpanel,i.icon.creative.commons,i.icon.css3,i.icon.css3.alternate,i.icon.cuttlefish,i.icon.d.and.d,i.icon.dashcube,i.icon.delicious,i.icon.deploydog,i.icon.deskpro,i.icon.deviantart,i.icon.digg,i.icon.digital.ocean,i.icon.discord,i.icon.discourse,i.icon.dochub,i.icon.docker,i.icon.draft2digital,i.icon.dribbble,i.icon.dribbble.square,i.icon.dropbox,i.icon.drupal,i.icon.dyalog,i.icon.earlybirds,i.icon.edge,i.icon.elementor,i.icon.ember,i.icon.empire,i.icon.envira,i.icon.erlang,i.icon.ethereum,i.icon.etsy,i.icon.expeditedssl,i.icon.facebook,i.icon.facebook.f,i.icon.facebook.messenger,i.icon.facebook.square,i.icon.firefox,i.icon.first.order,i.icon.firstdraft,i.icon.flickr,i.icon.flipboard,i.icon.fly,i.icon.font.awesome,i.icon.font.awesome.alternate,i.icon.font.awesome.flag,i.icon.fonticons,i.icon.fonticons.fi,i.icon.fort.awesome,i.icon.fort.awesome.alternate,i.icon.forumbee,i.icon.foursquare,i.icon.free.code.camp,i.icon.freebsd,i.icon.get.pocket,i.icon.gg,i.icon.gg.circle,i.icon.git,i.icon.git.square,i.icon.github,i.icon.github.alternate,i.icon.github.square,i.icon.gitkraken,i.icon.gitlab,i.icon.gitter,i.icon.glide,i.icon.glide.g,i.icon.gofore,i.icon.goodreads,i.icon.goodreads.g,i.icon.google,i.icon.google.drive,i.icon.google.play,i.icon.google.plus,i.icon.google.plus.g,i.icon.google.plus.square,i.icon.google.wallet,i.icon.gratipay,i.icon.grav,i.icon.gripfire,i.icon.grunt,i.icon.gulp,i.icon.hacker.news,i.icon.hacker.news.square,i.icon.hips,i.icon.hire.a.helper,i.icon.hooli,i.icon.hotjar,i.icon.houzz,i.icon.html5,i.icon.hubspot,i.icon.imdb,i.icon.instagram,i.icon.internet.explorer,i.icon.ioxhost,i.icon.itunes,i.icon.itunes.note,i.icon.jenkins,i.icon.joget,i.icon.joomla,i.icon.js,i.icon.js.square,i.icon.jsfiddle,i.icon.keycdn,i.icon.kickstarter,i.icon.kickstarter.k,i.icon.korvue,i.icon.laravel,i.icon.lastfm,i.icon.lastfm.square,i.icon.leanpub,i.icon.less,i.icon.linechat,i.icon.linkedin,i.icon.linkedin.alternate,i.icon.linkedin.in,i.icon.linode,i.icon.linux,i.icon.lyft,i.icon.magento,i.icon.maxcdn,i.icon.medapps,i.icon.medium,i.icon.medium.m,i.icon.medrt,i.icon.meetup,i.icon.microsoft,i.icon.mix,i.icon.mixcloud,i.icon.mizuni,i.icon.modx,i.icon.monero,i.icon.napster,i.icon.nintendo.switch,i.icon.node,i.icon.node.js,i.icon.npm,i.icon.ns8,i.icon.nutritionix,i.icon.odnoklassniki,i.icon.odnoklassniki.square,i.icon.opencart,i.icon.openid,i.icon.opera,i.icon.optin.monster,i.icon.osi,i.icon.page4,i.icon.pagelines,i.icon.palfed,i.icon.patreon,i.icon.paypal,i.icon.periscope,i.icon.phabricator,i.icon.phoenix.framework,i.icon.php,i.icon.pied.piper,i.icon.pied.piper.alternate,i.icon.pied.piper.pp,i.icon.pinterest,i.icon.pinterest.p,i.icon.pinterest.square,i.icon.playstation,i.icon.product.hunt,i.icon.pushed,i.icon.python,i.icon.qq,i.icon.quinscape,i.icon.quora,i.icon.ravelry,i.icon.react,i.icon.rebel,i.icon.reddit,i.icon.reddit.alien,i.icon.reddit.square,i.icon.redriver,i.icon.rendact,i.icon.renren,i.icon.replyd,i.icon.resolving,i.icon.rocketchat,i.icon.rockrms,i.icon.safari,i.icon.sass,i.icon.schlix,i.icon.scribd,i.icon.searchengin,i.icon.sellcast,i.icon.sellsy,i.icon.servicestack,i.icon.shirtsinbulk,i.icon.simplybuilt,i.icon.sistrix,i.icon.skyatlas,i.icon.skype,i.icon.slack,i.icon.slack.hash,i.icon.slideshare,i.icon.snapchat,i.icon.snapchat.ghost,i.icon.snapchat.square,i.icon.soundcloud,i.icon.speakap,i.icon.spotify,i.icon.stack.exchange,i.icon.stack.overflow,i.icon.staylinked,i.icon.steam,i.icon.steam.square,i.icon.steam.symbol,i.icon.sticker.mule,i.icon.strava,i.icon.stripe,i.icon.stripe.s,i.icon.studiovinari,i.icon.stumbleupon,i.icon.stumbleupon.circle,i.icon.superpowers,i.icon.supple,i.icon.telegram,i.icon.telegram.plane,i.icon.tencent.weibo,i.icon.themeisle,i.icon.trello,i.icon.tripadvisor,i.icon.tumblr,i.icon.tumblr.square,i.icon.twitch,i.icon.twitter,i.icon.twitter.square,i.icon.typo3,i.icon.uber,i.icon.uikit,i.icon.uniregistry,i.icon.untappd,i.icon.usb,i.icon.ussunnah,i.icon.vaadin,i.icon.viacoin,i.icon.viadeo,i.icon.viadeo.square,i.icon.viber,i.icon.vimeo,i.icon.vimeo.square,i.icon.vimeo.v,i.icon.vine,i.icon.vk,i.icon.vnv,i.icon.vuejs,i.icon.wechat,i.icon.weibo,i.icon.weixin,i.icon.whatsapp,i.icon.whatsapp.square,i.icon.whmcs,i.icon.wikipedia.w,i.icon.windows,i.icon.wordpress,i.icon.wordpress.simple,i.icon.wpbeginner,i.icon.wpexplorer,i.icon.wpforms,i.icon.xbox,i.icon.xing,i.icon.xing.square,i.icon.y.combinator,i.icon.yahoo,i.icon.yandex,i.icon.yandex.international,i.icon.yelp,i.icon.yoast,i.icon.youtube,i.icon.youtube.square{font-family:brand-icons}/*!
+ * # Semantic UI 2.4.2 - Image
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.image{position:relative;display:inline-block;vertical-align:middle;max-width:100%;background-color:transparent}img.ui.image{display:block}.ui.image img,.ui.image svg{display:block;max-width:100%;height:auto}.ui.hidden.image,.ui.hidden.images{display:none}.ui.hidden.transition.image,.ui.hidden.transition.images{display:block;visibility:hidden}.ui.images>.hidden.transition{display:inline-block;visibility:hidden}.ui.disabled.image,.ui.disabled.images{cursor:default;opacity:.45}.ui.inline.image,.ui.inline.image img,.ui.inline.image svg{display:inline-block}.ui.top.aligned.image,.ui.top.aligned.image img,.ui.top.aligned.image svg,.ui.top.aligned.images .image{display:inline-block;vertical-align:top}.ui.middle.aligned.image,.ui.middle.aligned.image img,.ui.middle.aligned.image svg,.ui.middle.aligned.images .image{display:inline-block;vertical-align:middle}.ui.bottom.aligned.image,.ui.bottom.aligned.image img,.ui.bottom.aligned.image svg,.ui.bottom.aligned.images .image{display:inline-block;vertical-align:bottom}.ui.rounded.image,.ui.rounded.image>*,.ui.rounded.images .image,.ui.rounded.images .image>*{border-radius:.3125em}.ui.bordered.image img,.ui.bordered.image svg,.ui.bordered.images .image,.ui.bordered.images img,.ui.bordered.images svg,img.ui.bordered.image{border:1px solid rgba(0,0,0,.1)}.ui.circular.image,.ui.circular.images{overflow:hidden}.ui.circular.image,.ui.circular.image>*,.ui.circular.images .image,.ui.circular.images .image>*{border-radius:500rem}.ui.fluid.image,.ui.fluid.image img,.ui.fluid.image svg,.ui.fluid.images,.ui.fluid.images img,.ui.fluid.images svg{display:block;width:100%;height:auto}.ui.avatar.image,.ui.avatar.image img,.ui.avatar.image svg,.ui.avatar.images .image,.ui.avatar.images img,.ui.avatar.images svg{margin-right:.25em;display:inline-block;width:2em;height:2em;border-radius:500rem}.ui.spaced.image{display:inline-block!important;margin-left:.5em;margin-right:.5em}.ui[class*="left spaced"].image{margin-left:.5em;margin-right:0}.ui[class*="right spaced"].image{margin-left:0;margin-right:.5em}.ui.floated.image,.ui.floated.images{float:left;margin-right:1em;margin-bottom:1em}.ui.right.floated.image,.ui.right.floated.images{float:right;margin-right:0;margin-bottom:1em;margin-left:1em}.ui.floated.image:last-child,.ui.floated.images:last-child{margin-bottom:0}.ui.centered.image,.ui.centered.images{margin-left:auto;margin-right:auto}.ui.mini.image,.ui.mini.images .image,.ui.mini.images img,.ui.mini.images svg{width:35px;height:auto;font-size:.78571429rem}.ui.tiny.image,.ui.tiny.images .image,.ui.tiny.images img,.ui.tiny.images svg{width:80px;height:auto;font-size:.85714286rem}.ui.small.image,.ui.small.images .image,.ui.small.images img,.ui.small.images svg{width:150px;height:auto;font-size:.92857143rem}.ui.medium.image,.ui.medium.images .image,.ui.medium.images img,.ui.medium.images svg{width:300px;height:auto;font-size:1rem}.ui.large.image,.ui.large.images .image,.ui.large.images img,.ui.large.images svg{width:450px;height:auto;font-size:1.14285714rem}.ui.big.image,.ui.big.images .image,.ui.big.images img,.ui.big.images svg{width:600px;height:auto;font-size:1.28571429rem}.ui.huge.image,.ui.huge.images .image,.ui.huge.images img,.ui.huge.images svg{width:800px;height:auto;font-size:1.42857143rem}.ui.massive.image,.ui.massive.images .image,.ui.massive.images img,.ui.massive.images svg{width:960px;height:auto;font-size:1.71428571rem}.ui.images{font-size:0;margin:0 -.25rem 0}.ui.images .image,.ui.images>img,.ui.images>svg{display:inline-block;margin:0 .25rem .5rem}/*!
+ * # Semantic UI 2.4.2 - Input
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.input{position:relative;font-weight:400;font-style:normal;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;color:rgba(0,0,0,.87)}.ui.input>input{margin:0;max-width:100%;-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;outline:0;-webkit-tap-highlight-color:rgba(255,255,255,0);text-align:left;line-height:1.21428571em;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;padding:.67857143em 1em;background:#fff;border:1px solid rgba(34,36,38,.15);color:rgba(0,0,0,.87);border-radius:.28571429rem;-webkit-transition:border-color .1s ease,-webkit-box-shadow .1s ease;transition:border-color .1s ease,-webkit-box-shadow .1s ease;transition:box-shadow .1s ease,border-color .1s ease;transition:box-shadow .1s ease,border-color .1s ease,-webkit-box-shadow .1s ease;-webkit-box-shadow:none;box-shadow:none}.ui.input>input::-webkit-input-placeholder{color:rgba(191,191,191,.87)}.ui.input>input::-moz-placeholder{color:rgba(191,191,191,.87)}.ui.input>input:-ms-input-placeholder{color:rgba(191,191,191,.87)}.ui.disabled.input,.ui.input:not(.disabled) input[disabled]{opacity:.45}.ui.disabled.input>input,.ui.input:not(.disabled) input[disabled]{pointer-events:none}.ui.input.down input,.ui.input>input:active{border-color:rgba(0,0,0,.3);background:#fafafa;color:rgba(0,0,0,.87);-webkit-box-shadow:none;box-shadow:none}.ui.loading.loading.input>i.icon:before{position:absolute;content:'';top:50%;left:50%;margin:-.64285714em 0 0 -.64285714em;width:1.28571429em;height:1.28571429em;border-radius:500rem;border:.2em solid rgba(0,0,0,.1)}.ui.loading.loading.input>i.icon:after{position:absolute;content:'';top:50%;left:50%;margin:-.64285714em 0 0 -.64285714em;width:1.28571429em;height:1.28571429em;-webkit-animation:button-spin .6s linear;animation:button-spin .6s linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;border-radius:500rem;border-color:#767676 transparent transparent;border-style:solid;border-width:.2em;-webkit-box-shadow:0 0 0 1px transparent;box-shadow:0 0 0 1px transparent}.ui.input.focus>input,.ui.input>input:focus{border-color:#85b7d9;background:#fff;color:rgba(0,0,0,.8);-webkit-box-shadow:none;box-shadow:none}.ui.input.focus>input::-webkit-input-placeholder,.ui.input>input:focus::-webkit-input-placeholder{color:rgba(115,115,115,.87)}.ui.input.focus>input::-moz-placeholder,.ui.input>input:focus::-moz-placeholder{color:rgba(115,115,115,.87)}.ui.input.focus>input:-ms-input-placeholder,.ui.input>input:focus:-ms-input-placeholder{color:rgba(115,115,115,.87)}.ui.input.error>input{background-color:#fff6f6;border-color:#e0b4b4;color:#9f3a38;-webkit-box-shadow:none;box-shadow:none}.ui.input.error>input::-webkit-input-placeholder{color:#e7bdbc}.ui.input.error>input::-moz-placeholder{color:#e7bdbc}.ui.input.error>input:-ms-input-placeholder{color:#e7bdbc!important}.ui.input.error>input:focus::-webkit-input-placeholder{color:#da9796}.ui.input.error>input:focus::-moz-placeholder{color:#da9796}.ui.input.error>input:focus:-ms-input-placeholder{color:#da9796!important}.ui.transparent.input>input{border-color:transparent!important;background-color:transparent!important;padding:0!important;-webkit-box-shadow:none!important;box-shadow:none!important;border-radius:0!important}.ui.transparent.icon.input>i.icon{width:1.1em}.ui.transparent.icon.input>input{padding-left:0!important;padding-right:2em!important}.ui.transparent[class*="left icon"].input>input{padding-left:2em!important;padding-right:0!important}.ui.transparent.inverted.input{color:#fff}.ui.transparent.inverted.input>input{color:inherit}.ui.transparent.inverted.input>input::-webkit-input-placeholder{color:rgba(255,255,255,.5)}.ui.transparent.inverted.input>input::-moz-placeholder{color:rgba(255,255,255,.5)}.ui.transparent.inverted.input>input:-ms-input-placeholder{color:rgba(255,255,255,.5)}.ui.icon.input>i.icon{cursor:default;position:absolute;line-height:1;text-align:center;top:0;right:0;margin:0;height:100%;width:2.67142857em;opacity:.5;border-radius:0 .28571429rem .28571429rem 0;-webkit-transition:opacity .3s ease;transition:opacity .3s ease}.ui.icon.input>i.icon:not(.link){pointer-events:none}.ui.icon.input>input{padding-right:2.67142857em!important}.ui.icon.input>i.icon:after,.ui.icon.input>i.icon:before{left:0;position:absolute;text-align:center;top:50%;width:100%;margin-top:-.5em}.ui.icon.input>i.link.icon{cursor:pointer}.ui.icon.input>i.circular.icon{top:.35em;right:.5em}.ui[class*="left icon"].input>i.icon{right:auto;left:1px;border-radius:.28571429rem 0 0 .28571429rem}.ui[class*="left icon"].input>i.circular.icon{right:auto;left:.5em}.ui[class*="left icon"].input>input{padding-left:2.67142857em!important;padding-right:1em!important}.ui.icon.input>input:focus~i.icon{opacity:1}.ui.labeled.input>.label{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;margin:0;font-size:1em}.ui.labeled.input>.label:not(.corner){padding-top:.78571429em;padding-bottom:.78571429em}.ui.labeled.input:not([class*="corner labeled"]) .label:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.ui.labeled.input:not([class*="corner labeled"]) .label:first-child+input{border-top-left-radius:0;border-bottom-left-radius:0;border-left-color:transparent}.ui.labeled.input:not([class*="corner labeled"]) .label:first-child+input:focus{border-left-color:#85b7d9}.ui[class*="right labeled"].input>input{border-top-right-radius:0!important;border-bottom-right-radius:0!important;border-right-color:transparent!important}.ui[class*="right labeled"].input>input+.label{border-top-left-radius:0;border-bottom-left-radius:0}.ui[class*="right labeled"].input>input:focus{border-right-color:#85b7d9!important}.ui.labeled.input .corner.label{top:1px;right:1px;font-size:.64285714em;border-radius:0 .28571429rem 0 0}.ui[class*="corner labeled"]:not([class*="left corner labeled"]).labeled.input>input{padding-right:2.5em!important}.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"])>input{padding-right:3.25em!important}.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"])>.icon{margin-right:1.25em}.ui[class*="left corner labeled"].labeled.input>input{padding-left:2.5em!important}.ui[class*="left corner labeled"].icon.input>input{padding-left:3.25em!important}.ui[class*="left corner labeled"].icon.input>.icon{margin-left:1.25em}.ui.input>.ui.corner.label{top:1px;right:1px}.ui.input>.ui.left.corner.label{right:auto;left:1px}.ui.action.input>.button,.ui.action.input>.buttons{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto}.ui.action.input>.button,.ui.action.input>.buttons>.button{padding-top:.78571429em;padding-bottom:.78571429em;margin:0}.ui.action.input:not([class*="left action"])>input{border-top-right-radius:0!important;border-bottom-right-radius:0!important;border-right-color:transparent!important}.ui.action.input:not([class*="left action"])>.button:not(:first-child),.ui.action.input:not([class*="left action"])>.buttons:not(:first-child)>.button,.ui.action.input:not([class*="left action"])>.dropdown:not(:first-child){border-radius:0}.ui.action.input:not([class*="left action"])>.button:last-child,.ui.action.input:not([class*="left action"])>.buttons:last-child>.button,.ui.action.input:not([class*="left action"])>.dropdown:last-child{border-radius:0 .28571429rem .28571429rem 0}.ui.action.input:not([class*="left action"])>input:focus{border-right-color:#85b7d9!important}.ui[class*="left action"].input>input{border-top-left-radius:0!important;border-bottom-left-radius:0!important;border-left-color:transparent!important}.ui[class*="left action"].input>.button,.ui[class*="left action"].input>.buttons>.button,.ui[class*="left action"].input>.dropdown{border-radius:0}.ui[class*="left action"].input>.button:first-child,.ui[class*="left action"].input>.buttons:first-child>.button,.ui[class*="left action"].input>.dropdown:first-child{border-radius:.28571429rem 0 0 .28571429rem}.ui[class*="left action"].input>input:focus{border-left-color:#85b7d9!important}.ui.inverted.input>input{border:none}.ui.fluid.input{display:-webkit-box;display:-ms-flexbox;display:flex}.ui.fluid.input>input{width:0!important}.ui.mini.input{font-size:.78571429em}.ui.small.input{font-size:.92857143em}.ui.input{font-size:1em}.ui.large.input{font-size:1.14285714em}.ui.big.input{font-size:1.28571429em}.ui.huge.input{font-size:1.42857143em}.ui.massive.input{font-size:1.71428571em}/*!
+ * # Semantic UI 2.4.2 - Label
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.label{display:inline-block;line-height:1;vertical-align:baseline;margin:0 .14285714em;background-color:#e8e8e8;background-image:none;padding:.5833em .833em;color:rgba(0,0,0,.6);text-transform:none;font-weight:700;border:0 solid transparent;border-radius:.28571429rem;-webkit-transition:background .1s ease;transition:background .1s ease}.ui.label:first-child{margin-left:0}.ui.label:last-child{margin-right:0}a.ui.label{cursor:pointer}.ui.label>a{cursor:pointer;color:inherit;opacity:.5;-webkit-transition:.1s opacity ease;transition:.1s opacity ease}.ui.label>a:hover{opacity:1}.ui.label>img{width:auto!important;vertical-align:middle;height:2.1666em!important}.ui.label>.icon{width:auto;margin:0 .75em 0 0}.ui.label>.detail{display:inline-block;vertical-align:top;font-weight:700;margin-left:1em;opacity:.8}.ui.label>.detail .icon{margin:0 .25em 0 0}.ui.label>.close.icon,.ui.label>.delete.icon{cursor:pointer;margin-right:0;margin-left:.5em;font-size:.92857143em;opacity:.5;-webkit-transition:background .1s ease;transition:background .1s ease}.ui.label>.delete.icon:hover{opacity:1}.ui.labels>.label{margin:0 .5em .5em 0}.ui.header>.ui.label{margin-top:-.29165em}.ui.attached.segment>.ui.top.left.attached.label,.ui.bottom.attached.segment>.ui.top.left.attached.label{border-top-left-radius:0}.ui.attached.segment>.ui.top.right.attached.label,.ui.bottom.attached.segment>.ui.top.right.attached.label{border-top-right-radius:0}.ui.top.attached.segment>.ui.bottom.left.attached.label{border-bottom-left-radius:0}.ui.top.attached.segment>.ui.bottom.right.attached.label{border-bottom-right-radius:0}.ui.top.attached.label+[class*="right floated"]+*,.ui.top.attached.label:first-child+:not(.attached){margin-top:2rem!important}.ui.bottom.attached.label:first-child~:last-child:not(.attached){margin-top:0;margin-bottom:2rem!important}.ui.image.label{width:auto!important;margin-top:0;margin-bottom:0;max-width:9999px;vertical-align:baseline;text-transform:none;background:#e8e8e8;padding:.5833em .833em .5833em .5em;border-radius:.28571429rem;-webkit-box-shadow:none;box-shadow:none}.ui.image.label img{display:inline-block;vertical-align:top;height:2.1666em;margin:-.5833em .5em -.5833em -.5em;border-radius:.28571429rem 0 0 .28571429rem}.ui.image.label .detail{background:rgba(0,0,0,.1);margin:-.5833em -.833em -.5833em .5em;padding:.5833em .833em;border-radius:0 .28571429rem .28571429rem 0}.ui.tag.label,.ui.tag.labels .label{margin-left:1em;position:relative;padding-left:1.5em;padding-right:1.5em;border-radius:0 .28571429rem .28571429rem 0;-webkit-transition:none;transition:none}.ui.tag.label:before,.ui.tag.labels .label:before{position:absolute;-webkit-transform:translateY(-50%) translateX(50%) rotate(-45deg);transform:translateY(-50%) translateX(50%) rotate(-45deg);top:50%;right:100%;content:'';background-color:inherit;background-image:none;width:1.56em;height:1.56em;-webkit-transition:none;transition:none}.ui.tag.label:after,.ui.tag.labels .label:after{position:absolute;content:'';top:50%;left:-.25em;margin-top:-.25em;background-color:#fff!important;width:.5em;height:.5em;-webkit-box-shadow:0 -1px 1px 0 rgba(0,0,0,.3);box-shadow:0 -1px 1px 0 rgba(0,0,0,.3);border-radius:500rem}.ui.corner.label{position:absolute;top:0;right:0;margin:0;padding:0;text-align:center;border-color:#e8e8e8;width:4em;height:4em;z-index:1;-webkit-transition:border-color .1s ease;transition:border-color .1s ease}.ui.corner.label{background-color:transparent!important}.ui.corner.label:after{position:absolute;content:"";right:0;top:0;z-index:-1;width:0;height:0;background-color:transparent!important;border-top:0 solid transparent;border-right:4em solid transparent;border-bottom:4em solid transparent;border-left:0 solid transparent;border-right-color:inherit;-webkit-transition:border-color .1s ease;transition:border-color .1s ease}.ui.corner.label .icon{cursor:default;position:relative;top:.64285714em;left:.78571429em;font-size:1.14285714em;margin:0}.ui.left.corner.label,.ui.left.corner.label:after{right:auto;left:0}.ui.left.corner.label:after{border-top:4em solid transparent;border-right:4em solid transparent;border-bottom:0 solid transparent;border-left:0 solid transparent;border-top-color:inherit}.ui.left.corner.label .icon{left:-.78571429em}.ui.segment>.ui.corner.label{top:-1px;right:-1px}.ui.segment>.ui.left.corner.label{right:auto;left:-1px}.ui.ribbon.label{position:relative;margin:0;min-width:-webkit-max-content;min-width:-moz-max-content;min-width:max-content;border-radius:0 .28571429rem .28571429rem 0;border-color:rgba(0,0,0,.15)}.ui.ribbon.label:after{position:absolute;content:'';top:100%;left:0;background-color:transparent!important;border-style:solid;border-width:0 1.2em 1.2em 0;border-color:transparent;border-right-color:inherit;width:0;height:0}.ui.ribbon.label{left:calc(-1rem - 1.2em);margin-right:-1.2em;padding-left:calc(1rem + 1.2em);padding-right:1.2em}.ui[class*="right ribbon"].label{left:calc(100% + 1rem + 1.2em);padding-left:1.2em;padding-right:calc(1rem + 1.2em)}.ui[class*="right ribbon"].label{text-align:left;-webkit-transform:translateX(-100%);transform:translateX(-100%);border-radius:.28571429rem 0 0 .28571429rem}.ui[class*="right ribbon"].label:after{left:auto;right:0;border-style:solid;border-width:1.2em 1.2em 0 0;border-color:transparent;border-top-color:inherit}.ui.card .image>.ribbon.label,.ui.image>.ribbon.label{position:absolute;top:1rem}.ui.card .image>.ui.ribbon.label,.ui.image>.ui.ribbon.label{left:calc(--.05rem - 1.2em)}.ui.card .image>.ui[class*="right ribbon"].label,.ui.image>.ui[class*="right ribbon"].label{left:calc(100% + -.05rem + 1.2em);padding-left:.833em}.ui.table td>.ui.ribbon.label{left:calc(-.78571429em - 1.2em)}.ui.table td>.ui[class*="right ribbon"].label{left:calc(100% + .78571429em + 1.2em);padding-left:.833em}.ui.attached.label,.ui[class*="top attached"].label{width:100%;position:absolute;margin:0;top:0;left:0;padding:.75em 1em;border-radius:.21428571rem .21428571rem 0 0}.ui[class*="bottom attached"].label{top:auto;bottom:0;border-radius:0 0 .21428571rem .21428571rem}.ui[class*="top left attached"].label{width:auto;margin-top:0!important;border-radius:.21428571rem 0 .28571429rem 0}.ui[class*="top right attached"].label{width:auto;left:auto;right:0;border-radius:0 .21428571rem 0 .28571429rem}.ui[class*="bottom left attached"].label{width:auto;top:auto;bottom:0;border-radius:0 .28571429rem 0 .21428571rem}.ui[class*="bottom right attached"].label{top:auto;bottom:0;left:auto;right:0;width:auto;border-radius:.28571429rem 0 .21428571rem 0}.ui.label.disabled{opacity:.5}a.ui.label:hover,a.ui.labels .label:hover{background-color:#e0e0e0;border-color:#e0e0e0;background-image:none;color:rgba(0,0,0,.8)}.ui.labels a.label:hover:before,a.ui.label:hover:before{color:rgba(0,0,0,.8)}.ui.active.label{background-color:#d0d0d0;border-color:#d0d0d0;background-image:none;color:rgba(0,0,0,.95)}.ui.active.label:before{background-color:#d0d0d0;background-image:none;color:rgba(0,0,0,.95)}a.ui.active.label:hover,a.ui.labels .active.label:hover{background-color:#c8c8c8;border-color:#c8c8c8;background-image:none;color:rgba(0,0,0,.95)}.ui.labels a.active.label:ActiveHover:before,a.ui.active.label:ActiveHover:before{background-color:#c8c8c8;background-image:none;color:rgba(0,0,0,.95)}.ui.label.visible:not(.dropdown),.ui.labels.visible .label{display:inline-block!important}.ui.label.hidden,.ui.labels.hidden .label{display:none!important}.ui.red.label,.ui.red.labels .label{background-color:#db2828!important;border-color:#db2828!important;color:#fff!important}.ui.red.labels .label:hover,a.ui.red.label:hover{background-color:#d01919!important;border-color:#d01919!important;color:#fff!important}.ui.red.corner.label,.ui.red.corner.label:hover{background-color:transparent!important}.ui.red.ribbon.label{border-color:#b21e1e!important}.ui.basic.red.label{background:none #fff!important;color:#db2828!important;border-color:#db2828!important}.ui.basic.red.labels a.label:hover,a.ui.basic.red.label:hover{background-color:#fff!important;color:#d01919!important;border-color:#d01919!important}.ui.orange.label,.ui.orange.labels .label{background-color:#f2711c!important;border-color:#f2711c!important;color:#fff!important}.ui.orange.labels .label:hover,a.ui.orange.label:hover{background-color:#f26202!important;border-color:#f26202!important;color:#fff!important}.ui.orange.corner.label,.ui.orange.corner.label:hover{background-color:transparent!important}.ui.orange.ribbon.label{border-color:#cf590c!important}.ui.basic.orange.label{background:none #fff!important;color:#f2711c!important;border-color:#f2711c!important}.ui.basic.orange.labels a.label:hover,a.ui.basic.orange.label:hover{background-color:#fff!important;color:#f26202!important;border-color:#f26202!important}.ui.yellow.label,.ui.yellow.labels .label{background-color:#fbbd08!important;border-color:#fbbd08!important;color:#fff!important}.ui.yellow.labels .label:hover,a.ui.yellow.label:hover{background-color:#eaae00!important;border-color:#eaae00!important;color:#fff!important}.ui.yellow.corner.label,.ui.yellow.corner.label:hover{background-color:transparent!important}.ui.yellow.ribbon.label{border-color:#cd9903!important}.ui.basic.yellow.label{background:none #fff!important;color:#fbbd08!important;border-color:#fbbd08!important}.ui.basic.yellow.labels a.label:hover,a.ui.basic.yellow.label:hover{background-color:#fff!important;color:#eaae00!important;border-color:#eaae00!important}.ui.olive.label,.ui.olive.labels .label{background-color:#b5cc18!important;border-color:#b5cc18!important;color:#fff!important}.ui.olive.labels .label:hover,a.ui.olive.label:hover{background-color:#a7bd0d!important;border-color:#a7bd0d!important;color:#fff!important}.ui.olive.corner.label,.ui.olive.corner.label:hover{background-color:transparent!important}.ui.olive.ribbon.label{border-color:#198f35!important}.ui.basic.olive.label{background:none #fff!important;color:#b5cc18!important;border-color:#b5cc18!important}.ui.basic.olive.labels a.label:hover,a.ui.basic.olive.label:hover{background-color:#fff!important;color:#a7bd0d!important;border-color:#a7bd0d!important}.ui.green.label,.ui.green.labels .label{background-color:#21ba45!important;border-color:#21ba45!important;color:#fff!important}.ui.green.labels .label:hover,a.ui.green.label:hover{background-color:#16ab39!important;border-color:#16ab39!important;color:#fff!important}.ui.green.corner.label,.ui.green.corner.label:hover{background-color:transparent!important}.ui.green.ribbon.label{border-color:#198f35!important}.ui.basic.green.label{background:none #fff!important;color:#21ba45!important;border-color:#21ba45!important}.ui.basic.green.labels a.label:hover,a.ui.basic.green.label:hover{background-color:#fff!important;color:#16ab39!important;border-color:#16ab39!important}.ui.teal.label,.ui.teal.labels .label{background-color:#00b5ad!important;border-color:#00b5ad!important;color:#fff!important}.ui.teal.labels .label:hover,a.ui.teal.label:hover{background-color:#009c95!important;border-color:#009c95!important;color:#fff!important}.ui.teal.corner.label,.ui.teal.corner.label:hover{background-color:transparent!important}.ui.teal.ribbon.label{border-color:#00827c!important}.ui.basic.teal.label{background:none #fff!important;color:#00b5ad!important;border-color:#00b5ad!important}.ui.basic.teal.labels a.label:hover,a.ui.basic.teal.label:hover{background-color:#fff!important;color:#009c95!important;border-color:#009c95!important}.ui.blue.label,.ui.blue.labels .label{background-color:#2185d0!important;border-color:#2185d0!important;color:#fff!important}.ui.blue.labels .label:hover,a.ui.blue.label:hover{background-color:#1678c2!important;border-color:#1678c2!important;color:#fff!important}.ui.blue.corner.label,.ui.blue.corner.label:hover{background-color:transparent!important}.ui.blue.ribbon.label{border-color:#1a69a4!important}.ui.basic.blue.label{background:none #fff!important;color:#2185d0!important;border-color:#2185d0!important}.ui.basic.blue.labels a.label:hover,a.ui.basic.blue.label:hover{background-color:#fff!important;color:#1678c2!important;border-color:#1678c2!important}.ui.violet.label,.ui.violet.labels .label{background-color:#6435c9!important;border-color:#6435c9!important;color:#fff!important}.ui.violet.labels .label:hover,a.ui.violet.label:hover{background-color:#5829bb!important;border-color:#5829bb!important;color:#fff!important}.ui.violet.corner.label,.ui.violet.corner.label:hover{background-color:transparent!important}.ui.violet.ribbon.label{border-color:#502aa1!important}.ui.basic.violet.label{background:none #fff!important;color:#6435c9!important;border-color:#6435c9!important}.ui.basic.violet.labels a.label:hover,a.ui.basic.violet.label:hover{background-color:#fff!important;color:#5829bb!important;border-color:#5829bb!important}.ui.purple.label,.ui.purple.labels .label{background-color:#a333c8!important;border-color:#a333c8!important;color:#fff!important}.ui.purple.labels .label:hover,a.ui.purple.label:hover{background-color:#9627ba!important;border-color:#9627ba!important;color:#fff!important}.ui.purple.corner.label,.ui.purple.corner.label:hover{background-color:transparent!important}.ui.purple.ribbon.label{border-color:#82299f!important}.ui.basic.purple.label{background:none #fff!important;color:#a333c8!important;border-color:#a333c8!important}.ui.basic.purple.labels a.label:hover,a.ui.basic.purple.label:hover{background-color:#fff!important;color:#9627ba!important;border-color:#9627ba!important}.ui.pink.label,.ui.pink.labels .label{background-color:#e03997!important;border-color:#e03997!important;color:#fff!important}.ui.pink.labels .label:hover,a.ui.pink.label:hover{background-color:#e61a8d!important;border-color:#e61a8d!important;color:#fff!important}.ui.pink.corner.label,.ui.pink.corner.label:hover{background-color:transparent!important}.ui.pink.ribbon.label{border-color:#c71f7e!important}.ui.basic.pink.label{background:none #fff!important;color:#e03997!important;border-color:#e03997!important}.ui.basic.pink.labels a.label:hover,a.ui.basic.pink.label:hover{background-color:#fff!important;color:#e61a8d!important;border-color:#e61a8d!important}.ui.brown.label,.ui.brown.labels .label{background-color:#a5673f!important;border-color:#a5673f!important;color:#fff!important}.ui.brown.labels .label:hover,a.ui.brown.label:hover{background-color:#975b33!important;border-color:#975b33!important;color:#fff!important}.ui.brown.corner.label,.ui.brown.corner.label:hover{background-color:transparent!important}.ui.brown.ribbon.label{border-color:#805031!important}.ui.basic.brown.label{background:none #fff!important;color:#a5673f!important;border-color:#a5673f!important}.ui.basic.brown.labels a.label:hover,a.ui.basic.brown.label:hover{background-color:#fff!important;color:#975b33!important;border-color:#975b33!important}.ui.grey.label,.ui.grey.labels .label{background-color:#767676!important;border-color:#767676!important;color:#fff!important}.ui.grey.labels .label:hover,a.ui.grey.label:hover{background-color:#838383!important;border-color:#838383!important;color:#fff!important}.ui.grey.corner.label,.ui.grey.corner.label:hover{background-color:transparent!important}.ui.grey.ribbon.label{border-color:#805031!important}.ui.basic.grey.label{background:none #fff!important;color:#767676!important;border-color:#767676!important}.ui.basic.grey.labels a.label:hover,a.ui.basic.grey.label:hover{background-color:#fff!important;color:#838383!important;border-color:#838383!important}.ui.black.label,.ui.black.labels .label{background-color:#1b1c1d!important;border-color:#1b1c1d!important;color:#fff!important}.ui.black.labels .label:hover,a.ui.black.label:hover{background-color:#27292a!important;border-color:#27292a!important;color:#fff!important}.ui.black.corner.label,.ui.black.corner.label:hover{background-color:transparent!important}.ui.black.ribbon.label{border-color:#805031!important}.ui.basic.black.label{background:none #fff!important;color:#1b1c1d!important;border-color:#1b1c1d!important}.ui.basic.black.labels a.label:hover,a.ui.basic.black.label:hover{background-color:#fff!important;color:#27292a!important;border-color:#27292a!important}.ui.basic.label{background:none #fff;border:1px solid rgba(34,36,38,.15);color:rgba(0,0,0,.87);-webkit-box-shadow:none;box-shadow:none}a.ui.basic.label:hover{text-decoration:none;background:none #fff;color:#1e70bf;-webkit-box-shadow:1px solid rgba(34,36,38,.15);box-shadow:1px solid rgba(34,36,38,.15);-webkit-box-shadow:none;box-shadow:none}.ui.basic.pointing.label:before{border-color:inherit}.ui.fluid.labels>.label,.ui.label.fluid{width:100%;-webkit-box-sizing:border-box;box-sizing:border-box}.ui.inverted.label,.ui.inverted.labels .label{color:rgba(255,255,255,.9)!important}.ui.horizontal.label,.ui.horizontal.labels .label{margin:0 .5em 0 0;padding:.4em .833em;min-width:3em;text-align:center}.ui.circular.label,.ui.circular.labels .label{min-width:2em;min-height:2em;padding:.5em!important;line-height:1em;text-align:center;border-radius:500rem}.ui.empty.circular.label,.ui.empty.circular.labels .label{min-width:0;min-height:0;overflow:hidden;width:.5em;height:.5em;vertical-align:baseline}.ui.pointing.label{position:relative}.ui.attached.pointing.label{position:absolute}.ui.pointing.label:before{background-color:inherit;background-image:inherit;border-width:none;border-style:solid;border-color:inherit}.ui.pointing.label:before{position:absolute;content:'';-webkit-transform:rotate(45deg);transform:rotate(45deg);background-image:none;z-index:2;width:.6666em;height:.6666em;-webkit-transition:background .1s ease;transition:background .1s ease}.ui.pointing.label,.ui[class*="pointing above"].label{margin-top:1em}.ui.pointing.label:before,.ui[class*="pointing above"].label:before{border-width:1px 0 0 1px;-webkit-transform:translateX(-50%) translateY(-50%) rotate(45deg);transform:translateX(-50%) translateY(-50%) rotate(45deg);top:0;left:50%}.ui[class*="bottom pointing"].label,.ui[class*="pointing below"].label{margin-top:0;margin-bottom:1em}.ui[class*="bottom pointing"].label:before,.ui[class*="pointing below"].label:before{border-width:0 1px 1px 0;top:auto;right:auto;-webkit-transform:translateX(-50%) translateY(-50%) rotate(45deg);transform:translateX(-50%) translateY(-50%) rotate(45deg);top:100%;left:50%}.ui[class*="left pointing"].label{margin-top:0;margin-left:.6666em}.ui[class*="left pointing"].label:before{border-width:0 0 1px 1px;-webkit-transform:translateX(-50%) translateY(-50%) rotate(45deg);transform:translateX(-50%) translateY(-50%) rotate(45deg);bottom:auto;right:auto;top:50%;left:0}.ui[class*="right pointing"].label{margin-top:0;margin-right:.6666em}.ui[class*="right pointing"].label:before{border-width:1px 1px 0 0;-webkit-transform:translateX(50%) translateY(-50%) rotate(45deg);transform:translateX(50%) translateY(-50%) rotate(45deg);top:50%;right:0;bottom:auto;left:auto}.ui.basic.pointing.label:before,.ui.basic[class*="pointing above"].label:before{margin-top:-1px}.ui.basic[class*="bottom pointing"].label:before,.ui.basic[class*="pointing below"].label:before{bottom:auto;top:100%;margin-top:1px}.ui.basic[class*="left pointing"].label:before{top:50%;left:-1px}.ui.basic[class*="right pointing"].label:before{top:50%;right:-1px}.ui.floating.label{position:absolute;z-index:100;top:-1em;left:100%;margin:0 0 0 -1.5em!important}.ui.mini.label,.ui.mini.labels .label{font-size:.64285714rem}.ui.tiny.label,.ui.tiny.labels .label{font-size:.71428571rem}.ui.small.label,.ui.small.labels .label{font-size:.78571429rem}.ui.label,.ui.labels .label{font-size:.85714286rem}.ui.large.label,.ui.large.labels .label{font-size:1rem}.ui.big.label,.ui.big.labels .label{font-size:1.28571429rem}.ui.huge.label,.ui.huge.labels .label{font-size:1.42857143rem}.ui.massive.label,.ui.massive.labels .label{font-size:1.71428571rem}/*!
+ * # Semantic UI 2.4.2 - List
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.list,ol.ui.list,ul.ui.list{list-style-type:none;margin:1em 0;padding:0 0}.ui.list:first-child,ol.ui.list:first-child,ul.ui.list:first-child{margin-top:0;padding-top:0}.ui.list:last-child,ol.ui.list:last-child,ul.ui.list:last-child{margin-bottom:0;padding-bottom:0}.ui.list .list>.item,.ui.list>.item,ol.ui.list li,ul.ui.list li{display:list-item;table-layout:fixed;list-style-type:none;list-style-position:outside;padding:.21428571em 0;line-height:1.14285714em}.ui.list>.item:after,.ui.list>.list>.item,ol.ui.list>li:first-child:after,ul.ui.list>li:first-child:after{content:'';display:block;height:0;clear:both;visibility:hidden}.ui.list .list>.item:first-child,.ui.list>.item:first-child,ol.ui.list li:first-child,ul.ui.list li:first-child{padding-top:0}.ui.list .list>.item:last-child,.ui.list>.item:last-child,ol.ui.list li:last-child,ul.ui.list li:last-child{padding-bottom:0}.ui.list .list,ol.ui.list ol,ul.ui.list ul{clear:both;margin:0;padding:.75em 0 .25em .5em}.ui.list .list>.item,ol.ui.list ol li,ul.ui.list ul li{padding:.14285714em 0;line-height:inherit}.ui.list .list>.item>i.icon,.ui.list>.item>i.icon{display:table-cell;margin:0;padding-top:0;padding-right:.28571429em;vertical-align:top;-webkit-transition:color .1s ease;transition:color .1s ease}.ui.list .list>.item>i.icon:only-child,.ui.list>.item>i.icon:only-child{display:inline-block;vertical-align:top}.ui.list .list>.item>.image,.ui.list>.item>.image{display:table-cell;background-color:transparent;margin:0;vertical-align:top}.ui.list .list>.item>.image:not(:only-child):not(img),.ui.list>.item>.image:not(:only-child):not(img){padding-right:.5em}.ui.list .list>.item>.image img,.ui.list>.item>.image img{vertical-align:top}.ui.list .list>.item>.image:only-child,.ui.list .list>.item>img.image,.ui.list>.item>.image:only-child,.ui.list>.item>img.image{display:inline-block}.ui.list .list>.item>.content,.ui.list>.item>.content{line-height:1.14285714em}.ui.list .list>.item>.icon+.content,.ui.list .list>.item>.image+.content,.ui.list>.item>.icon+.content,.ui.list>.item>.image+.content{display:table-cell;width:100%;padding:0 0 0 .5em;vertical-align:top}.ui.list .list>.item>img.image+.content,.ui.list>.item>img.image+.content{display:inline-block;width:auto}.ui.list .list>.item>.content>.list,.ui.list>.item>.content>.list{margin-left:0;padding-left:0}.ui.list .list>.item .header,.ui.list>.item .header{display:block;margin:0;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-weight:700;color:rgba(0,0,0,.87)}.ui.list .list>.item .description,.ui.list>.item .description{display:block;color:rgba(0,0,0,.7)}.ui.list .list>.item a,.ui.list>.item a{cursor:pointer}.ui.list .list>a.item,.ui.list>a.item{cursor:pointer;color:#4183c4}.ui.list .list>a.item:hover,.ui.list>a.item:hover{color:#1e70bf}.ui.list .list>a.item i.icon,.ui.list>a.item i.icon{color:rgba(0,0,0,.4)}.ui.list .list>.item a.header,.ui.list>.item a.header{cursor:pointer;color:#4183c4!important}.ui.list .list>.item a.header:hover,.ui.list>.item a.header:hover{color:#1e70bf!important}.ui[class*="left floated"].list{float:left}.ui[class*="right floated"].list{float:right}.ui.list .list>.item [class*="left floated"],.ui.list>.item [class*="left floated"]{float:left;margin:0 1em 0 0}.ui.list .list>.item [class*="right floated"],.ui.list>.item [class*="right floated"]{float:right;margin:0 0 0 1em}.ui.menu .ui.list .list>.item,.ui.menu .ui.list>.item{display:list-item;table-layout:fixed;background-color:transparent;list-style-type:none;list-style-position:outside;padding:.21428571em 0;line-height:1.14285714em}.ui.menu .ui.list .list>.item:before,.ui.menu .ui.list>.item:before{border:none;background:0 0}.ui.menu .ui.list .list>.item:first-child,.ui.menu .ui.list>.item:first-child{padding-top:0}.ui.menu .ui.list .list>.item:last-child,.ui.menu .ui.list>.item:last-child{padding-bottom:0}.ui.horizontal.list{display:inline-block;font-size:0}.ui.horizontal.list>.item{display:inline-block;margin-left:1em;font-size:1rem}.ui.horizontal.list:not(.celled)>.item:first-child{margin-left:0!important;padding-left:0!important}.ui.horizontal.list .list{padding-left:0;padding-bottom:0}.ui.horizontal.list .list>.item>.content,.ui.horizontal.list .list>.item>.icon,.ui.horizontal.list .list>.item>.image,.ui.horizontal.list>.item>.content,.ui.horizontal.list>.item>.icon,.ui.horizontal.list>.item>.image{vertical-align:middle}.ui.horizontal.list>.item:first-child,.ui.horizontal.list>.item:last-child{padding-top:.21428571em;padding-bottom:.21428571em}.ui.horizontal.list>.item>i.icon{margin:0;padding:0 .25em 0 0}.ui.horizontal.list>.item>.icon,.ui.horizontal.list>.item>.icon+.content{float:none;display:inline-block}.ui.list .list>.disabled.item,.ui.list>.disabled.item{pointer-events:none;color:rgba(40,40,40,.3)!important}.ui.inverted.list .list>.disabled.item,.ui.inverted.list>.disabled.item{color:rgba(225,225,225,.3)!important}.ui.list .list>a.item:hover .icon,.ui.list>a.item:hover .icon{color:rgba(0,0,0,.87)}.ui.inverted.list .list>a.item>.icon,.ui.inverted.list>a.item>.icon{color:rgba(255,255,255,.7)}.ui.inverted.list .list>.item .header,.ui.inverted.list>.item .header{color:rgba(255,255,255,.9)}.ui.inverted.list .list>.item .description,.ui.inverted.list>.item .description{color:rgba(255,255,255,.7)}.ui.inverted.list .list>a.item,.ui.inverted.list>a.item{cursor:pointer;color:rgba(255,255,255,.9)}.ui.inverted.list .list>a.item:hover,.ui.inverted.list>a.item:hover{color:#1e70bf}.ui.inverted.list .item a:not(.ui){color:rgba(255,255,255,.9)!important}.ui.inverted.list .item a:not(.ui):hover{color:#1e70bf!important}.ui.list [class*="top aligned"],.ui.list[class*="top aligned"] .content,.ui.list[class*="top aligned"] .image{vertical-align:top!important}.ui.list [class*="middle aligned"],.ui.list[class*="middle aligned"] .content,.ui.list[class*="middle aligned"] .image{vertical-align:middle!important}.ui.list [class*="bottom aligned"],.ui.list[class*="bottom aligned"] .content,.ui.list[class*="bottom aligned"] .image{vertical-align:bottom!important}.ui.link.list .item,.ui.link.list .item a:not(.ui),.ui.link.list a.item{color:rgba(0,0,0,.4);-webkit-transition:.1s color ease;transition:.1s color ease}.ui.link.list.list .item a:not(.ui):hover,.ui.link.list.list a.item:hover{color:rgba(0,0,0,.8)}.ui.link.list.list .item a:not(.ui):active,.ui.link.list.list a.item:active{color:rgba(0,0,0,.9)}.ui.link.list.list .active.item,.ui.link.list.list .active.item a:not(.ui){color:rgba(0,0,0,.95)}.ui.inverted.link.list .item,.ui.inverted.link.list .item a:not(.ui),.ui.inverted.link.list a.item{color:rgba(255,255,255,.5)}.ui.inverted.link.list.list .item a:not(.ui):hover,.ui.inverted.link.list.list a.item:hover{color:#fff}.ui.inverted.link.list.list .item a:not(.ui):active,.ui.inverted.link.list.list a.item:active{color:#fff}.ui.inverted.link.list.list .active.item a:not(.ui),.ui.inverted.link.list.list a.active.item{color:#fff}.ui.selection.list .list>.item,.ui.selection.list>.item{cursor:pointer;background:0 0;padding:.5em .5em;margin:0;color:rgba(0,0,0,.4);border-radius:.5em;-webkit-transition:.1s color ease,.1s padding-left ease,.1s background-color ease;transition:.1s color ease,.1s padding-left ease,.1s background-color ease}.ui.selection.list .list>.item:last-child,.ui.selection.list>.item:last-child{margin-bottom:0}.ui.selection.list.list>.item:hover,.ui.selection.list>.item:hover{background:rgba(0,0,0,.03);color:rgba(0,0,0,.8)}.ui.selection.list .list>.item:active,.ui.selection.list>.item:active{background:rgba(0,0,0,.05);color:rgba(0,0,0,.9)}.ui.selection.list .list>.item.active,.ui.selection.list>.item.active{background:rgba(0,0,0,.05);color:rgba(0,0,0,.95)}.ui.inverted.selection.list>.item{background:0 0;color:rgba(255,255,255,.5)}.ui.inverted.selection.list>.item:hover{background:rgba(255,255,255,.02);color:#fff}.ui.inverted.selection.list>.item:active{background:rgba(255,255,255,.08);color:#fff}.ui.inverted.selection.list>.item.active{background:rgba(255,255,255,.08);color:#fff}.ui.celled.selection.list .list>.item,.ui.celled.selection.list>.item,.ui.divided.selection.list .list>.item,.ui.divided.selection.list>.item{border-radius:0}.ui.animated.list>.item{-webkit-transition:.25s color ease .1s,.25s padding-left ease .1s,.25s background-color ease .1s;transition:.25s color ease .1s,.25s padding-left ease .1s,.25s background-color ease .1s}.ui.animated.list:not(.horizontal)>.item:hover{padding-left:1em}.ui.fitted.list:not(.selection) .list>.item,.ui.fitted.list:not(.selection)>.item{padding-left:0;padding-right:0}.ui.fitted.selection.list .list>.item,.ui.fitted.selection.list>.item{margin-left:-.5em;margin-right:-.5em}.ui.bulleted.list,ul.ui.list{margin-left:1.25rem}.ui.bulleted.list .list>.item,.ui.bulleted.list>.item,ul.ui.list li{position:relative}.ui.bulleted.list .list>.item:before,.ui.bulleted.list>.item:before,ul.ui.list li:before{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;position:absolute;top:auto;left:auto;font-weight:400;margin-left:-1.25rem;content:'•';opacity:1;color:inherit;vertical-align:top}.ui.bulleted.list .list>a.item:before,.ui.bulleted.list>a.item:before,ul.ui.list li:before{color:rgba(0,0,0,.87)}.ui.bulleted.list .list,ul.ui.list ul{padding-left:1.25rem}.ui.horizontal.bulleted.list,ul.ui.horizontal.bulleted.list{margin-left:0}.ui.horizontal.bulleted.list>.item,ul.ui.horizontal.bulleted.list li{margin-left:1.75rem}.ui.horizontal.bulleted.list>.item:first-child,ul.ui.horizontal.bulleted.list li:first-child{margin-left:0}.ui.horizontal.bulleted.list>.item::before,ul.ui.horizontal.bulleted.list li::before{color:rgba(0,0,0,.87)}.ui.horizontal.bulleted.list>.item:first-child::before,ul.ui.horizontal.bulleted.list li:first-child::before{display:none}.ui.ordered.list,.ui.ordered.list .list,ol.ui.list,ol.ui.list ol{counter-reset:ordered;margin-left:1.25rem;list-style-type:none}.ui.ordered.list .list>.item,.ui.ordered.list>.item,ol.ui.list li{list-style-type:none;position:relative}.ui.ordered.list .list>.item:before,.ui.ordered.list>.item:before,ol.ui.list li:before{position:absolute;top:auto;left:auto;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;margin-left:-1.25rem;counter-increment:ordered;content:counters(ordered, ".") " ";text-align:right;color:rgba(0,0,0,.87);vertical-align:middle;opacity:.8}.ui.ordered.inverted.list .list>.item:before,.ui.ordered.inverted.list>.item:before,ol.ui.inverted.list li:before{color:rgba(255,255,255,.7)}.ui.ordered.list>.item[data-value],.ui.ordered.list>.list>.item[data-value]{content:attr(data-value)}ol.ui.list li[value]:before{content:attr(value)}.ui.ordered.list .list,ol.ui.list ol{margin-left:1em}.ui.ordered.list .list>.item:before,ol.ui.list ol li:before{margin-left:-2em}.ui.ordered.horizontal.list,ol.ui.horizontal.list{margin-left:0}.ui.ordered.horizontal.list .list>.item:before,.ui.ordered.horizontal.list>.item:before,ol.ui.horizontal.list li:before{position:static;margin:0 .5em 0 0}.ui.divided.list>.item{border-top:1px solid rgba(34,36,38,.15)}.ui.divided.list .list>.item{border-top:none}.ui.divided.list .item .list>.item{border-top:none}.ui.divided.list .list>.item:first-child,.ui.divided.list>.item:first-child{border-top:none}.ui.divided.list:not(.horizontal) .list>.item:first-child{border-top-width:1px}.ui.divided.bulleted.list .list,.ui.divided.bulleted.list:not(.horizontal){margin-left:0;padding-left:0}.ui.divided.bulleted.list>.item:not(.horizontal){padding-left:1.25rem}.ui.divided.ordered.list{margin-left:0}.ui.divided.ordered.list .list>.item,.ui.divided.ordered.list>.item{padding-left:1.25rem}.ui.divided.ordered.list .item .list{margin-left:0;margin-right:0;padding-bottom:.21428571em}.ui.divided.ordered.list .item .list>.item{padding-left:1em}.ui.divided.selection.list .list>.item,.ui.divided.selection.list>.item{margin:0;border-radius:0}.ui.divided.horizontal.list{margin-left:0}.ui.divided.horizontal.list>.item:not(:first-child){padding-left:.5em}.ui.divided.horizontal.list>.item:not(:last-child){padding-right:.5em}.ui.divided.horizontal.list>.item{border-top:none;border-left:1px solid rgba(34,36,38,.15);margin:0;line-height:.6}.ui.horizontal.divided.list>.item:first-child{border-left:none}.ui.divided.inverted.horizontal.list>.item,.ui.divided.inverted.list>.item,.ui.divided.inverted.list>.list{border-color:rgba(255,255,255,.1)}.ui.celled.list>.item,.ui.celled.list>.list{border-top:1px solid rgba(34,36,38,.15);padding-left:.5em;padding-right:.5em}.ui.celled.list>.item:last-child{border-bottom:1px solid rgba(34,36,38,.15)}.ui.celled.list>.item:first-child,.ui.celled.list>.item:last-child{padding-top:.21428571em;padding-bottom:.21428571em}.ui.celled.list .item .list>.item{border-width:0}.ui.celled.list .list>.item:first-child{border-top-width:0}.ui.celled.bulleted.list{margin-left:0}.ui.celled.bulleted.list .list>.item,.ui.celled.bulleted.list>.item{padding-left:1.25rem}.ui.celled.bulleted.list .item .list{margin-left:-1.25rem;margin-right:-1.25rem;padding-bottom:.21428571em}.ui.celled.ordered.list{margin-left:0}.ui.celled.ordered.list .list>.item,.ui.celled.ordered.list>.item{padding-left:1.25rem}.ui.celled.ordered.list .item .list{margin-left:0;margin-right:0;padding-bottom:.21428571em}.ui.celled.ordered.list .list>.item{padding-left:1em}.ui.horizontal.celled.list{margin-left:0}.ui.horizontal.celled.list .list>.item,.ui.horizontal.celled.list>.item{border-top:none;border-left:1px solid rgba(34,36,38,.15);margin:0;padding-left:.5em;padding-right:.5em;line-height:.6}.ui.horizontal.celled.list .list>.item:last-child,.ui.horizontal.celled.list>.item:last-child{border-bottom:none;border-right:1px solid rgba(34,36,38,.15)}.ui.celled.inverted.list>.item,.ui.celled.inverted.list>.list{border-color:1px solid rgba(255,255,255,.1)}.ui.celled.inverted.horizontal.list .list>.item,.ui.celled.inverted.horizontal.list>.item{border-color:1px solid rgba(255,255,255,.1)}.ui.relaxed.list:not(.horizontal)>.item:not(:first-child){padding-top:.42857143em}.ui.relaxed.list:not(.horizontal)>.item:not(:last-child){padding-bottom:.42857143em}.ui.horizontal.relaxed.list .list>.item:not(:first-child),.ui.horizontal.relaxed.list>.item:not(:first-child){padding-left:1rem}.ui.horizontal.relaxed.list .list>.item:not(:last-child),.ui.horizontal.relaxed.list>.item:not(:last-child){padding-right:1rem}.ui[class*="very relaxed"].list:not(.horizontal)>.item:not(:first-child){padding-top:.85714286em}.ui[class*="very relaxed"].list:not(.horizontal)>.item:not(:last-child){padding-bottom:.85714286em}.ui.horizontal[class*="very relaxed"].list .list>.item:not(:first-child),.ui.horizontal[class*="very relaxed"].list>.item:not(:first-child){padding-left:1.5rem}.ui.horizontal[class*="very relaxed"].list .list>.item:not(:last-child),.ui.horizontal[class*="very relaxed"].list>.item:not(:last-child){padding-right:1.5rem}.ui.mini.list{font-size:.78571429em}.ui.tiny.list{font-size:.85714286em}.ui.small.list{font-size:.92857143em}.ui.list{font-size:1em}.ui.large.list{font-size:1.14285714em}.ui.big.list{font-size:1.28571429em}.ui.huge.list{font-size:1.42857143em}.ui.massive.list{font-size:1.71428571em}.ui.mini.horizontal.list .list>.item,.ui.mini.horizontal.list>.item{font-size:.78571429rem}.ui.tiny.horizontal.list .list>.item,.ui.tiny.horizontal.list>.item{font-size:.85714286rem}.ui.small.horizontal.list .list>.item,.ui.small.horizontal.list>.item{font-size:.92857143rem}.ui.horizontal.list .list>.item,.ui.horizontal.list>.item{font-size:1rem}.ui.large.horizontal.list .list>.item,.ui.large.horizontal.list>.item{font-size:1.14285714rem}.ui.big.horizontal.list .list>.item,.ui.big.horizontal.list>.item{font-size:1.28571429rem}.ui.huge.horizontal.list .list>.item,.ui.huge.horizontal.list>.item{font-size:1.42857143rem}.ui.massive.horizontal.list .list>.item,.ui.massive.horizontal.list>.item{font-size:1.71428571rem}/*!
+ * # Semantic UI 2.4.2 - Loader
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.loader{display:none;position:absolute;top:50%;left:50%;margin:0;text-align:center;z-index:1000;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%)}.ui.loader:before{position:absolute;content:'';top:0;left:50%;width:100%;height:100%;border-radius:500rem;border:.2em solid rgba(0,0,0,.1)}.ui.loader:after{position:absolute;content:'';top:0;left:50%;width:100%;height:100%;-webkit-animation:loader .6s linear;animation:loader .6s linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;border-radius:500rem;border-color:#767676 transparent transparent;border-style:solid;border-width:.2em;-webkit-box-shadow:0 0 0 1px transparent;box-shadow:0 0 0 1px transparent}@-webkit-keyframes loader{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes loader{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.ui.mini.loader:after,.ui.mini.loader:before{width:1rem;height:1rem;margin:0 0 0 -.5rem}.ui.tiny.loader:after,.ui.tiny.loader:before{width:1.14285714rem;height:1.14285714rem;margin:0 0 0 -.57142857rem}.ui.small.loader:after,.ui.small.loader:before{width:1.71428571rem;height:1.71428571rem;margin:0 0 0 -.85714286rem}.ui.loader:after,.ui.loader:before{width:2.28571429rem;height:2.28571429rem;margin:0 0 0 -1.14285714rem}.ui.large.loader:after,.ui.large.loader:before{width:3.42857143rem;height:3.42857143rem;margin:0 0 0 -1.71428571rem}.ui.big.loader:after,.ui.big.loader:before{width:3.71428571rem;height:3.71428571rem;margin:0 0 0 -1.85714286rem}.ui.huge.loader:after,.ui.huge.loader:before{width:4.14285714rem;height:4.14285714rem;margin:0 0 0 -2.07142857rem}.ui.massive.loader:after,.ui.massive.loader:before{width:4.57142857rem;height:4.57142857rem;margin:0 0 0 -2.28571429rem}.ui.dimmer .loader{display:block}.ui.dimmer .ui.loader{color:rgba(255,255,255,.9)}.ui.dimmer .ui.loader:before{border-color:rgba(255,255,255,.15)}.ui.dimmer .ui.loader:after{border-color:#fff transparent transparent}.ui.inverted.dimmer .ui.loader{color:rgba(0,0,0,.87)}.ui.inverted.dimmer .ui.loader:before{border-color:rgba(0,0,0,.1)}.ui.inverted.dimmer .ui.loader:after{border-color:#767676 transparent transparent}.ui.text.loader{width:auto!important;height:auto!important;text-align:center;font-style:normal}.ui.indeterminate.loader:after{animation-direction:reverse;-webkit-animation-duration:1.2s;animation-duration:1.2s}.ui.loader.active,.ui.loader.visible{display:block}.ui.loader.disabled,.ui.loader.hidden{display:none}.ui.inverted.dimmer .ui.mini.loader,.ui.mini.loader{width:1rem;height:1rem;font-size:.78571429em}.ui.inverted.dimmer .ui.tiny.loader,.ui.tiny.loader{width:1.14285714rem;height:1.14285714rem;font-size:.85714286em}.ui.inverted.dimmer .ui.small.loader,.ui.small.loader{width:1.71428571rem;height:1.71428571rem;font-size:.92857143em}.ui.inverted.dimmer .ui.loader,.ui.loader{width:2.28571429rem;height:2.28571429rem;font-size:1em}.ui.inverted.dimmer .ui.large.loader,.ui.large.loader{width:3.42857143rem;height:3.42857143rem;font-size:1.14285714em}.ui.big.loader,.ui.inverted.dimmer .ui.big.loader{width:3.71428571rem;height:3.71428571rem;font-size:1.28571429em}.ui.huge.loader,.ui.inverted.dimmer .ui.huge.loader{width:4.14285714rem;height:4.14285714rem;font-size:1.42857143em}.ui.inverted.dimmer .ui.massive.loader,.ui.massive.loader{width:4.57142857rem;height:4.57142857rem;font-size:1.71428571em}.ui.mini.text.loader{min-width:1rem;padding-top:1.78571429rem}.ui.tiny.text.loader{min-width:1.14285714rem;padding-top:1.92857143rem}.ui.small.text.loader{min-width:1.71428571rem;padding-top:2.5rem}.ui.text.loader{min-width:2.28571429rem;padding-top:3.07142857rem}.ui.large.text.loader{min-width:3.42857143rem;padding-top:4.21428571rem}.ui.big.text.loader{min-width:3.71428571rem;padding-top:4.5rem}.ui.huge.text.loader{min-width:4.14285714rem;padding-top:4.92857143rem}.ui.massive.text.loader{min-width:4.57142857rem;padding-top:5.35714286rem}.ui.inverted.loader{color:rgba(255,255,255,.9)}.ui.inverted.loader:before{border-color:rgba(255,255,255,.15)}.ui.inverted.loader:after{border-top-color:#fff}.ui.inline.loader{position:relative;vertical-align:middle;margin:0;left:0;top:0;-webkit-transform:none;transform:none}.ui.inline.loader.active,.ui.inline.loader.visible{display:inline-block}.ui.centered.inline.loader.active,.ui.centered.inline.loader.visible{display:block;margin-left:auto;margin-right:auto}/*!
+ * # Semantic UI 2.4.2 - Loader
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.placeholder{position:static;overflow:hidden;-webkit-animation:placeholderShimmer 2s linear;animation:placeholderShimmer 2s linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;background-color:#fff;background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.08)),color-stop(15%,rgba(0,0,0,.15)),color-stop(30%,rgba(0,0,0,.08)));background-image:-webkit-linear-gradient(left,rgba(0,0,0,.08) 0,rgba(0,0,0,.15) 15%,rgba(0,0,0,.08) 30%);background-image:linear-gradient(to right,rgba(0,0,0,.08) 0,rgba(0,0,0,.15) 15%,rgba(0,0,0,.08) 30%);background-size:1200px 100%;max-width:30rem}@-webkit-keyframes placeholderShimmer{0%{background-position:-1200px 0}100%{background-position:1200px 0}}@keyframes placeholderShimmer{0%{background-position:-1200px 0}100%{background-position:1200px 0}}.ui.placeholder+.ui.placeholder{margin-top:2rem}.ui.placeholder+.ui.placeholder{-webkit-animation-delay:.15s;animation-delay:.15s}.ui.placeholder+.ui.placeholder+.ui.placeholder{-webkit-animation-delay:.3s;animation-delay:.3s}.ui.placeholder+.ui.placeholder+.ui.placeholder+.ui.placeholder{-webkit-animation-delay:.45s;animation-delay:.45s}.ui.placeholder+.ui.placeholder+.ui.placeholder+.ui.placeholder+.ui.placeholder{-webkit-animation-delay:.6s;animation-delay:.6s}.ui.placeholder,.ui.placeholder .image.header:after,.ui.placeholder .line,.ui.placeholder .line:after,.ui.placeholder>:before{background-color:#fff}.ui.placeholder .image:not(.header):not(.ui){height:100px}.ui.placeholder .square.image:not(.header){height:0;overflow:hidden;padding-top:100%}.ui.placeholder .rectangular.image:not(.header){height:0;overflow:hidden;padding-top:75%}.ui.placeholder .line{position:relative;height:.85714286em}.ui.placeholder .line:after,.ui.placeholder .line:before{top:100%;position:absolute;content:'';background-color:inherit}.ui.placeholder .line:before{left:0}.ui.placeholder .line:after{right:0}.ui.placeholder .line{margin-bottom:.5em}.ui.placeholder .line:after,.ui.placeholder .line:before{height:.5em}.ui.placeholder .line:not(:first-child){margin-top:.5em}.ui.placeholder .header{position:relative;overflow:hidden}.ui.placeholder .line:nth-child(1):after{width:0%}.ui.placeholder .line:nth-child(2):after{width:50%}.ui.placeholder .line:nth-child(3):after{width:10%}.ui.placeholder .line:nth-child(4):after{width:35%}.ui.placeholder .line:nth-child(5):after{width:65%}.ui.placeholder .header .line{margin-bottom:.64285714em}.ui.placeholder .header .line:after,.ui.placeholder .header .line:before{height:.64285714em}.ui.placeholder .header .line:not(:first-child){margin-top:.64285714em}.ui.placeholder .header .line:after{width:20%}.ui.placeholder .header .line:nth-child(2):after{width:60%}.ui.placeholder .image.header .line{margin-left:3em}.ui.placeholder .image.header .line:before{width:.71428571rem}.ui.placeholder .image.header:after{display:block;height:.85714286em;content:'';margin-left:3em}.ui.placeholder .header .line:first-child,.ui.placeholder .image .line:first-child,.ui.placeholder .paragraph .line:first-child{height:.01px}.ui.placeholder .header:not(:first-child):before,.ui.placeholder .image:not(:first-child):before,.ui.placeholder .paragraph:not(:first-child):before{height:1.42857143em;content:'';display:block}.ui.inverted.placeholder{background-image:-webkit-gradient(linear,left top,right top,from(rgba(255,255,255,.08)),color-stop(15%,rgba(255,255,255,.14)),color-stop(30%,rgba(255,255,255,.08)));background-image:-webkit-linear-gradient(left,rgba(255,255,255,.08) 0,rgba(255,255,255,.14) 15%,rgba(255,255,255,.08) 30%);background-image:linear-gradient(to right,rgba(255,255,255,.08) 0,rgba(255,255,255,.14) 15%,rgba(255,255,255,.08) 30%)}.ui.inverted.placeholder,.ui.inverted.placeholder .image.header:after,.ui.inverted.placeholder .line,.ui.inverted.placeholder .line:after,.ui.inverted.placeholder>:before{background-color:#1b1c1d}.ui.placeholder .full.line.line.line:after{width:0%}.ui.placeholder .very.long.line.line.line:after{width:10%}.ui.placeholder .long.line.line.line:after{width:35%}.ui.placeholder .medium.line.line.line:after{width:50%}.ui.placeholder .short.line.line.line:after{width:65%}.ui.placeholder .very.short.line.line.line:after{width:80%}.ui.fluid.placeholder{max-width:none}/*!
+ * # Semantic UI 2.4.2 - Rail
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.rail{position:absolute;top:0;width:300px;height:100%}.ui.left.rail{left:auto;right:100%;padding:0 2rem 0 0;margin:0 2rem 0 0}.ui.right.rail{left:100%;right:auto;padding:0 0 0 2rem;margin:0 0 0 2rem}.ui.left.internal.rail{left:0;right:auto;padding:0 0 0 2rem;margin:0 0 0 2rem}.ui.right.internal.rail{left:auto;right:0;padding:0 2rem 0 0;margin:0 2rem 0 0}.ui.dividing.rail{width:302.5px}.ui.left.dividing.rail{padding:0 2.5rem 0 0;margin:0 2.5rem 0 0;border-right:1px solid rgba(34,36,38,.15)}.ui.right.dividing.rail{border-left:1px solid rgba(34,36,38,.15);padding:0 0 0 2.5rem;margin:0 0 0 2.5rem}.ui.close.rail{width:calc(300px + 1em)}.ui.close.left.rail{padding:0 1em 0 0;margin:0 1em 0 0}.ui.close.right.rail{padding:0 0 0 1em;margin:0 0 0 1em}.ui.very.close.rail{width:calc(300px + .5em)}.ui.very.close.left.rail{padding:0 .5em 0 0;margin:0 .5em 0 0}.ui.very.close.right.rail{padding:0 0 0 .5em;margin:0 0 0 .5em}.ui.attached.left.rail,.ui.attached.right.rail{padding:0;margin:0}.ui.mini.rail{font-size:.78571429rem}.ui.tiny.rail{font-size:.85714286rem}.ui.small.rail{font-size:.92857143rem}.ui.rail{font-size:1rem}.ui.large.rail{font-size:1.14285714rem}.ui.big.rail{font-size:1.28571429rem}.ui.huge.rail{font-size:1.42857143rem}.ui.massive.rail{font-size:1.71428571rem}/*!
+ * # Semantic UI 2.4.2 - Reveal
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.reveal{display:inherit;position:relative!important;font-size:0!important}.ui.reveal>.visible.content{position:absolute!important;top:0!important;left:0!important;z-index:3!important;-webkit-transition:all .5s ease .1s;transition:all .5s ease .1s}.ui.reveal>.hidden.content{position:relative!important;z-index:2!important}.ui.active.reveal .visible.content,.ui.reveal:hover .visible.content{z-index:4!important}.ui.slide.reveal{position:relative!important;overflow:hidden!important;white-space:nowrap}.ui.slide.reveal>.content{display:block;width:100%;white-space:normal;float:left;margin:0;-webkit-transition:-webkit-transform .5s ease .1s;transition:-webkit-transform .5s ease .1s;transition:transform .5s ease .1s;transition:transform .5s ease .1s,-webkit-transform .5s ease .1s}.ui.slide.reveal>.visible.content{position:relative!important}.ui.slide.reveal>.hidden.content{position:absolute!important;left:0!important;width:100%!important;-webkit-transform:translateX(100%)!important;transform:translateX(100%)!important}.ui.slide.active.reveal>.visible.content,.ui.slide.reveal:hover>.visible.content{-webkit-transform:translateX(-100%)!important;transform:translateX(-100%)!important}.ui.slide.active.reveal>.hidden.content,.ui.slide.reveal:hover>.hidden.content{-webkit-transform:translateX(0)!important;transform:translateX(0)!important}.ui.slide.right.reveal>.visible.content{-webkit-transform:translateX(0)!important;transform:translateX(0)!important}.ui.slide.right.reveal>.hidden.content{-webkit-transform:translateX(-100%)!important;transform:translateX(-100%)!important}.ui.slide.right.active.reveal>.visible.content,.ui.slide.right.reveal:hover>.visible.content{-webkit-transform:translateX(100%)!important;transform:translateX(100%)!important}.ui.slide.right.active.reveal>.hidden.content,.ui.slide.right.reveal:hover>.hidden.content{-webkit-transform:translateX(0)!important;transform:translateX(0)!important}.ui.slide.up.reveal>.hidden.content{-webkit-transform:translateY(100%)!important;transform:translateY(100%)!important}.ui.slide.up.active.reveal>.visible.content,.ui.slide.up.reveal:hover>.visible.content{-webkit-transform:translateY(-100%)!important;transform:translateY(-100%)!important}.ui.slide.up.active.reveal>.hidden.content,.ui.slide.up.reveal:hover>.hidden.content{-webkit-transform:translateY(0)!important;transform:translateY(0)!important}.ui.slide.down.reveal>.hidden.content{-webkit-transform:translateY(-100%)!important;transform:translateY(-100%)!important}.ui.slide.down.active.reveal>.visible.content,.ui.slide.down.reveal:hover>.visible.content{-webkit-transform:translateY(100%)!important;transform:translateY(100%)!important}.ui.slide.down.active.reveal>.hidden.content,.ui.slide.down.reveal:hover>.hidden.content{-webkit-transform:translateY(0)!important;transform:translateY(0)!important}.ui.fade.reveal>.visible.content{opacity:1}.ui.fade.active.reveal>.visible.content,.ui.fade.reveal:hover>.visible.content{opacity:0}.ui.move.reveal{position:relative!important;overflow:hidden!important;white-space:nowrap}.ui.move.reveal>.content{display:block;float:left;white-space:normal;margin:0;-webkit-transition:-webkit-transform .5s cubic-bezier(.175,.885,.32,1) .1s;transition:-webkit-transform .5s cubic-bezier(.175,.885,.32,1) .1s;transition:transform .5s cubic-bezier(.175,.885,.32,1) .1s;transition:transform .5s cubic-bezier(.175,.885,.32,1) .1s,-webkit-transform .5s cubic-bezier(.175,.885,.32,1) .1s}.ui.move.reveal>.visible.content{position:relative!important}.ui.move.reveal>.hidden.content{position:absolute!important;left:0!important;width:100%!important}.ui.move.active.reveal>.visible.content,.ui.move.reveal:hover>.visible.content{-webkit-transform:translateX(-100%)!important;transform:translateX(-100%)!important}.ui.move.right.active.reveal>.visible.content,.ui.move.right.reveal:hover>.visible.content{-webkit-transform:translateX(100%)!important;transform:translateX(100%)!important}.ui.move.up.active.reveal>.visible.content,.ui.move.up.reveal:hover>.visible.content{-webkit-transform:translateY(-100%)!important;transform:translateY(-100%)!important}.ui.move.down.active.reveal>.visible.content,.ui.move.down.reveal:hover>.visible.content{-webkit-transform:translateY(100%)!important;transform:translateY(100%)!important}.ui.rotate.reveal>.visible.content{-webkit-transition-duration:.5s;transition-duration:.5s;-webkit-transform:rotate(0);transform:rotate(0)}.ui.rotate.reveal>.visible.content,.ui.rotate.right.reveal>.visible.content{-webkit-transform-origin:bottom right;transform-origin:bottom right}.ui.rotate.active.reveal>.visible.content,.ui.rotate.reveal:hover>.visible.content,.ui.rotate.right.active.reveal>.visible.content,.ui.rotate.right.reveal:hover>.visible.content{-webkit-transform:rotate(110deg);transform:rotate(110deg)}.ui.rotate.left.reveal>.visible.content{-webkit-transform-origin:bottom left;transform-origin:bottom left}.ui.rotate.left.active.reveal>.visible.content,.ui.rotate.left.reveal:hover>.visible.content{-webkit-transform:rotate(-110deg);transform:rotate(-110deg)}.ui.disabled.reveal:hover>.visible.visible.content{position:static!important;display:block!important;opacity:1!important;top:0!important;left:0!important;right:auto!important;bottom:auto!important;-webkit-transform:none!important;transform:none!important}.ui.disabled.reveal:hover>.hidden.hidden.content{display:none!important}.ui.reveal>.ui.ribbon.label{z-index:5}.ui.visible.reveal{overflow:visible}.ui.instant.reveal>.content{-webkit-transition-delay:0s!important;transition-delay:0s!important}.ui.reveal>.content{font-size:1rem!important}/*!
+ * # Semantic UI 2.4.2 - Segment
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.segment{position:relative;background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15);box-shadow:0 1px 2px 0 rgba(34,36,38,.15);margin:1rem 0;padding:1em 1em;border-radius:.28571429rem;border:1px solid rgba(34,36,38,.15)}.ui.segment:first-child{margin-top:0}.ui.segment:last-child{margin-bottom:0}.ui.vertical.segment{margin:0;padding-left:0;padding-right:0;background:none transparent;border-radius:0;-webkit-box-shadow:none;box-shadow:none;border:none;border-bottom:1px solid rgba(34,36,38,.15)}.ui.vertical.segment:last-child{border-bottom:none}.ui.inverted.segment>.ui.header{color:#fff}.ui[class*="bottom attached"].segment>[class*="top attached"].label{border-top-left-radius:0;border-top-right-radius:0}.ui[class*="top attached"].segment>[class*="bottom attached"].label{border-bottom-left-radius:0;border-bottom-right-radius:0}.ui.attached.segment:not(.top):not(.bottom)>[class*="top attached"].label{border-top-left-radius:0;border-top-right-radius:0}.ui.attached.segment:not(.top):not(.bottom)>[class*="bottom attached"].label{border-bottom-left-radius:0;border-bottom-right-radius:0}.ui.grid>.row>.ui.segment.column,.ui.grid>.ui.segment.column,.ui.page.grid.segment{padding-top:2em;padding-bottom:2em}.ui.grid.segment{margin:1rem 0;border-radius:.28571429rem}.ui.basic.table.segment{background:#fff;border:1px solid rgba(34,36,38,.15);-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15);box-shadow:0 1px 2px 0 rgba(34,36,38,.15)}.ui[class*="very basic"].table.segment{padding:1em 1em}.ui.placeholder.segment{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;max-width:initial;-webkit-animation:none;animation:none;overflow:visible;padding:1em 1em;min-height:18rem;background:#f9fafb;border-color:rgba(34,36,38,.15);-webkit-box-shadow:0 2px 25px 0 rgba(34,36,38,.05) inset;box-shadow:0 2px 25px 0 rgba(34,36,38,.05) inset}.ui.placeholder.segment .button,.ui.placeholder.segment textarea{display:block}.ui.placeholder.segment .button,.ui.placeholder.segment .field,.ui.placeholder.segment textarea,.ui.placeholder.segment>.ui.input{max-width:15rem;margin-left:auto;margin-right:auto}.ui.placeholder.segment .column .button,.ui.placeholder.segment .column .field,.ui.placeholder.segment .column textarea,.ui.placeholder.segment .column>.ui.input{max-width:15rem;margin-left:auto;margin-right:auto}.ui.placeholder.segment>.inline{-ms-flex-item-align:center;align-self:center}.ui.placeholder.segment>.inline>.button{display:inline-block;width:auto;margin:0 .35714286rem 0 0}.ui.placeholder.segment>.inline>.button:last-child{margin-right:0}.ui.piled.segment,.ui.piled.segments{margin:3em 0;-webkit-box-shadow:'';box-shadow:'';z-index:auto}.ui.piled.segment:first-child{margin-top:0}.ui.piled.segment:last-child{margin-bottom:0}.ui.piled.segment:after,.ui.piled.segment:before,.ui.piled.segments:after,.ui.piled.segments:before{background-color:#fff;visibility:visible;content:'';display:block;height:100%;left:0;position:absolute;width:100%;border:1px solid rgba(34,36,38,.15);-webkit-box-shadow:'';box-shadow:''}.ui.piled.segment:before,.ui.piled.segments:before{-webkit-transform:rotate(-1.2deg);transform:rotate(-1.2deg);top:0;z-index:-2}.ui.piled.segment:after,.ui.piled.segments:after{-webkit-transform:rotate(1.2deg);transform:rotate(1.2deg);top:0;z-index:-1}.ui[class*="top attached"].piled.segment{margin-top:3em;margin-bottom:0}.ui.piled.segment[class*="top attached"]:first-child{margin-top:0}.ui.piled.segment[class*="bottom attached"]{margin-top:0;margin-bottom:3em}.ui.piled.segment[class*="bottom attached"]:last-child{margin-bottom:0}.ui.stacked.segment{padding-bottom:1.4em}.ui.stacked.segment:after,.ui.stacked.segment:before,.ui.stacked.segments:after,.ui.stacked.segments:before{content:'';position:absolute;bottom:-3px;left:0;border-top:1px solid rgba(34,36,38,.15);background:rgba(0,0,0,.03);width:100%;height:6px;visibility:visible}.ui.stacked.segment:before,.ui.stacked.segments:before{display:none}.ui.tall.stacked.segment:before,.ui.tall.stacked.segments:before{display:block;bottom:0}.ui.stacked.inverted.segment:after,.ui.stacked.inverted.segment:before,.ui.stacked.inverted.segments:after,.ui.stacked.inverted.segments:before{background-color:rgba(0,0,0,.03);border-top:1px solid rgba(34,36,38,.35)}.ui.padded.segment{padding:1.5em}.ui[class*="very padded"].segment{padding:3em}.ui.padded.segment.vertical.segment,.ui[class*="very padded"].vertical.segment{padding-left:0;padding-right:0}.ui.compact.segment{display:table}.ui.compact.segments{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.ui.compact.segments .segment,.ui.segments .compact.segment{display:block;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto}.ui.circular.segment{display:table-cell;padding:2em;text-align:center;vertical-align:middle;border-radius:500em}.ui.raised.segment,.ui.raised.segments{-webkit-box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15);box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15)}.ui.segments{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;position:relative;margin:1rem 0;border:1px solid rgba(34,36,38,.15);-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15);box-shadow:0 1px 2px 0 rgba(34,36,38,.15);border-radius:.28571429rem}.ui.segments:first-child{margin-top:0}.ui.segments:last-child{margin-bottom:0}.ui.segments>.segment{top:0;bottom:0;border-radius:0;margin:0;width:auto;-webkit-box-shadow:none;box-shadow:none;border:none;border-top:1px solid rgba(34,36,38,.15)}.ui.segments:not(.horizontal)>.segment:first-child{border-top:none;margin-top:0;bottom:0;margin-bottom:0;top:0;border-radius:.28571429rem .28571429rem 0 0}.ui.segments:not(.horizontal)>.segment:last-child{top:0;bottom:0;margin-top:0;margin-bottom:0;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),none;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),none;border-radius:0 0 .28571429rem .28571429rem}.ui.segments:not(.horizontal)>.segment:only-child{border-radius:.28571429rem}.ui.segments>.ui.segments{border-top:1px solid rgba(34,36,38,.15);margin:1rem 1rem}.ui.segments>.segments:first-child{border-top:none}.ui.segments>.segment+.segments:not(.horizontal){margin-top:0}.ui.horizontal.segments{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;background-color:transparent;border-radius:0;padding:0;background-color:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15);box-shadow:0 1px 2px 0 rgba(34,36,38,.15);margin:1rem 0;border-radius:.28571429rem;border:1px solid rgba(34,36,38,.15)}.ui.segments>.horizontal.segments{margin:0;background-color:transparent;border-radius:0;border:none;-webkit-box-shadow:none;box-shadow:none;border-top:1px solid rgba(34,36,38,.15)}.ui.horizontal.segments>.segment{-webkit-box-flex:1;flex:1 1 auto;-ms-flex:1 1 0px;margin:0;min-width:0;background-color:transparent;border-radius:0;border:none;-webkit-box-shadow:none;box-shadow:none;border-left:1px solid rgba(34,36,38,.15)}.ui.segments>.horizontal.segments:first-child{border-top:none}.ui.horizontal.segments>.segment:first-child{border-left:none}.ui.disabled.segment{opacity:.45;color:rgba(40,40,40,.3)}.ui.loading.segment{position:relative;cursor:default;pointer-events:none;text-shadow:none!important;color:transparent!important;-webkit-transition:all 0s linear;transition:all 0s linear}.ui.loading.segment:before{position:absolute;content:'';top:0;left:0;background:rgba(255,255,255,.8);width:100%;height:100%;border-radius:.28571429rem;z-index:100}.ui.loading.segment:after{position:absolute;content:'';top:50%;left:50%;margin:-1.5em 0 0 -1.5em;width:3em;height:3em;-webkit-animation:segment-spin .6s linear;animation:segment-spin .6s linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;border-radius:500rem;border-color:#767676 rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.1);border-style:solid;border-width:.2em;-webkit-box-shadow:0 0 0 1px transparent;box-shadow:0 0 0 1px transparent;visibility:visible;z-index:101}@-webkit-keyframes segment-spin{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes segment-spin{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.ui.basic.segment{background:none transparent;-webkit-box-shadow:none;box-shadow:none;border:none;border-radius:0}.ui.clearing.segment:after{content:".";display:block;height:0;clear:both;visibility:hidden}.ui.red.segment:not(.inverted){border-top:2px solid #db2828!important}.ui.inverted.red.segment{background-color:#db2828!important;color:#fff!important}.ui.orange.segment:not(.inverted){border-top:2px solid #f2711c!important}.ui.inverted.orange.segment{background-color:#f2711c!important;color:#fff!important}.ui.yellow.segment:not(.inverted){border-top:2px solid #fbbd08!important}.ui.inverted.yellow.segment{background-color:#fbbd08!important;color:#fff!important}.ui.olive.segment:not(.inverted){border-top:2px solid #b5cc18!important}.ui.inverted.olive.segment{background-color:#b5cc18!important;color:#fff!important}.ui.green.segment:not(.inverted){border-top:2px solid #21ba45!important}.ui.inverted.green.segment{background-color:#21ba45!important;color:#fff!important}.ui.teal.segment:not(.inverted){border-top:2px solid #00b5ad!important}.ui.inverted.teal.segment{background-color:#00b5ad!important;color:#fff!important}.ui.blue.segment:not(.inverted){border-top:2px solid #2185d0!important}.ui.inverted.blue.segment{background-color:#2185d0!important;color:#fff!important}.ui.violet.segment:not(.inverted){border-top:2px solid #6435c9!important}.ui.inverted.violet.segment{background-color:#6435c9!important;color:#fff!important}.ui.purple.segment:not(.inverted){border-top:2px solid #a333c8!important}.ui.inverted.purple.segment{background-color:#a333c8!important;color:#fff!important}.ui.pink.segment:not(.inverted){border-top:2px solid #e03997!important}.ui.inverted.pink.segment{background-color:#e03997!important;color:#fff!important}.ui.brown.segment:not(.inverted){border-top:2px solid #a5673f!important}.ui.inverted.brown.segment{background-color:#a5673f!important;color:#fff!important}.ui.grey.segment:not(.inverted){border-top:2px solid #767676!important}.ui.inverted.grey.segment{background-color:#767676!important;color:#fff!important}.ui.black.segment:not(.inverted){border-top:2px solid #1b1c1d!important}.ui.inverted.black.segment{background-color:#1b1c1d!important;color:#fff!important}.ui[class*="left aligned"].segment{text-align:left}.ui[class*="right aligned"].segment{text-align:right}.ui[class*="center aligned"].segment{text-align:center}.ui.floated.segment,.ui[class*="left floated"].segment{float:left;margin-right:1em}.ui[class*="right floated"].segment{float:right;margin-left:1em}.ui.inverted.segment{border:none;-webkit-box-shadow:none;box-shadow:none}.ui.inverted.segment,.ui.primary.inverted.segment{background:#1b1c1d;color:rgba(255,255,255,.9)}.ui.inverted.segment .segment{color:rgba(0,0,0,.87)}.ui.inverted.segment .inverted.segment{color:rgba(255,255,255,.9)}.ui.inverted.attached.segment{border-color:#555}.ui.secondary.segment{background:#f3f4f5;color:rgba(0,0,0,.6)}.ui.secondary.inverted.segment{background:#4c4f52 -webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,.2)),to(rgba(255,255,255,.2)));background:#4c4f52 -webkit-linear-gradient(rgba(255,255,255,.2) 0,rgba(255,255,255,.2) 100%);background:#4c4f52 linear-gradient(rgba(255,255,255,.2) 0,rgba(255,255,255,.2) 100%);color:rgba(255,255,255,.8)}.ui.tertiary.segment{background:#dcddde;color:rgba(0,0,0,.6)}.ui.tertiary.inverted.segment{background:#717579 -webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,.35)),to(rgba(255,255,255,.35)));background:#717579 -webkit-linear-gradient(rgba(255,255,255,.35) 0,rgba(255,255,255,.35) 100%);background:#717579 linear-gradient(rgba(255,255,255,.35) 0,rgba(255,255,255,.35) 100%);color:rgba(255,255,255,.8)}.ui.attached.segment{top:0;bottom:0;border-radius:0;margin:0 -1px;width:calc(100% - (-1px * 2));max-width:calc(100% - (-1px * 2));-webkit-box-shadow:none;box-shadow:none;border:1px solid #d4d4d5}.ui.attached:not(.message)+.ui.attached.segment:not(.top){border-top:none}.ui[class*="top attached"].segment{bottom:0;margin-bottom:0;top:0;margin-top:1rem;border-radius:.28571429rem .28571429rem 0 0}.ui.segment[class*="top attached"]:first-child{margin-top:0}.ui.segment[class*="bottom attached"]{bottom:0;margin-top:0;top:0;margin-bottom:1rem;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),none;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),none;border-radius:0 0 .28571429rem .28571429rem}.ui.segment[class*="bottom attached"]:last-child{margin-bottom:0}.ui.mini.segment,.ui.mini.segments .segment{font-size:.78571429rem}.ui.tiny.segment,.ui.tiny.segments .segment{font-size:.85714286rem}.ui.small.segment,.ui.small.segments .segment{font-size:.92857143rem}.ui.segment,.ui.segments .segment{font-size:1rem}.ui.large.segment,.ui.large.segments .segment{font-size:1.14285714rem}.ui.big.segment,.ui.big.segments .segment{font-size:1.28571429rem}.ui.huge.segment,.ui.huge.segments .segment{font-size:1.42857143rem}.ui.massive.segment,.ui.massive.segments .segment{font-size:1.71428571rem}/*!
+ * # Semantic UI 2.4.2 - Step
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.steps{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;margin:1em 0;background:'';-webkit-box-shadow:none;box-shadow:none;line-height:1.14285714em;border-radius:.28571429rem;border:1px solid rgba(34,36,38,.15)}.ui.steps:first-child{margin-top:0}.ui.steps:last-child{margin-bottom:0}.ui.steps .step{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;vertical-align:middle;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin:0 0;padding:1.14285714em 2em;background:#fff;color:rgba(0,0,0,.87);-webkit-box-shadow:none;box-shadow:none;border-radius:0;border:none;border-right:1px solid rgba(34,36,38,.15);-webkit-transition:background-color .1s ease,opacity .1s ease,color .1s ease,-webkit-box-shadow .1s ease;transition:background-color .1s ease,opacity .1s ease,color .1s ease,-webkit-box-shadow .1s ease;transition:background-color .1s ease,opacity .1s ease,color .1s ease,box-shadow .1s ease;transition:background-color .1s ease,opacity .1s ease,color .1s ease,box-shadow .1s ease,-webkit-box-shadow .1s ease}.ui.steps .step:after{display:none;position:absolute;z-index:2;content:'';top:50%;right:0;border:medium none;background-color:#fff;width:1.14285714em;height:1.14285714em;border-style:solid;border-color:rgba(34,36,38,.15);border-width:0 1px 1px 0;-webkit-transition:background-color .1s ease,opacity .1s ease,color .1s ease,-webkit-box-shadow .1s ease;transition:background-color .1s ease,opacity .1s ease,color .1s ease,-webkit-box-shadow .1s ease;transition:background-color .1s ease,opacity .1s ease,color .1s ease,box-shadow .1s ease;transition:background-color .1s ease,opacity .1s ease,color .1s ease,box-shadow .1s ease,-webkit-box-shadow .1s ease;-webkit-transform:translateY(-50%) translateX(50%) rotate(-45deg);transform:translateY(-50%) translateX(50%) rotate(-45deg)}.ui.steps .step:first-child{padding-left:2em;border-radius:.28571429rem 0 0 .28571429rem}.ui.steps .step:last-child{border-radius:0 .28571429rem .28571429rem 0}.ui.steps .step:last-child{border-right:none;margin-right:0}.ui.steps .step:only-child{border-radius:.28571429rem}.ui.steps .step .title{font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-size:1.14285714em;font-weight:700}.ui.steps .step>.title{width:100%}.ui.steps .step .description{font-weight:400;font-size:.92857143em;color:rgba(0,0,0,.87)}.ui.steps .step>.description{width:100%}.ui.steps .step .title~.description{margin-top:.25em}.ui.steps .step>.icon{line-height:1;font-size:2.5em;margin:0 1rem 0 0}.ui.steps .step>.icon,.ui.steps .step>.icon~.content{display:block;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;-ms-flex-item-align:middle;align-self:middle}.ui.steps .step>.icon~.content{-webkit-box-flex:1 0 auto;-ms-flex-positive:1 0 auto;flex-grow:1 0 auto}.ui.steps:not(.vertical) .step>.icon{width:auto}.ui.steps .link.step,.ui.steps a.step{cursor:pointer}.ui.ordered.steps{counter-reset:ordered}.ui.ordered.steps .step:before{display:block;position:static;text-align:center;content:counters(ordered, ".");-ms-flex-item-align:middle;align-self:middle;margin-right:1rem;font-size:2.5em;counter-increment:ordered;font-family:inherit;font-weight:700}.ui.ordered.steps .step>*{display:block;-ms-flex-item-align:middle;align-self:middle}.ui.vertical.steps{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;overflow:visible}.ui.vertical.steps .step{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;border-radius:0;padding:1.14285714em 2em;border-right:none;border-bottom:1px solid rgba(34,36,38,.15)}.ui.vertical.steps .step:first-child{padding:1.14285714em 2em;border-radius:.28571429rem .28571429rem 0 0}.ui.vertical.steps .step:last-child{border-bottom:none;border-radius:0 0 .28571429rem .28571429rem}.ui.vertical.steps .step:only-child{border-radius:.28571429rem}.ui.vertical.steps .step:after{display:none}.ui.vertical.steps .step:after{top:50%;right:0;border-width:0 1px 1px 0}.ui.vertical.steps .step:after{display:none}.ui.vertical.steps .active.step:after{display:block}.ui.vertical.steps .step:last-child:after{display:none}.ui.vertical.steps .active.step:last-child:after{display:block}@media only screen and (max-width:767px){.ui.steps:not(.unstackable){display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;overflow:visible;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.ui.steps:not(.unstackable) .step{width:100%!important;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;border-radius:0;padding:1.14285714em 2em}.ui.steps:not(.unstackable) .step:first-child{padding:1.14285714em 2em;border-radius:.28571429rem .28571429rem 0 0}.ui.steps:not(.unstackable) .step:last-child{border-radius:0 0 .28571429rem .28571429rem}.ui.steps:not(.unstackable) .step:after{display:none!important}.ui.steps:not(.unstackable) .step .content{text-align:center}.ui.ordered.steps:not(.unstackable) .step:before,.ui.steps:not(.unstackable) .step>.icon{margin:0 0 1rem 0}}.ui.steps .link.step:hover,.ui.steps .link.step:hover::after,.ui.steps a.step:hover,.ui.steps a.step:hover::after{background:#f9fafb;color:rgba(0,0,0,.8)}.ui.steps .link.step:active,.ui.steps .link.step:active::after,.ui.steps a.step:active,.ui.steps a.step:active::after{background:#f3f4f5;color:rgba(0,0,0,.9)}.ui.steps .step.active{cursor:auto;background:#f3f4f5}.ui.steps .step.active:after{background:#f3f4f5}.ui.steps .step.active .title{color:#4183c4}.ui.ordered.steps .step.active:before,.ui.steps .active.step .icon{color:rgba(0,0,0,.85)}.ui.steps .step:after{display:block}.ui.steps .active.step:after{display:block}.ui.steps .step:last-child:after{display:none}.ui.steps .active.step:last-child:after{display:none}.ui.steps .link.active.step:hover,.ui.steps .link.active.step:hover::after,.ui.steps a.active.step:hover,.ui.steps a.active.step:hover::after{cursor:pointer;background:#dcddde;color:rgba(0,0,0,.87)}.ui.ordered.steps .step.completed:before,.ui.steps .step.completed>.icon:before{color:#21ba45}.ui.steps .disabled.step{cursor:auto;background:#fff;pointer-events:none}.ui.steps .disabled.step,.ui.steps .disabled.step .description,.ui.steps .disabled.step .title{color:rgba(40,40,40,.3)}.ui.steps .disabled.step:after{background:#fff}@media only screen and (max-width:991px){.ui[class*="tablet stackable"].steps{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;overflow:visible;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.ui[class*="tablet stackable"].steps .step{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;border-radius:0;padding:1.14285714em 2em}.ui[class*="tablet stackable"].steps .step:first-child{padding:1.14285714em 2em;border-radius:.28571429rem .28571429rem 0 0}.ui[class*="tablet stackable"].steps .step:last-child{border-radius:0 0 .28571429rem .28571429rem}.ui[class*="tablet stackable"].steps .step:after{display:none!important}.ui[class*="tablet stackable"].steps .step .content{text-align:center}.ui[class*="tablet stackable"].ordered.steps .step:before,.ui[class*="tablet stackable"].steps .step>.icon{margin:0 0 1rem 0}}.ui.fluid.steps{display:-webkit-box;display:-ms-flexbox;display:flex;width:100%}.ui.attached.steps{width:calc(100% + (--1px * 2))!important;margin:0 -1px 0;max-width:calc(100% + (--1px * 2));border-radius:.28571429rem .28571429rem 0 0}.ui.attached.steps .step:first-child{border-radius:.28571429rem 0 0 0}.ui.attached.steps .step:last-child{border-radius:0 .28571429rem 0 0}.ui.bottom.attached.steps{margin:0 -1px 0;border-radius:0 0 .28571429rem .28571429rem}.ui.bottom.attached.steps .step:first-child{border-radius:0 0 0 .28571429rem}.ui.bottom.attached.steps .step:last-child{border-radius:0 0 .28571429rem 0}.ui.eight.steps,.ui.five.steps,.ui.four.steps,.ui.one.steps,.ui.seven.steps,.ui.six.steps,.ui.three.steps,.ui.two.steps{width:100%}.ui.eight.steps>.step,.ui.five.steps>.step,.ui.four.steps>.step,.ui.one.steps>.step,.ui.seven.steps>.step,.ui.six.steps>.step,.ui.three.steps>.step,.ui.two.steps>.step{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.ui.one.steps>.step{width:100%}.ui.two.steps>.step{width:50%}.ui.three.steps>.step{width:33.333%}.ui.four.steps>.step{width:25%}.ui.five.steps>.step{width:20%}.ui.six.steps>.step{width:16.666%}.ui.seven.steps>.step{width:14.285%}.ui.eight.steps>.step{width:12.5%}.ui.mini.step,.ui.mini.steps .step{font-size:.78571429rem}.ui.tiny.step,.ui.tiny.steps .step{font-size:.85714286rem}.ui.small.step,.ui.small.steps .step{font-size:.92857143rem}.ui.step,.ui.steps .step{font-size:1rem}.ui.large.step,.ui.large.steps .step{font-size:1.14285714rem}.ui.big.step,.ui.big.steps .step{font-size:1.28571429rem}.ui.huge.step,.ui.huge.steps .step{font-size:1.42857143rem}.ui.massive.step,.ui.massive.steps .step{font-size:1.71428571rem}@font-face{font-family:Step;src:url(data:application/x-font-ttf;charset=utf-8;;base64,AAEAAAAOAIAAAwBgT1MvMj3hSQEAAADsAAAAVmNtYXDQEhm3AAABRAAAAUpjdnQgBkn/lAAABuwAAAAcZnBnbYoKeDsAAAcIAAAJkWdhc3AAAAAQAAAG5AAAAAhnbHlm32cEdgAAApAAAAC2aGVhZAErPHsAAANIAAAANmhoZWEHUwNNAAADgAAAACRobXR4CykAAAAAA6QAAAAMbG9jYQA4AFsAAAOwAAAACG1heHAApgm8AAADuAAAACBuYW1lzJ0aHAAAA9gAAALNcG9zdK69QJgAAAaoAAAAO3ByZXCSoZr/AAAQnAAAAFYAAQO4AZAABQAIAnoCvAAAAIwCegK8AAAB4AAxAQIAAAIABQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUGZFZABA6ADoAQNS/2oAWgMLAE8AAAABAAAAAAAAAAAAAwAAAAMAAAAcAAEAAAAAAEQAAwABAAAAHAAEACgAAAAGAAQAAQACAADoAf//AAAAAOgA//8AABgBAAEAAAAAAAAAAAEGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAADpAKYABUAHEAZDwEAAQFCAAIBAmoAAQABagAAAGEUFxQDEisBFAcBBiInASY0PwE2Mh8BATYyHwEWA6QP/iAQLBD+6g8PTBAsEKQBbhAsEEwPAhYWEP4gDw8BFhAsEEwQEKUBbxAQTBAAAAH//f+xA18DCwAMABJADwABAQpDAAAACwBEFRMCESsBFA4BIi4CPgEyHgEDWXLG6MhuBnq89Lp+AV51xHR0xOrEdHTEAAAAAAEAAAABAADDeRpdXw889QALA+gAAAAAzzWYjQAAAADPNWBN//3/sQOkAwsAAAAIAAIAAAAAAAAAAQAAA1L/agBaA+gAAP/3A6QAAQAAAAAAAAAAAAAAAAAAAAMD6AAAA+gAAANZAAAAAAAAADgAWwABAAAAAwAWAAEAAAAAAAIABgATAG4AAAAtCZEAAAAAAAAAEgDeAAEAAAAAAAAANQAAAAEAAAAAAAEACAA1AAEAAAAAAAIABwA9AAEAAAAAAAMACABEAAEAAAAAAAQACABMAAEAAAAAAAUACwBUAAEAAAAAAAYACABfAAEAAAAAAAoAKwBnAAEAAAAAAAsAEwCSAAMAAQQJAAAAagClAAMAAQQJAAEAEAEPAAMAAQQJAAIADgEfAAMAAQQJAAMAEAEtAAMAAQQJAAQAEAE9AAMAAQQJAAUAFgFNAAMAAQQJAAYAEAFjAAMAAQQJAAoAVgFzAAMAAQQJAAsAJgHJQ29weXJpZ2h0IChDKSAyMDE0IGJ5IG9yaWdpbmFsIGF1dGhvcnMgQCBmb250ZWxsby5jb21mb250ZWxsb1JlZ3VsYXJmb250ZWxsb2ZvbnRlbGxvVmVyc2lvbiAxLjBmb250ZWxsb0dlbmVyYXRlZCBieSBzdmcydHRmIGZyb20gRm9udGVsbG8gcHJvamVjdC5odHRwOi8vZm9udGVsbG8uY29tAEMAbwBwAHkAcgBpAGcAaAB0ACAAKABDACkAIAAyADAAMQA0ACAAYgB5ACAAbwByAGkAZwBpAG4AYQBsACAAYQB1AHQAaABvAHIAcwAgAEAAIABmAG8AbgB0AGUAbABsAG8ALgBjAG8AbQBmAG8AbgB0AGUAbABsAG8AUgBlAGcAdQBsAGEAcgBmAG8AbgB0AGUAbABsAG8AZgBvAG4AdABlAGwAbABvAFYAZQByAHMAaQBvAG4AIAAxAC4AMABmAG8AbgB0AGUAbABsAG8ARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABzAHYAZwAyAHQAdABmACAAZgByAG8AbQAgAEYAbwBuAHQAZQBsAGwAbwAgAHAAcgBvAGoAZQBjAHQALgBoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAAAAAAIAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAQIBAwljaGVja21hcmsGY2lyY2xlAAAAAAEAAf//AA8AAAAAAAAAAAAAAAAAAAAAADIAMgML/7EDC/+xsAAssCBgZi2wASwgZCCwwFCwBCZasARFW1ghIyEbilggsFBQWCGwQFkbILA4UFghsDhZWSCwCkVhZLAoUFghsApFILAwUFghsDBZGyCwwFBYIGYgiophILAKUFhgGyCwIFBYIbAKYBsgsDZQWCGwNmAbYFlZWRuwACtZWSOwAFBYZVlZLbACLCBFILAEJWFkILAFQ1BYsAUjQrAGI0IbISFZsAFgLbADLCMhIyEgZLEFYkIgsAYjQrIKAAIqISCwBkMgiiCKsAArsTAFJYpRWGBQG2FSWVgjWSEgsEBTWLAAKxshsEBZI7AAUFhlWS2wBCywB0MrsgACAENgQi2wBSywByNCIyCwACNCYbCAYrABYLAEKi2wBiwgIEUgsAJFY7ABRWJgRLABYC2wBywgIEUgsAArI7ECBCVgIEWKI2EgZCCwIFBYIbAAG7AwUFiwIBuwQFlZI7AAUFhlWbADJSNhRESwAWAtsAgssQUFRbABYUQtsAkssAFgICCwCUNKsABQWCCwCSNCWbAKQ0qwAFJYILAKI0JZLbAKLCC4BABiILgEAGOKI2GwC0NgIIpgILALI0IjLbALLEtUWLEHAURZJLANZSN4LbAMLEtRWEtTWLEHAURZGyFZJLATZSN4LbANLLEADENVWLEMDEOwAWFCsAorWbAAQ7ACJUKxCQIlQrEKAiVCsAEWIyCwAyVQWLEBAENgsAQlQoqKIIojYbAJKiEjsAFhIIojYbAJKiEbsQEAQ2CwAiVCsAIlYbAJKiFZsAlDR7AKQ0dgsIBiILACRWOwAUViYLEAABMjRLABQ7AAPrIBAQFDYEItsA4ssQAFRVRYALAMI0IgYLABYbUNDQEACwBCQopgsQ0FK7BtKxsiWS2wDyyxAA4rLbAQLLEBDistsBEssQIOKy2wEiyxAw4rLbATLLEEDistsBQssQUOKy2wFSyxBg4rLbAWLLEHDistsBcssQgOKy2wGCyxCQ4rLbAZLLAIK7EABUVUWACwDCNCIGCwAWG1DQ0BAAsAQkKKYLENBSuwbSsbIlktsBossQAZKy2wGyyxARkrLbAcLLECGSstsB0ssQMZKy2wHiyxBBkrLbAfLLEFGSstsCAssQYZKy2wISyxBxkrLbAiLLEIGSstsCMssQkZKy2wJCwgPLABYC2wJSwgYLANYCBDI7ABYEOwAiVhsAFgsCQqIS2wJiywJSuwJSotsCcsICBHICCwAkVjsAFFYmAjYTgjIIpVWCBHICCwAkVjsAFFYmAjYTgbIVktsCgssQAFRVRYALABFrAnKrABFTAbIlktsCkssAgrsQAFRVRYALABFrAnKrABFTAbIlktsCosIDWwAWAtsCssALADRWOwAUVisAArsAJFY7ABRWKwACuwABa0AAAAAABEPiM4sSoBFSotsCwsIDwgRyCwAkVjsAFFYmCwAENhOC2wLSwuFzwtsC4sIDwgRyCwAkVjsAFFYmCwAENhsAFDYzgtsC8ssQIAFiUgLiBHsAAjQrACJUmKikcjRyNhIFhiGyFZsAEjQrIuAQEVFCotsDAssAAWsAQlsAQlRyNHI2GwBkUrZYouIyAgPIo4LbAxLLAAFrAEJbAEJSAuRyNHI2EgsAQjQrAGRSsgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjILAIQyCKI0cjRyNhI0ZgsARDsIBiYCCwACsgiophILACQ2BkI7ADQ2FkUFiwAkNhG7ADQ2BZsAMlsIBiYSMgILAEJiNGYTgbI7AIQ0awAiWwCENHI0cjYWAgsARDsIBiYCMgsAArI7AEQ2CwACuwBSVhsAUlsIBisAQmYSCwBCVgZCOwAyVgZFBYIRsjIVkjICCwBCYjRmE4WS2wMiywABYgICCwBSYgLkcjRyNhIzw4LbAzLLAAFiCwCCNCICAgRiNHsAArI2E4LbA0LLAAFrADJbACJUcjRyNhsABUWC4gPCMhG7ACJbACJUcjRyNhILAFJbAEJUcjRyNhsAYlsAUlSbACJWGwAUVjIyBYYhshWWOwAUViYCMuIyAgPIo4IyFZLbA1LLAAFiCwCEMgLkcjRyNhIGCwIGBmsIBiIyAgPIo4LbA2LCMgLkawAiVGUlggPFkusSYBFCstsDcsIyAuRrACJUZQWCA8WS6xJgEUKy2wOCwjIC5GsAIlRlJYIDxZIyAuRrACJUZQWCA8WS6xJgEUKy2wOSywMCsjIC5GsAIlRlJYIDxZLrEmARQrLbA6LLAxK4ogIDywBCNCijgjIC5GsAIlRlJYIDxZLrEmARQrsARDLrAmKy2wOyywABawBCWwBCYgLkcjRyNhsAZFKyMgPCAuIzixJgEUKy2wPCyxCAQlQrAAFrAEJbAEJSAuRyNHI2EgsAQjQrAGRSsgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjIEewBEOwgGJgILAAKyCKimEgsAJDYGQjsANDYWRQWLACQ2EbsANDYFmwAyWwgGJhsAIlRmE4IyA8IzgbISAgRiNHsAArI2E4IVmxJgEUKy2wPSywMCsusSYBFCstsD4ssDErISMgIDywBCNCIzixJgEUK7AEQy6wJistsD8ssAAVIEewACNCsgABARUUEy6wLCotsEAssAAVIEewACNCsgABARUUEy6wLCotsEEssQABFBOwLSotsEIssC8qLbBDLLAAFkUjIC4gRoojYTixJgEUKy2wRCywCCNCsEMrLbBFLLIAADwrLbBGLLIAATwrLbBHLLIBADwrLbBILLIBATwrLbBJLLIAAD0rLbBKLLIAAT0rLbBLLLIBAD0rLbBMLLIBAT0rLbBNLLIAADkrLbBOLLIAATkrLbBPLLIBADkrLbBQLLIBATkrLbBRLLIAADsrLbBSLLIAATsrLbBTLLIBADsrLbBULLIBATsrLbBVLLIAAD4rLbBWLLIAAT4rLbBXLLIBAD4rLbBYLLIBAT4rLbBZLLIAADorLbBaLLIAATorLbBbLLIBADorLbBcLLIBATorLbBdLLAyKy6xJgEUKy2wXiywMiuwNistsF8ssDIrsDcrLbBgLLAAFrAyK7A4Ky2wYSywMysusSYBFCstsGIssDMrsDYrLbBjLLAzK7A3Ky2wZCywMyuwOCstsGUssDQrLrEmARQrLbBmLLA0K7A2Ky2wZyywNCuwNystsGgssDQrsDgrLbBpLLA1Ky6xJgEUKy2waiywNSuwNistsGsssDUrsDcrLbBsLLA1K7A4Ky2wbSwrsAhlsAMkUHiwARUwLQAAAEu4AMhSWLEBAY5ZuQgACABjILABI0SwAyNwsgQoCUVSRLIKAgcqsQYBRLEkAYhRWLBAiFixBgNEsSYBiFFYuAQAiFixBgFEWVlZWbgB/4WwBI2xBQBEAAA=) format('truetype'),url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAoUAA4AAAAAEPQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABRAAAAEQAAABWPeFJAWNtYXAAAAGIAAAAOgAAAUrQEhm3Y3Z0IAAAAcQAAAAUAAAAHAZJ/5RmcGdtAAAB2AAABPkAAAmRigp4O2dhc3AAAAbUAAAACAAAAAgAAAAQZ2x5ZgAABtwAAACuAAAAtt9nBHZoZWFkAAAHjAAAADUAAAA2ASs8e2hoZWEAAAfEAAAAIAAAACQHUwNNaG10eAAAB+QAAAAMAAAADAspAABsb2NhAAAH8AAAAAgAAAAIADgAW21heHAAAAf4AAAAIAAAACAApgm8bmFtZQAACBgAAAF3AAACzcydGhxwb3N0AAAJkAAAACoAAAA7rr1AmHByZXAAAAm8AAAAVgAAAFaSoZr/eJxjYGTewTiBgZWBg6mKaQ8DA0MPhGZ8wGDIyMTAwMTAysyAFQSkuaYwOLxgeMHIHPQ/iyGKmZvBHyjMCJIDAPe9C2B4nGNgYGBmgGAZBkYGEHAB8hjBfBYGDSDNBqQZGZgYGF4w/v8PUvCCAURLMELVAwEjG8OIBwBk5AavAAB4nGNgQANGDEbM3P83gjAAELQD4XicnVXZdtNWFJU8ZHASOmSgoA7X3DhQ68qEKRgwaSrFdiEdHAitBB2kDHTkncc+62uOQrtWH/m07n09JLR0rbYsls++R1tn2DrnRhwjKn0aiGvUoZKXA6msPZZK90lc13Uvj5UMBnFdthJPSZuonSRKat3sUC7xWOsqWSdYJ+PlIFZPVZ5noAziFB5lSUQbRBuplyZJ4onjJ4kWZxAfJUkgJaMQp9LIUEI1GsRS1aFM6dCr1xNx00DKRqMedVhU90PFJ8c1p9SsA0YqVznCFevVRr4bpwMve5DEOsGzrYcxHnisfpQqkIqR6cg/dkpOlIaBVHHUoVbi6DCTX/eRTCrNQKaMYkWl7oG43f102xYxPXQ6vi5KlUaqurnOKJrt0fGogygP2cbppNzQ2fbw5RlTVKtdcbPtQGYNXErJbHSfRAAdJlLj6QFONZwCqRn1R8XZ588BEslclKo8VTKHegOZMzt7cTHtbiersnCknwcyb3Z2452HQ6dXh3/R+hdM4cxHj+Jifj5C+lBqfiJOJKVGWMzyp4YfcVcgQrkxiAsXyuBThDl0RdrZZl3jtTH2hs/5SqlhPQna6KP4fgr9TiQrHGdRo/VInM1j13Wt3GdQS7W7Fzsyr0OVIu7vCwuuM+eEYZ4WC1VfnvneBTT/Bohn/EDeNIVL+5YpSrRvm6JMu2iKCu0SVKVdNsUU7YoppmnPmmKG9h1TzNKeMzLj/8vc55H7HN7xkJv2XeSmfQ+5ad9HbtoPkJtWITdtHblpLyA3rUZu2lWjOnYEGgZpF1IVQdA0svph3Fab9UDWjDR8aWDyLmLI+upER521tcofxX914gsHcmmip7siF5viLq/bFj483e6rj5pG3bDV+MaR8jAeRnocmtBZ+c3hv+1N3S6a7jKqMugBFUwKwABl7UAC0zrbCaT1mqf48gdgXIZ4zkpDtVSfO4am7+V5X/exOfG+x+3GLrdcd3kJWdYNcmP28N9SZKrrH+UtrVQnR6wrJ49VaxhDKrwour6SlHu0tRu/KKmy8l6U1srnk5CbPYMbQlu27mGwI0xpyiUeXlOlKD3UUo6yQyxvKco84JSLC1qGxLgOdQ9qa8TpoXoYGwshhqG0vRBwSCldFd+0ynfxHqtr2Oj4xRXh6XpyEhGf4ir7UfBU10b96A7avGbdMoMpVaqn+4xPsa/b9lFZaaSOsxe3VAfXNOsaORXTT+Rr4HRvOGjdAz1UfDRBI1U1x+jGKGM0ljXl3wR0MVZ+w2jVYvs93E+dpFWsuUuY7JsT9+C0u/0q+7WcW0bW/dcGvW3kip8jMb8tCvw7B2K3ZA3UO5OBGAvIWdAYxhYmdxiug23EbfY/Jqf/34aFRXJXOxq7eerD1ZNRJXfZ8rjLTXZZ16M2R9VOGvsIjS0PN+bY4XIstsRgQbb+wf8x7gF3aVEC4NDIZZiI2nShnurh6h6rsW04VxIBds2x43QAegAuQd8cu9bzCYD13CPnLsB9cgh2yCH4lByCz8i5BfA5OQRfkEMwIIdgl5w7AA/IIXhIDsEeOQSPyNkE+JIcgq/IIYjJIUjIuQ3wmByCJ+QQfE0OwTdGrk5k/pYH2QD6zqKbQKmdGhzaOGRGrk3Y+zxY9oFFZB9aROqRkesT6lMeLPV7i0j9wSJSfzRyY0L9iQdL/dkiUn+xiNRnxpeZIymvDp7zjg7+BJfqrV4AAAAAAQAB//8AD3icY2BkAALmJUwzGEQZZBwk+RkZGBmdGJgYmbIYgMwsoGSiiLgIs5A2owg7I5uSOqOaiT2jmZE8I5gQY17C/09BQEfg3yt+fh8gvYQxD0j68DOJiQn8U+DnZxQDcQUEljLmCwBpBgbG/3//b2SOZ+Zm4GEQcuAH2sblDLSEm8FFVJhJEGgLH6OSHpMdo5EcI3Nk0bEXJ/LYqvZ82VXHGFd6pKTkyCsQwQAAq+QkqAAAeJxjYGRgYADiw5VSsfH8Nl8ZuJlfAEUYzpvO6IXQCb7///7fyLyEmRvI5WBgAokCAFb/DJAAAAB4nGNgZGBgDvqfxRDF/IKB4f935iUMQBEUwAwAi5YFpgPoAAAD6AAAA1kAAAAAAAAAOABbAAEAAAADABYAAQAAAAAAAgAGABMAbgAAAC0JkQAAAAB4nHWQy2rCQBSG//HSi0JbWui2sypKabxgN4IgWHTTbqS4LTHGJBIzMhkFX6Pv0IfpS/RZ+puMpShNmMx3vjlz5mQAXOMbAvnzxJGzwBmjnAs4Rc9ykf7Zcon8YrmMKt4sn9C/W67gAYHlKm7wwQqidM5ogU/LAlfi0nIBF+LOcpH+0XKJ3LNcxq14tXxC71muYCJSy1Xci6+BWm11FIRG1gZ12W62OnK6lYoqStxYumsTKp3KvpyrxPhxrBxPLfc89oN17Op9uJ8nvk4jlciW09yrkZ/42jX+bFc93QRtY+ZyrtVSDm2GXGm18D3jhMasuo3G3/MwgMIKW2hEvKoQBhI12jrnNppooUOaMkMyM8+KkMBFTONizR1htpIy7nPMGSW0PjNisgOP3+WRH5MC7o9ZRR+tHsYT0u6MKPOSfTns7jBrREqyTDezs9/eU2x4WpvWcNeuS511JTE8qCF5H7u1BY1H72S3Ymi7aPD95/9+AN1fhEsAeJxjYGKAAC4G7ICZgYGRiZGZMzkjNTk7N7Eomy05syg5J5WBAQBE1QZBAABLuADIUlixAQGOWbkIAAgAYyCwASNEsAMjcLIEKAlFUkSyCgIHKrEGAUSxJAGIUViwQIhYsQYDRLEmAYhRWLgEAIhYsQYBRFlZWVm4Af+FsASNsQUARAAA) format('woff')}.ui.ordered.steps .step.completed:before,.ui.steps .step.completed>.icon:before{font-family:Step;content:'\e800'}/*!
+ * # Semantic UI 2.4.2 - Breadcrumb
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.breadcrumb{line-height:1;display:inline-block;margin:0 0;vertical-align:middle}.ui.breadcrumb:first-child{margin-top:0}.ui.breadcrumb:last-child{margin-bottom:0}.ui.breadcrumb .divider{display:inline-block;opacity:.7;margin:0 .21428571rem 0;font-size:.92857143em;color:rgba(0,0,0,.4);vertical-align:baseline}.ui.breadcrumb a{color:#4183c4}.ui.breadcrumb a:hover{color:#1e70bf}.ui.breadcrumb .icon.divider{font-size:.85714286em;vertical-align:baseline}.ui.breadcrumb a.section{cursor:pointer}.ui.breadcrumb .section{display:inline-block;margin:0;padding:0}.ui.breadcrumb.segment{display:inline-block;padding:.78571429em 1em}.ui.breadcrumb .active.section{font-weight:700}.ui.mini.breadcrumb{font-size:.78571429rem}.ui.tiny.breadcrumb{font-size:.85714286rem}.ui.small.breadcrumb{font-size:.92857143rem}.ui.breadcrumb{font-size:1rem}.ui.large.breadcrumb{font-size:1.14285714rem}.ui.big.breadcrumb{font-size:1.28571429rem}.ui.huge.breadcrumb{font-size:1.42857143rem}.ui.massive.breadcrumb{font-size:1.71428571rem}/*!
+ * # Semantic UI 2.4.2 - Form
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.form{position:relative;max-width:100%}.ui.form>p{margin:1em 0}.ui.form .field{clear:both;margin:0 0 1em}.ui.form .field:last-child,.ui.form .fields:last-child .field{margin-bottom:0}.ui.form .fields .field{clear:both;margin:0}.ui.form .field>label{display:block;margin:0 0 .28571429rem 0;color:rgba(0,0,0,.87);font-size:.92857143em;font-weight:700;text-transform:none}.ui.form input:not([type]),.ui.form input[type=date],.ui.form input[type=datetime-local],.ui.form input[type=email],.ui.form input[type=file],.ui.form input[type=number],.ui.form input[type=password],.ui.form input[type=search],.ui.form input[type=tel],.ui.form input[type=text],.ui.form input[type=time],.ui.form input[type=url],.ui.form textarea{width:100%;vertical-align:top}.ui.form ::-webkit-datetime-edit,.ui.form ::-webkit-inner-spin-button{height:1.21428571em}.ui.form input:not([type]),.ui.form input[type=date],.ui.form input[type=datetime-local],.ui.form input[type=email],.ui.form input[type=file],.ui.form input[type=number],.ui.form input[type=password],.ui.form input[type=search],.ui.form input[type=tel],.ui.form input[type=text],.ui.form input[type=time],.ui.form input[type=url]{font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;margin:0;outline:0;-webkit-appearance:none;tap-highlight-color:rgba(255,255,255,0);line-height:1.21428571em;padding:.67857143em 1em;font-size:1em;background:#fff;border:1px solid rgba(34,36,38,.15);color:rgba(0,0,0,.87);border-radius:.28571429rem;-webkit-box-shadow:0 0 0 0 transparent inset;box-shadow:0 0 0 0 transparent inset;-webkit-transition:color .1s ease,border-color .1s ease;transition:color .1s ease,border-color .1s ease}.ui.form textarea{margin:0;-webkit-appearance:none;tap-highlight-color:rgba(255,255,255,0);padding:.78571429em 1em;background:#fff;border:1px solid rgba(34,36,38,.15);outline:0;color:rgba(0,0,0,.87);border-radius:.28571429rem;-webkit-box-shadow:0 0 0 0 transparent inset;box-shadow:0 0 0 0 transparent inset;-webkit-transition:color .1s ease,border-color .1s ease;transition:color .1s ease,border-color .1s ease;font-size:1em;line-height:1.2857;resize:vertical}.ui.form textarea:not([rows]){height:12em;min-height:8em;max-height:24em}.ui.form input[type=checkbox],.ui.form textarea{vertical-align:top}.ui.form input.attached{width:auto}.ui.form select{display:block;height:auto;width:100%;background:#fff;border:1px solid rgba(34,36,38,.15);border-radius:.28571429rem;-webkit-box-shadow:0 0 0 0 transparent inset;box-shadow:0 0 0 0 transparent inset;padding:.62em 1em;color:rgba(0,0,0,.87);-webkit-transition:color .1s ease,border-color .1s ease;transition:color .1s ease,border-color .1s ease}.ui.form .field>.selection.dropdown{width:100%}.ui.form .field>.selection.dropdown>.dropdown.icon{float:right}.ui.form .inline.field>.selection.dropdown,.ui.form .inline.fields .field>.selection.dropdown{width:auto}.ui.form .inline.field>.selection.dropdown>.dropdown.icon,.ui.form .inline.fields .field>.selection.dropdown>.dropdown.icon{float:none}.ui.form .field .ui.input,.ui.form .fields .field .ui.input,.ui.form .wide.field .ui.input{width:100%}.ui.form .inline.field:not(.wide) .ui.input,.ui.form .inline.fields .field:not(.wide) .ui.input{width:auto;vertical-align:middle}.ui.form .field .ui.input input,.ui.form .fields .field .ui.input input{width:auto}.ui.form .eight.fields .ui.input input,.ui.form .five.fields .ui.input input,.ui.form .four.fields .ui.input input,.ui.form .nine.fields .ui.input input,.ui.form .seven.fields .ui.input input,.ui.form .six.fields .ui.input input,.ui.form .ten.fields .ui.input input,.ui.form .three.fields .ui.input input,.ui.form .two.fields .ui.input input,.ui.form .wide.field .ui.input input{-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;width:0}.ui.form .error.message,.ui.form .success.message,.ui.form .warning.message{display:none}.ui.form .message:first-child{margin-top:0}.ui.form .field .prompt.label{white-space:normal;background:#fff!important;border:1px solid #e0b4b4!important;color:#9f3a38!important}.ui.form .inline.field .prompt,.ui.form .inline.fields .field .prompt{vertical-align:top;margin:-.25em 0 -.5em .5em}.ui.form .inline.field .prompt:before,.ui.form .inline.fields .field .prompt:before{border-width:0 0 1px 1px;bottom:auto;right:auto;top:50%;left:0}.ui.form .field.field input:-webkit-autofill{-webkit-box-shadow:0 0 0 100px ivory inset!important;box-shadow:0 0 0 100px ivory inset!important;border-color:#e5dfa1!important}.ui.form .field.field input:-webkit-autofill:focus{-webkit-box-shadow:0 0 0 100px ivory inset!important;box-shadow:0 0 0 100px ivory inset!important;border-color:#d5c315!important}.ui.form .error.error input:-webkit-autofill{-webkit-box-shadow:0 0 0 100px #fffaf0 inset!important;box-shadow:0 0 0 100px #fffaf0 inset!important;border-color:#e0b4b4!important}.ui.form ::-webkit-input-placeholder{color:rgba(191,191,191,.87)}.ui.form :-ms-input-placeholder{color:rgba(191,191,191,.87)!important}.ui.form ::-moz-placeholder{color:rgba(191,191,191,.87)}.ui.form :focus::-webkit-input-placeholder{color:rgba(115,115,115,.87)}.ui.form :focus:-ms-input-placeholder{color:rgba(115,115,115,.87)!important}.ui.form :focus::-moz-placeholder{color:rgba(115,115,115,.87)}.ui.form .error ::-webkit-input-placeholder{color:#e7bdbc}.ui.form .error :-ms-input-placeholder{color:#e7bdbc!important}.ui.form .error ::-moz-placeholder{color:#e7bdbc}.ui.form .error :focus::-webkit-input-placeholder{color:#da9796}.ui.form .error :focus:-ms-input-placeholder{color:#da9796!important}.ui.form .error :focus::-moz-placeholder{color:#da9796}.ui.form input:not([type]):focus,.ui.form input[type=date]:focus,.ui.form input[type=datetime-local]:focus,.ui.form input[type=email]:focus,.ui.form input[type=file]:focus,.ui.form input[type=number]:focus,.ui.form input[type=password]:focus,.ui.form input[type=search]:focus,.ui.form input[type=tel]:focus,.ui.form input[type=text]:focus,.ui.form input[type=time]:focus,.ui.form input[type=url]:focus{color:rgba(0,0,0,.95);border-color:#85b7d9;border-radius:.28571429rem;background:#fff;-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.35) inset;box-shadow:0 0 0 0 rgba(34,36,38,.35) inset}.ui.form textarea:focus{color:rgba(0,0,0,.95);border-color:#85b7d9;border-radius:.28571429rem;background:#fff;-webkit-box-shadow:0 0 0 0 rgba(34,36,38,.35) inset;box-shadow:0 0 0 0 rgba(34,36,38,.35) inset;-webkit-appearance:none}.ui.form.success .success.message:not(:empty){display:block}.ui.form.success .compact.success.message:not(:empty){display:inline-block}.ui.form.success .icon.success.message:not(:empty){display:-webkit-box;display:-ms-flexbox;display:flex}.ui.form.warning .warning.message:not(:empty){display:block}.ui.form.warning .compact.warning.message:not(:empty){display:inline-block}.ui.form.warning .icon.warning.message:not(:empty){display:-webkit-box;display:-ms-flexbox;display:flex}.ui.form.error .error.message:not(:empty){display:block}.ui.form.error .compact.error.message:not(:empty){display:inline-block}.ui.form.error .icon.error.message:not(:empty){display:-webkit-box;display:-ms-flexbox;display:flex}.ui.form .field.error .input,.ui.form .field.error label,.ui.form .fields.error .field .input,.ui.form .fields.error .field label{color:#9f3a38}.ui.form .field.error .corner.label,.ui.form .fields.error .field .corner.label{border-color:#9f3a38;color:#fff}.ui.form .field.error input:not([type]),.ui.form .field.error input[type=date],.ui.form .field.error input[type=datetime-local],.ui.form .field.error input[type=email],.ui.form .field.error input[type=file],.ui.form .field.error input[type=number],.ui.form .field.error input[type=password],.ui.form .field.error input[type=search],.ui.form .field.error input[type=tel],.ui.form .field.error input[type=text],.ui.form .field.error input[type=time],.ui.form .field.error input[type=url],.ui.form .field.error select,.ui.form .field.error textarea,.ui.form .fields.error .field input:not([type]),.ui.form .fields.error .field input[type=date],.ui.form .fields.error .field input[type=datetime-local],.ui.form .fields.error .field input[type=email],.ui.form .fields.error .field input[type=file],.ui.form .fields.error .field input[type=number],.ui.form .fields.error .field input[type=password],.ui.form .fields.error .field input[type=search],.ui.form .fields.error .field input[type=tel],.ui.form .fields.error .field input[type=text],.ui.form .fields.error .field input[type=time],.ui.form .fields.error .field input[type=url],.ui.form .fields.error .field select,.ui.form .fields.error .field textarea{background:#fff6f6;border-color:#e0b4b4;color:#9f3a38;border-radius:'';-webkit-box-shadow:none;box-shadow:none}.ui.form .field.error input:not([type]):focus,.ui.form .field.error input[type=date]:focus,.ui.form .field.error input[type=datetime-local]:focus,.ui.form .field.error input[type=email]:focus,.ui.form .field.error input[type=file]:focus,.ui.form .field.error input[type=number]:focus,.ui.form .field.error input[type=password]:focus,.ui.form .field.error input[type=search]:focus,.ui.form .field.error input[type=tel]:focus,.ui.form .field.error input[type=text]:focus,.ui.form .field.error input[type=time]:focus,.ui.form .field.error input[type=url]:focus,.ui.form .field.error select:focus,.ui.form .field.error textarea:focus{background:#fff6f6;border-color:#e0b4b4;color:#9f3a38;-webkit-appearance:none;-webkit-box-shadow:none;box-shadow:none}.ui.form .field.error select{-webkit-appearance:menulist-button}.ui.form .field.error .ui.dropdown,.ui.form .field.error .ui.dropdown .item,.ui.form .field.error .ui.dropdown .text,.ui.form .fields.error .field .ui.dropdown,.ui.form .fields.error .field .ui.dropdown .item{background:#fff6f6;color:#9f3a38}.ui.form .field.error .ui.dropdown,.ui.form .fields.error .field .ui.dropdown{border-color:#e0b4b4!important}.ui.form .field.error .ui.dropdown:hover,.ui.form .fields.error .field .ui.dropdown:hover{border-color:#e0b4b4!important}.ui.form .field.error .ui.dropdown:hover .menu,.ui.form .fields.error .field .ui.dropdown:hover .menu{border-color:#e0b4b4}.ui.form .field.error .ui.multiple.selection.dropdown>.label,.ui.form .fields.error .field .ui.multiple.selection.dropdown>.label{background-color:#eacbcb;color:#9f3a38}.ui.form .field.error .ui.dropdown .menu .item:hover,.ui.form .fields.error .field .ui.dropdown .menu .item:hover{background-color:#fbe7e7}.ui.form .field.error .ui.dropdown .menu .selected.item,.ui.form .fields.error .field .ui.dropdown .menu .selected.item{background-color:#fbe7e7}.ui.form .field.error .ui.dropdown .menu .active.item,.ui.form .fields.error .field .ui.dropdown .menu .active.item{background-color:#fdcfcf!important}.ui.form .field.error .checkbox:not(.toggle):not(.slider) .box,.ui.form .field.error .checkbox:not(.toggle):not(.slider) label,.ui.form .fields.error .field .checkbox:not(.toggle):not(.slider) .box,.ui.form .fields.error .field .checkbox:not(.toggle):not(.slider) label{color:#9f3a38}.ui.form .field.error .checkbox:not(.toggle):not(.slider) .box:before,.ui.form .field.error .checkbox:not(.toggle):not(.slider) label:before,.ui.form .fields.error .field .checkbox:not(.toggle):not(.slider) .box:before,.ui.form .fields.error .field .checkbox:not(.toggle):not(.slider) label:before{background:#fff6f6;border-color:#e0b4b4}.ui.form .field.error .checkbox .box:after,.ui.form .field.error .checkbox label:after,.ui.form .fields.error .field .checkbox .box:after,.ui.form .fields.error .field .checkbox label:after{color:#9f3a38}.ui.form .disabled.field,.ui.form .disabled.fields .field,.ui.form .field :disabled{pointer-events:none;opacity:.45}.ui.form .field.disabled>label,.ui.form .fields.disabled>label{opacity:.45}.ui.form .field.disabled :disabled{opacity:1}.ui.loading.form{position:relative;cursor:default;pointer-events:none}.ui.loading.form:before{position:absolute;content:'';top:0;left:0;background:rgba(255,255,255,.8);width:100%;height:100%;z-index:100}.ui.loading.form:after{position:absolute;content:'';top:50%;left:50%;margin:-1.5em 0 0 -1.5em;width:3em;height:3em;-webkit-animation:form-spin .6s linear;animation:form-spin .6s linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;border-radius:500rem;border-color:#767676 rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.1);border-style:solid;border-width:.2em;-webkit-box-shadow:0 0 0 1px transparent;box-shadow:0 0 0 1px transparent;visibility:visible;z-index:101}@-webkit-keyframes form-spin{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes form-spin{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.ui.form .required.field>.checkbox:after,.ui.form .required.field>label:after,.ui.form .required.fields.grouped>label:after,.ui.form .required.fields:not(.grouped)>.field>.checkbox:after,.ui.form .required.fields:not(.grouped)>.field>label:after{margin:-.2em 0 0 .2em;content:'*';color:#db2828}.ui.form .required.field>label:after,.ui.form .required.fields.grouped>label:after,.ui.form .required.fields:not(.grouped)>.field>label:after{display:inline-block;vertical-align:top}.ui.form .required.field>.checkbox:after,.ui.form .required.fields:not(.grouped)>.field>.checkbox:after{position:absolute;top:0;left:100%}.ui.form .inverted.segment .ui.checkbox .box,.ui.form .inverted.segment .ui.checkbox label,.ui.form .inverted.segment label,.ui.inverted.form .inline.field>label,.ui.inverted.form .inline.field>p,.ui.inverted.form .inline.fields .field>label,.ui.inverted.form .inline.fields .field>p,.ui.inverted.form .inline.fields>label,.ui.inverted.form .ui.checkbox .box,.ui.inverted.form .ui.checkbox label,.ui.inverted.form label{color:rgba(255,255,255,.9)}.ui.inverted.form input:not([type]),.ui.inverted.form input[type=date],.ui.inverted.form input[type=datetime-local],.ui.inverted.form input[type=email],.ui.inverted.form input[type=file],.ui.inverted.form input[type=number],.ui.inverted.form input[type=password],.ui.inverted.form input[type=search],.ui.inverted.form input[type=tel],.ui.inverted.form input[type=text],.ui.inverted.form input[type=time],.ui.inverted.form input[type=url]{background:#fff;border-color:rgba(255,255,255,.1);color:rgba(0,0,0,.87);-webkit-box-shadow:none;box-shadow:none}.ui.form .grouped.fields{display:block;margin:0 0 1em}.ui.form .grouped.fields:last-child{margin-bottom:0}.ui.form .grouped.fields>label{margin:0 0 .28571429rem 0;color:rgba(0,0,0,.87);font-size:.92857143em;font-weight:700;text-transform:none}.ui.form .grouped.fields .field,.ui.form .grouped.inline.fields .field{display:block;margin:.5em 0;padding:0}.ui.form .fields{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;margin:0 -.5em 1em}.ui.form .fields>.field{-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;padding-left:.5em;padding-right:.5em}.ui.form .fields>.field:first-child{border-left:none;-webkit-box-shadow:none;box-shadow:none}.ui.form .two.fields>.field,.ui.form .two.fields>.fields{width:50%}.ui.form .three.fields>.field,.ui.form .three.fields>.fields{width:33.33333333%}.ui.form .four.fields>.field,.ui.form .four.fields>.fields{width:25%}.ui.form .five.fields>.field,.ui.form .five.fields>.fields{width:20%}.ui.form .six.fields>.field,.ui.form .six.fields>.fields{width:16.66666667%}.ui.form .seven.fields>.field,.ui.form .seven.fields>.fields{width:14.28571429%}.ui.form .eight.fields>.field,.ui.form .eight.fields>.fields{width:12.5%}.ui.form .nine.fields>.field,.ui.form .nine.fields>.fields{width:11.11111111%}.ui.form .ten.fields>.field,.ui.form .ten.fields>.fields{width:10%}@media only screen and (max-width:767px){.ui.form .fields{-ms-flex-wrap:wrap;flex-wrap:wrap}.ui.form:not(.unstackable) .eight.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .eight.fields:not(.unstackable)>.fields,.ui.form:not(.unstackable) .five.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .five.fields:not(.unstackable)>.fields,.ui.form:not(.unstackable) .four.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .four.fields:not(.unstackable)>.fields,.ui.form:not(.unstackable) .nine.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .nine.fields:not(.unstackable)>.fields,.ui.form:not(.unstackable) .seven.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .seven.fields:not(.unstackable)>.fields,.ui.form:not(.unstackable) .six.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .six.fields:not(.unstackable)>.fields,.ui.form:not(.unstackable) .ten.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .ten.fields:not(.unstackable)>.fields,.ui.form:not(.unstackable) .three.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .three.fields:not(.unstackable)>.fields,.ui.form:not(.unstackable) .two.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .two.fields:not(.unstackable)>.fields,.ui.form:not(.unstackable) [class*="equal width"].fields:not(.unstackable)>.field,.ui[class*="equal width"].form:not(.unstackable) .fields>.field{width:100%!important;margin:0 0 1em}}.ui.form .fields .wide.field{width:6.25%;padding-left:.5em;padding-right:.5em}.ui.form .one.wide.field{width:6.25%!important}.ui.form .two.wide.field{width:12.5%!important}.ui.form .three.wide.field{width:18.75%!important}.ui.form .four.wide.field{width:25%!important}.ui.form .five.wide.field{width:31.25%!important}.ui.form .six.wide.field{width:37.5%!important}.ui.form .seven.wide.field{width:43.75%!important}.ui.form .eight.wide.field{width:50%!important}.ui.form .nine.wide.field{width:56.25%!important}.ui.form .ten.wide.field{width:62.5%!important}.ui.form .eleven.wide.field{width:68.75%!important}.ui.form .twelve.wide.field{width:75%!important}.ui.form .thirteen.wide.field{width:81.25%!important}.ui.form .fourteen.wide.field{width:87.5%!important}.ui.form .fifteen.wide.field{width:93.75%!important}.ui.form .sixteen.wide.field{width:100%!important}@media only screen and (max-width:767px){.ui.form:not(.unstackable) .fields:not(.unstackable)>.eight.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.eleven.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.fifteen.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.five.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.four.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.fourteen.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.nine.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.seven.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.six.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.sixteen.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.ten.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.thirteen.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.three.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.twelve.wide.field,.ui.form:not(.unstackable) .fields:not(.unstackable)>.two.wide.field,.ui.form:not(.unstackable) .five.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .five.fields:not(.unstackable)>.fields,.ui.form:not(.unstackable) .four.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .four.fields:not(.unstackable)>.fields,.ui.form:not(.unstackable) .three.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .three.fields:not(.unstackable)>.fields,.ui.form:not(.unstackable) .two.fields:not(.unstackable)>.field,.ui.form:not(.unstackable) .two.fields:not(.unstackable)>.fields{width:100%!important}.ui.form .fields{margin-bottom:0}}.ui.form [class*="equal width"].fields>.field,.ui[class*="equal width"].form .fields>.field{width:100%;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.ui.form .inline.fields{margin:0 0 1em;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ui.form .inline.fields .field{margin:0;padding:0 1em 0 0}.ui.form .inline.field>label,.ui.form .inline.field>p,.ui.form .inline.fields .field>label,.ui.form .inline.fields .field>p,.ui.form .inline.fields>label{display:inline-block;width:auto;margin-top:0;margin-bottom:0;vertical-align:baseline;font-size:.92857143em;font-weight:700;color:rgba(0,0,0,.87);text-transform:none}.ui.form .inline.fields>label{margin:.035714em 1em 0 0}.ui.form .inline.field>input,.ui.form .inline.field>select,.ui.form .inline.fields .field>input,.ui.form .inline.fields .field>select{display:inline-block;width:auto;margin-top:0;margin-bottom:0;vertical-align:middle;font-size:1em}.ui.form .inline.field>:first-child,.ui.form .inline.fields .field>:first-child{margin:0 .85714286em 0 0}.ui.form .inline.field>:only-child,.ui.form .inline.fields .field>:only-child{margin:0}.ui.form .inline.fields .wide.field{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ui.form .inline.fields .wide.field>input,.ui.form .inline.fields .wide.field>select{width:100%}.ui.mini.form{font-size:.78571429rem}.ui.tiny.form{font-size:.85714286rem}.ui.small.form{font-size:.92857143rem}.ui.form{font-size:1rem}.ui.large.form{font-size:1.14285714rem}.ui.big.form{font-size:1.28571429rem}.ui.huge.form{font-size:1.42857143rem}.ui.massive.form{font-size:1.71428571rem}/*!
+ * # Semantic UI 2.4.2 - Grid
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.grid{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;padding:0}.ui.grid{margin-top:-1rem;margin-bottom:-1rem;margin-left:-1rem;margin-right:-1rem}.ui.relaxed.grid{margin-left:-1.5rem;margin-right:-1.5rem}.ui[class*="very relaxed"].grid{margin-left:-2.5rem;margin-right:-2.5rem}.ui.grid+.grid{margin-top:1rem}.ui.grid>.column:not(.row),.ui.grid>.row>.column{position:relative;display:inline-block;width:6.25%;padding-left:1rem;padding-right:1rem;vertical-align:top}.ui.grid>*{padding-left:1rem;padding-right:1rem}.ui.grid>.row{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:inherit;-ms-flex-pack:inherit;justify-content:inherit;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%!important;padding:0;padding-top:1rem;padding-bottom:1rem}.ui.grid>.column:not(.row){padding-top:1rem;padding-bottom:1rem}.ui.grid>.row>.column{margin-top:0;margin-bottom:0}.ui.grid>.row>.column>img,.ui.grid>.row>img{max-width:100%}.ui.grid>.ui.grid:first-child{margin-top:0}.ui.grid>.ui.grid:last-child{margin-bottom:0}.ui.aligned.grid .column>.segment:not(.compact):not(.attached),.ui.grid .aligned.row>.column>.segment:not(.compact):not(.attached){width:100%}.ui.grid .row+.ui.divider{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;margin:1rem 1rem}.ui.grid .column+.ui.vertical.divider{height:calc(50% - (2rem / 2))}.ui.grid>.column:last-child>.horizontal.segment,.ui.grid>.row>.column:last-child>.horizontal.segment{-webkit-box-shadow:none;box-shadow:none}@media only screen and (max-width:767px){.ui.page.grid{width:auto;padding-left:0;padding-right:0;margin-left:0;margin-right:0}}@media only screen and (min-width:768px) and (max-width:991px){.ui.page.grid{width:auto;margin-left:0;margin-right:0;padding-left:2em;padding-right:2em}}@media only screen and (min-width:992px) and (max-width:1199px){.ui.page.grid{width:auto;margin-left:0;margin-right:0;padding-left:3%;padding-right:3%}}@media only screen and (min-width:1200px) and (max-width:1919px){.ui.page.grid{width:auto;margin-left:0;margin-right:0;padding-left:15%;padding-right:15%}}@media only screen and (min-width:1920px){.ui.page.grid{width:auto;margin-left:0;margin-right:0;padding-left:23%;padding-right:23%}}.ui.grid>.column:only-child,.ui.grid>.row>.column:only-child{width:100%}.ui[class*="one column"].grid>.column:not(.row),.ui[class*="one column"].grid>.row>.column{width:100%}.ui[class*="two column"].grid>.column:not(.row),.ui[class*="two column"].grid>.row>.column{width:50%}.ui[class*="three column"].grid>.column:not(.row),.ui[class*="three column"].grid>.row>.column{width:33.33333333%}.ui[class*="four column"].grid>.column:not(.row),.ui[class*="four column"].grid>.row>.column{width:25%}.ui[class*="five column"].grid>.column:not(.row),.ui[class*="five column"].grid>.row>.column{width:20%}.ui[class*="six column"].grid>.column:not(.row),.ui[class*="six column"].grid>.row>.column{width:16.66666667%}.ui[class*="seven column"].grid>.column:not(.row),.ui[class*="seven column"].grid>.row>.column{width:14.28571429%}.ui[class*="eight column"].grid>.column:not(.row),.ui[class*="eight column"].grid>.row>.column{width:12.5%}.ui[class*="nine column"].grid>.column:not(.row),.ui[class*="nine column"].grid>.row>.column{width:11.11111111%}.ui[class*="ten column"].grid>.column:not(.row),.ui[class*="ten column"].grid>.row>.column{width:10%}.ui[class*="eleven column"].grid>.column:not(.row),.ui[class*="eleven column"].grid>.row>.column{width:9.09090909%}.ui[class*="twelve column"].grid>.column:not(.row),.ui[class*="twelve column"].grid>.row>.column{width:8.33333333%}.ui[class*="thirteen column"].grid>.column:not(.row),.ui[class*="thirteen column"].grid>.row>.column{width:7.69230769%}.ui[class*="fourteen column"].grid>.column:not(.row),.ui[class*="fourteen column"].grid>.row>.column{width:7.14285714%}.ui[class*="fifteen column"].grid>.column:not(.row),.ui[class*="fifteen column"].grid>.row>.column{width:6.66666667%}.ui[class*="sixteen column"].grid>.column:not(.row),.ui[class*="sixteen column"].grid>.row>.column{width:6.25%}.ui.grid>[class*="one column"].row>.column{width:100%!important}.ui.grid>[class*="two column"].row>.column{width:50%!important}.ui.grid>[class*="three column"].row>.column{width:33.33333333%!important}.ui.grid>[class*="four column"].row>.column{width:25%!important}.ui.grid>[class*="five column"].row>.column{width:20%!important}.ui.grid>[class*="six column"].row>.column{width:16.66666667%!important}.ui.grid>[class*="seven column"].row>.column{width:14.28571429%!important}.ui.grid>[class*="eight column"].row>.column{width:12.5%!important}.ui.grid>[class*="nine column"].row>.column{width:11.11111111%!important}.ui.grid>[class*="ten column"].row>.column{width:10%!important}.ui.grid>[class*="eleven column"].row>.column{width:9.09090909%!important}.ui.grid>[class*="twelve column"].row>.column{width:8.33333333%!important}.ui.grid>[class*="thirteen column"].row>.column{width:7.69230769%!important}.ui.grid>[class*="fourteen column"].row>.column{width:7.14285714%!important}.ui.grid>[class*="fifteen column"].row>.column{width:6.66666667%!important}.ui.grid>[class*="sixteen column"].row>.column{width:6.25%!important}.ui.celled.page.grid{-webkit-box-shadow:none;box-shadow:none}.ui.column.grid>[class*="one wide"].column,.ui.grid>.column.row>[class*="one wide"].column,.ui.grid>.row>[class*="one wide"].column,.ui.grid>[class*="one wide"].column{width:6.25%!important}.ui.column.grid>[class*="two wide"].column,.ui.grid>.column.row>[class*="two wide"].column,.ui.grid>.row>[class*="two wide"].column,.ui.grid>[class*="two wide"].column{width:12.5%!important}.ui.column.grid>[class*="three wide"].column,.ui.grid>.column.row>[class*="three wide"].column,.ui.grid>.row>[class*="three wide"].column,.ui.grid>[class*="three wide"].column{width:18.75%!important}.ui.column.grid>[class*="four wide"].column,.ui.grid>.column.row>[class*="four wide"].column,.ui.grid>.row>[class*="four wide"].column,.ui.grid>[class*="four wide"].column{width:25%!important}.ui.column.grid>[class*="five wide"].column,.ui.grid>.column.row>[class*="five wide"].column,.ui.grid>.row>[class*="five wide"].column,.ui.grid>[class*="five wide"].column{width:31.25%!important}.ui.column.grid>[class*="six wide"].column,.ui.grid>.column.row>[class*="six wide"].column,.ui.grid>.row>[class*="six wide"].column,.ui.grid>[class*="six wide"].column{width:37.5%!important}.ui.column.grid>[class*="seven wide"].column,.ui.grid>.column.row>[class*="seven wide"].column,.ui.grid>.row>[class*="seven wide"].column,.ui.grid>[class*="seven wide"].column{width:43.75%!important}.ui.column.grid>[class*="eight wide"].column,.ui.grid>.column.row>[class*="eight wide"].column,.ui.grid>.row>[class*="eight wide"].column,.ui.grid>[class*="eight wide"].column{width:50%!important}.ui.column.grid>[class*="nine wide"].column,.ui.grid>.column.row>[class*="nine wide"].column,.ui.grid>.row>[class*="nine wide"].column,.ui.grid>[class*="nine wide"].column{width:56.25%!important}.ui.column.grid>[class*="ten wide"].column,.ui.grid>.column.row>[class*="ten wide"].column,.ui.grid>.row>[class*="ten wide"].column,.ui.grid>[class*="ten wide"].column{width:62.5%!important}.ui.column.grid>[class*="eleven wide"].column,.ui.grid>.column.row>[class*="eleven wide"].column,.ui.grid>.row>[class*="eleven wide"].column,.ui.grid>[class*="eleven wide"].column{width:68.75%!important}.ui.column.grid>[class*="twelve wide"].column,.ui.grid>.column.row>[class*="twelve wide"].column,.ui.grid>.row>[class*="twelve wide"].column,.ui.grid>[class*="twelve wide"].column{width:75%!important}.ui.column.grid>[class*="thirteen wide"].column,.ui.grid>.column.row>[class*="thirteen wide"].column,.ui.grid>.row>[class*="thirteen wide"].column,.ui.grid>[class*="thirteen wide"].column{width:81.25%!important}.ui.column.grid>[class*="fourteen wide"].column,.ui.grid>.column.row>[class*="fourteen wide"].column,.ui.grid>.row>[class*="fourteen wide"].column,.ui.grid>[class*="fourteen wide"].column{width:87.5%!important}.ui.column.grid>[class*="fifteen wide"].column,.ui.grid>.column.row>[class*="fifteen wide"].column,.ui.grid>.row>[class*="fifteen wide"].column,.ui.grid>[class*="fifteen wide"].column{width:93.75%!important}.ui.column.grid>[class*="sixteen wide"].column,.ui.grid>.column.row>[class*="sixteen wide"].column,.ui.grid>.row>[class*="sixteen wide"].column,.ui.grid>[class*="sixteen wide"].column{width:100%!important}@media only screen and (min-width:320px) and (max-width:767px){.ui.column.grid>[class*="one wide mobile"].column,.ui.grid>.column.row>[class*="one wide mobile"].column,.ui.grid>.row>[class*="one wide mobile"].column,.ui.grid>[class*="one wide mobile"].column{width:6.25%!important}.ui.column.grid>[class*="two wide mobile"].column,.ui.grid>.column.row>[class*="two wide mobile"].column,.ui.grid>.row>[class*="two wide mobile"].column,.ui.grid>[class*="two wide mobile"].column{width:12.5%!important}.ui.column.grid>[class*="three wide mobile"].column,.ui.grid>.column.row>[class*="three wide mobile"].column,.ui.grid>.row>[class*="three wide mobile"].column,.ui.grid>[class*="three wide mobile"].column{width:18.75%!important}.ui.column.grid>[class*="four wide mobile"].column,.ui.grid>.column.row>[class*="four wide mobile"].column,.ui.grid>.row>[class*="four wide mobile"].column,.ui.grid>[class*="four wide mobile"].column{width:25%!important}.ui.column.grid>[class*="five wide mobile"].column,.ui.grid>.column.row>[class*="five wide mobile"].column,.ui.grid>.row>[class*="five wide mobile"].column,.ui.grid>[class*="five wide mobile"].column{width:31.25%!important}.ui.column.grid>[class*="six wide mobile"].column,.ui.grid>.column.row>[class*="six wide mobile"].column,.ui.grid>.row>[class*="six wide mobile"].column,.ui.grid>[class*="six wide mobile"].column{width:37.5%!important}.ui.column.grid>[class*="seven wide mobile"].column,.ui.grid>.column.row>[class*="seven wide mobile"].column,.ui.grid>.row>[class*="seven wide mobile"].column,.ui.grid>[class*="seven wide mobile"].column{width:43.75%!important}.ui.column.grid>[class*="eight wide mobile"].column,.ui.grid>.column.row>[class*="eight wide mobile"].column,.ui.grid>.row>[class*="eight wide mobile"].column,.ui.grid>[class*="eight wide mobile"].column{width:50%!important}.ui.column.grid>[class*="nine wide mobile"].column,.ui.grid>.column.row>[class*="nine wide mobile"].column,.ui.grid>.row>[class*="nine wide mobile"].column,.ui.grid>[class*="nine wide mobile"].column{width:56.25%!important}.ui.column.grid>[class*="ten wide mobile"].column,.ui.grid>.column.row>[class*="ten wide mobile"].column,.ui.grid>.row>[class*="ten wide mobile"].column,.ui.grid>[class*="ten wide mobile"].column{width:62.5%!important}.ui.column.grid>[class*="eleven wide mobile"].column,.ui.grid>.column.row>[class*="eleven wide mobile"].column,.ui.grid>.row>[class*="eleven wide mobile"].column,.ui.grid>[class*="eleven wide mobile"].column{width:68.75%!important}.ui.column.grid>[class*="twelve wide mobile"].column,.ui.grid>.column.row>[class*="twelve wide mobile"].column,.ui.grid>.row>[class*="twelve wide mobile"].column,.ui.grid>[class*="twelve wide mobile"].column{width:75%!important}.ui.column.grid>[class*="thirteen wide mobile"].column,.ui.grid>.column.row>[class*="thirteen wide mobile"].column,.ui.grid>.row>[class*="thirteen wide mobile"].column,.ui.grid>[class*="thirteen wide mobile"].column{width:81.25%!important}.ui.column.grid>[class*="fourteen wide mobile"].column,.ui.grid>.column.row>[class*="fourteen wide mobile"].column,.ui.grid>.row>[class*="fourteen wide mobile"].column,.ui.grid>[class*="fourteen wide mobile"].column{width:87.5%!important}.ui.column.grid>[class*="fifteen wide mobile"].column,.ui.grid>.column.row>[class*="fifteen wide mobile"].column,.ui.grid>.row>[class*="fifteen wide mobile"].column,.ui.grid>[class*="fifteen wide mobile"].column{width:93.75%!important}.ui.column.grid>[class*="sixteen wide mobile"].column,.ui.grid>.column.row>[class*="sixteen wide mobile"].column,.ui.grid>.row>[class*="sixteen wide mobile"].column,.ui.grid>[class*="sixteen wide mobile"].column{width:100%!important}}@media only screen and (min-width:768px) and (max-width:991px){.ui.column.grid>[class*="one wide tablet"].column,.ui.grid>.column.row>[class*="one wide tablet"].column,.ui.grid>.row>[class*="one wide tablet"].column,.ui.grid>[class*="one wide tablet"].column{width:6.25%!important}.ui.column.grid>[class*="two wide tablet"].column,.ui.grid>.column.row>[class*="two wide tablet"].column,.ui.grid>.row>[class*="two wide tablet"].column,.ui.grid>[class*="two wide tablet"].column{width:12.5%!important}.ui.column.grid>[class*="three wide tablet"].column,.ui.grid>.column.row>[class*="three wide tablet"].column,.ui.grid>.row>[class*="three wide tablet"].column,.ui.grid>[class*="three wide tablet"].column{width:18.75%!important}.ui.column.grid>[class*="four wide tablet"].column,.ui.grid>.column.row>[class*="four wide tablet"].column,.ui.grid>.row>[class*="four wide tablet"].column,.ui.grid>[class*="four wide tablet"].column{width:25%!important}.ui.column.grid>[class*="five wide tablet"].column,.ui.grid>.column.row>[class*="five wide tablet"].column,.ui.grid>.row>[class*="five wide tablet"].column,.ui.grid>[class*="five wide tablet"].column{width:31.25%!important}.ui.column.grid>[class*="six wide tablet"].column,.ui.grid>.column.row>[class*="six wide tablet"].column,.ui.grid>.row>[class*="six wide tablet"].column,.ui.grid>[class*="six wide tablet"].column{width:37.5%!important}.ui.column.grid>[class*="seven wide tablet"].column,.ui.grid>.column.row>[class*="seven wide tablet"].column,.ui.grid>.row>[class*="seven wide tablet"].column,.ui.grid>[class*="seven wide tablet"].column{width:43.75%!important}.ui.column.grid>[class*="eight wide tablet"].column,.ui.grid>.column.row>[class*="eight wide tablet"].column,.ui.grid>.row>[class*="eight wide tablet"].column,.ui.grid>[class*="eight wide tablet"].column{width:50%!important}.ui.column.grid>[class*="nine wide tablet"].column,.ui.grid>.column.row>[class*="nine wide tablet"].column,.ui.grid>.row>[class*="nine wide tablet"].column,.ui.grid>[class*="nine wide tablet"].column{width:56.25%!important}.ui.column.grid>[class*="ten wide tablet"].column,.ui.grid>.column.row>[class*="ten wide tablet"].column,.ui.grid>.row>[class*="ten wide tablet"].column,.ui.grid>[class*="ten wide tablet"].column{width:62.5%!important}.ui.column.grid>[class*="eleven wide tablet"].column,.ui.grid>.column.row>[class*="eleven wide tablet"].column,.ui.grid>.row>[class*="eleven wide tablet"].column,.ui.grid>[class*="eleven wide tablet"].column{width:68.75%!important}.ui.column.grid>[class*="twelve wide tablet"].column,.ui.grid>.column.row>[class*="twelve wide tablet"].column,.ui.grid>.row>[class*="twelve wide tablet"].column,.ui.grid>[class*="twelve wide tablet"].column{width:75%!important}.ui.column.grid>[class*="thirteen wide tablet"].column,.ui.grid>.column.row>[class*="thirteen wide tablet"].column,.ui.grid>.row>[class*="thirteen wide tablet"].column,.ui.grid>[class*="thirteen wide tablet"].column{width:81.25%!important}.ui.column.grid>[class*="fourteen wide tablet"].column,.ui.grid>.column.row>[class*="fourteen wide tablet"].column,.ui.grid>.row>[class*="fourteen wide tablet"].column,.ui.grid>[class*="fourteen wide tablet"].column{width:87.5%!important}.ui.column.grid>[class*="fifteen wide tablet"].column,.ui.grid>.column.row>[class*="fifteen wide tablet"].column,.ui.grid>.row>[class*="fifteen wide tablet"].column,.ui.grid>[class*="fifteen wide tablet"].column{width:93.75%!important}.ui.column.grid>[class*="sixteen wide tablet"].column,.ui.grid>.column.row>[class*="sixteen wide tablet"].column,.ui.grid>.row>[class*="sixteen wide tablet"].column,.ui.grid>[class*="sixteen wide tablet"].column{width:100%!important}}@media only screen and (min-width:992px){.ui.column.grid>[class*="one wide computer"].column,.ui.grid>.column.row>[class*="one wide computer"].column,.ui.grid>.row>[class*="one wide computer"].column,.ui.grid>[class*="one wide computer"].column{width:6.25%!important}.ui.column.grid>[class*="two wide computer"].column,.ui.grid>.column.row>[class*="two wide computer"].column,.ui.grid>.row>[class*="two wide computer"].column,.ui.grid>[class*="two wide computer"].column{width:12.5%!important}.ui.column.grid>[class*="three wide computer"].column,.ui.grid>.column.row>[class*="three wide computer"].column,.ui.grid>.row>[class*="three wide computer"].column,.ui.grid>[class*="three wide computer"].column{width:18.75%!important}.ui.column.grid>[class*="four wide computer"].column,.ui.grid>.column.row>[class*="four wide computer"].column,.ui.grid>.row>[class*="four wide computer"].column,.ui.grid>[class*="four wide computer"].column{width:25%!important}.ui.column.grid>[class*="five wide computer"].column,.ui.grid>.column.row>[class*="five wide computer"].column,.ui.grid>.row>[class*="five wide computer"].column,.ui.grid>[class*="five wide computer"].column{width:31.25%!important}.ui.column.grid>[class*="six wide computer"].column,.ui.grid>.column.row>[class*="six wide computer"].column,.ui.grid>.row>[class*="six wide computer"].column,.ui.grid>[class*="six wide computer"].column{width:37.5%!important}.ui.column.grid>[class*="seven wide computer"].column,.ui.grid>.column.row>[class*="seven wide computer"].column,.ui.grid>.row>[class*="seven wide computer"].column,.ui.grid>[class*="seven wide computer"].column{width:43.75%!important}.ui.column.grid>[class*="eight wide computer"].column,.ui.grid>.column.row>[class*="eight wide computer"].column,.ui.grid>.row>[class*="eight wide computer"].column,.ui.grid>[class*="eight wide computer"].column{width:50%!important}.ui.column.grid>[class*="nine wide computer"].column,.ui.grid>.column.row>[class*="nine wide computer"].column,.ui.grid>.row>[class*="nine wide computer"].column,.ui.grid>[class*="nine wide computer"].column{width:56.25%!important}.ui.column.grid>[class*="ten wide computer"].column,.ui.grid>.column.row>[class*="ten wide computer"].column,.ui.grid>.row>[class*="ten wide computer"].column,.ui.grid>[class*="ten wide computer"].column{width:62.5%!important}.ui.column.grid>[class*="eleven wide computer"].column,.ui.grid>.column.row>[class*="eleven wide computer"].column,.ui.grid>.row>[class*="eleven wide computer"].column,.ui.grid>[class*="eleven wide computer"].column{width:68.75%!important}.ui.column.grid>[class*="twelve wide computer"].column,.ui.grid>.column.row>[class*="twelve wide computer"].column,.ui.grid>.row>[class*="twelve wide computer"].column,.ui.grid>[class*="twelve wide computer"].column{width:75%!important}.ui.column.grid>[class*="thirteen wide computer"].column,.ui.grid>.column.row>[class*="thirteen wide computer"].column,.ui.grid>.row>[class*="thirteen wide computer"].column,.ui.grid>[class*="thirteen wide computer"].column{width:81.25%!important}.ui.column.grid>[class*="fourteen wide computer"].column,.ui.grid>.column.row>[class*="fourteen wide computer"].column,.ui.grid>.row>[class*="fourteen wide computer"].column,.ui.grid>[class*="fourteen wide computer"].column{width:87.5%!important}.ui.column.grid>[class*="fifteen wide computer"].column,.ui.grid>.column.row>[class*="fifteen wide computer"].column,.ui.grid>.row>[class*="fifteen wide computer"].column,.ui.grid>[class*="fifteen wide computer"].column{width:93.75%!important}.ui.column.grid>[class*="sixteen wide computer"].column,.ui.grid>.column.row>[class*="sixteen wide computer"].column,.ui.grid>.row>[class*="sixteen wide computer"].column,.ui.grid>[class*="sixteen wide computer"].column{width:100%!important}}@media only screen and (min-width:1200px) and (max-width:1919px){.ui.column.grid>[class*="one wide large screen"].column,.ui.grid>.column.row>[class*="one wide large screen"].column,.ui.grid>.row>[class*="one wide large screen"].column,.ui.grid>[class*="one wide large screen"].column{width:6.25%!important}.ui.column.grid>[class*="two wide large screen"].column,.ui.grid>.column.row>[class*="two wide large screen"].column,.ui.grid>.row>[class*="two wide large screen"].column,.ui.grid>[class*="two wide large screen"].column{width:12.5%!important}.ui.column.grid>[class*="three wide large screen"].column,.ui.grid>.column.row>[class*="three wide large screen"].column,.ui.grid>.row>[class*="three wide large screen"].column,.ui.grid>[class*="three wide large screen"].column{width:18.75%!important}.ui.column.grid>[class*="four wide large screen"].column,.ui.grid>.column.row>[class*="four wide large screen"].column,.ui.grid>.row>[class*="four wide large screen"].column,.ui.grid>[class*="four wide large screen"].column{width:25%!important}.ui.column.grid>[class*="five wide large screen"].column,.ui.grid>.column.row>[class*="five wide large screen"].column,.ui.grid>.row>[class*="five wide large screen"].column,.ui.grid>[class*="five wide large screen"].column{width:31.25%!important}.ui.column.grid>[class*="six wide large screen"].column,.ui.grid>.column.row>[class*="six wide large screen"].column,.ui.grid>.row>[class*="six wide large screen"].column,.ui.grid>[class*="six wide large screen"].column{width:37.5%!important}.ui.column.grid>[class*="seven wide large screen"].column,.ui.grid>.column.row>[class*="seven wide large screen"].column,.ui.grid>.row>[class*="seven wide large screen"].column,.ui.grid>[class*="seven wide large screen"].column{width:43.75%!important}.ui.column.grid>[class*="eight wide large screen"].column,.ui.grid>.column.row>[class*="eight wide large screen"].column,.ui.grid>.row>[class*="eight wide large screen"].column,.ui.grid>[class*="eight wide large screen"].column{width:50%!important}.ui.column.grid>[class*="nine wide large screen"].column,.ui.grid>.column.row>[class*="nine wide large screen"].column,.ui.grid>.row>[class*="nine wide large screen"].column,.ui.grid>[class*="nine wide large screen"].column{width:56.25%!important}.ui.column.grid>[class*="ten wide large screen"].column,.ui.grid>.column.row>[class*="ten wide large screen"].column,.ui.grid>.row>[class*="ten wide large screen"].column,.ui.grid>[class*="ten wide large screen"].column{width:62.5%!important}.ui.column.grid>[class*="eleven wide large screen"].column,.ui.grid>.column.row>[class*="eleven wide large screen"].column,.ui.grid>.row>[class*="eleven wide large screen"].column,.ui.grid>[class*="eleven wide large screen"].column{width:68.75%!important}.ui.column.grid>[class*="twelve wide large screen"].column,.ui.grid>.column.row>[class*="twelve wide large screen"].column,.ui.grid>.row>[class*="twelve wide large screen"].column,.ui.grid>[class*="twelve wide large screen"].column{width:75%!important}.ui.column.grid>[class*="thirteen wide large screen"].column,.ui.grid>.column.row>[class*="thirteen wide large screen"].column,.ui.grid>.row>[class*="thirteen wide large screen"].column,.ui.grid>[class*="thirteen wide large screen"].column{width:81.25%!important}.ui.column.grid>[class*="fourteen wide large screen"].column,.ui.grid>.column.row>[class*="fourteen wide large screen"].column,.ui.grid>.row>[class*="fourteen wide large screen"].column,.ui.grid>[class*="fourteen wide large screen"].column{width:87.5%!important}.ui.column.grid>[class*="fifteen wide large screen"].column,.ui.grid>.column.row>[class*="fifteen wide large screen"].column,.ui.grid>.row>[class*="fifteen wide large screen"].column,.ui.grid>[class*="fifteen wide large screen"].column{width:93.75%!important}.ui.column.grid>[class*="sixteen wide large screen"].column,.ui.grid>.column.row>[class*="sixteen wide large screen"].column,.ui.grid>.row>[class*="sixteen wide large screen"].column,.ui.grid>[class*="sixteen wide large screen"].column{width:100%!important}}@media only screen and (min-width:1920px){.ui.column.grid>[class*="one wide widescreen"].column,.ui.grid>.column.row>[class*="one wide widescreen"].column,.ui.grid>.row>[class*="one wide widescreen"].column,.ui.grid>[class*="one wide widescreen"].column{width:6.25%!important}.ui.column.grid>[class*="two wide widescreen"].column,.ui.grid>.column.row>[class*="two wide widescreen"].column,.ui.grid>.row>[class*="two wide widescreen"].column,.ui.grid>[class*="two wide widescreen"].column{width:12.5%!important}.ui.column.grid>[class*="three wide widescreen"].column,.ui.grid>.column.row>[class*="three wide widescreen"].column,.ui.grid>.row>[class*="three wide widescreen"].column,.ui.grid>[class*="three wide widescreen"].column{width:18.75%!important}.ui.column.grid>[class*="four wide widescreen"].column,.ui.grid>.column.row>[class*="four wide widescreen"].column,.ui.grid>.row>[class*="four wide widescreen"].column,.ui.grid>[class*="four wide widescreen"].column{width:25%!important}.ui.column.grid>[class*="five wide widescreen"].column,.ui.grid>.column.row>[class*="five wide widescreen"].column,.ui.grid>.row>[class*="five wide widescreen"].column,.ui.grid>[class*="five wide widescreen"].column{width:31.25%!important}.ui.column.grid>[class*="six wide widescreen"].column,.ui.grid>.column.row>[class*="six wide widescreen"].column,.ui.grid>.row>[class*="six wide widescreen"].column,.ui.grid>[class*="six wide widescreen"].column{width:37.5%!important}.ui.column.grid>[class*="seven wide widescreen"].column,.ui.grid>.column.row>[class*="seven wide widescreen"].column,.ui.grid>.row>[class*="seven wide widescreen"].column,.ui.grid>[class*="seven wide widescreen"].column{width:43.75%!important}.ui.column.grid>[class*="eight wide widescreen"].column,.ui.grid>.column.row>[class*="eight wide widescreen"].column,.ui.grid>.row>[class*="eight wide widescreen"].column,.ui.grid>[class*="eight wide widescreen"].column{width:50%!important}.ui.column.grid>[class*="nine wide widescreen"].column,.ui.grid>.column.row>[class*="nine wide widescreen"].column,.ui.grid>.row>[class*="nine wide widescreen"].column,.ui.grid>[class*="nine wide widescreen"].column{width:56.25%!important}.ui.column.grid>[class*="ten wide widescreen"].column,.ui.grid>.column.row>[class*="ten wide widescreen"].column,.ui.grid>.row>[class*="ten wide widescreen"].column,.ui.grid>[class*="ten wide widescreen"].column{width:62.5%!important}.ui.column.grid>[class*="eleven wide widescreen"].column,.ui.grid>.column.row>[class*="eleven wide widescreen"].column,.ui.grid>.row>[class*="eleven wide widescreen"].column,.ui.grid>[class*="eleven wide widescreen"].column{width:68.75%!important}.ui.column.grid>[class*="twelve wide widescreen"].column,.ui.grid>.column.row>[class*="twelve wide widescreen"].column,.ui.grid>.row>[class*="twelve wide widescreen"].column,.ui.grid>[class*="twelve wide widescreen"].column{width:75%!important}.ui.column.grid>[class*="thirteen wide widescreen"].column,.ui.grid>.column.row>[class*="thirteen wide widescreen"].column,.ui.grid>.row>[class*="thirteen wide widescreen"].column,.ui.grid>[class*="thirteen wide widescreen"].column{width:81.25%!important}.ui.column.grid>[class*="fourteen wide widescreen"].column,.ui.grid>.column.row>[class*="fourteen wide widescreen"].column,.ui.grid>.row>[class*="fourteen wide widescreen"].column,.ui.grid>[class*="fourteen wide widescreen"].column{width:87.5%!important}.ui.column.grid>[class*="fifteen wide widescreen"].column,.ui.grid>.column.row>[class*="fifteen wide widescreen"].column,.ui.grid>.row>[class*="fifteen wide widescreen"].column,.ui.grid>[class*="fifteen wide widescreen"].column{width:93.75%!important}.ui.column.grid>[class*="sixteen wide widescreen"].column,.ui.grid>.column.row>[class*="sixteen wide widescreen"].column,.ui.grid>.row>[class*="sixteen wide widescreen"].column,.ui.grid>[class*="sixteen wide widescreen"].column{width:100%!important}}.ui.centered.grid,.ui.centered.grid>.row,.ui.grid>.centered.row{text-align:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.ui.centered.grid>.column:not(.aligned):not(.justified):not(.row),.ui.centered.grid>.row>.column:not(.aligned):not(.justified),.ui.grid .centered.row>.column:not(.aligned):not(.justified){text-align:left}.ui.grid>.centered.column,.ui.grid>.row>.centered.column{display:block;margin-left:auto;margin-right:auto}.ui.grid>.relaxed.row>.column,.ui.relaxed.grid>.column:not(.row),.ui.relaxed.grid>.row>.column{padding-left:1.5rem;padding-right:1.5rem}.ui.grid>[class*="very relaxed"].row>.column,.ui[class*="very relaxed"].grid>.column:not(.row),.ui[class*="very relaxed"].grid>.row>.column{padding-left:2.5rem;padding-right:2.5rem}.ui.grid .relaxed.row+.ui.divider,.ui.relaxed.grid .row+.ui.divider{margin-left:1.5rem;margin-right:1.5rem}.ui.grid [class*="very relaxed"].row+.ui.divider,.ui[class*="very relaxed"].grid .row+.ui.divider{margin-left:2.5rem;margin-right:2.5rem}.ui.padded.grid:not(.vertically):not(.horizontally){margin:0!important}[class*="horizontally padded"].ui.grid{margin-left:0!important;margin-right:0!important}[class*="vertically padded"].ui.grid{margin-top:0!important;margin-bottom:0!important}.ui.grid [class*="left floated"].column{margin-right:auto}.ui.grid [class*="right floated"].column{margin-left:auto}.ui.divided.grid:not([class*="vertically divided"])>.column:not(.row),.ui.divided.grid:not([class*="vertically divided"])>.row>.column{-webkit-box-shadow:-1px 0 0 0 rgba(34,36,38,.15);box-shadow:-1px 0 0 0 rgba(34,36,38,.15)}.ui[class*="vertically divided"].grid>.column:not(.row),.ui[class*="vertically divided"].grid>.row>.column{margin-top:1rem;margin-bottom:1rem;padding-top:0;padding-bottom:0}.ui[class*="vertically divided"].grid>.row{margin-top:0;margin-bottom:0}.ui.divided.grid:not([class*="vertically divided"])>.column:first-child,.ui.divided.grid:not([class*="vertically divided"])>.row>.column:first-child{-webkit-box-shadow:none;box-shadow:none}.ui[class*="vertically divided"].grid>.row:first-child>.column{margin-top:0}.ui.grid>.divided.row>.column{-webkit-box-shadow:-1px 0 0 0 rgba(34,36,38,.15);box-shadow:-1px 0 0 0 rgba(34,36,38,.15)}.ui.grid>.divided.row>.column:first-child{-webkit-box-shadow:none;box-shadow:none}.ui[class*="vertically divided"].grid>.row{position:relative}.ui[class*="vertically divided"].grid>.row:before{position:absolute;content:"";top:0;left:0;width:calc(100% - 2rem);height:1px;margin:0 1rem;-webkit-box-shadow:0 -1px 0 0 rgba(34,36,38,.15);box-shadow:0 -1px 0 0 rgba(34,36,38,.15)}.ui.padded.divided.grid:not(.vertically):not(.horizontally),[class*="horizontally padded"].ui.divided.grid{width:100%}.ui[class*="vertically divided"].grid>.row:first-child:before{-webkit-box-shadow:none;box-shadow:none}.ui.inverted.divided.grid:not([class*="vertically divided"])>.column:not(.row),.ui.inverted.divided.grid:not([class*="vertically divided"])>.row>.column{-webkit-box-shadow:-1px 0 0 0 rgba(255,255,255,.1);box-shadow:-1px 0 0 0 rgba(255,255,255,.1)}.ui.inverted.divided.grid:not([class*="vertically divided"])>.column:not(.row):first-child,.ui.inverted.divided.grid:not([class*="vertically divided"])>.row>.column:first-child{-webkit-box-shadow:none;box-shadow:none}.ui.inverted[class*="vertically divided"].grid>.row:before{-webkit-box-shadow:0 -1px 0 0 rgba(255,255,255,.1);box-shadow:0 -1px 0 0 rgba(255,255,255,.1)}.ui.relaxed[class*="vertically divided"].grid>.row:before{margin-left:1.5rem;margin-right:1.5rem;width:calc(100% - 3rem)}.ui[class*="very relaxed"][class*="vertically divided"].grid>.row:before{margin-left:2.5rem;margin-right:2.5rem;width:calc(100% - 5rem)}.ui.celled.grid{width:100%;margin:1em 0;-webkit-box-shadow:0 0 0 1px #d4d4d5;box-shadow:0 0 0 1px #d4d4d5}.ui.celled.grid>.row{width:100%!important;margin:0;padding:0;-webkit-box-shadow:0 -1px 0 0 #d4d4d5;box-shadow:0 -1px 0 0 #d4d4d5}.ui.celled.grid>.column:not(.row),.ui.celled.grid>.row>.column{-webkit-box-shadow:-1px 0 0 0 #d4d4d5;box-shadow:-1px 0 0 0 #d4d4d5}.ui.celled.grid>.column:first-child,.ui.celled.grid>.row>.column:first-child{-webkit-box-shadow:none;box-shadow:none}.ui.celled.grid>.column:not(.row),.ui.celled.grid>.row>.column{padding:1em}.ui.relaxed.celled.grid>.column:not(.row),.ui.relaxed.celled.grid>.row>.column{padding:1.5em}.ui[class*="very relaxed"].celled.grid>.column:not(.row),.ui[class*="very relaxed"].celled.grid>.row>.column{padding:2em}.ui[class*="internally celled"].grid{-webkit-box-shadow:none;box-shadow:none;margin:0}.ui[class*="internally celled"].grid>.row:first-child{-webkit-box-shadow:none;box-shadow:none}.ui[class*="internally celled"].grid>.row>.column:first-child{-webkit-box-shadow:none;box-shadow:none}.ui.grid>.row>[class*="top aligned"].column,.ui.grid>[class*="top aligned"].column:not(.row),.ui.grid>[class*="top aligned"].row>.column,.ui[class*="top aligned"].grid>.column:not(.row),.ui[class*="top aligned"].grid>.row>.column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;vertical-align:top;-ms-flex-item-align:start!important;align-self:flex-start!important}.ui.grid>.row>[class*="middle aligned"].column,.ui.grid>[class*="middle aligned"].column:not(.row),.ui.grid>[class*="middle aligned"].row>.column,.ui[class*="middle aligned"].grid>.column:not(.row),.ui[class*="middle aligned"].grid>.row>.column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;vertical-align:middle;-ms-flex-item-align:center!important;align-self:center!important}.ui.grid>.row>[class*="bottom aligned"].column,.ui.grid>[class*="bottom aligned"].column:not(.row),.ui.grid>[class*="bottom aligned"].row>.column,.ui[class*="bottom aligned"].grid>.column:not(.row),.ui[class*="bottom aligned"].grid>.row>.column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;vertical-align:bottom;-ms-flex-item-align:end!important;align-self:flex-end!important}.ui.grid>.row>.stretched.column,.ui.grid>.stretched.column:not(.row),.ui.grid>.stretched.row>.column,.ui.stretched.grid>.column,.ui.stretched.grid>.row>.column{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important;-ms-flex-item-align:stretch;align-self:stretch;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.ui.grid>.row>.stretched.column>*,.ui.grid>.stretched.column:not(.row)>*,.ui.grid>.stretched.row>.column>*,.ui.stretched.grid>.column>*,.ui.stretched.grid>.row>.column>*{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.ui.grid>.row>[class*="left aligned"].column.column,.ui.grid>[class*="left aligned"].column.column,.ui.grid>[class*="left aligned"].row>.column,.ui[class*="left aligned"].grid>.column,.ui[class*="left aligned"].grid>.row>.column{text-align:left;-ms-flex-item-align:inherit;align-self:inherit}.ui.grid>.row>[class*="center aligned"].column.column,.ui.grid>[class*="center aligned"].column.column,.ui.grid>[class*="center aligned"].row>.column,.ui[class*="center aligned"].grid>.column,.ui[class*="center aligned"].grid>.row>.column{text-align:center;-ms-flex-item-align:inherit;align-self:inherit}.ui[class*="center aligned"].grid{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.ui.grid>.row>[class*="right aligned"].column.column,.ui.grid>[class*="right aligned"].column.column,.ui.grid>[class*="right aligned"].row>.column,.ui[class*="right aligned"].grid>.column,.ui[class*="right aligned"].grid>.row>.column{text-align:right;-ms-flex-item-align:inherit;align-self:inherit}.ui.grid>.justified.column.column,.ui.grid>.justified.row>.column,.ui.grid>.row>.justified.column.column,.ui.justified.grid>.column,.ui.justified.grid>.row>.column{text-align:justify;-webkit-hyphens:auto;-ms-hyphens:auto;hyphens:auto}.ui.grid>.row>.black.column,.ui.grid>.row>.blue.column,.ui.grid>.row>.brown.column,.ui.grid>.row>.green.column,.ui.grid>.row>.grey.column,.ui.grid>.row>.olive.column,.ui.grid>.row>.orange.column,.ui.grid>.row>.pink.column,.ui.grid>.row>.purple.column,.ui.grid>.row>.red.column,.ui.grid>.row>.teal.column,.ui.grid>.row>.violet.column,.ui.grid>.row>.yellow.column{margin-top:-1rem;margin-bottom:-1rem;padding-top:1rem;padding-bottom:1rem}.ui.grid>.red.column,.ui.grid>.red.row,.ui.grid>.row>.red.column{background-color:#db2828!important;color:#fff}.ui.grid>.orange.column,.ui.grid>.orange.row,.ui.grid>.row>.orange.column{background-color:#f2711c!important;color:#fff}.ui.grid>.row>.yellow.column,.ui.grid>.yellow.column,.ui.grid>.yellow.row{background-color:#fbbd08!important;color:#fff}.ui.grid>.olive.column,.ui.grid>.olive.row,.ui.grid>.row>.olive.column{background-color:#b5cc18!important;color:#fff}.ui.grid>.green.column,.ui.grid>.green.row,.ui.grid>.row>.green.column{background-color:#21ba45!important;color:#fff}.ui.grid>.row>.teal.column,.ui.grid>.teal.column,.ui.grid>.teal.row{background-color:#00b5ad!important;color:#fff}.ui.grid>.blue.column,.ui.grid>.blue.row,.ui.grid>.row>.blue.column{background-color:#2185d0!important;color:#fff}.ui.grid>.row>.violet.column,.ui.grid>.violet.column,.ui.grid>.violet.row{background-color:#6435c9!important;color:#fff}.ui.grid>.purple.column,.ui.grid>.purple.row,.ui.grid>.row>.purple.column{background-color:#a333c8!important;color:#fff}.ui.grid>.pink.column,.ui.grid>.pink.row,.ui.grid>.row>.pink.column{background-color:#e03997!important;color:#fff}.ui.grid>.brown.column,.ui.grid>.brown.row,.ui.grid>.row>.brown.column{background-color:#a5673f!important;color:#fff}.ui.grid>.grey.column,.ui.grid>.grey.row,.ui.grid>.row>.grey.column{background-color:#767676!important;color:#fff}.ui.grid>.black.column,.ui.grid>.black.row,.ui.grid>.row>.black.column{background-color:#1b1c1d!important;color:#fff}.ui.grid>[class*="equal width"].row>.column,.ui[class*="equal width"].grid>.column:not(.row),.ui[class*="equal width"].grid>.row>.column{display:inline-block;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.ui.grid>[class*="equal width"].row>.wide.column,.ui[class*="equal width"].grid>.row>.wide.column,.ui[class*="equal width"].grid>.wide.column{-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0}@media only screen and (max-width:767px){.ui.grid>[class*="mobile reversed"].row,.ui[class*="mobile reversed"].grid,.ui[class*="mobile reversed"].grid>.row{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.ui.stackable[class*="mobile reversed"],.ui[class*="mobile vertically reversed"].grid{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"])>.column:first-child,.ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"])>.row>.column:first-child{-webkit-box-shadow:-1px 0 0 0 rgba(34,36,38,.15);box-shadow:-1px 0 0 0 rgba(34,36,38,.15)}.ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"])>.column:last-child,.ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"])>.row>.column:last-child{-webkit-box-shadow:none;box-shadow:none}.ui.grid[class*="vertically divided"][class*="mobile vertically reversed"]>.row:first-child:before{-webkit-box-shadow:0 -1px 0 0 rgba(34,36,38,.15);box-shadow:0 -1px 0 0 rgba(34,36,38,.15)}.ui.grid[class*="vertically divided"][class*="mobile vertically reversed"]>.row:last-child:before{-webkit-box-shadow:none;box-shadow:none}.ui[class*="mobile reversed"].celled.grid>.row>.column:first-child{-webkit-box-shadow:-1px 0 0 0 #d4d4d5;box-shadow:-1px 0 0 0 #d4d4d5}.ui[class*="mobile reversed"].celled.grid>.row>.column:last-child{-webkit-box-shadow:none;box-shadow:none}}@media only screen and (min-width:768px) and (max-width:991px){.ui.grid>[class*="tablet reversed"].row,.ui[class*="tablet reversed"].grid,.ui[class*="tablet reversed"].grid>.row{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.ui[class*="tablet vertically reversed"].grid{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"])>.column:first-child,.ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"])>.row>.column:first-child{-webkit-box-shadow:-1px 0 0 0 rgba(34,36,38,.15);box-shadow:-1px 0 0 0 rgba(34,36,38,.15)}.ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"])>.column:last-child,.ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"])>.row>.column:last-child{-webkit-box-shadow:none;box-shadow:none}.ui.grid[class*="vertically divided"][class*="tablet vertically reversed"]>.row:first-child:before{-webkit-box-shadow:0 -1px 0 0 rgba(34,36,38,.15);box-shadow:0 -1px 0 0 rgba(34,36,38,.15)}.ui.grid[class*="vertically divided"][class*="tablet vertically reversed"]>.row:last-child:before{-webkit-box-shadow:none;box-shadow:none}.ui[class*="tablet reversed"].celled.grid>.row>.column:first-child{-webkit-box-shadow:-1px 0 0 0 #d4d4d5;box-shadow:-1px 0 0 0 #d4d4d5}.ui[class*="tablet reversed"].celled.grid>.row>.column:last-child{-webkit-box-shadow:none;box-shadow:none}}@media only screen and (min-width:992px){.ui.grid>[class*="computer reversed"].row,.ui[class*="computer reversed"].grid,.ui[class*="computer reversed"].grid>.row{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.ui[class*="computer vertically reversed"].grid{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"])>.column:first-child,.ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"])>.row>.column:first-child{-webkit-box-shadow:-1px 0 0 0 rgba(34,36,38,.15);box-shadow:-1px 0 0 0 rgba(34,36,38,.15)}.ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"])>.column:last-child,.ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"])>.row>.column:last-child{-webkit-box-shadow:none;box-shadow:none}.ui.grid[class*="vertically divided"][class*="computer vertically reversed"]>.row:first-child:before{-webkit-box-shadow:0 -1px 0 0 rgba(34,36,38,.15);box-shadow:0 -1px 0 0 rgba(34,36,38,.15)}.ui.grid[class*="vertically divided"][class*="computer vertically reversed"]>.row:last-child:before{-webkit-box-shadow:none;box-shadow:none}.ui[class*="computer reversed"].celled.grid>.row>.column:first-child{-webkit-box-shadow:-1px 0 0 0 #d4d4d5;box-shadow:-1px 0 0 0 #d4d4d5}.ui[class*="computer reversed"].celled.grid>.row>.column:last-child{-webkit-box-shadow:none;box-shadow:none}}@media only screen and (min-width:768px) and (max-width:991px){.ui.doubling.grid{width:auto}.ui.doubling.grid>.row,.ui.grid>.doubling.row{margin:0!important;padding:0!important}.ui.doubling.grid>.row>.column,.ui.grid>.doubling.row>.column{display:inline-block!important;padding-top:1rem!important;padding-bottom:1rem!important;-webkit-box-shadow:none!important;box-shadow:none!important;margin:0}.ui.grid>[class*="two column"].doubling.row.row>.column,.ui[class*="two column"].doubling.grid>.column:not(.row),.ui[class*="two column"].doubling.grid>.row>.column{width:100%!important}.ui.grid>[class*="three column"].doubling.row.row>.column,.ui[class*="three column"].doubling.grid>.column:not(.row),.ui[class*="three column"].doubling.grid>.row>.column{width:50%!important}.ui.grid>[class*="four column"].doubling.row.row>.column,.ui[class*="four column"].doubling.grid>.column:not(.row),.ui[class*="four column"].doubling.grid>.row>.column{width:50%!important}.ui.grid>[class*="five column"].doubling.row.row>.column,.ui[class*="five column"].doubling.grid>.column:not(.row),.ui[class*="five column"].doubling.grid>.row>.column{width:33.33333333%!important}.ui.grid>[class*="six column"].doubling.row.row>.column,.ui[class*="six column"].doubling.grid>.column:not(.row),.ui[class*="six column"].doubling.grid>.row>.column{width:33.33333333%!important}.ui.grid>[class*="seven column"].doubling.row.row>.column,.ui[class*="seven column"].doubling.grid>.column:not(.row),.ui[class*="seven column"].doubling.grid>.row>.column{width:33.33333333%!important}.ui.grid>[class*="eight column"].doubling.row.row>.column,.ui[class*="eight column"].doubling.grid>.column:not(.row),.ui[class*="eight column"].doubling.grid>.row>.column{width:25%!important}.ui.grid>[class*="nine column"].doubling.row.row>.column,.ui[class*="nine column"].doubling.grid>.column:not(.row),.ui[class*="nine column"].doubling.grid>.row>.column{width:25%!important}.ui.grid>[class*="ten column"].doubling.row.row>.column,.ui[class*="ten column"].doubling.grid>.column:not(.row),.ui[class*="ten column"].doubling.grid>.row>.column{width:20%!important}.ui.grid>[class*="eleven column"].doubling.row.row>.column,.ui[class*="eleven column"].doubling.grid>.column:not(.row),.ui[class*="eleven column"].doubling.grid>.row>.column{width:20%!important}.ui.grid>[class*="twelve column"].doubling.row.row>.column,.ui[class*="twelve column"].doubling.grid>.column:not(.row),.ui[class*="twelve column"].doubling.grid>.row>.column{width:16.66666667%!important}.ui.grid>[class*="thirteen column"].doubling.row.row>.column,.ui[class*="thirteen column"].doubling.grid>.column:not(.row),.ui[class*="thirteen column"].doubling.grid>.row>.column{width:16.66666667%!important}.ui.grid>[class*="fourteen column"].doubling.row.row>.column,.ui[class*="fourteen column"].doubling.grid>.column:not(.row),.ui[class*="fourteen column"].doubling.grid>.row>.column{width:14.28571429%!important}.ui.grid>[class*="fifteen column"].doubling.row.row>.column,.ui[class*="fifteen column"].doubling.grid>.column:not(.row),.ui[class*="fifteen column"].doubling.grid>.row>.column{width:14.28571429%!important}.ui.grid>[class*="sixteen column"].doubling.row.row>.column,.ui[class*="sixteen column"].doubling.grid>.column:not(.row),.ui[class*="sixteen column"].doubling.grid>.row>.column{width:12.5%!important}}@media only screen and (max-width:767px){.ui.doubling.grid>.row,.ui.grid>.doubling.row{margin:0!important;padding:0!important}.ui.doubling.grid>.row>.column,.ui.grid>.doubling.row>.column{padding-top:1rem!important;padding-bottom:1rem!important;margin:0!important;-webkit-box-shadow:none!important;box-shadow:none!important}.ui.grid>[class*="two column"].doubling:not(.stackable).row.row>.column,.ui[class*="two column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="two column"].doubling:not(.stackable).grid>.row>.column{width:100%!important}.ui.grid>[class*="three column"].doubling:not(.stackable).row.row>.column,.ui[class*="three column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="three column"].doubling:not(.stackable).grid>.row>.column{width:50%!important}.ui.grid>[class*="four column"].doubling:not(.stackable).row.row>.column,.ui[class*="four column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="four column"].doubling:not(.stackable).grid>.row>.column{width:50%!important}.ui.grid>[class*="five column"].doubling:not(.stackable).row.row>.column,.ui[class*="five column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="five column"].doubling:not(.stackable).grid>.row>.column{width:50%!important}.ui.grid>[class*="six column"].doubling:not(.stackable).row.row>.column,.ui[class*="six column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="six column"].doubling:not(.stackable).grid>.row>.column{width:50%!important}.ui.grid>[class*="seven column"].doubling:not(.stackable).row.row>.column,.ui[class*="seven column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="seven column"].doubling:not(.stackable).grid>.row>.column{width:50%!important}.ui.grid>[class*="eight column"].doubling:not(.stackable).row.row>.column,.ui[class*="eight column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="eight column"].doubling:not(.stackable).grid>.row>.column{width:50%!important}.ui.grid>[class*="nine column"].doubling:not(.stackable).row.row>.column,.ui[class*="nine column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="nine column"].doubling:not(.stackable).grid>.row>.column{width:33.33333333%!important}.ui.grid>[class*="ten column"].doubling:not(.stackable).row.row>.column,.ui[class*="ten column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="ten column"].doubling:not(.stackable).grid>.row>.column{width:33.33333333%!important}.ui.grid>[class*="eleven column"].doubling:not(.stackable).row.row>.column,.ui[class*="eleven column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="eleven column"].doubling:not(.stackable).grid>.row>.column{width:33.33333333%!important}.ui.grid>[class*="twelve column"].doubling:not(.stackable).row.row>.column,.ui[class*="twelve column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="twelve column"].doubling:not(.stackable).grid>.row>.column{width:33.33333333%!important}.ui.grid>[class*="thirteen column"].doubling:not(.stackable).row.row>.column,.ui[class*="thirteen column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="thirteen column"].doubling:not(.stackable).grid>.row>.column{width:33.33333333%!important}.ui.grid>[class*="fourteen column"].doubling:not(.stackable).row.row>.column,.ui[class*="fourteen column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="fourteen column"].doubling:not(.stackable).grid>.row>.column{width:25%!important}.ui.grid>[class*="fifteen column"].doubling:not(.stackable).row.row>.column,.ui[class*="fifteen column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="fifteen column"].doubling:not(.stackable).grid>.row>.column{width:25%!important}.ui.grid>[class*="sixteen column"].doubling:not(.stackable).row.row>.column,.ui[class*="sixteen column"].doubling:not(.stackable).grid>.column:not(.row),.ui[class*="sixteen column"].doubling:not(.stackable).grid>.row>.column{width:25%!important}}@media only screen and (max-width:767px){.ui.stackable.grid{width:auto;margin-left:0!important;margin-right:0!important}.ui.grid>.stackable.stackable.row>.column,.ui.stackable.grid>.column.grid>.column,.ui.stackable.grid>.column.row>.column,.ui.stackable.grid>.column:not(.row),.ui.stackable.grid>.row>.column,.ui.stackable.grid>.row>.wide.column,.ui.stackable.grid>.wide.column{width:100%!important;margin:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important;padding:1rem 1rem!important}.ui.stackable.grid:not(.vertically)>.row{margin:0;padding:0}.ui.container>.ui.stackable.grid>.column,.ui.container>.ui.stackable.grid>.row>.column{padding-left:0!important;padding-right:0!important}.ui.grid .ui.stackable.grid,.ui.segment:not(.vertical) .ui.stackable.page.grid{margin-left:-1rem!important;margin-right:-1rem!important}.ui.stackable.celled.grid>.column:not(.row):first-child,.ui.stackable.celled.grid>.row:first-child>.column:first-child,.ui.stackable.divided.grid>.column:not(.row):first-child,.ui.stackable.divided.grid>.row:first-child>.column:first-child{border-top:none!important}.ui.inverted.stackable.celled.grid>.column:not(.row),.ui.inverted.stackable.celled.grid>.row>.column,.ui.inverted.stackable.divided.grid>.column:not(.row),.ui.inverted.stackable.divided.grid>.row>.column{border-top:1px solid rgba(255,255,255,.1)}.ui.stackable.celled.grid>.column:not(.row),.ui.stackable.celled.grid>.row>.column,.ui.stackable.divided:not(.vertically).grid>.column:not(.row),.ui.stackable.divided:not(.vertically).grid>.row>.column{border-top:1px solid rgba(34,36,38,.15);-webkit-box-shadow:none!important;box-shadow:none!important;padding-top:2rem!important;padding-bottom:2rem!important}.ui.stackable.celled.grid>.row{-webkit-box-shadow:none!important;box-shadow:none!important}.ui.stackable.divided:not(.vertically).grid>.column:not(.row),.ui.stackable.divided:not(.vertically).grid>.row>.column{padding-left:0!important;padding-right:0!important}}@media only screen and (max-width:767px){.ui.grid.grid.grid>.row>[class*="tablet only"].column:not(.mobile),.ui.grid.grid.grid>[class*="tablet only"].column:not(.mobile),.ui.grid.grid.grid>[class*="tablet only"].row:not(.mobile),.ui[class*="tablet only"].grid.grid.grid:not(.mobile){display:none!important}.ui.grid.grid.grid>.row>[class*="computer only"].column:not(.mobile),.ui.grid.grid.grid>[class*="computer only"].column:not(.mobile),.ui.grid.grid.grid>[class*="computer only"].row:not(.mobile),.ui[class*="computer only"].grid.grid.grid:not(.mobile){display:none!important}.ui.grid.grid.grid>.row>[class*="large screen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="large screen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="large screen only"].row:not(.mobile),.ui[class*="large screen only"].grid.grid.grid:not(.mobile){display:none!important}.ui.grid.grid.grid>.row>[class*="widescreen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="widescreen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="widescreen only"].row:not(.mobile),.ui[class*="widescreen only"].grid.grid.grid:not(.mobile){display:none!important}}@media only screen and (min-width:768px) and (max-width:991px){.ui.grid.grid.grid>.row>[class*="mobile only"].column:not(.tablet),.ui.grid.grid.grid>[class*="mobile only"].column:not(.tablet),.ui.grid.grid.grid>[class*="mobile only"].row:not(.tablet),.ui[class*="mobile only"].grid.grid.grid:not(.tablet){display:none!important}.ui.grid.grid.grid>.row>[class*="computer only"].column:not(.tablet),.ui.grid.grid.grid>[class*="computer only"].column:not(.tablet),.ui.grid.grid.grid>[class*="computer only"].row:not(.tablet),.ui[class*="computer only"].grid.grid.grid:not(.tablet){display:none!important}.ui.grid.grid.grid>.row>[class*="large screen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="large screen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="large screen only"].row:not(.mobile),.ui[class*="large screen only"].grid.grid.grid:not(.mobile){display:none!important}.ui.grid.grid.grid>.row>[class*="widescreen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="widescreen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="widescreen only"].row:not(.mobile),.ui[class*="widescreen only"].grid.grid.grid:not(.mobile){display:none!important}}@media only screen and (min-width:992px) and (max-width:1199px){.ui.grid.grid.grid>.row>[class*="mobile only"].column:not(.computer),.ui.grid.grid.grid>[class*="mobile only"].column:not(.computer),.ui.grid.grid.grid>[class*="mobile only"].row:not(.computer),.ui[class*="mobile only"].grid.grid.grid:not(.computer){display:none!important}.ui.grid.grid.grid>.row>[class*="tablet only"].column:not(.computer),.ui.grid.grid.grid>[class*="tablet only"].column:not(.computer),.ui.grid.grid.grid>[class*="tablet only"].row:not(.computer),.ui[class*="tablet only"].grid.grid.grid:not(.computer){display:none!important}.ui.grid.grid.grid>.row>[class*="large screen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="large screen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="large screen only"].row:not(.mobile),.ui[class*="large screen only"].grid.grid.grid:not(.mobile){display:none!important}.ui.grid.grid.grid>.row>[class*="widescreen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="widescreen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="widescreen only"].row:not(.mobile),.ui[class*="widescreen only"].grid.grid.grid:not(.mobile){display:none!important}}@media only screen and (min-width:1200px) and (max-width:1919px){.ui.grid.grid.grid>.row>[class*="mobile only"].column:not(.computer),.ui.grid.grid.grid>[class*="mobile only"].column:not(.computer),.ui.grid.grid.grid>[class*="mobile only"].row:not(.computer),.ui[class*="mobile only"].grid.grid.grid:not(.computer){display:none!important}.ui.grid.grid.grid>.row>[class*="tablet only"].column:not(.computer),.ui.grid.grid.grid>[class*="tablet only"].column:not(.computer),.ui.grid.grid.grid>[class*="tablet only"].row:not(.computer),.ui[class*="tablet only"].grid.grid.grid:not(.computer){display:none!important}.ui.grid.grid.grid>.row>[class*="widescreen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="widescreen only"].column:not(.mobile),.ui.grid.grid.grid>[class*="widescreen only"].row:not(.mobile),.ui[class*="widescreen only"].grid.grid.grid:not(.mobile){display:none!important}}@media only screen and (min-width:1920px){.ui.grid.grid.grid>.row>[class*="mobile only"].column:not(.computer),.ui.grid.grid.grid>[class*="mobile only"].column:not(.computer),.ui.grid.grid.grid>[class*="mobile only"].row:not(.computer),.ui[class*="mobile only"].grid.grid.grid:not(.computer){display:none!important}.ui.grid.grid.grid>.row>[class*="tablet only"].column:not(.computer),.ui.grid.grid.grid>[class*="tablet only"].column:not(.computer),.ui.grid.grid.grid>[class*="tablet only"].row:not(.computer),.ui[class*="tablet only"].grid.grid.grid:not(.computer){display:none!important}}.ui.menu{display:-webkit-box;display:-ms-flexbox;display:flex;margin:1rem 0;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;background:#fff;font-weight:400;border:1px solid rgba(34,36,38,.15);-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15);box-shadow:0 1px 2px 0 rgba(34,36,38,.15);border-radius:.28571429rem;min-height:2.85714286em}.ui.menu:after{content:'';display:block;height:0;clear:both;visibility:hidden}.ui.menu:first-child{margin-top:0}.ui.menu:last-child{margin-bottom:0}.ui.menu .menu{margin:0}.ui.menu:not(.vertical)>.menu{display:-webkit-box;display:-ms-flexbox;display:flex}.ui.menu:not(.vertical) .item{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ui.menu .item{position:relative;vertical-align:middle;line-height:1;text-decoration:none;-webkit-tap-highlight-color:transparent;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background:0 0;padding:.92857143em 1.14285714em;text-transform:none;color:rgba(0,0,0,.87);font-weight:400;-webkit-transition:background .1s ease,color .1s ease,-webkit-box-shadow .1s ease;transition:background .1s ease,color .1s ease,-webkit-box-shadow .1s ease;transition:background .1s ease,box-shadow .1s ease,color .1s ease;transition:background .1s ease,box-shadow .1s ease,color .1s ease,-webkit-box-shadow .1s ease}.ui.menu>.item:first-child{border-radius:.28571429rem 0 0 .28571429rem}.ui.menu .item:before{position:absolute;content:'';top:0;right:0;height:100%;width:1px;background:rgba(34,36,38,.1)}.ui.menu .item>a:not(.ui),.ui.menu .item>p:only-child,.ui.menu .text.item>*{-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;line-height:1.3}.ui.menu .item>p:first-child{margin-top:0}.ui.menu .item>p:last-child{margin-bottom:0}.ui.menu .item>i.icon{opacity:.9;float:none;margin:0 .35714286em 0 0}.ui.menu:not(.vertical) .item>.button{position:relative;top:0;margin:-.5em 0;padding-bottom:.78571429em;padding-top:.78571429em;font-size:1em}.ui.menu>.container,.ui.menu>.grid{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:inherit;-ms-flex-align:inherit;align-items:inherit;-webkit-box-orient:inherit;-webkit-box-direction:inherit;-ms-flex-direction:inherit;flex-direction:inherit}.ui.menu .item>.input{width:100%}.ui.menu:not(.vertical) .item>.input{position:relative;top:0;margin:-.5em 0}.ui.menu .item>.input input{font-size:1em;padding-top:.57142857em;padding-bottom:.57142857em}.ui.menu .header.item,.ui.vertical.menu .header.item{margin:0;background:'';text-transform:normal;font-weight:700}.ui.vertical.menu .item>.header:not(.ui){margin:0 0 .5em;font-size:1em;font-weight:700}.ui.menu .item>i.dropdown.icon{padding:0;float:right;margin:0 0 0 1em}.ui.menu .dropdown.item .menu{min-width:calc(100% - 1px);border-radius:0 0 .28571429rem .28571429rem;background:#fff;margin:0 0 0;-webkit-box-shadow:0 1px 3px 0 rgba(0,0,0,.08);box-shadow:0 1px 3px 0 rgba(0,0,0,.08);-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.ui.menu .ui.dropdown .menu>.item{margin:0;text-align:left;font-size:1em!important;padding:.78571429em 1.14285714em!important;background:0 0!important;color:rgba(0,0,0,.87)!important;text-transform:none!important;font-weight:400!important;-webkit-box-shadow:none!important;box-shadow:none!important;-webkit-transition:none!important;transition:none!important}.ui.menu .ui.dropdown .menu>.item:hover{background:rgba(0,0,0,.05)!important;color:rgba(0,0,0,.95)!important}.ui.menu .ui.dropdown .menu>.selected.item{background:rgba(0,0,0,.05)!important;color:rgba(0,0,0,.95)!important}.ui.menu .ui.dropdown .menu>.active.item{background:rgba(0,0,0,.03)!important;font-weight:700!important;color:rgba(0,0,0,.95)!important}.ui.menu .ui.dropdown.item .menu .item:not(.filtered){display:block}.ui.menu .ui.dropdown .menu>.item .icon:not(.dropdown){display:inline-block;font-size:1em!important;float:none;margin:0 .75em 0 0!important}.ui.secondary.menu .dropdown.item>.menu,.ui.text.menu .dropdown.item>.menu{border-radius:.28571429rem;margin-top:.35714286em}.ui.menu .pointing.dropdown.item .menu{margin-top:.75em}.ui.inverted.menu .search.dropdown.item>.search,.ui.inverted.menu .search.dropdown.item>.text{color:rgba(255,255,255,.9)}.ui.vertical.menu .dropdown.item>.icon{float:right;content:"\f0da";margin-left:1em}.ui.vertical.menu .dropdown.item .menu{left:100%;min-width:0;margin:0;-webkit-box-shadow:0 1px 3px 0 rgba(0,0,0,.08);box-shadow:0 1px 3px 0 rgba(0,0,0,.08);border-radius:0 .28571429rem .28571429rem .28571429rem}.ui.vertical.menu .dropdown.item.upward .menu{bottom:0}.ui.vertical.menu .dropdown.item:not(.upward) .menu{top:0}.ui.vertical.menu .active.dropdown.item{border-top-right-radius:0;border-bottom-right-radius:0}.ui.vertical.menu .dropdown.active.item{-webkit-box-shadow:none;box-shadow:none}.ui.item.menu .dropdown .menu .item{width:100%}.ui.menu .item>.label{background:#999;color:#fff;margin-left:1em;padding:.3em .78571429em}.ui.vertical.menu .item>.label{background:#999;color:#fff;margin-top:-.15em;margin-bottom:-.15em;padding:.3em .78571429em}.ui.menu .item>.floating.label{padding:.3em .78571429em}.ui.menu .item>img:not(.ui){display:inline-block;vertical-align:middle;margin:-.3em 0;width:2.5em}.ui.vertical.menu .item>img:not(.ui):only-child{display:block;max-width:100%;width:auto}.ui.menu .list .item:before{background:0 0!important}.ui.vertical.sidebar.menu>.item:first-child:before{display:block!important}.ui.vertical.sidebar.menu>.item::before{top:auto;bottom:0}@media only screen and (max-width:767px){.ui.menu>.ui.container{width:100%!important;margin-left:0!important;margin-right:0!important}}@media only screen and (min-width:768px){.ui.menu:not(.secondary):not(.text):not(.tabular):not(.borderless)>.container>.item:not(.right):not(.borderless):first-child{border-left:1px solid rgba(34,36,38,.1)}}.ui.link.menu .item:hover,.ui.menu .dropdown.item:hover,.ui.menu .link.item:hover,.ui.menu a.item:hover{cursor:pointer;background:rgba(0,0,0,.03);color:rgba(0,0,0,.95)}.ui.link.menu .item:active,.ui.menu .link.item:active,.ui.menu a.item:active{background:rgba(0,0,0,.03);color:rgba(0,0,0,.95)}.ui.menu .active.item{background:rgba(0,0,0,.05);color:rgba(0,0,0,.95);font-weight:400;-webkit-box-shadow:none;box-shadow:none}.ui.menu .active.item>i.icon{opacity:1}.ui.menu .active.item:hover,.ui.vertical.menu .active.item:hover{background-color:rgba(0,0,0,.05);color:rgba(0,0,0,.95)}.ui.menu .item.disabled,.ui.menu .item.disabled:hover{cursor:default!important;background-color:transparent!important;color:rgba(40,40,40,.3)!important}.ui.menu:not(.vertical) .left.item,.ui.menu:not(.vertical) :not(.dropdown)>.left.menu{display:-webkit-box;display:-ms-flexbox;display:flex;margin-right:auto!important}.ui.menu:not(.vertical) .right.item,.ui.menu:not(.vertical) .right.menu{display:-webkit-box;display:-ms-flexbox;display:flex;margin-left:auto!important}.ui.menu .right.item::before,.ui.menu .right.menu>.item::before{right:auto;left:0}.ui.vertical.menu{display:block;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15);box-shadow:0 1px 2px 0 rgba(34,36,38,.15)}.ui.vertical.menu .item{display:block;background:0 0;border-top:none;border-right:none}.ui.vertical.menu>.item:first-child{border-radius:.28571429rem .28571429rem 0 0}.ui.vertical.menu>.item:last-child{border-radius:0 0 .28571429rem .28571429rem}.ui.vertical.menu .item>.label{float:right;text-align:center}.ui.vertical.menu .item>i.icon{width:1.18em;float:right;margin:0 0 0 .5em}.ui.vertical.menu .item>.label+i.icon{float:none;margin:0 .5em 0 0}.ui.vertical.menu .item:before{position:absolute;content:'';top:0;left:0;width:100%;height:1px;background:rgba(34,36,38,.1)}.ui.vertical.menu .item:first-child:before{display:none!important}.ui.vertical.menu .item>.menu{margin:.5em -1.14285714em 0}.ui.vertical.menu .menu .item{background:0 0;padding:.5em 1.33333333em;font-size:.85714286em;color:rgba(0,0,0,.5)}.ui.vertical.menu .item .menu .link.item:hover,.ui.vertical.menu .item .menu a.item:hover{color:rgba(0,0,0,.85)}.ui.vertical.menu .menu .item:before{display:none}.ui.vertical.menu .active.item{background:rgba(0,0,0,.05);border-radius:0;-webkit-box-shadow:none;box-shadow:none}.ui.vertical.menu>.active.item:first-child{border-radius:.28571429rem .28571429rem 0 0}.ui.vertical.menu>.active.item:last-child{border-radius:0 0 .28571429rem .28571429rem}.ui.vertical.menu>.active.item:only-child{border-radius:.28571429rem}.ui.vertical.menu .active.item .menu .active.item{border-left:none}.ui.vertical.menu .item .menu .active.item{background-color:transparent;font-weight:700;color:rgba(0,0,0,.95)}.ui.tabular.menu{border-radius:0;-webkit-box-shadow:none!important;box-shadow:none!important;border:none;background:none transparent;border-bottom:1px solid #d4d4d5}.ui.tabular.fluid.menu{width:calc(100% + (1px * 2))!important}.ui.tabular.menu .item{background:0 0;border-bottom:none;border-left:1px solid transparent;border-right:1px solid transparent;border-top:2px solid transparent;padding:.92857143em 1.42857143em;color:rgba(0,0,0,.87)}.ui.tabular.menu .item:before{display:none}.ui.tabular.menu .item:hover{background-color:transparent;color:rgba(0,0,0,.8)}.ui.tabular.menu .active.item{background:none #fff;color:rgba(0,0,0,.95);border-top-width:1px;border-color:#d4d4d5;font-weight:700;margin-bottom:-1px;-webkit-box-shadow:none;box-shadow:none;border-radius:.28571429rem .28571429rem 0 0!important}.ui.tabular.menu+.attached:not(.top).segment,.ui.tabular.menu+.attached:not(.top).segment+.attached:not(.top).segment{border-top:none;margin-left:0;margin-top:0;margin-right:0;width:100%}.top.attached.segment+.ui.bottom.tabular.menu{position:relative;width:calc(100% + (1px * 2));left:-1px}.ui.bottom.tabular.menu{background:none transparent;border-radius:0;-webkit-box-shadow:none!important;box-shadow:none!important;border-bottom:none;border-top:1px solid #d4d4d5}.ui.bottom.tabular.menu .item{background:0 0;border-left:1px solid transparent;border-right:1px solid transparent;border-bottom:1px solid transparent;border-top:none}.ui.bottom.tabular.menu .active.item{background:none #fff;color:rgba(0,0,0,.95);border-color:#d4d4d5;margin:-1px 0 0 0;border-radius:0 0 .28571429rem .28571429rem!important}.ui.vertical.tabular.menu{background:none transparent;border-radius:0;-webkit-box-shadow:none!important;box-shadow:none!important;border-bottom:none;border-right:1px solid #d4d4d5}.ui.vertical.tabular.menu .item{background:0 0;border-left:1px solid transparent;border-bottom:1px solid transparent;border-top:1px solid transparent;border-right:none}.ui.vertical.tabular.menu .active.item{background:none #fff;color:rgba(0,0,0,.95);border-color:#d4d4d5;margin:0 -1px 0 0;border-radius:.28571429rem 0 0 .28571429rem!important}.ui.vertical.right.tabular.menu{background:none transparent;border-radius:0;-webkit-box-shadow:none!important;box-shadow:none!important;border-bottom:none;border-right:none;border-left:1px solid #d4d4d5}.ui.vertical.right.tabular.menu .item{background:0 0;border-right:1px solid transparent;border-bottom:1px solid transparent;border-top:1px solid transparent;border-left:none}.ui.vertical.right.tabular.menu .active.item{background:none #fff;color:rgba(0,0,0,.95);border-color:#d4d4d5;margin:0 0 0 -1px;border-radius:0 .28571429rem .28571429rem 0!important}.ui.tabular.menu .active.dropdown.item{margin-bottom:0;border-left:1px solid transparent;border-right:1px solid transparent;border-top:2px solid transparent;border-bottom:none}.ui.pagination.menu{margin:0;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.ui.pagination.menu .item:last-child{border-radius:0 .28571429rem .28571429rem 0}.ui.compact.menu .item:last-child{border-radius:0 .28571429rem .28571429rem 0}.ui.pagination.menu .item:last-child:before{display:none}.ui.pagination.menu .item{min-width:3em;text-align:center}.ui.pagination.menu .icon.item i.icon{vertical-align:top}.ui.pagination.menu .active.item{border-top:none;padding-top:.92857143em;background-color:rgba(0,0,0,.05);color:rgba(0,0,0,.95);-webkit-box-shadow:none;box-shadow:none}.ui.secondary.menu{background:0 0;margin-left:-.35714286em;margin-right:-.35714286em;border-radius:0;border:none;-webkit-box-shadow:none;box-shadow:none}.ui.secondary.menu .item{-ms-flex-item-align:center;align-self:center;-webkit-box-shadow:none;box-shadow:none;border:none;padding:.78571429em .92857143em;margin:0 .35714286em;background:0 0;-webkit-transition:color .1s ease;transition:color .1s ease;border-radius:.28571429rem}.ui.secondary.menu .item:before{display:none!important}.ui.secondary.menu .header.item{border-radius:0;border-right:none;background:none transparent}.ui.secondary.menu .item>img:not(.ui){margin:0}.ui.secondary.menu .dropdown.item:hover,.ui.secondary.menu .link.item:hover,.ui.secondary.menu a.item:hover{background:rgba(0,0,0,.05);color:rgba(0,0,0,.95)}.ui.secondary.menu .active.item{-webkit-box-shadow:none;box-shadow:none;background:rgba(0,0,0,.05);color:rgba(0,0,0,.95);border-radius:.28571429rem}.ui.secondary.menu .active.item:hover{-webkit-box-shadow:none;box-shadow:none;background:rgba(0,0,0,.05);color:rgba(0,0,0,.95)}.ui.secondary.inverted.menu .link.item,.ui.secondary.inverted.menu a.item{color:rgba(255,255,255,.7)!important}.ui.secondary.inverted.menu .dropdown.item:hover,.ui.secondary.inverted.menu .link.item:hover,.ui.secondary.inverted.menu a.item:hover{background:rgba(255,255,255,.08);color:#fff!important}.ui.secondary.inverted.menu .active.item{background:rgba(255,255,255,.15);color:#fff!important}.ui.secondary.item.menu{margin-left:0;margin-right:0}.ui.secondary.item.menu .item:last-child{margin-right:0}.ui.secondary.attached.menu{-webkit-box-shadow:none;box-shadow:none}.ui.vertical.secondary.menu .item:not(.dropdown)>.menu{margin:0 -.92857143em}.ui.vertical.secondary.menu .item:not(.dropdown)>.menu>.item{margin:0;padding:.5em 1.33333333em}.ui.secondary.vertical.menu>.item{border:none;margin:0 0 .35714286em;border-radius:.28571429rem!important}.ui.secondary.vertical.menu>.header.item{border-radius:0}.ui.vertical.secondary.menu .item>.menu .item{background-color:transparent}.ui.secondary.inverted.menu{background-color:transparent}.ui.secondary.pointing.menu{margin-left:0;margin-right:0;border-bottom:2px solid rgba(34,36,38,.15)}.ui.secondary.pointing.menu .item{border-bottom-color:transparent;border-bottom-style:solid;border-radius:0;-ms-flex-item-align:end;align-self:flex-end;margin:0 0 -2px;padding:.85714286em 1.14285714em;border-bottom-width:2px;-webkit-transition:color .1s ease;transition:color .1s ease}.ui.secondary.pointing.menu .header.item{color:rgba(0,0,0,.85)!important}.ui.secondary.pointing.menu .text.item{-webkit-box-shadow:none!important;box-shadow:none!important}.ui.secondary.pointing.menu .item:after{display:none}.ui.secondary.pointing.menu .dropdown.item:hover,.ui.secondary.pointing.menu .link.item:hover,.ui.secondary.pointing.menu a.item:hover{background-color:transparent;color:rgba(0,0,0,.87)}.ui.secondary.pointing.menu .dropdown.item:active,.ui.secondary.pointing.menu .link.item:active,.ui.secondary.pointing.menu a.item:active{background-color:transparent;border-color:rgba(34,36,38,.15)}.ui.secondary.pointing.menu .active.item{background-color:transparent;-webkit-box-shadow:none;box-shadow:none;border-color:#1b1c1d;font-weight:700;color:rgba(0,0,0,.95)}.ui.secondary.pointing.menu .active.item:hover{border-color:#1b1c1d;color:rgba(0,0,0,.95)}.ui.secondary.pointing.menu .active.dropdown.item{border-color:transparent}.ui.secondary.vertical.pointing.menu{border-bottom-width:0;border-right-width:2px;border-right-style:solid;border-right-color:rgba(34,36,38,.15)}.ui.secondary.vertical.pointing.menu .item{border-bottom:none;border-right-style:solid;border-right-color:transparent;border-radius:0!important;margin:0 -2px 0 0;border-right-width:2px}.ui.secondary.vertical.pointing.menu .active.item{border-color:#1b1c1d}.ui.secondary.inverted.pointing.menu{border-color:rgba(255,255,255,.1)}.ui.secondary.inverted.pointing.menu{border-width:2px;border-color:rgba(34,36,38,.15)}.ui.secondary.inverted.pointing.menu .item{color:rgba(255,255,255,.9)}.ui.secondary.inverted.pointing.menu .header.item{color:#fff!important}.ui.secondary.inverted.pointing.menu .link.item:hover,.ui.secondary.inverted.pointing.menu a.item:hover{color:rgba(0,0,0,.95)}.ui.secondary.inverted.pointing.menu .active.item{border-color:#fff;color:#fff}.ui.text.menu{background:none transparent;border-radius:0;-webkit-box-shadow:none;box-shadow:none;border:none;margin:1em -.5em}.ui.text.menu .item{border-radius:0;-webkit-box-shadow:none;box-shadow:none;-ms-flex-item-align:center;align-self:center;margin:0 0;padding:.35714286em .5em;font-weight:400;color:rgba(0,0,0,.6);-webkit-transition:opacity .1s ease;transition:opacity .1s ease}.ui.text.menu .item:before,.ui.text.menu .menu .item:before{display:none!important}.ui.text.menu .header.item{background-color:transparent;opacity:1;color:rgba(0,0,0,.85);font-size:.92857143em;text-transform:uppercase;font-weight:700}.ui.text.menu .item>img:not(.ui){margin:0}.ui.text.item.menu .item{margin:0}.ui.vertical.text.menu{margin:1em 0}.ui.vertical.text.menu:first-child{margin-top:0}.ui.vertical.text.menu:last-child{margin-bottom:0}.ui.vertical.text.menu .item{margin:.57142857em 0;padding-left:0;padding-right:0}.ui.vertical.text.menu .item>i.icon{float:none;margin:0 .35714286em 0 0}.ui.vertical.text.menu .header.item{margin:.57142857em 0 .71428571em}.ui.vertical.text.menu .item:not(.dropdown)>.menu{margin:0}.ui.vertical.text.menu .item:not(.dropdown)>.menu>.item{margin:0;padding:.5em 0}.ui.text.menu .item:hover{opacity:1;background-color:transparent}.ui.text.menu .active.item{background-color:transparent;border:none;-webkit-box-shadow:none;box-shadow:none;font-weight:400;color:rgba(0,0,0,.95)}.ui.text.menu .active.item:hover{background-color:transparent}.ui.text.pointing.menu .active.item:after{-webkit-box-shadow:none;box-shadow:none}.ui.text.attached.menu{-webkit-box-shadow:none;box-shadow:none}.ui.inverted.text.menu,.ui.inverted.text.menu .active.item,.ui.inverted.text.menu .item,.ui.inverted.text.menu .item:hover{background-color:transparent!important}.ui.fluid.text.menu{margin-left:0;margin-right:0}.ui.vertical.icon.menu{display:inline-block;width:auto}.ui.icon.menu .item{height:auto;text-align:center;color:#1b1c1d}.ui.icon.menu .item>.icon:not(.dropdown){margin:0;opacity:1}.ui.icon.menu .icon:before{opacity:1}.ui.menu .icon.item>.icon{width:auto;margin:0 auto}.ui.vertical.icon.menu .item>.icon:not(.dropdown){display:block;opacity:1;margin:0 auto;float:none}.ui.inverted.icon.menu .item{color:#fff}.ui.labeled.icon.menu{text-align:center}.ui.labeled.icon.menu .item{min-width:6em;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.ui.labeled.icon.menu .item>.icon:not(.dropdown){height:1em;display:block;font-size:1.71428571em!important;margin:0 auto .5rem!important}.ui.fluid.labeled.icon.menu>.item{min-width:0}@media only screen and (max-width:767px){.ui.stackable.menu{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.ui.stackable.menu .item{width:100%!important}.ui.stackable.menu .item:before{position:absolute;content:'';top:auto;bottom:0;left:0;width:100%;height:1px;background:rgba(34,36,38,.1)}.ui.stackable.menu .left.item,.ui.stackable.menu .left.menu{margin-right:0!important}.ui.stackable.menu .right.item,.ui.stackable.menu .right.menu{margin-left:0!important}.ui.stackable.menu .left.menu,.ui.stackable.menu .right.menu{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.ui.menu .red.active.item,.ui.red.menu .active.item{border-color:#db2828!important;color:#db2828!important}.ui.menu .orange.active.item,.ui.orange.menu .active.item{border-color:#f2711c!important;color:#f2711c!important}.ui.menu .yellow.active.item,.ui.yellow.menu .active.item{border-color:#fbbd08!important;color:#fbbd08!important}.ui.menu .olive.active.item,.ui.olive.menu .active.item{border-color:#b5cc18!important;color:#b5cc18!important}.ui.green.menu .active.item,.ui.menu .green.active.item{border-color:#21ba45!important;color:#21ba45!important}.ui.menu .teal.active.item,.ui.teal.menu .active.item{border-color:#00b5ad!important;color:#00b5ad!important}.ui.blue.menu .active.item,.ui.menu .blue.active.item{border-color:#2185d0!important;color:#2185d0!important}.ui.menu .violet.active.item,.ui.violet.menu .active.item{border-color:#6435c9!important;color:#6435c9!important}.ui.menu .purple.active.item,.ui.purple.menu .active.item{border-color:#a333c8!important;color:#a333c8!important}.ui.menu .pink.active.item,.ui.pink.menu .active.item{border-color:#e03997!important;color:#e03997!important}.ui.brown.menu .active.item,.ui.menu .brown.active.item{border-color:#a5673f!important;color:#a5673f!important}.ui.grey.menu .active.item,.ui.menu .grey.active.item{border-color:#767676!important;color:#767676!important}.ui.inverted.menu{border:0 solid transparent;background:#1b1c1d;-webkit-box-shadow:none;box-shadow:none}.ui.inverted.menu .item,.ui.inverted.menu .item>a:not(.ui){background:0 0;color:rgba(255,255,255,.9)}.ui.inverted.menu .item.menu{background:0 0}.ui.inverted.menu .item:before{background:rgba(255,255,255,.08)}.ui.vertical.inverted.menu .item:before{background:rgba(255,255,255,.08)}.ui.vertical.inverted.menu .menu .item,.ui.vertical.inverted.menu .menu .item a:not(.ui){color:rgba(255,255,255,.5)}.ui.inverted.menu .header.item{margin:0;background:0 0;-webkit-box-shadow:none;box-shadow:none}.ui.inverted.menu .item.disabled,.ui.inverted.menu .item.disabled:hover{color:rgba(225,225,225,.3)}.ui.inverted.menu .dropdown.item:hover,.ui.inverted.menu .link.item:hover,.ui.inverted.menu a.item:hover,.ui.link.inverted.menu .item:hover{background:rgba(255,255,255,.08);color:#fff}.ui.vertical.inverted.menu .item .menu .link.item:hover,.ui.vertical.inverted.menu .item .menu a.item:hover{background:0 0;color:#fff}.ui.inverted.menu .link.item:active,.ui.inverted.menu a.item:active{background:rgba(255,255,255,.08);color:#fff}.ui.inverted.menu .active.item{background:rgba(255,255,255,.15);color:#fff!important}.ui.inverted.vertical.menu .item .menu .active.item{background:0 0;color:#fff}.ui.inverted.pointing.menu .active.item:after{background:#3d3e3f!important;margin:0!important;-webkit-box-shadow:none!important;box-shadow:none!important;border:none!important}.ui.inverted.menu .active.item:hover{background:rgba(255,255,255,.15);color:#fff!important}.ui.inverted.pointing.menu .active.item:hover:after{background:#3d3e3f!important}.ui.floated.menu{float:left;margin:0 .5rem 0 0}.ui.floated.menu .item:last-child:before{display:none}.ui.right.floated.menu{float:right;margin:0 0 0 .5rem}.ui.inverted.menu .red.active.item,.ui.inverted.red.menu{background-color:#db2828}.ui.inverted.red.menu .item:before{background-color:rgba(34,36,38,.1)}.ui.inverted.red.menu .active.item{background-color:rgba(0,0,0,.1)!important}.ui.inverted.menu .orange.active.item,.ui.inverted.orange.menu{background-color:#f2711c}.ui.inverted.orange.menu .item:before{background-color:rgba(34,36,38,.1)}.ui.inverted.orange.menu .active.item{background-color:rgba(0,0,0,.1)!important}.ui.inverted.menu .yellow.active.item,.ui.inverted.yellow.menu{background-color:#fbbd08}.ui.inverted.yellow.menu .item:before{background-color:rgba(34,36,38,.1)}.ui.inverted.yellow.menu .active.item{background-color:rgba(0,0,0,.1)!important}.ui.inverted.menu .olive.active.item,.ui.inverted.olive.menu{background-color:#b5cc18}.ui.inverted.olive.menu .item:before{background-color:rgba(34,36,38,.1)}.ui.inverted.olive.menu .active.item{background-color:rgba(0,0,0,.1)!important}.ui.inverted.green.menu,.ui.inverted.menu .green.active.item{background-color:#21ba45}.ui.inverted.green.menu .item:before{background-color:rgba(34,36,38,.1)}.ui.inverted.green.menu .active.item{background-color:rgba(0,0,0,.1)!important}.ui.inverted.menu .teal.active.item,.ui.inverted.teal.menu{background-color:#00b5ad}.ui.inverted.teal.menu .item:before{background-color:rgba(34,36,38,.1)}.ui.inverted.teal.menu .active.item{background-color:rgba(0,0,0,.1)!important}.ui.inverted.blue.menu,.ui.inverted.menu .blue.active.item{background-color:#2185d0}.ui.inverted.blue.menu .item:before{background-color:rgba(34,36,38,.1)}.ui.inverted.blue.menu .active.item{background-color:rgba(0,0,0,.1)!important}.ui.inverted.menu .violet.active.item,.ui.inverted.violet.menu{background-color:#6435c9}.ui.inverted.violet.menu .item:before{background-color:rgba(34,36,38,.1)}.ui.inverted.violet.menu .active.item{background-color:rgba(0,0,0,.1)!important}.ui.inverted.menu .purple.active.item,.ui.inverted.purple.menu{background-color:#a333c8}.ui.inverted.purple.menu .item:before{background-color:rgba(34,36,38,.1)}.ui.inverted.purple.menu .active.item{background-color:rgba(0,0,0,.1)!important}.ui.inverted.menu .pink.active.item,.ui.inverted.pink.menu{background-color:#e03997}.ui.inverted.pink.menu .item:before{background-color:rgba(34,36,38,.1)}.ui.inverted.pink.menu .active.item{background-color:rgba(0,0,0,.1)!important}.ui.inverted.brown.menu,.ui.inverted.menu .brown.active.item{background-color:#a5673f}.ui.inverted.brown.menu .item:before{background-color:rgba(34,36,38,.1)}.ui.inverted.brown.menu .active.item{background-color:rgba(0,0,0,.1)!important}.ui.inverted.grey.menu,.ui.inverted.menu .grey.active.item{background-color:#767676}.ui.inverted.grey.menu .item:before{background-color:rgba(34,36,38,.1)}.ui.inverted.grey.menu .active.item{background-color:rgba(0,0,0,.1)!important}.ui.fitted.menu .item,.ui.fitted.menu .item .menu .item,.ui.menu .fitted.item{padding:0}.ui.horizontally.fitted.menu .item,.ui.horizontally.fitted.menu .item .menu .item,.ui.menu .horizontally.fitted.item{padding-top:.92857143em;padding-bottom:.92857143em}.ui.menu .vertically.fitted.item,.ui.vertically.fitted.menu .item,.ui.vertically.fitted.menu .item .menu .item{padding-left:1.14285714em;padding-right:1.14285714em}.ui.borderless.menu .item .menu .item:before,.ui.borderless.menu .item:before,.ui.menu .borderless.item:before{background:0 0!important}.ui.compact.menu{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin:0;vertical-align:middle}.ui.compact.vertical.menu{display:inline-block}.ui.compact.menu .item:last-child{border-radius:0 .28571429rem .28571429rem 0}.ui.compact.menu .item:last-child:before{display:none}.ui.compact.vertical.menu{width:auto!important}.ui.compact.vertical.menu .item:last-child::before{display:block}.ui.menu.fluid,.ui.vertical.menu.fluid{width:100%!important}.ui.item.menu,.ui.item.menu .item{width:100%;padding-left:0!important;padding-right:0!important;margin-left:0!important;margin-right:0!important;text-align:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.ui.attached.item.menu{margin:0 -1px!important}.ui.item.menu .item:last-child:before{display:none}.ui.menu.two.item .item{width:50%}.ui.menu.three.item .item{width:33.333%}.ui.menu.four.item .item{width:25%}.ui.menu.five.item .item{width:20%}.ui.menu.six.item .item{width:16.666%}.ui.menu.seven.item .item{width:14.285%}.ui.menu.eight.item .item{width:12.5%}.ui.menu.nine.item .item{width:11.11%}.ui.menu.ten.item .item{width:10%}.ui.menu.eleven.item .item{width:9.09%}.ui.menu.twelve.item .item{width:8.333%}.ui.menu.fixed{position:fixed;z-index:101;margin:0;width:100%}.ui.menu.fixed,.ui.menu.fixed .item:first-child,.ui.menu.fixed .item:last-child{border-radius:0!important}.ui.fixed.menu,.ui[class*="top fixed"].menu{top:0;left:0;right:auto;bottom:auto}.ui[class*="top fixed"].menu{border-top:none;border-left:none;border-right:none}.ui[class*="right fixed"].menu{border-top:none;border-bottom:none;border-right:none;top:0;right:0;left:auto;bottom:auto;width:auto;height:100%}.ui[class*="bottom fixed"].menu{border-bottom:none;border-left:none;border-right:none;bottom:0;left:0;top:auto;right:auto}.ui[class*="left fixed"].menu{border-top:none;border-bottom:none;border-left:none;top:0;left:0;right:auto;bottom:auto;width:auto;height:100%}.ui.fixed.menu+.ui.grid{padding-top:2.75rem}.ui.pointing.menu .item:after{visibility:hidden;position:absolute;content:'';top:100%;left:50%;-webkit-transform:translateX(-50%) translateY(-50%) rotate(45deg);transform:translateX(-50%) translateY(-50%) rotate(45deg);background:0 0;margin:.5px 0 0;width:.57142857em;height:.57142857em;border:none;border-bottom:1px solid #d4d4d5;border-right:1px solid #d4d4d5;z-index:2;-webkit-transition:background .1s ease;transition:background .1s ease}.ui.vertical.pointing.menu .item:after{position:absolute;top:50%;right:0;bottom:auto;left:auto;-webkit-transform:translateX(50%) translateY(-50%) rotate(45deg);transform:translateX(50%) translateY(-50%) rotate(45deg);margin:0 -.5px 0 0;border:none;border-top:1px solid #d4d4d5;border-right:1px solid #d4d4d5}.ui.pointing.menu .active.item:after{visibility:visible}.ui.pointing.menu .active.dropdown.item:after{visibility:hidden}.ui.pointing.menu .active.item .menu .active.item:after,.ui.pointing.menu .dropdown.active.item:after{display:none}.ui.pointing.menu .active.item:hover:after{background-color:#f2f2f2}.ui.pointing.menu .active.item:after{background-color:#f2f2f2}.ui.pointing.menu .active.item:hover:after{background-color:#f2f2f2}.ui.vertical.pointing.menu .active.item:hover:after{background-color:#f2f2f2}.ui.vertical.pointing.menu .active.item:after{background-color:#f2f2f2}.ui.vertical.pointing.menu .menu .active.item:after{background-color:#fff}.ui.attached.menu{top:0;bottom:0;border-radius:0;margin:0 -1px;width:calc(100% - (-1px * 2));max-width:calc(100% - (-1px * 2));-webkit-box-shadow:none;box-shadow:none}.ui.attached+.ui.attached.menu:not(.top){border-top:none}.ui[class*="top attached"].menu{bottom:0;margin-bottom:0;top:0;margin-top:1rem;border-radius:.28571429rem .28571429rem 0 0}.ui.menu[class*="top attached"]:first-child{margin-top:0}.ui[class*="bottom attached"].menu{bottom:0;margin-top:0;top:0;margin-bottom:1rem;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),none;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),none;border-radius:0 0 .28571429rem .28571429rem}.ui[class*="bottom attached"].menu:last-child{margin-bottom:0}.ui.top.attached.menu>.item:first-child{border-radius:.28571429rem 0 0 0}.ui.bottom.attached.menu>.item:first-child{border-radius:0 0 0 .28571429rem}.ui.attached.menu:not(.tabular){border:1px solid #d4d4d5}.ui.attached.inverted.menu{border:none}.ui.attached.tabular.menu{margin-left:0;margin-right:0;width:100%}.ui.mini.menu{font-size:.78571429rem}.ui.mini.vertical.menu{width:9rem}.ui.tiny.menu{font-size:.85714286rem}.ui.tiny.vertical.menu{width:11rem}.ui.small.menu{font-size:.92857143rem}.ui.small.vertical.menu{width:13rem}.ui.menu{font-size:1rem}.ui.vertical.menu{width:15rem}.ui.large.menu{font-size:1.07142857rem}.ui.large.vertical.menu{width:18rem}.ui.huge.menu{font-size:1.21428571rem}.ui.huge.vertical.menu{width:22rem}.ui.big.menu{font-size:1.14285714rem}.ui.big.vertical.menu{width:20rem}.ui.massive.menu{font-size:1.28571429rem}.ui.massive.vertical.menu{width:25rem}/*!
+ * # Semantic UI 2.4.2 - Message
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.message{position:relative;min-height:1em;margin:1em 0;background:#f8f8f9;padding:1em 1.5em;line-height:1.4285em;color:rgba(0,0,0,.87);-webkit-transition:opacity .1s ease,color .1s ease,background .1s ease,-webkit-box-shadow .1s ease;transition:opacity .1s ease,color .1s ease,background .1s ease,-webkit-box-shadow .1s ease;transition:opacity .1s ease,color .1s ease,background .1s ease,box-shadow .1s ease;transition:opacity .1s ease,color .1s ease,background .1s ease,box-shadow .1s ease,-webkit-box-shadow .1s ease;border-radius:.28571429rem;-webkit-box-shadow:0 0 0 1px rgba(34,36,38,.22) inset,0 0 0 0 transparent;box-shadow:0 0 0 1px rgba(34,36,38,.22) inset,0 0 0 0 transparent}.ui.message:first-child{margin-top:0}.ui.message:last-child{margin-bottom:0}.ui.message .header{display:block;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-weight:700;margin:-.14285714em 0 0 0}.ui.message .header:not(.ui){font-size:1.14285714em}.ui.message p{opacity:.85;margin:.75em 0}.ui.message p:first-child{margin-top:0}.ui.message p:last-child{margin-bottom:0}.ui.message .header+p{margin-top:.25em}.ui.message .list:not(.ui){text-align:left;padding:0;opacity:.85;list-style-position:inside;margin:.5em 0 0}.ui.message .list:not(.ui):first-child{margin-top:0}.ui.message .list:not(.ui):last-child{margin-bottom:0}.ui.message .list:not(.ui) li{position:relative;list-style-type:none;margin:0 0 .3em 1em;padding:0}.ui.message .list:not(.ui) li:before{position:absolute;content:'•';left:-1em;height:100%;vertical-align:baseline}.ui.message .list:not(.ui) li:last-child{margin-bottom:0}.ui.message>.icon{margin-right:.6em}.ui.message>.close.icon{cursor:pointer;position:absolute;margin:0;top:.78575em;right:.5em;opacity:.7;-webkit-transition:opacity .1s ease;transition:opacity .1s ease}.ui.message>.close.icon:hover{opacity:1}.ui.message>:first-child{margin-top:0}.ui.message>:last-child{margin-bottom:0}.ui.dropdown .menu>.message{margin:0 -1px}.ui.visible.visible.visible.visible.message{display:block}.ui.icon.visible.visible.visible.visible.message{display:-webkit-box;display:-ms-flexbox;display:flex}.ui.hidden.hidden.hidden.hidden.message{display:none}.ui.compact.message{display:inline-block}.ui.compact.icon.message{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.ui.attached.message{margin-bottom:-1px;border-radius:.28571429rem .28571429rem 0 0;-webkit-box-shadow:0 0 0 1px rgba(34,36,38,.15) inset;box-shadow:0 0 0 1px rgba(34,36,38,.15) inset;margin-left:-1px;margin-right:-1px}.ui.attached+.ui.attached.message:not(.top):not(.bottom){margin-top:-1px;border-radius:0}.ui.bottom.attached.message{margin-top:-1px;border-radius:0 0 .28571429rem .28571429rem;-webkit-box-shadow:0 0 0 1px rgba(34,36,38,.15) inset,0 1px 2px 0 rgba(34,36,38,.15);box-shadow:0 0 0 1px rgba(34,36,38,.15) inset,0 1px 2px 0 rgba(34,36,38,.15)}.ui.bottom.attached.message:not(:last-child){margin-bottom:1em}.ui.attached.icon.message{width:auto}.ui.icon.message{display:-webkit-box;display:-ms-flexbox;display:flex;width:100%;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ui.icon.message>.icon:not(.close){display:block;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;line-height:1;vertical-align:middle;font-size:3em;opacity:.8}.ui.icon.message>.content{display:block;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;vertical-align:middle}.ui.icon.message .icon:not(.close)+.content{padding-left:0}.ui.icon.message .circular.icon{width:1em}.ui.floating.message{-webkit-box-shadow:0 0 0 1px rgba(34,36,38,.22) inset,0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15);box-shadow:0 0 0 1px rgba(34,36,38,.22) inset,0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15)}.ui.black.message{background-color:#1b1c1d;color:rgba(255,255,255,.9)}.ui.positive.message{background-color:#fcfff5;color:#2c662d}.ui.attached.positive.message,.ui.positive.message{-webkit-box-shadow:0 0 0 1px #a3c293 inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #a3c293 inset,0 0 0 0 transparent}.ui.positive.message .header{color:#1a531b}.ui.negative.message{background-color:#fff6f6;color:#9f3a38}.ui.attached.negative.message,.ui.negative.message{-webkit-box-shadow:0 0 0 1px #e0b4b4 inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #e0b4b4 inset,0 0 0 0 transparent}.ui.negative.message .header{color:#912d2b}.ui.info.message{background-color:#f8ffff;color:#276f86}.ui.attached.info.message,.ui.info.message{-webkit-box-shadow:0 0 0 1px #a9d5de inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #a9d5de inset,0 0 0 0 transparent}.ui.info.message .header{color:#0e566c}.ui.warning.message{background-color:#fffaf3;color:#573a08}.ui.attached.warning.message,.ui.warning.message{-webkit-box-shadow:0 0 0 1px #c9ba9b inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #c9ba9b inset,0 0 0 0 transparent}.ui.warning.message .header{color:#794b02}.ui.error.message{background-color:#fff6f6;color:#9f3a38}.ui.attached.error.message,.ui.error.message{-webkit-box-shadow:0 0 0 1px #e0b4b4 inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #e0b4b4 inset,0 0 0 0 transparent}.ui.error.message .header{color:#912d2b}.ui.success.message{background-color:#fcfff5;color:#2c662d}.ui.attached.success.message,.ui.success.message{-webkit-box-shadow:0 0 0 1px #a3c293 inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #a3c293 inset,0 0 0 0 transparent}.ui.success.message .header{color:#1a531b}.ui.black.message,.ui.inverted.message{background-color:#1b1c1d;color:rgba(255,255,255,.9)}.ui.red.message{background-color:#ffe8e6;color:#db2828;-webkit-box-shadow:0 0 0 1px #db2828 inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #db2828 inset,0 0 0 0 transparent}.ui.red.message .header{color:#c82121}.ui.orange.message{background-color:#ffedde;color:#f2711c;-webkit-box-shadow:0 0 0 1px #f2711c inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #f2711c inset,0 0 0 0 transparent}.ui.orange.message .header{color:#e7640d}.ui.yellow.message{background-color:#fff8db;color:#b58105;-webkit-box-shadow:0 0 0 1px #b58105 inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #b58105 inset,0 0 0 0 transparent}.ui.yellow.message .header{color:#9c6f04}.ui.olive.message{background-color:#fbfdef;color:#8abc1e;-webkit-box-shadow:0 0 0 1px #8abc1e inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #8abc1e inset,0 0 0 0 transparent}.ui.olive.message .header{color:#7aa61a}.ui.green.message{background-color:#e5f9e7;color:#1ebc30;-webkit-box-shadow:0 0 0 1px #1ebc30 inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #1ebc30 inset,0 0 0 0 transparent}.ui.green.message .header{color:#1aa62a}.ui.teal.message{background-color:#e1f7f7;color:#10a3a3;-webkit-box-shadow:0 0 0 1px #10a3a3 inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #10a3a3 inset,0 0 0 0 transparent}.ui.teal.message .header{color:#0e8c8c}.ui.blue.message{background-color:#dff0ff;color:#2185d0;-webkit-box-shadow:0 0 0 1px #2185d0 inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #2185d0 inset,0 0 0 0 transparent}.ui.blue.message .header{color:#1e77ba}.ui.violet.message{background-color:#eae7ff;color:#6435c9;-webkit-box-shadow:0 0 0 1px #6435c9 inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #6435c9 inset,0 0 0 0 transparent}.ui.violet.message .header{color:#5a30b5}.ui.purple.message{background-color:#f6e7ff;color:#a333c8;-webkit-box-shadow:0 0 0 1px #a333c8 inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #a333c8 inset,0 0 0 0 transparent}.ui.purple.message .header{color:#922eb4}.ui.pink.message{background-color:#ffe3fb;color:#e03997;-webkit-box-shadow:0 0 0 1px #e03997 inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #e03997 inset,0 0 0 0 transparent}.ui.pink.message .header{color:#dd238b}.ui.brown.message{background-color:#f1e2d3;color:#a5673f;-webkit-box-shadow:0 0 0 1px #a5673f inset,0 0 0 0 transparent;box-shadow:0 0 0 1px #a5673f inset,0 0 0 0 transparent}.ui.brown.message .header{color:#935b38}.ui.mini.message{font-size:.78571429em}.ui.tiny.message{font-size:.85714286em}.ui.small.message{font-size:.92857143em}.ui.message{font-size:1em}.ui.large.message{font-size:1.14285714em}.ui.big.message{font-size:1.28571429em}.ui.huge.message{font-size:1.42857143em}.ui.massive.message{font-size:1.71428571em}/*!
+ * # Semantic UI 2.4.2 - Table
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.table{width:100%;background:#fff;margin:1em 0;border:1px solid rgba(34,36,38,.15);-webkit-box-shadow:none;box-shadow:none;border-radius:.28571429rem;text-align:left;color:rgba(0,0,0,.87);border-collapse:separate;border-spacing:0}.ui.table:first-child{margin-top:0}.ui.table:last-child{margin-bottom:0}.ui.table td,.ui.table th{-webkit-transition:background .1s ease,color .1s ease;transition:background .1s ease,color .1s ease}.ui.table thead{-webkit-box-shadow:none;box-shadow:none}.ui.table thead th{cursor:auto;background:#f9fafb;text-align:inherit;color:rgba(0,0,0,.87);padding:.92857143em .78571429em;vertical-align:inherit;font-style:none;font-weight:700;text-transform:none;border-bottom:1px solid rgba(34,36,38,.1);border-left:none}.ui.table thead tr>th:first-child{border-left:none}.ui.table thead tr:first-child>th:first-child{border-radius:.28571429rem 0 0 0}.ui.table thead tr:first-child>th:last-child{border-radius:0 .28571429rem 0 0}.ui.table thead tr:first-child>th:only-child{border-radius:.28571429rem .28571429rem 0 0}.ui.table tfoot{-webkit-box-shadow:none;box-shadow:none}.ui.table tfoot th{cursor:auto;border-top:1px solid rgba(34,36,38,.15);background:#f9fafb;text-align:inherit;color:rgba(0,0,0,.87);padding:.78571429em .78571429em;vertical-align:middle;font-style:normal;font-weight:400;text-transform:none}.ui.table tfoot tr>th:first-child{border-left:none}.ui.table tfoot tr:first-child>th:first-child{border-radius:0 0 0 .28571429rem}.ui.table tfoot tr:first-child>th:last-child{border-radius:0 0 .28571429rem 0}.ui.table tfoot tr:first-child>th:only-child{border-radius:0 0 .28571429rem .28571429rem}.ui.table tr td{border-top:1px solid rgba(34,36,38,.1)}.ui.table tr:first-child td{border-top:none}.ui.table tbody+tbody tr:first-child td{border-top:1px solid rgba(34,36,38,.1)}.ui.table td{padding:.78571429em .78571429em;text-align:inherit}.ui.table>.icon{vertical-align:baseline}.ui.table>.icon:only-child{margin:0}.ui.table.segment{padding:0}.ui.table.segment:after{display:none}.ui.table.segment.stacked:after{display:block}@media only screen and (max-width:767px){.ui.table:not(.unstackable){width:100%}.ui.table:not(.unstackable) tbody,.ui.table:not(.unstackable) tr,.ui.table:not(.unstackable) tr>td,.ui.table:not(.unstackable) tr>th{width:auto!important;display:block!important}.ui.table:not(.unstackable){padding:0}.ui.table:not(.unstackable) thead{display:block}.ui.table:not(.unstackable) tfoot{display:block}.ui.table:not(.unstackable) tr{padding-top:1em;padding-bottom:1em;-webkit-box-shadow:0 -1px 0 0 rgba(0,0,0,.1) inset!important;box-shadow:0 -1px 0 0 rgba(0,0,0,.1) inset!important}.ui.table:not(.unstackable) tr>td,.ui.table:not(.unstackable) tr>th{background:0 0;border:none!important;padding:.25em .75em!important;-webkit-box-shadow:none!important;box-shadow:none!important}.ui.table:not(.unstackable) td:first-child,.ui.table:not(.unstackable) th:first-child{font-weight:700}.ui.definition.table:not(.unstackable) thead th:first-child{-webkit-box-shadow:none!important;box-shadow:none!important}}.ui.table td .image,.ui.table td .image img,.ui.table th .image,.ui.table th .image img{max-width:none}.ui.structured.table{border-collapse:collapse}.ui.structured.table thead th{border-left:none;border-right:none}.ui.structured.sortable.table thead th{border-left:1px solid rgba(34,36,38,.15);border-right:1px solid rgba(34,36,38,.15)}.ui.structured.basic.table th{border-left:none;border-right:none}.ui.structured.celled.table tr td,.ui.structured.celled.table tr th{border-left:1px solid rgba(34,36,38,.1);border-right:1px solid rgba(34,36,38,.1)}.ui.definition.table thead:not(.full-width) th:first-child{pointer-events:none;background:0 0;font-weight:400;color:rgba(0,0,0,.4);-webkit-box-shadow:-1px -1px 0 1px #fff;box-shadow:-1px -1px 0 1px #fff}.ui.definition.table tfoot:not(.full-width) th:first-child{pointer-events:none;background:0 0;font-weight:rgba(0,0,0,.4);color:normal;-webkit-box-shadow:1px 1px 0 1px #fff;box-shadow:1px 1px 0 1px #fff}.ui.celled.definition.table thead:not(.full-width) th:first-child{-webkit-box-shadow:0 -1px 0 1px #fff;box-shadow:0 -1px 0 1px #fff}.ui.celled.definition.table tfoot:not(.full-width) th:first-child{-webkit-box-shadow:0 1px 0 1px #fff;box-shadow:0 1px 0 1px #fff}.ui.definition.table tr td.definition,.ui.definition.table tr td:first-child:not(.ignored){background:rgba(0,0,0,.03);font-weight:700;color:rgba(0,0,0,.95);text-transform:'';-webkit-box-shadow:'';box-shadow:'';text-align:'';font-size:1em;padding-left:'';padding-right:''}.ui.definition.table thead:not(.full-width) th:nth-child(2){border-left:1px solid rgba(34,36,38,.15)}.ui.definition.table tfoot:not(.full-width) th:nth-child(2){border-left:1px solid rgba(34,36,38,.15)}.ui.definition.table td:nth-child(2){border-left:1px solid rgba(34,36,38,.15)}.ui.table td.positive,.ui.table tr.positive{-webkit-box-shadow:0 0 0 #a3c293 inset;box-shadow:0 0 0 #a3c293 inset}.ui.table td.positive,.ui.table tr.positive{background:#fcfff5!important;color:#2c662d!important}.ui.table td.negative,.ui.table tr.negative{-webkit-box-shadow:0 0 0 #e0b4b4 inset;box-shadow:0 0 0 #e0b4b4 inset}.ui.table td.negative,.ui.table tr.negative{background:#fff6f6!important;color:#9f3a38!important}.ui.table td.error,.ui.table tr.error{-webkit-box-shadow:0 0 0 #e0b4b4 inset;box-shadow:0 0 0 #e0b4b4 inset}.ui.table td.error,.ui.table tr.error{background:#fff6f6!important;color:#9f3a38!important}.ui.table td.warning,.ui.table tr.warning{-webkit-box-shadow:0 0 0 #c9ba9b inset;box-shadow:0 0 0 #c9ba9b inset}.ui.table td.warning,.ui.table tr.warning{background:#fffaf3!important;color:#573a08!important}.ui.table td.active,.ui.table tr.active{-webkit-box-shadow:0 0 0 rgba(0,0,0,.87) inset;box-shadow:0 0 0 rgba(0,0,0,.87) inset}.ui.table td.active,.ui.table tr.active{background:#e0e0e0!important;color:rgba(0,0,0,.87)!important}.ui.table tr td.disabled,.ui.table tr.disabled td,.ui.table tr.disabled:hover,.ui.table tr:hover td.disabled{pointer-events:none;color:rgba(40,40,40,.3)}@media only screen and (max-width:991px){.ui[class*="tablet stackable"].table,.ui[class*="tablet stackable"].table tbody,.ui[class*="tablet stackable"].table tr,.ui[class*="tablet stackable"].table tr>td,.ui[class*="tablet stackable"].table tr>th{width:100%!important;display:block!important}.ui[class*="tablet stackable"].table{padding:0}.ui[class*="tablet stackable"].table thead{display:block}.ui[class*="tablet stackable"].table tfoot{display:block}.ui[class*="tablet stackable"].table tr{padding-top:1em;padding-bottom:1em;-webkit-box-shadow:0 -1px 0 0 rgba(0,0,0,.1) inset!important;box-shadow:0 -1px 0 0 rgba(0,0,0,.1) inset!important}.ui[class*="tablet stackable"].table tr>td,.ui[class*="tablet stackable"].table tr>th{background:0 0;border:none!important;padding:.25em .75em;-webkit-box-shadow:none!important;box-shadow:none!important}.ui.definition[class*="tablet stackable"].table thead th:first-child{-webkit-box-shadow:none!important;box-shadow:none!important}}.ui.table [class*="left aligned"],.ui.table[class*="left aligned"]{text-align:left}.ui.table [class*="center aligned"],.ui.table[class*="center aligned"]{text-align:center}.ui.table [class*="right aligned"],.ui.table[class*="right aligned"]{text-align:right}.ui.table [class*="top aligned"],.ui.table[class*="top aligned"]{vertical-align:top}.ui.table [class*="middle aligned"],.ui.table[class*="middle aligned"]{vertical-align:middle}.ui.table [class*="bottom aligned"],.ui.table[class*="bottom aligned"]{vertical-align:bottom}.ui.table td.collapsing,.ui.table th.collapsing{width:1px;white-space:nowrap}.ui.fixed.table{table-layout:fixed}.ui.fixed.table td,.ui.fixed.table th{overflow:hidden;text-overflow:ellipsis}.ui.selectable.table tbody tr:hover,.ui.table tbody tr td.selectable:hover{background:rgba(0,0,0,.05)!important;color:rgba(0,0,0,.95)!important}.ui.inverted.table tbody tr td.selectable:hover,.ui.selectable.inverted.table tbody tr:hover{background:rgba(255,255,255,.08)!important;color:#fff!important}.ui.table tbody tr td.selectable{padding:0}.ui.table tbody tr td.selectable>a:not(.ui){display:block;color:inherit;padding:.78571429em .78571429em}.ui.selectable.table tr.error:hover,.ui.selectable.table tr:hover td.error,.ui.table tr td.selectable.error:hover{background:#ffe7e7!important;color:#943634!important}.ui.selectable.table tr.warning:hover,.ui.selectable.table tr:hover td.warning,.ui.table tr td.selectable.warning:hover{background:#fff4e4!important;color:#493107!important}.ui.selectable.table tr.active:hover,.ui.selectable.table tr:hover td.active,.ui.table tr td.selectable.active:hover{background:#e0e0e0!important;color:rgba(0,0,0,.87)!important}.ui.selectable.table tr.positive:hover,.ui.selectable.table tr:hover td.positive,.ui.table tr td.selectable.positive:hover{background:#f7ffe6!important;color:#275b28!important}.ui.selectable.table tr.negative:hover,.ui.selectable.table tr:hover td.negative,.ui.table tr td.selectable.negative:hover{background:#ffe7e7!important;color:#943634!important}.ui.attached.table{top:0;bottom:0;border-radius:0;margin:0 -1px;width:calc(100% - (-1px * 2));max-width:calc(100% - (-1px * 2));-webkit-box-shadow:none;box-shadow:none;border:1px solid #d4d4d5}.ui.attached+.ui.attached.table:not(.top){border-top:none}.ui[class*="top attached"].table{bottom:0;margin-bottom:0;top:0;margin-top:1em;border-radius:.28571429rem .28571429rem 0 0}.ui.table[class*="top attached"]:first-child{margin-top:0}.ui[class*="bottom attached"].table{bottom:0;margin-top:0;top:0;margin-bottom:1em;-webkit-box-shadow:none,none;box-shadow:none,none;border-radius:0 0 .28571429rem .28571429rem}.ui[class*="bottom attached"].table:last-child{margin-bottom:0}.ui.striped.table tbody tr:nth-child(2n),.ui.striped.table>tr:nth-child(2n){background-color:rgba(0,0,50,.02)}.ui.inverted.striped.table tbody tr:nth-child(2n),.ui.inverted.striped.table>tr:nth-child(2n){background-color:rgba(255,255,255,.05)}.ui.striped.selectable.selectable.selectable.table tbody tr.active:hover{background:#efefef!important;color:rgba(0,0,0,.95)!important}.ui.table [class*="single line"],.ui.table[class*="single line"]{white-space:nowrap}.ui.table [class*="single line"],.ui.table[class*="single line"]{white-space:nowrap}.ui.red.table{border-top:.2em solid #db2828}.ui.inverted.red.table{background-color:#db2828!important;color:#fff!important}.ui.orange.table{border-top:.2em solid #f2711c}.ui.inverted.orange.table{background-color:#f2711c!important;color:#fff!important}.ui.yellow.table{border-top:.2em solid #fbbd08}.ui.inverted.yellow.table{background-color:#fbbd08!important;color:#fff!important}.ui.olive.table{border-top:.2em solid #b5cc18}.ui.inverted.olive.table{background-color:#b5cc18!important;color:#fff!important}.ui.green.table{border-top:.2em solid #21ba45}.ui.inverted.green.table{background-color:#21ba45!important;color:#fff!important}.ui.teal.table{border-top:.2em solid #00b5ad}.ui.inverted.teal.table{background-color:#00b5ad!important;color:#fff!important}.ui.blue.table{border-top:.2em solid #2185d0}.ui.inverted.blue.table{background-color:#2185d0!important;color:#fff!important}.ui.violet.table{border-top:.2em solid #6435c9}.ui.inverted.violet.table{background-color:#6435c9!important;color:#fff!important}.ui.purple.table{border-top:.2em solid #a333c8}.ui.inverted.purple.table{background-color:#a333c8!important;color:#fff!important}.ui.pink.table{border-top:.2em solid #e03997}.ui.inverted.pink.table{background-color:#e03997!important;color:#fff!important}.ui.brown.table{border-top:.2em solid #a5673f}.ui.inverted.brown.table{background-color:#a5673f!important;color:#fff!important}.ui.grey.table{border-top:.2em solid #767676}.ui.inverted.grey.table{background-color:#767676!important;color:#fff!important}.ui.black.table{border-top:.2em solid #1b1c1d}.ui.inverted.black.table{background-color:#1b1c1d!important;color:#fff!important}.ui.one.column.table td{width:100%}.ui.two.column.table td{width:50%}.ui.three.column.table td{width:33.33333333%}.ui.four.column.table td{width:25%}.ui.five.column.table td{width:20%}.ui.six.column.table td{width:16.66666667%}.ui.seven.column.table td{width:14.28571429%}.ui.eight.column.table td{width:12.5%}.ui.nine.column.table td{width:11.11111111%}.ui.ten.column.table td{width:10%}.ui.eleven.column.table td{width:9.09090909%}.ui.twelve.column.table td{width:8.33333333%}.ui.thirteen.column.table td{width:7.69230769%}.ui.fourteen.column.table td{width:7.14285714%}.ui.fifteen.column.table td{width:6.66666667%}.ui.sixteen.column.table td{width:6.25%}.ui.table td.one.wide,.ui.table th.one.wide{width:6.25%}.ui.table td.two.wide,.ui.table th.two.wide{width:12.5%}.ui.table td.three.wide,.ui.table th.three.wide{width:18.75%}.ui.table td.four.wide,.ui.table th.four.wide{width:25%}.ui.table td.five.wide,.ui.table th.five.wide{width:31.25%}.ui.table td.six.wide,.ui.table th.six.wide{width:37.5%}.ui.table td.seven.wide,.ui.table th.seven.wide{width:43.75%}.ui.table td.eight.wide,.ui.table th.eight.wide{width:50%}.ui.table td.nine.wide,.ui.table th.nine.wide{width:56.25%}.ui.table td.ten.wide,.ui.table th.ten.wide{width:62.5%}.ui.table td.eleven.wide,.ui.table th.eleven.wide{width:68.75%}.ui.table td.twelve.wide,.ui.table th.twelve.wide{width:75%}.ui.table td.thirteen.wide,.ui.table th.thirteen.wide{width:81.25%}.ui.table td.fourteen.wide,.ui.table th.fourteen.wide{width:87.5%}.ui.table td.fifteen.wide,.ui.table th.fifteen.wide{width:93.75%}.ui.table td.sixteen.wide,.ui.table th.sixteen.wide{width:100%}.ui.sortable.table thead th{cursor:pointer;white-space:nowrap;border-left:1px solid rgba(34,36,38,.15);color:rgba(0,0,0,.87)}.ui.sortable.table thead th:first-child{border-left:none}.ui.sortable.table thead th.sorted,.ui.sortable.table thead th.sorted:hover{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ui.sortable.table thead th:after{display:none;font-style:normal;font-weight:400;text-decoration:inherit;content:'';height:1em;width:auto;opacity:.8;margin:0 0 0 .5em;font-family:Icons}.ui.sortable.table thead th.ascending:after{content:'\f0d8'}.ui.sortable.table thead th.descending:after{content:'\f0d7'}.ui.sortable.table th.disabled:hover{cursor:auto;color:rgba(40,40,40,.3)}.ui.sortable.table thead th:hover{background:rgba(0,0,0,.05);color:rgba(0,0,0,.8)}.ui.sortable.table thead th.sorted{background:rgba(0,0,0,.05);color:rgba(0,0,0,.95)}.ui.sortable.table thead th.sorted:after{display:inline-block}.ui.sortable.table thead th.sorted:hover{background:rgba(0,0,0,.05);color:rgba(0,0,0,.95)}.ui.inverted.sortable.table thead th.sorted{background:rgba(255,255,255,.15) -webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.05)));background:rgba(255,255,255,.15) -webkit-linear-gradient(transparent,rgba(0,0,0,.05));background:rgba(255,255,255,.15) linear-gradient(transparent,rgba(0,0,0,.05));color:#fff}.ui.inverted.sortable.table thead th:hover{background:rgba(255,255,255,.08) -webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.05)));background:rgba(255,255,255,.08) -webkit-linear-gradient(transparent,rgba(0,0,0,.05));background:rgba(255,255,255,.08) linear-gradient(transparent,rgba(0,0,0,.05));color:#fff}.ui.inverted.sortable.table thead th{border-left-color:transparent;border-right-color:transparent}.ui.inverted.table{background:#333;color:rgba(255,255,255,.9);border:none}.ui.inverted.table th{background-color:rgba(0,0,0,.15);border-color:rgba(255,255,255,.1)!important;color:rgba(255,255,255,.9)!important}.ui.inverted.table tr td{border-color:rgba(255,255,255,.1)!important}.ui.inverted.table tr td.disabled,.ui.inverted.table tr.disabled td,.ui.inverted.table tr.disabled:hover td,.ui.inverted.table tr:hover td.disabled{pointer-events:none;color:rgba(225,225,225,.3)}.ui.inverted.definition.table tfoot:not(.full-width) th:first-child,.ui.inverted.definition.table thead:not(.full-width) th:first-child{background:#fff}.ui.inverted.definition.table tr td:first-child{background:rgba(255,255,255,.02);color:#fff}.ui.collapsing.table{width:auto}.ui.basic.table{background:0 0;border:1px solid rgba(34,36,38,.15);-webkit-box-shadow:none;box-shadow:none}.ui.basic.table tfoot,.ui.basic.table thead{-webkit-box-shadow:none;box-shadow:none}.ui.basic.table th{background:0 0;border-left:none}.ui.basic.table tbody tr{border-bottom:1px solid rgba(0,0,0,.1)}.ui.basic.table td{background:0 0}.ui.basic.striped.table tbody tr:nth-child(2n){background-color:rgba(0,0,0,.05)!important}.ui[class*="very basic"].table{border:none}.ui[class*="very basic"].table:not(.sortable):not(.striped) td,.ui[class*="very basic"].table:not(.sortable):not(.striped) th{padding:''}.ui[class*="very basic"].table:not(.sortable):not(.striped) td:first-child,.ui[class*="very basic"].table:not(.sortable):not(.striped) th:first-child{padding-left:0}.ui[class*="very basic"].table:not(.sortable):not(.striped) td:last-child,.ui[class*="very basic"].table:not(.sortable):not(.striped) th:last-child{padding-right:0}.ui[class*="very basic"].table:not(.sortable):not(.striped) thead tr:first-child th{padding-top:0}.ui.celled.table tr td,.ui.celled.table tr th{border-left:1px solid rgba(34,36,38,.1)}.ui.celled.table tr td:first-child,.ui.celled.table tr th:first-child{border-left:none}.ui.padded.table th{padding-left:1em;padding-right:1em}.ui.padded.table td,.ui.padded.table th{padding:1em 1em}.ui[class*="very padded"].table th{padding-left:1.5em;padding-right:1.5em}.ui[class*="very padded"].table td{padding:1.5em 1.5em}.ui.compact.table th{padding-left:.7em;padding-right:.7em}.ui.compact.table td{padding:.5em .7em}.ui[class*="very compact"].table th{padding-left:.6em;padding-right:.6em}.ui[class*="very compact"].table td{padding:.4em .6em}.ui.small.table{font-size:.9em}.ui.table{font-size:1em}.ui.large.table{font-size:1.1em}/*!
+ * # Semantic UI 2.4.2 - Ad
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Copyright 2013 Contributors
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.ad{display:block;overflow:hidden;margin:1em 0}.ui.ad:first-child{margin:0}.ui.ad:last-child{margin:0}.ui.ad iframe{margin:0;padding:0;border:none;overflow:hidden}.ui.leaderboard.ad{width:728px;height:90px}.ui[class*="medium rectangle"].ad{width:300px;height:250px}.ui[class*="large rectangle"].ad{width:336px;height:280px}.ui[class*="half page"].ad{width:300px;height:600px}.ui.square.ad{width:250px;height:250px}.ui[class*="small square"].ad{width:200px;height:200px}.ui[class*="small rectangle"].ad{width:180px;height:150px}.ui[class*="vertical rectangle"].ad{width:240px;height:400px}.ui.button.ad{width:120px;height:90px}.ui[class*="square button"].ad{width:125px;height:125px}.ui[class*="small button"].ad{width:120px;height:60px}.ui.skyscraper.ad{width:120px;height:600px}.ui[class*="wide skyscraper"].ad{width:160px}.ui.banner.ad{width:468px;height:60px}.ui[class*="vertical banner"].ad{width:120px;height:240px}.ui[class*="top banner"].ad{width:930px;height:180px}.ui[class*="half banner"].ad{width:234px;height:60px}.ui[class*="large leaderboard"].ad{width:970px;height:90px}.ui.billboard.ad{width:970px;height:250px}.ui.panorama.ad{width:980px;height:120px}.ui.netboard.ad{width:580px;height:400px}.ui[class*="large mobile banner"].ad{width:320px;height:100px}.ui[class*="mobile leaderboard"].ad{width:320px;height:50px}.ui.mobile.ad{display:none}@media only screen and (max-width:767px){.ui.mobile.ad{display:block}}.ui.centered.ad{margin-left:auto;margin-right:auto}.ui.test.ad{position:relative;background:#545454}.ui.test.ad:after{position:absolute;top:50%;left:50%;width:100%;text-align:center;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%);content:'Ad';color:#fff;font-size:1em;font-weight:700}.ui.mobile.test.ad:after{font-size:.85714286em}.ui.test.ad[data-text]:after{content:attr(data-text)}/*!
+ * # Semantic UI 2.4.2 - Item
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.card,.ui.cards>.card{max-width:100%;position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:290px;min-height:0;background:#fff;padding:0;border:none;border-radius:.28571429rem;-webkit-box-shadow:0 1px 3px 0 #d4d4d5,0 0 0 1px #d4d4d5;box-shadow:0 1px 3px 0 #d4d4d5,0 0 0 1px #d4d4d5;-webkit-transition:-webkit-box-shadow .1s ease,-webkit-transform .1s ease;transition:-webkit-box-shadow .1s ease,-webkit-transform .1s ease;transition:box-shadow .1s ease,transform .1s ease;transition:box-shadow .1s ease,transform .1s ease,-webkit-box-shadow .1s ease,-webkit-transform .1s ease;z-index:''}.ui.card{margin:1em 0}.ui.card a,.ui.cards>.card a{cursor:pointer}.ui.card:first-child{margin-top:0}.ui.card:last-child{margin-bottom:0}.ui.cards{display:-webkit-box;display:-ms-flexbox;display:flex;margin:-.875em -.5em;-ms-flex-wrap:wrap;flex-wrap:wrap}.ui.cards>.card{display:-webkit-box;display:-ms-flexbox;display:flex;margin:.875em .5em;float:none}.ui.card:after,.ui.cards:after{display:block;content:' ';height:0;clear:both;overflow:hidden;visibility:hidden}.ui.cards~.ui.cards{margin-top:.875em}.ui.card>:first-child,.ui.cards>.card>:first-child{border-radius:.28571429rem .28571429rem 0 0!important;border-top:none!important}.ui.card>:last-child,.ui.cards>.card>:last-child{border-radius:0 0 .28571429rem .28571429rem!important}.ui.card>:only-child,.ui.cards>.card>:only-child{border-radius:.28571429rem!important}.ui.card>.image,.ui.cards>.card>.image{position:relative;display:block;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;padding:0;background:rgba(0,0,0,.05)}.ui.card>.image>img,.ui.cards>.card>.image>img{display:block;width:100%;height:auto;border-radius:inherit}.ui.card>.image:not(.ui)>img,.ui.cards>.card>.image:not(.ui)>img{border:none}.ui.card>.content,.ui.cards>.card>.content{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;border:none;border-top:1px solid rgba(34,36,38,.1);background:0 0;margin:0;padding:1em 1em;-webkit-box-shadow:none;box-shadow:none;font-size:1em;border-radius:0}.ui.card>.content:after,.ui.cards>.card>.content:after{display:block;content:' ';height:0;clear:both;overflow:hidden;visibility:hidden}.ui.card>.content>.header,.ui.cards>.card>.content>.header{display:block;margin:'';font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;color:rgba(0,0,0,.85)}.ui.card>.content>.header:not(.ui),.ui.cards>.card>.content>.header:not(.ui){font-weight:700;font-size:1.28571429em;margin-top:-.21425em;line-height:1.28571429em}.ui.card>.content>.header+.description,.ui.card>.content>.meta+.description,.ui.cards>.card>.content>.header+.description,.ui.cards>.card>.content>.meta+.description{margin-top:.5em}.ui.card [class*="left floated"],.ui.cards>.card [class*="left floated"]{float:left}.ui.card [class*="right floated"],.ui.cards>.card [class*="right floated"]{float:right}.ui.card [class*="left aligned"],.ui.cards>.card [class*="left aligned"]{text-align:left}.ui.card [class*="center aligned"],.ui.cards>.card [class*="center aligned"]{text-align:center}.ui.card [class*="right aligned"],.ui.cards>.card [class*="right aligned"]{text-align:right}.ui.card .content img,.ui.cards>.card .content img{display:inline-block;vertical-align:middle;width:''}.ui.card .avatar img,.ui.card img.avatar,.ui.cards>.card .avatar img,.ui.cards>.card img.avatar{width:2em;height:2em;border-radius:500rem}.ui.card>.content>.description,.ui.cards>.card>.content>.description{clear:both;color:rgba(0,0,0,.68)}.ui.card>.content p,.ui.cards>.card>.content p{margin:0 0 .5em}.ui.card>.content p:last-child,.ui.cards>.card>.content p:last-child{margin-bottom:0}.ui.card .meta,.ui.cards>.card .meta{font-size:1em;color:rgba(0,0,0,.4)}.ui.card .meta *,.ui.cards>.card .meta *{margin-right:.3em}.ui.card .meta :last-child,.ui.cards>.card .meta :last-child{margin-right:0}.ui.card .meta [class*="right floated"],.ui.cards>.card .meta [class*="right floated"]{margin-right:0;margin-left:.3em}.ui.card>.content a:not(.ui),.ui.cards>.card>.content a:not(.ui){color:'';-webkit-transition:color .1s ease;transition:color .1s ease}.ui.card>.content a:not(.ui):hover,.ui.cards>.card>.content a:not(.ui):hover{color:''}.ui.card>.content>a.header,.ui.cards>.card>.content>a.header{color:rgba(0,0,0,.85)}.ui.card>.content>a.header:hover,.ui.cards>.card>.content>a.header:hover{color:#1e70bf}.ui.card .meta>a:not(.ui),.ui.cards>.card .meta>a:not(.ui){color:rgba(0,0,0,.4)}.ui.card .meta>a:not(.ui):hover,.ui.cards>.card .meta>a:not(.ui):hover{color:rgba(0,0,0,.87)}.ui.card>.button,.ui.card>.buttons,.ui.cards>.card>.button,.ui.cards>.card>.buttons{margin:0 -1px;width:calc(100% + 2px)}.ui.card .dimmer,.ui.cards>.card .dimmer{background-color:'';z-index:10}.ui.card>.content .star.icon,.ui.cards>.card>.content .star.icon{cursor:pointer;opacity:.75;-webkit-transition:color .1s ease;transition:color .1s ease}.ui.card>.content .star.icon:hover,.ui.cards>.card>.content .star.icon:hover{opacity:1;color:#ffb70a}.ui.card>.content .active.star.icon,.ui.cards>.card>.content .active.star.icon{color:#ffe623}.ui.card>.content .like.icon,.ui.cards>.card>.content .like.icon{cursor:pointer;opacity:.75;-webkit-transition:color .1s ease;transition:color .1s ease}.ui.card>.content .like.icon:hover,.ui.cards>.card>.content .like.icon:hover{opacity:1;color:#ff2733}.ui.card>.content .active.like.icon,.ui.cards>.card>.content .active.like.icon{color:#ff2733}.ui.card>.extra,.ui.cards>.card>.extra{max-width:100%;min-height:0!important;-webkit-box-flex:0;-ms-flex-positive:0;flex-grow:0;border-top:1px solid rgba(0,0,0,.05)!important;position:static;background:0 0;width:auto;margin:0 0;padding:.75em 1em;top:0;left:0;color:rgba(0,0,0,.4);-webkit-box-shadow:none;box-shadow:none;-webkit-transition:color .1s ease;transition:color .1s ease}.ui.card>.extra a:not(.ui),.ui.cards>.card>.extra a:not(.ui){color:rgba(0,0,0,.4)}.ui.card>.extra a:not(.ui):hover,.ui.cards>.card>.extra a:not(.ui):hover{color:#1e70bf}.ui.raised.card,.ui.raised.cards>.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15);box-shadow:0 0 0 1px #d4d4d5,0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15)}.ui.link.cards .raised.card:hover,.ui.link.raised.card:hover,.ui.raised.cards a.card:hover,a.ui.raised.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 4px 0 rgba(34,36,38,.15),0 2px 10px 0 rgba(34,36,38,.25);box-shadow:0 0 0 1px #d4d4d5,0 2px 4px 0 rgba(34,36,38,.15),0 2px 10px 0 rgba(34,36,38,.25)}.ui.raised.card,.ui.raised.cards>.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15);box-shadow:0 0 0 1px #d4d4d5,0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15)}.ui.centered.cards{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.ui.centered.card{margin-left:auto;margin-right:auto}.ui.fluid.card{width:100%;max-width:9999px}.ui.cards a.card,.ui.link.card,.ui.link.cards .card,a.ui.card{-webkit-transform:none;transform:none}.ui.cards a.card:hover,.ui.link.card:hover,.ui.link.cards .card:hover,a.ui.card:hover{cursor:pointer;z-index:5;background:#fff;border:none;-webkit-box-shadow:0 1px 3px 0 #bcbdbd,0 0 0 1px #d4d4d5;box-shadow:0 1px 3px 0 #bcbdbd,0 0 0 1px #d4d4d5;-webkit-transform:translateY(-3px);transform:translateY(-3px)}.ui.cards>.red.card,.ui.red.card,.ui.red.cards>.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #db2828,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #db2828,0 1px 3px 0 #d4d4d5}.ui.cards>.red.card:hover,.ui.red.card:hover,.ui.red.cards>.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #d01919,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #d01919,0 1px 3px 0 #bcbdbd}.ui.cards>.orange.card,.ui.orange.card,.ui.orange.cards>.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #f2711c,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #f2711c,0 1px 3px 0 #d4d4d5}.ui.cards>.orange.card:hover,.ui.orange.card:hover,.ui.orange.cards>.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #f26202,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #f26202,0 1px 3px 0 #bcbdbd}.ui.cards>.yellow.card,.ui.yellow.card,.ui.yellow.cards>.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #fbbd08,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #fbbd08,0 1px 3px 0 #d4d4d5}.ui.cards>.yellow.card:hover,.ui.yellow.card:hover,.ui.yellow.cards>.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #eaae00,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #eaae00,0 1px 3px 0 #bcbdbd}.ui.cards>.olive.card,.ui.olive.card,.ui.olive.cards>.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #b5cc18,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #b5cc18,0 1px 3px 0 #d4d4d5}.ui.cards>.olive.card:hover,.ui.olive.card:hover,.ui.olive.cards>.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #a7bd0d,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #a7bd0d,0 1px 3px 0 #bcbdbd}.ui.cards>.green.card,.ui.green.card,.ui.green.cards>.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #21ba45,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #21ba45,0 1px 3px 0 #d4d4d5}.ui.cards>.green.card:hover,.ui.green.card:hover,.ui.green.cards>.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #16ab39,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #16ab39,0 1px 3px 0 #bcbdbd}.ui.cards>.teal.card,.ui.teal.card,.ui.teal.cards>.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #00b5ad,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #00b5ad,0 1px 3px 0 #d4d4d5}.ui.cards>.teal.card:hover,.ui.teal.card:hover,.ui.teal.cards>.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #009c95,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #009c95,0 1px 3px 0 #bcbdbd}.ui.blue.card,.ui.blue.cards>.card,.ui.cards>.blue.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #2185d0,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #2185d0,0 1px 3px 0 #d4d4d5}.ui.blue.card:hover,.ui.blue.cards>.card:hover,.ui.cards>.blue.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #1678c2,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #1678c2,0 1px 3px 0 #bcbdbd}.ui.cards>.violet.card,.ui.violet.card,.ui.violet.cards>.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #6435c9,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #6435c9,0 1px 3px 0 #d4d4d5}.ui.cards>.violet.card:hover,.ui.violet.card:hover,.ui.violet.cards>.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #5829bb,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #5829bb,0 1px 3px 0 #bcbdbd}.ui.cards>.purple.card,.ui.purple.card,.ui.purple.cards>.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #a333c8,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #a333c8,0 1px 3px 0 #d4d4d5}.ui.cards>.purple.card:hover,.ui.purple.card:hover,.ui.purple.cards>.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #9627ba,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #9627ba,0 1px 3px 0 #bcbdbd}.ui.cards>.pink.card,.ui.pink.card,.ui.pink.cards>.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #e03997,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #e03997,0 1px 3px 0 #d4d4d5}.ui.cards>.pink.card:hover,.ui.pink.card:hover,.ui.pink.cards>.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #e61a8d,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #e61a8d,0 1px 3px 0 #bcbdbd}.ui.brown.card,.ui.brown.cards>.card,.ui.cards>.brown.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #a5673f,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #a5673f,0 1px 3px 0 #d4d4d5}.ui.brown.card:hover,.ui.brown.cards>.card:hover,.ui.cards>.brown.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #975b33,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #975b33,0 1px 3px 0 #bcbdbd}.ui.cards>.grey.card,.ui.grey.card,.ui.grey.cards>.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #767676,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #767676,0 1px 3px 0 #d4d4d5}.ui.cards>.grey.card:hover,.ui.grey.card:hover,.ui.grey.cards>.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #838383,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #838383,0 1px 3px 0 #bcbdbd}.ui.black.card,.ui.black.cards>.card,.ui.cards>.black.card{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #1b1c1d,0 1px 3px 0 #d4d4d5;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #1b1c1d,0 1px 3px 0 #d4d4d5}.ui.black.card:hover,.ui.black.cards>.card:hover,.ui.cards>.black.card:hover{-webkit-box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #27292a,0 1px 3px 0 #bcbdbd;box-shadow:0 0 0 1px #d4d4d5,0 2px 0 0 #27292a,0 1px 3px 0 #bcbdbd}.ui.one.cards{margin-left:0;margin-right:0}.ui.one.cards>.card{width:100%}.ui.two.cards{margin-left:-1em;margin-right:-1em}.ui.two.cards>.card{width:calc(50% - 2em);margin-left:1em;margin-right:1em}.ui.three.cards{margin-left:-1em;margin-right:-1em}.ui.three.cards>.card{width:calc(33.33333333% - 2em);margin-left:1em;margin-right:1em}.ui.four.cards{margin-left:-.75em;margin-right:-.75em}.ui.four.cards>.card{width:calc(25% - 1.5em);margin-left:.75em;margin-right:.75em}.ui.five.cards{margin-left:-.75em;margin-right:-.75em}.ui.five.cards>.card{width:calc(20% - 1.5em);margin-left:.75em;margin-right:.75em}.ui.six.cards{margin-left:-.75em;margin-right:-.75em}.ui.six.cards>.card{width:calc(16.66666667% - 1.5em);margin-left:.75em;margin-right:.75em}.ui.seven.cards{margin-left:-.5em;margin-right:-.5em}.ui.seven.cards>.card{width:calc(14.28571429% - 1em);margin-left:.5em;margin-right:.5em}.ui.eight.cards{margin-left:-.5em;margin-right:-.5em}.ui.eight.cards>.card{width:calc(12.5% - 1em);margin-left:.5em;margin-right:.5em;font-size:11px}.ui.nine.cards{margin-left:-.5em;margin-right:-.5em}.ui.nine.cards>.card{width:calc(11.11111111% - 1em);margin-left:.5em;margin-right:.5em;font-size:10px}.ui.ten.cards{margin-left:-.5em;margin-right:-.5em}.ui.ten.cards>.card{width:calc(10% - 1em);margin-left:.5em;margin-right:.5em}@media only screen and (max-width:767px){.ui.two.doubling.cards{margin-left:0;margin-right:0}.ui.two.doubling.cards>.card{width:100%;margin-left:0;margin-right:0}.ui.three.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.three.doubling.cards>.card{width:calc(50% - 2em);margin-left:1em;margin-right:1em}.ui.four.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.four.doubling.cards>.card{width:calc(50% - 2em);margin-left:1em;margin-right:1em}.ui.five.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.five.doubling.cards>.card{width:calc(50% - 2em);margin-left:1em;margin-right:1em}.ui.six.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.six.doubling.cards>.card{width:calc(50% - 2em);margin-left:1em;margin-right:1em}.ui.seven.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.seven.doubling.cards>.card{width:calc(33.33333333% - 2em);margin-left:1em;margin-right:1em}.ui.eight.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.eight.doubling.cards>.card{width:calc(33.33333333% - 2em);margin-left:1em;margin-right:1em}.ui.nine.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.nine.doubling.cards>.card{width:calc(33.33333333% - 2em);margin-left:1em;margin-right:1em}.ui.ten.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.ten.doubling.cards>.card{width:calc(33.33333333% - 2em);margin-left:1em;margin-right:1em}}@media only screen and (min-width:768px) and (max-width:991px){.ui.two.doubling.cards{margin-left:0;margin-right:0}.ui.two.doubling.cards>.card{width:100%;margin-left:0;margin-right:0}.ui.three.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.three.doubling.cards>.card{width:calc(50% - 2em);margin-left:1em;margin-right:1em}.ui.four.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.four.doubling.cards>.card{width:calc(50% - 2em);margin-left:1em;margin-right:1em}.ui.five.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.five.doubling.cards>.card{width:calc(33.33333333% - 2em);margin-left:1em;margin-right:1em}.ui.six.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.six.doubling.cards>.card{width:calc(33.33333333% - 2em);margin-left:1em;margin-right:1em}.ui.eight.doubling.cards{margin-left:-1em;margin-right:-1em}.ui.eight.doubling.cards>.card{width:calc(33.33333333% - 2em);margin-left:1em;margin-right:1em}.ui.eight.doubling.cards{margin-left:-.75em;margin-right:-.75em}.ui.eight.doubling.cards>.card{width:calc(25% - 1.5em);margin-left:.75em;margin-right:.75em}.ui.nine.doubling.cards{margin-left:-.75em;margin-right:-.75em}.ui.nine.doubling.cards>.card{width:calc(25% - 1.5em);margin-left:.75em;margin-right:.75em}.ui.ten.doubling.cards{margin-left:-.75em;margin-right:-.75em}.ui.ten.doubling.cards>.card{width:calc(20% - 1.5em);margin-left:.75em;margin-right:.75em}}@media only screen and (max-width:767px){.ui.stackable.cards{display:block!important}.ui.stackable.cards .card:first-child{margin-top:0!important}.ui.stackable.cards>.card{display:block!important;height:auto!important;margin:1em 1em;padding:0!important;width:calc(100% - 2em)!important}}.ui.cards>.card{font-size:1em}/*!
+ * # Semantic UI 2.4.2 - Comment
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.comments{margin:1.5em 0;max-width:650px}.ui.comments:first-child{margin-top:0}.ui.comments:last-child{margin-bottom:0}.ui.comments .comment{position:relative;background:0 0;margin:.5em 0 0;padding:.5em 0 0;border:none;border-top:none;line-height:1.2}.ui.comments .comment:first-child{margin-top:0;padding-top:0}.ui.comments .comment .comments{margin:0 0 .5em .5em;padding:1em 0 1em 1em}.ui.comments .comment .comments:before{position:absolute;top:0;left:0}.ui.comments .comment .comments .comment{border:none;border-top:none;background:0 0}.ui.comments .comment .avatar{display:block;width:2.5em;height:auto;float:left;margin:.2em 0 0}.ui.comments .comment .avatar img,.ui.comments .comment img.avatar{display:block;margin:0 auto;width:100%;height:100%;border-radius:.25rem}.ui.comments .comment>.content{display:block}.ui.comments .comment>.avatar~.content{margin-left:3.5em}.ui.comments .comment .author{font-size:1em;color:rgba(0,0,0,.87);font-weight:700}.ui.comments .comment a.author{cursor:pointer}.ui.comments .comment a.author:hover{color:#1e70bf}.ui.comments .comment .metadata{display:inline-block;margin-left:.5em;color:rgba(0,0,0,.4);font-size:.875em}.ui.comments .comment .metadata>*{display:inline-block;margin:0 .5em 0 0}.ui.comments .comment .metadata>:last-child{margin-right:0}.ui.comments .comment .text{margin:.25em 0 .5em;font-size:1em;word-wrap:break-word;color:rgba(0,0,0,.87);line-height:1.3}.ui.comments .comment .actions{font-size:.875em}.ui.comments .comment .actions a{cursor:pointer;display:inline-block;margin:0 .75em 0 0;color:rgba(0,0,0,.4)}.ui.comments .comment .actions a:last-child{margin-right:0}.ui.comments .comment .actions a.active,.ui.comments .comment .actions a:hover{color:rgba(0,0,0,.8)}.ui.comments>.reply.form{margin-top:1em}.ui.comments .comment .reply.form{width:100%;margin-top:1em}.ui.comments .reply.form textarea{font-size:1em;height:12em}.ui.collapsed.comments,.ui.comments .collapsed.comment,.ui.comments .collapsed.comments{display:none}.ui.threaded.comments .comment .comments{margin:-1.5em 0 -1em 1.25em;padding:3em 0 2em 2.25em;-webkit-box-shadow:-1px 0 0 rgba(34,36,38,.15);box-shadow:-1px 0 0 rgba(34,36,38,.15)}.ui.minimal.comments .comment .actions{opacity:0;position:absolute;top:0;right:0;left:auto;-webkit-transition:opacity .2s ease;transition:opacity .2s ease;-webkit-transition-delay:.1s;transition-delay:.1s}.ui.minimal.comments .comment>.content:hover>.actions{opacity:1}.ui.mini.comments{font-size:.78571429rem}.ui.tiny.comments{font-size:.85714286rem}.ui.small.comments{font-size:.92857143rem}.ui.comments{font-size:1rem}.ui.large.comments{font-size:1.14285714rem}.ui.big.comments{font-size:1.28571429rem}.ui.huge.comments{font-size:1.42857143rem}.ui.massive.comments{font-size:1.71428571rem}/*!
+ * # Semantic UI 2.4.2 - Feed
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.feed{margin:1em 0}.ui.feed:first-child{margin-top:0}.ui.feed:last-child{margin-bottom:0}.ui.feed>.event{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;width:100%;padding:.21428571rem 0;margin:0;background:0 0;border-top:none}.ui.feed>.event:first-child{border-top:0;padding-top:0}.ui.feed>.event:last-child{padding-bottom:0}.ui.feed>.event>.label{display:block;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:2.5em;height:auto;-ms-flex-item-align:stretch;align-self:stretch;text-align:left}.ui.feed>.event>.label .icon{opacity:1;font-size:1.5em;width:100%;padding:.25em;background:0 0;border:none;border-radius:none;color:rgba(0,0,0,.6)}.ui.feed>.event>.label img{width:100%;height:auto;border-radius:500rem}.ui.feed>.event>.label+.content{margin:.5em 0 .35714286em 1.14285714em}.ui.feed>.event>.content{display:block;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;-ms-flex-item-align:stretch;align-self:stretch;text-align:left;word-wrap:break-word}.ui.feed>.event:last-child>.content{padding-bottom:0}.ui.feed>.event>.content a{cursor:pointer}.ui.feed>.event>.content .date{margin:-.5rem 0 0;padding:0;font-weight:400;font-size:1em;font-style:normal;color:rgba(0,0,0,.4)}.ui.feed>.event>.content .summary{margin:0;font-size:1em;font-weight:700;color:rgba(0,0,0,.87)}.ui.feed>.event>.content .summary img{display:inline-block;width:auto;height:10em;margin:-.25em .25em 0 0;border-radius:.25em;vertical-align:middle}.ui.feed>.event>.content .user{display:inline-block;font-weight:700;margin-right:0;vertical-align:baseline}.ui.feed>.event>.content .user img{margin:-.25em .25em 0 0;width:auto;height:10em;vertical-align:middle}.ui.feed>.event>.content .summary>.date{display:inline-block;float:none;font-weight:400;font-size:.85714286em;font-style:normal;margin:0 0 0 .5em;padding:0;color:rgba(0,0,0,.4)}.ui.feed>.event>.content .extra{margin:.5em 0 0;background:0 0;padding:0;color:rgba(0,0,0,.87)}.ui.feed>.event>.content .extra.images img{display:inline-block;margin:0 .25em 0 0;width:6em}.ui.feed>.event>.content .extra.text{padding:0;border-left:none;font-size:1em;max-width:500px;line-height:1.4285em}.ui.feed>.event>.content .meta{display:inline-block;font-size:.85714286em;margin:.5em 0 0;background:0 0;border:none;border-radius:0;-webkit-box-shadow:none;box-shadow:none;padding:0;color:rgba(0,0,0,.6)}.ui.feed>.event>.content .meta>*{position:relative;margin-left:.75em}.ui.feed>.event>.content .meta>:after{content:'';color:rgba(0,0,0,.2);top:0;left:-1em;opacity:1;position:absolute;vertical-align:top}.ui.feed>.event>.content .meta .like{color:'';-webkit-transition:.2s color ease;transition:.2s color ease}.ui.feed>.event>.content .meta .like:hover .icon{color:#ff2733}.ui.feed>.event>.content .meta .active.like .icon{color:#ef404a}.ui.feed>.event>.content .meta>:first-child{margin-left:0}.ui.feed>.event>.content .meta>:first-child::after{display:none}.ui.feed>.event>.content .meta a,.ui.feed>.event>.content .meta>.icon{cursor:pointer;opacity:1;color:rgba(0,0,0,.5);-webkit-transition:color .1s ease;transition:color .1s ease}.ui.feed>.event>.content .meta a:hover,.ui.feed>.event>.content .meta a:hover .icon,.ui.feed>.event>.content .meta>.icon:hover{color:rgba(0,0,0,.95)}.ui.small.feed{font-size:.92857143rem}.ui.feed{font-size:1rem}.ui.large.feed{font-size:1.14285714rem}/*!
+ * # Semantic UI 2.4.2 - Item
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.items>.item{display:-webkit-box;display:-ms-flexbox;display:flex;margin:1em 0;width:100%;min-height:0;background:0 0;padding:0;border:none;border-radius:0;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:-webkit-box-shadow .1s ease;transition:-webkit-box-shadow .1s ease;transition:box-shadow .1s ease;transition:box-shadow .1s ease,-webkit-box-shadow .1s ease;z-index:''}.ui.items>.item a{cursor:pointer}.ui.items{margin:1.5em 0}.ui.items:first-child{margin-top:0!important}.ui.items:last-child{margin-bottom:0!important}.ui.items>.item:after{display:block;content:' ';height:0;clear:both;overflow:hidden;visibility:hidden}.ui.items>.item:first-child{margin-top:0}.ui.items>.item:last-child{margin-bottom:0}.ui.items>.item>.image{position:relative;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;display:block;float:none;margin:0;padding:0;max-height:'';-ms-flex-item-align:top;align-self:top}.ui.items>.item>.image>img{display:block;width:100%;height:auto;border-radius:.125rem;border:none}.ui.items>.item>.image:only-child>img{border-radius:0}.ui.items>.item>.content{display:block;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;background:0 0;margin:0;padding:0;-webkit-box-shadow:none;box-shadow:none;font-size:1em;border:none;border-radius:0}.ui.items>.item>.content:after{display:block;content:' ';height:0;clear:both;overflow:hidden;visibility:hidden}.ui.items>.item>.image+.content{min-width:0;width:auto;display:block;margin-left:0;-ms-flex-item-align:top;align-self:top;padding-left:1.5em}.ui.items>.item>.content>.header{display:inline-block;margin:-.21425em 0 0;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-weight:700;color:rgba(0,0,0,.85)}.ui.items>.item>.content>.header:not(.ui){font-size:1.28571429em}.ui.items>.item [class*="left floated"]{float:left}.ui.items>.item [class*="right floated"]{float:right}.ui.items>.item .content img{-ms-flex-item-align:middle;align-self:middle;width:''}.ui.items>.item .avatar img,.ui.items>.item img.avatar{width:'';height:'';border-radius:500rem}.ui.items>.item>.content>.description{margin-top:.6em;max-width:auto;font-size:1em;line-height:1.4285em;color:rgba(0,0,0,.87)}.ui.items>.item>.content p{margin:0 0 .5em}.ui.items>.item>.content p:last-child{margin-bottom:0}.ui.items>.item .meta{margin:.5em 0 .5em;font-size:1em;line-height:1em;color:rgba(0,0,0,.6)}.ui.items>.item .meta *{margin-right:.3em}.ui.items>.item .meta :last-child{margin-right:0}.ui.items>.item .meta [class*="right floated"]{margin-right:0;margin-left:.3em}.ui.items>.item>.content a:not(.ui){color:'';-webkit-transition:color .1s ease;transition:color .1s ease}.ui.items>.item>.content a:not(.ui):hover{color:''}.ui.items>.item>.content>a.header{color:rgba(0,0,0,.85)}.ui.items>.item>.content>a.header:hover{color:#1e70bf}.ui.items>.item .meta>a:not(.ui){color:rgba(0,0,0,.4)}.ui.items>.item .meta>a:not(.ui):hover{color:rgba(0,0,0,.87)}.ui.items>.item>.content .favorite.icon{cursor:pointer;opacity:.75;-webkit-transition:color .1s ease;transition:color .1s ease}.ui.items>.item>.content .favorite.icon:hover{opacity:1;color:#ffb70a}.ui.items>.item>.content .active.favorite.icon{color:#ffe623}.ui.items>.item>.content .like.icon{cursor:pointer;opacity:.75;-webkit-transition:color .1s ease;transition:color .1s ease}.ui.items>.item>.content .like.icon:hover{opacity:1;color:#ff2733}.ui.items>.item>.content .active.like.icon{color:#ff2733}.ui.items>.item .extra{display:block;position:relative;background:0 0;margin:.5rem 0 0;width:100%;padding:0 0 0;top:0;left:0;color:rgba(0,0,0,.4);-webkit-box-shadow:none;box-shadow:none;-webkit-transition:color .1s ease;transition:color .1s ease;border-top:none}.ui.items>.item .extra>*{margin:.25rem .5rem .25rem 0}.ui.items>.item .extra>[class*="right floated"]{margin:.25rem 0 .25rem .5rem}.ui.items>.item .extra:after{display:block;content:' ';height:0;clear:both;overflow:hidden;visibility:hidden}.ui.items>.item>.image:not(.ui){width:175px}@media only screen and (min-width:768px) and (max-width:991px){.ui.items>.item{margin:1em 0}.ui.items>.item>.image:not(.ui){width:150px}.ui.items>.item>.image+.content{display:block;padding:0 0 0 1em}}@media only screen and (max-width:767px){.ui.items:not(.unstackable)>.item{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:2em 0}.ui.items:not(.unstackable)>.item>.image{display:block;margin-left:auto;margin-right:auto}.ui.items:not(.unstackable)>.item>.image,.ui.items:not(.unstackable)>.item>.image>img{max-width:100%!important;width:auto!important;max-height:250px!important}.ui.items:not(.unstackable)>.item>.image+.content{display:block;padding:1.5em 0 0}}.ui.items>.item>.image+[class*="top aligned"].content{-ms-flex-item-align:start;align-self:flex-start}.ui.items>.item>.image+[class*="middle aligned"].content{-ms-flex-item-align:center;align-self:center}.ui.items>.item>.image+[class*="bottom aligned"].content{-ms-flex-item-align:end;align-self:flex-end}.ui.relaxed.items>.item{margin:1.5em 0}.ui[class*="very relaxed"].items>.item{margin:2em 0}.ui.divided.items>.item{border-top:1px solid rgba(34,36,38,.15);margin:0;padding:1em 0}.ui.divided.items>.item:first-child{border-top:none;margin-top:0!important;padding-top:0!important}.ui.divided.items>.item:last-child{margin-bottom:0!important;padding-bottom:0!important}.ui.relaxed.divided.items>.item{margin:0;padding:1.5em 0}.ui[class*="very relaxed"].divided.items>.item{margin:0;padding:2em 0}.ui.items a.item:hover,.ui.link.items>.item:hover{cursor:pointer}.ui.items a.item:hover .content .header,.ui.link.items>.item:hover .content .header{color:#1e70bf}.ui.items>.item{font-size:1em}@media only screen and (max-width:767px){.ui.unstackable.items>.item>.image,.ui.unstackable.items>.item>.image>img{width:125px!important}}/*!
+ * # Semantic UI 2.4.2 - Statistic
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.statistic{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:1em 0;max-width:auto}.ui.statistic+.ui.statistic{margin:0 0 0 1.5em}.ui.statistic:first-child{margin-top:0}.ui.statistic:last-child{margin-bottom:0}.ui.statistics{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-ms-flex-wrap:wrap;flex-wrap:wrap}.ui.statistics>.statistic{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 1.5em 1em;max-width:auto}.ui.statistics{display:-webkit-box;display:-ms-flexbox;display:flex;margin:1em -1.5em -1em}.ui.statistics:after{display:block;content:' ';height:0;clear:both;overflow:hidden;visibility:hidden}.ui.statistics:first-child{margin-top:0}.ui.statistic>.value,.ui.statistics .statistic>.value{font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-size:4rem;font-weight:400;line-height:1em;color:#1b1c1d;text-transform:uppercase;text-align:center}.ui.statistic>.label,.ui.statistics .statistic>.label{font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-size:1em;font-weight:700;color:rgba(0,0,0,.87);text-transform:uppercase;text-align:center}.ui.statistic>.label~.value,.ui.statistics .statistic>.label~.value{margin-top:0}.ui.statistic>.value~.label,.ui.statistics .statistic>.value~.label{margin-top:0}.ui.statistic>.value .icon,.ui.statistics .statistic>.value .icon{opacity:1;width:auto;margin:0}.ui.statistic>.text.value,.ui.statistics .statistic>.text.value{line-height:1em;min-height:2em;font-weight:700;text-align:center}.ui.statistic>.text.value+.label,.ui.statistics .statistic>.text.value+.label{text-align:center}.ui.statistic>.value img,.ui.statistics .statistic>.value img{max-height:3rem;vertical-align:baseline}.ui.ten.statistics{margin:0 0 -1em}.ui.ten.statistics .statistic{min-width:10%;margin:0 0 1em}.ui.nine.statistics{margin:0 0 -1em}.ui.nine.statistics .statistic{min-width:11.11111111%;margin:0 0 1em}.ui.eight.statistics{margin:0 0 -1em}.ui.eight.statistics .statistic{min-width:12.5%;margin:0 0 1em}.ui.seven.statistics{margin:0 0 -1em}.ui.seven.statistics .statistic{min-width:14.28571429%;margin:0 0 1em}.ui.six.statistics{margin:0 0 -1em}.ui.six.statistics .statistic{min-width:16.66666667%;margin:0 0 1em}.ui.five.statistics{margin:0 0 -1em}.ui.five.statistics .statistic{min-width:20%;margin:0 0 1em}.ui.four.statistics{margin:0 0 -1em}.ui.four.statistics .statistic{min-width:25%;margin:0 0 1em}.ui.three.statistics{margin:0 0 -1em}.ui.three.statistics .statistic{min-width:33.33333333%;margin:0 0 1em}.ui.two.statistics{margin:0 0 -1em}.ui.two.statistics .statistic{min-width:50%;margin:0 0 1em}.ui.one.statistics{margin:0 0 -1em}.ui.one.statistics .statistic{min-width:100%;margin:0 0 1em}.ui.horizontal.statistic{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ui.horizontal.statistics{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0;max-width:none}.ui.horizontal.statistics .statistic{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-ms-flex-align:center;align-items:center;max-width:none;margin:1em 0}.ui.horizontal.statistic>.text.value,.ui.horizontal.statistics>.statistic>.text.value{min-height:0!important}.ui.horizontal.statistic>.value .icon,.ui.horizontal.statistics .statistic>.value .icon{width:1.18em}.ui.horizontal.statistic>.value,.ui.horizontal.statistics .statistic>.value{display:inline-block;vertical-align:middle}.ui.horizontal.statistic>.label,.ui.horizontal.statistics .statistic>.label{display:inline-block;vertical-align:middle;margin:0 0 0 .75em}.ui.red.statistic>.value,.ui.red.statistics .statistic>.value,.ui.statistics .red.statistic>.value{color:#db2828}.ui.orange.statistic>.value,.ui.orange.statistics .statistic>.value,.ui.statistics .orange.statistic>.value{color:#f2711c}.ui.statistics .yellow.statistic>.value,.ui.yellow.statistic>.value,.ui.yellow.statistics .statistic>.value{color:#fbbd08}.ui.olive.statistic>.value,.ui.olive.statistics .statistic>.value,.ui.statistics .olive.statistic>.value{color:#b5cc18}.ui.green.statistic>.value,.ui.green.statistics .statistic>.value,.ui.statistics .green.statistic>.value{color:#21ba45}.ui.statistics .teal.statistic>.value,.ui.teal.statistic>.value,.ui.teal.statistics .statistic>.value{color:#00b5ad}.ui.blue.statistic>.value,.ui.blue.statistics .statistic>.value,.ui.statistics .blue.statistic>.value{color:#2185d0}.ui.statistics .violet.statistic>.value,.ui.violet.statistic>.value,.ui.violet.statistics .statistic>.value{color:#6435c9}.ui.purple.statistic>.value,.ui.purple.statistics .statistic>.value,.ui.statistics .purple.statistic>.value{color:#a333c8}.ui.pink.statistic>.value,.ui.pink.statistics .statistic>.value,.ui.statistics .pink.statistic>.value{color:#e03997}.ui.brown.statistic>.value,.ui.brown.statistics .statistic>.value,.ui.statistics .brown.statistic>.value{color:#a5673f}.ui.grey.statistic>.value,.ui.grey.statistics .statistic>.value,.ui.statistics .grey.statistic>.value{color:#767676}.ui.inverted.statistic .value,.ui.inverted.statistics .statistic>.value{color:#fff}.ui.inverted.statistic .label,.ui.inverted.statistics .statistic>.label{color:rgba(255,255,255,.9)}.ui.inverted.red.statistic>.value,.ui.inverted.red.statistics .statistic>.value,.ui.statistics .inverted.red.statistic>.value{color:#ff695e}.ui.inverted.orange.statistic>.value,.ui.inverted.orange.statistics .statistic>.value,.ui.statistics .inverted.orange.statistic>.value{color:#ff851b}.ui.inverted.yellow.statistic>.value,.ui.inverted.yellow.statistics .statistic>.value,.ui.statistics .inverted.yellow.statistic>.value{color:#ffe21f}.ui.inverted.olive.statistic>.value,.ui.inverted.olive.statistics .statistic>.value,.ui.statistics .inverted.olive.statistic>.value{color:#d9e778}.ui.inverted.green.statistic>.value,.ui.inverted.green.statistics .statistic>.value,.ui.statistics .inverted.green.statistic>.value{color:#2ecc40}.ui.inverted.teal.statistic>.value,.ui.inverted.teal.statistics .statistic>.value,.ui.statistics .inverted.teal.statistic>.value{color:#6dffff}.ui.inverted.blue.statistic>.value,.ui.inverted.blue.statistics .statistic>.value,.ui.statistics .inverted.blue.statistic>.value{color:#54c8ff}.ui.inverted.violet.statistic>.value,.ui.inverted.violet.statistics .statistic>.value,.ui.statistics .inverted.violet.statistic>.value{color:#a291fb}.ui.inverted.purple.statistic>.value,.ui.inverted.purple.statistics .statistic>.value,.ui.statistics .inverted.purple.statistic>.value{color:#dc73ff}.ui.inverted.pink.statistic>.value,.ui.inverted.pink.statistics .statistic>.value,.ui.statistics .inverted.pink.statistic>.value{color:#ff8edf}.ui.inverted.brown.statistic>.value,.ui.inverted.brown.statistics .statistic>.value,.ui.statistics .inverted.brown.statistic>.value{color:#d67c1c}.ui.inverted.grey.statistic>.value,.ui.inverted.grey.statistics .statistic>.value,.ui.statistics .inverted.grey.statistic>.value{color:#dcddde}.ui[class*="left floated"].statistic{float:left;margin:0 2em 1em 0}.ui[class*="right floated"].statistic{float:right;margin:0 0 1em 2em}.ui.floated.statistic:last-child{margin-bottom:0}.ui.mini.statistic>.value,.ui.mini.statistics .statistic>.value{font-size:1.5rem!important}.ui.mini.horizontal.statistic>.value,.ui.mini.horizontal.statistics .statistic>.value{font-size:1.5rem!important}.ui.mini.statistic>.text.value,.ui.mini.statistics .statistic>.text.value{font-size:1rem!important}.ui.tiny.statistic>.value,.ui.tiny.statistics .statistic>.value{font-size:2rem!important}.ui.tiny.horizontal.statistic>.value,.ui.tiny.horizontal.statistics .statistic>.value{font-size:2rem!important}.ui.tiny.statistic>.text.value,.ui.tiny.statistics .statistic>.text.value{font-size:1rem!important}.ui.small.statistic>.value,.ui.small.statistics .statistic>.value{font-size:3rem!important}.ui.small.horizontal.statistic>.value,.ui.small.horizontal.statistics .statistic>.value{font-size:2rem!important}.ui.small.statistic>.text.value,.ui.small.statistics .statistic>.text.value{font-size:1rem!important}.ui.statistic>.value,.ui.statistics .statistic>.value{font-size:4rem!important}.ui.horizontal.statistic>.value,.ui.horizontal.statistics .statistic>.value{font-size:3rem!important}.ui.statistic>.text.value,.ui.statistics .statistic>.text.value{font-size:2rem!important}.ui.large.statistic>.value,.ui.large.statistics .statistic>.value{font-size:5rem!important}.ui.large.horizontal.statistic>.value,.ui.large.horizontal.statistics .statistic>.value{font-size:4rem!important}.ui.large.statistic>.text.value,.ui.large.statistics .statistic>.text.value{font-size:2.5rem!important}.ui.huge.statistic>.value,.ui.huge.statistics .statistic>.value{font-size:6rem!important}.ui.huge.horizontal.statistic>.value,.ui.huge.horizontal.statistics .statistic>.value{font-size:5rem!important}.ui.huge.statistic>.text.value,.ui.huge.statistics .statistic>.text.value{font-size:2.5rem!important}/*!
+ * # Semantic UI 2.4.2 - Accordion
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.accordion,.ui.accordion .accordion{max-width:100%}.ui.accordion .accordion{margin:1em 0 0;padding:0}.ui.accordion .accordion .title,.ui.accordion .title{cursor:pointer}.ui.accordion .title:not(.ui){padding:.5em 0;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-size:1em;color:rgba(0,0,0,.87)}.ui.accordion .accordion .title~.content,.ui.accordion .title~.content{display:none}.ui.accordion:not(.styled) .accordion .title~.content:not(.ui),.ui.accordion:not(.styled) .title~.content:not(.ui){margin:'';padding:.5em 0 1em}.ui.accordion:not(.styled) .title~.content:not(.ui):last-child{padding-bottom:0}.ui.accordion .accordion .title .dropdown.icon,.ui.accordion .title .dropdown.icon{display:inline-block;float:none;opacity:1;width:1.25em;height:1em;margin:0 .25rem 0 0;padding:0;font-size:1em;-webkit-transition:opacity .1s ease,-webkit-transform .1s ease;transition:opacity .1s ease,-webkit-transform .1s ease;transition:transform .1s ease,opacity .1s ease;transition:transform .1s ease,opacity .1s ease,-webkit-transform .1s ease;vertical-align:baseline;-webkit-transform:none;transform:none}.ui.accordion.menu .item .title{display:block;padding:0}.ui.accordion.menu .item .title>.dropdown.icon{float:right;margin:.21425em 0 0 1em;-webkit-transform:rotate(180deg);transform:rotate(180deg)}.ui.accordion .ui.header .dropdown.icon{font-size:1em;margin:0 .25rem 0 0}.ui.accordion .accordion .active.title .dropdown.icon,.ui.accordion .active.title .dropdown.icon{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.ui.accordion.menu .item .active.title>.dropdown.icon{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.ui.styled.accordion{width:600px}.ui.styled.accordion,.ui.styled.accordion .accordion{border-radius:.28571429rem;background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15);box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15)}.ui.styled.accordion .accordion .title,.ui.styled.accordion .title{margin:0;padding:.75em 1em;color:rgba(0,0,0,.4);font-weight:700;border-top:1px solid rgba(34,36,38,.15);-webkit-transition:background .1s ease,color .1s ease;transition:background .1s ease,color .1s ease}.ui.styled.accordion .accordion .title:first-child,.ui.styled.accordion>.title:first-child{border-top:none}.ui.styled.accordion .accordion .content,.ui.styled.accordion .content{margin:0;padding:.5em 1em 1.5em}.ui.styled.accordion .accordion .content{padding:0;padding:.5em 1em 1.5em}.ui.styled.accordion .accordion .active.title,.ui.styled.accordion .accordion .title:hover,.ui.styled.accordion .active.title,.ui.styled.accordion .title:hover{background:0 0;color:rgba(0,0,0,.87)}.ui.styled.accordion .accordion .active.title,.ui.styled.accordion .accordion .title:hover{background:0 0;color:rgba(0,0,0,.87)}.ui.styled.accordion .active.title{background:0 0;color:rgba(0,0,0,.95)}.ui.styled.accordion .accordion .active.title{background:0 0;color:rgba(0,0,0,.95)}.ui.accordion .accordion .active.content,.ui.accordion .active.content{display:block}.ui.fluid.accordion,.ui.fluid.accordion .accordion{width:100%}.ui.inverted.accordion .title:not(.ui){color:rgba(255,255,255,.9)}@font-face{font-family:Accordion;src:url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMggjB5AAAAC8AAAAYGNtYXAPfOIKAAABHAAAAExnYXNwAAAAEAAAAWgAAAAIZ2x5Zryj6HgAAAFwAAAAyGhlYWT/0IhHAAACOAAAADZoaGVhApkB5wAAAnAAAAAkaG10eAJuABIAAAKUAAAAGGxvY2EAjABWAAACrAAAAA5tYXhwAAgAFgAAArwAAAAgbmFtZfC1n04AAALcAAABPHBvc3QAAwAAAAAEGAAAACAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADw2gHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADgAAAAKAAgAAgACAAEAIPDa//3//wAAAAAAIPDZ//3//wAB/+MPKwADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQASAEkAtwFuABMAADc0PwE2FzYXFh0BFAcGJwYvASY1EgaABQgHBQYGBQcIBYAG2wcGfwcBAQcECf8IBAcBAQd/BgYAAAAAAQAAAEkApQFuABMAADcRNDc2MzIfARYVFA8BBiMiJyY1AAUGBwgFgAYGgAUIBwYFWwEACAUGBoAFCAcFgAYGBQcAAAABAAAAAQAAqWYls18PPPUACwIAAAAAAM/9o+4AAAAAz/2j7gAAAAAAtwFuAAAACAACAAAAAAAAAAEAAAHg/+AAAAIAAAAAAAC3AAEAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAQAAAAC3ABIAtwAAAAAAAAAKABQAHgBCAGQAAAABAAAABgAUAAEAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAADgCuAAEAAAAAAAEADAAAAAEAAAAAAAIADgBAAAEAAAAAAAMADAAiAAEAAAAAAAQADABOAAEAAAAAAAUAFgAMAAEAAAAAAAYABgAuAAEAAAAAAAoANABaAAMAAQQJAAEADAAAAAMAAQQJAAIADgBAAAMAAQQJAAMADAAiAAMAAQQJAAQADABOAAMAAQQJAAUAFgAMAAMAAQQJAAYADAA0AAMAAQQJAAoANABaAHIAYQB0AGkAbgBnAFYAZQByAHMAaQBvAG4AIAAxAC4AMAByAGEAdABpAG4AZ3JhdGluZwByAGEAdABpAG4AZwBSAGUAZwB1AGwAYQByAHIAYQB0AGkAbgBnAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('truetype'),url(data:application/font-woff;charset=utf-8;base64,d09GRk9UVE8AAASwAAoAAAAABGgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAAS0AAAEtFpovuE9TLzIAAAIkAAAAYAAAAGAIIweQY21hcAAAAoQAAABMAAAATA984gpnYXNwAAAC0AAAAAgAAAAIAAAAEGhlYWQAAALYAAAANgAAADb/0IhHaGhlYQAAAxAAAAAkAAAAJAKZAedobXR4AAADNAAAABgAAAAYAm4AEm1heHAAAANMAAAABgAAAAYABlAAbmFtZQAAA1QAAAE8AAABPPC1n05wb3N0AAAEkAAAACAAAAAgAAMAAAEABAQAAQEBB3JhdGluZwABAgABADr4HAL4GwP4GAQeCgAZU/+Lix4KABlT/4uLDAeLa/iU+HQFHQAAAHkPHQAAAH4RHQAAAAkdAAABJBIABwEBBw0PERQZHnJhdGluZ3JhdGluZ3UwdTF1MjB1RjBEOXVGMERBAAACAYkABAAGAQEEBwoNVp38lA78lA78lA77lA773Z33bxWLkI2Qj44I9xT3FAWOj5CNkIuQi4+JjoePiI2Gi4YIi/uUBYuGiYeHiIiHh4mGi4aLho2Ijwj7FPcUBYeOiY+LkAgO+92L5hWL95QFi5CNkI6Oj4+PjZCLkIuQiY6HCPcU+xQFj4iNhouGi4aJh4eICPsU+xQFiIeGiYaLhouHjYePiI6Jj4uQCA74lBT4lBWLDAoAAAAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADw2gHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADgAAAAKAAgAAgACAAEAIPDa//3//wAAAAAAIPDZ//3//wAB/+MPKwADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAEAADfYOJZfDzz1AAsCAAAAAADP/aPuAAAAAM/9o+4AAAAAALcBbgAAAAgAAgAAAAAAAAABAAAB4P/gAAACAAAAAAAAtwABAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAEAAAAAtwASALcAAAAAUAAABgAAAAAADgCuAAEAAAAAAAEADAAAAAEAAAAAAAIADgBAAAEAAAAAAAMADAAiAAEAAAAAAAQADABOAAEAAAAAAAUAFgAMAAEAAAAAAAYABgAuAAEAAAAAAAoANABaAAMAAQQJAAEADAAAAAMAAQQJAAIADgBAAAMAAQQJAAMADAAiAAMAAQQJAAQADABOAAMAAQQJAAUAFgAMAAMAAQQJAAYADAA0AAMAAQQJAAoANABaAHIAYQB0AGkAbgBnAFYAZQByAHMAaQBvAG4AIAAxAC4AMAByAGEAdABpAG4AZ3JhdGluZwByAGEAdABpAG4AZwBSAGUAZwB1AGwAYQByAHIAYQB0AGkAbgBnAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('woff');font-weight:400;font-style:normal}.ui.accordion .accordion .title .dropdown.icon,.ui.accordion .title .dropdown.icon{font-family:Accordion;line-height:1;-webkit-backface-visibility:hidden;backface-visibility:hidden;font-weight:400;font-style:normal;text-align:center}.ui.accordion .accordion .title .dropdown.icon:before,.ui.accordion .title .dropdown.icon:before{content:'\f0da'}/*!
+ * # Semantic UI 2.4.2 - Checkbox
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.checkbox{position:relative;display:inline-block;-webkit-backface-visibility:hidden;backface-visibility:hidden;outline:0;vertical-align:baseline;font-style:normal;min-height:17px;font-size:1rem;line-height:17px;min-width:17px}.ui.checkbox input[type=checkbox],.ui.checkbox input[type=radio]{cursor:pointer;position:absolute;top:0;left:0;opacity:0!important;outline:0;z-index:3;width:17px;height:17px}.ui.checkbox .box,.ui.checkbox label{cursor:auto;position:relative;display:block;padding-left:1.85714em;outline:0;font-size:1em}.ui.checkbox .box:before,.ui.checkbox label:before{position:absolute;top:0;left:0;width:17px;height:17px;content:'';background:#fff;border-radius:.21428571rem;-webkit-transition:border .1s ease,opacity .1s ease,-webkit-transform .1s ease,-webkit-box-shadow .1s ease;transition:border .1s ease,opacity .1s ease,-webkit-transform .1s ease,-webkit-box-shadow .1s ease;transition:border .1s ease,opacity .1s ease,transform .1s ease,box-shadow .1s ease;transition:border .1s ease,opacity .1s ease,transform .1s ease,box-shadow .1s ease,-webkit-transform .1s ease,-webkit-box-shadow .1s ease;border:1px solid #d4d4d5}.ui.checkbox .box:after,.ui.checkbox label:after{position:absolute;font-size:14px;top:0;left:0;width:17px;height:17px;text-align:center;opacity:0;color:rgba(0,0,0,.87);-webkit-transition:border .1s ease,opacity .1s ease,-webkit-transform .1s ease,-webkit-box-shadow .1s ease;transition:border .1s ease,opacity .1s ease,-webkit-transform .1s ease,-webkit-box-shadow .1s ease;transition:border .1s ease,opacity .1s ease,transform .1s ease,box-shadow .1s ease;transition:border .1s ease,opacity .1s ease,transform .1s ease,box-shadow .1s ease,-webkit-transform .1s ease,-webkit-box-shadow .1s ease}.ui.checkbox label,.ui.checkbox+label{color:rgba(0,0,0,.87);-webkit-transition:color .1s ease;transition:color .1s ease}.ui.checkbox+label{vertical-align:middle}.ui.checkbox .box:hover::before,.ui.checkbox label:hover::before{background:#fff;border-color:rgba(34,36,38,.35)}.ui.checkbox label:hover,.ui.checkbox+label:hover{color:rgba(0,0,0,.8)}.ui.checkbox .box:active::before,.ui.checkbox label:active::before{background:#f9fafb;border-color:rgba(34,36,38,.35)}.ui.checkbox .box:active::after,.ui.checkbox label:active::after{color:rgba(0,0,0,.95)}.ui.checkbox input:active~label{color:rgba(0,0,0,.95)}.ui.checkbox input:focus~.box:before,.ui.checkbox input:focus~label:before{background:#fff;border-color:#96c8da}.ui.checkbox input:focus~.box:after,.ui.checkbox input:focus~label:after{color:rgba(0,0,0,.95)}.ui.checkbox input:focus~label{color:rgba(0,0,0,.95)}.ui.checkbox input:checked~.box:before,.ui.checkbox input:checked~label:before{background:#fff;border-color:rgba(34,36,38,.35)}.ui.checkbox input:checked~.box:after,.ui.checkbox input:checked~label:after{opacity:1;color:rgba(0,0,0,.95)}.ui.checkbox input:not([type=radio]):indeterminate~.box:before,.ui.checkbox input:not([type=radio]):indeterminate~label:before{background:#fff;border-color:rgba(34,36,38,.35)}.ui.checkbox input:not([type=radio]):indeterminate~.box:after,.ui.checkbox input:not([type=radio]):indeterminate~label:after{opacity:1;color:rgba(0,0,0,.95)}.ui.checkbox input:checked:focus~.box:before,.ui.checkbox input:checked:focus~label:before,.ui.checkbox input:not([type=radio]):indeterminate:focus~.box:before,.ui.checkbox input:not([type=radio]):indeterminate:focus~label:before{background:#fff;border-color:#96c8da}.ui.checkbox input:checked:focus~.box:after,.ui.checkbox input:checked:focus~label:after,.ui.checkbox input:not([type=radio]):indeterminate:focus~.box:after,.ui.checkbox input:not([type=radio]):indeterminate:focus~label:after{color:rgba(0,0,0,.95)}.ui.read-only.checkbox,.ui.read-only.checkbox label{cursor:default}.ui.checkbox input[disabled]~.box:after,.ui.checkbox input[disabled]~label,.ui.disabled.checkbox .box:after,.ui.disabled.checkbox label{cursor:default!important;opacity:.5;color:#000}.ui.checkbox input.hidden{z-index:-1}.ui.checkbox input.hidden+label{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ui.radio.checkbox{min-height:15px}.ui.radio.checkbox .box,.ui.radio.checkbox label{padding-left:1.85714em}.ui.radio.checkbox .box:before,.ui.radio.checkbox label:before{content:'';-webkit-transform:none;transform:none;width:15px;height:15px;border-radius:500rem;top:1px;left:0}.ui.radio.checkbox .box:after,.ui.radio.checkbox label:after{border:none;content:''!important;width:15px;height:15px;line-height:15px}.ui.radio.checkbox .box:after,.ui.radio.checkbox label:after{top:1px;left:0;width:15px;height:15px;border-radius:500rem;-webkit-transform:scale(.46666667);transform:scale(.46666667);background-color:rgba(0,0,0,.87)}.ui.radio.checkbox input:focus~.box:before,.ui.radio.checkbox input:focus~label:before{background-color:#fff}.ui.radio.checkbox input:focus~.box:after,.ui.radio.checkbox input:focus~label:after{background-color:rgba(0,0,0,.95)}.ui.radio.checkbox input:indeterminate~.box:after,.ui.radio.checkbox input:indeterminate~label:after{opacity:0}.ui.radio.checkbox input:checked~.box:before,.ui.radio.checkbox input:checked~label:before{background-color:#fff}.ui.radio.checkbox input:checked~.box:after,.ui.radio.checkbox input:checked~label:after{background-color:rgba(0,0,0,.95)}.ui.radio.checkbox input:focus:checked~.box:before,.ui.radio.checkbox input:focus:checked~label:before{background-color:#fff}.ui.radio.checkbox input:focus:checked~.box:after,.ui.radio.checkbox input:focus:checked~label:after{background-color:rgba(0,0,0,.95)}.ui.slider.checkbox{min-height:1.25rem}.ui.slider.checkbox input{width:3.5rem;height:1.25rem}.ui.slider.checkbox .box,.ui.slider.checkbox label{padding-left:4.5rem;line-height:1rem;color:rgba(0,0,0,.4)}.ui.slider.checkbox .box:before,.ui.slider.checkbox label:before{display:block;position:absolute;content:'';border:none!important;left:0;z-index:1;top:.4rem;background-color:rgba(0,0,0,.05);width:3.5rem;height:.21428571rem;-webkit-transform:none;transform:none;border-radius:500rem;-webkit-transition:background .3s ease;transition:background .3s ease}.ui.slider.checkbox .box:after,.ui.slider.checkbox label:after{background:#fff -webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.05)));background:#fff -webkit-linear-gradient(transparent,rgba(0,0,0,.05));background:#fff linear-gradient(transparent,rgba(0,0,0,.05));position:absolute;content:''!important;opacity:1;z-index:2;border:none;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset;width:1.5rem;height:1.5rem;top:-.25rem;left:0;-webkit-transform:none;transform:none;border-radius:500rem;-webkit-transition:left .3s ease;transition:left .3s ease}.ui.slider.checkbox input:focus~.box:before,.ui.slider.checkbox input:focus~label:before{background-color:rgba(0,0,0,.15);border:none}.ui.slider.checkbox .box:hover,.ui.slider.checkbox label:hover{color:rgba(0,0,0,.8)}.ui.slider.checkbox .box:hover::before,.ui.slider.checkbox label:hover::before{background:rgba(0,0,0,.15)}.ui.slider.checkbox input:checked~.box,.ui.slider.checkbox input:checked~label{color:rgba(0,0,0,.95)!important}.ui.slider.checkbox input:checked~.box:before,.ui.slider.checkbox input:checked~label:before{background-color:#545454!important}.ui.slider.checkbox input:checked~.box:after,.ui.slider.checkbox input:checked~label:after{left:2rem}.ui.slider.checkbox input:focus:checked~.box,.ui.slider.checkbox input:focus:checked~label{color:rgba(0,0,0,.95)!important}.ui.slider.checkbox input:focus:checked~.box:before,.ui.slider.checkbox input:focus:checked~label:before{background-color:#000!important}.ui.toggle.checkbox{min-height:1.5rem}.ui.toggle.checkbox input{width:3.5rem;height:1.5rem}.ui.toggle.checkbox .box,.ui.toggle.checkbox label{min-height:1.5rem;padding-left:4.5rem;color:rgba(0,0,0,.87)}.ui.toggle.checkbox label{padding-top:.15em}.ui.toggle.checkbox .box:before,.ui.toggle.checkbox label:before{display:block;position:absolute;content:'';z-index:1;-webkit-transform:none;transform:none;border:none;top:0;background:rgba(0,0,0,.05);-webkit-box-shadow:none;box-shadow:none;width:3.5rem;height:1.5rem;border-radius:500rem}.ui.toggle.checkbox .box:after,.ui.toggle.checkbox label:after{background:#fff -webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.05)));background:#fff -webkit-linear-gradient(transparent,rgba(0,0,0,.05));background:#fff linear-gradient(transparent,rgba(0,0,0,.05));position:absolute;content:''!important;opacity:1;z-index:2;border:none;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset;width:1.5rem;height:1.5rem;top:0;left:0;border-radius:500rem;-webkit-transition:background .3s ease,left .3s ease;transition:background .3s ease,left .3s ease}.ui.toggle.checkbox input~.box:after,.ui.toggle.checkbox input~label:after{left:-.05rem;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset}.ui.toggle.checkbox input:focus~.box:before,.ui.toggle.checkbox input:focus~label:before{background-color:rgba(0,0,0,.15);border:none}.ui.toggle.checkbox .box:hover::before,.ui.toggle.checkbox label:hover::before{background-color:rgba(0,0,0,.15);border:none}.ui.toggle.checkbox input:checked~.box,.ui.toggle.checkbox input:checked~label{color:rgba(0,0,0,.95)!important}.ui.toggle.checkbox input:checked~.box:before,.ui.toggle.checkbox input:checked~label:before{background-color:#2185d0!important}.ui.toggle.checkbox input:checked~.box:after,.ui.toggle.checkbox input:checked~label:after{left:2.15rem;-webkit-box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset;box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset}.ui.toggle.checkbox input:focus:checked~.box,.ui.toggle.checkbox input:focus:checked~label{color:rgba(0,0,0,.95)!important}.ui.toggle.checkbox input:focus:checked~.box:before,.ui.toggle.checkbox input:focus:checked~label:before{background-color:#0d71bb!important}.ui.fitted.checkbox .box,.ui.fitted.checkbox label{padding-left:0!important}.ui.fitted.toggle.checkbox{width:3.5rem}.ui.fitted.slider.checkbox{width:3.5rem}@font-face{font-family:Checkbox;src:url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBD8AAAC8AAAAYGNtYXAYVtCJAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5Zn4huwUAAAF4AAABYGhlYWQGPe1ZAAAC2AAAADZoaGVhB30DyAAAAxAAAAAkaG10eBBKAEUAAAM0AAAAHGxvY2EAmgESAAADUAAAABBtYXhwAAkALwAAA2AAAAAgbmFtZSC8IugAAAOAAAABknBvc3QAAwAAAAAFFAAAACAAAwMTAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADoAgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6AL//f//AAAAAAAg6AD//f//AAH/4xgEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAEUAUQO7AvgAGgAAARQHAQYjIicBJjU0PwE2MzIfAQE2MzIfARYVA7sQ/hQQFhcQ/uMQEE4QFxcQqAF2EBcXEE4QAnMWEP4UEBABHRAXFhBOEBCoAXcQEE4QFwAAAAABAAABbgMlAkkAFAAAARUUBwYjISInJj0BNDc2MyEyFxYVAyUQEBf9SRcQEBAQFwK3FxAQAhJtFxAQEBAXbRcQEBAQFwAAAAABAAAASQMlA24ALAAAARUUBwYrARUUBwYrASInJj0BIyInJj0BNDc2OwE1NDc2OwEyFxYdATMyFxYVAyUQEBfuEBAXbhYQEO4XEBAQEBfuEBAWbhcQEO4XEBACEm0XEBDuFxAQEBAX7hAQF20XEBDuFxAQEBAX7hAQFwAAAQAAAAIAAHRSzT9fDzz1AAsEAAAAAADRsdR3AAAAANGx1HcAAAAAA7sDbgAAAAgAAgAAAAAAAAABAAADwP/AAAAEAAAAAAADuwABAAAAAAAAAAAAAAAAAAAABwQAAAAAAAAAAAAAAAIAAAAEAABFAyUAAAMlAAAAAAAAAAoAFAAeAE4AcgCwAAEAAAAHAC0AAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAIAAAAAQAAAAAAAgAHAGkAAQAAAAAAAwAIADkAAQAAAAAABAAIAH4AAQAAAAAABQALABgAAQAAAAAABgAIAFEAAQAAAAAACgAaAJYAAwABBAkAAQAQAAgAAwABBAkAAgAOAHAAAwABBAkAAwAQAEEAAwABBAkABAAQAIYAAwABBAkABQAWACMAAwABBAkABgAQAFkAAwABBAkACgA0ALBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhWZXJzaW9uIDIuMABWAGUAcgBzAGkAbwBuACAAMgAuADBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhDaGVja2JveABDAGgAZQBjAGsAYgBvAHhSZWd1bGFyAFIAZQBnAHUAbABhAHJDaGVja2JveABDAGgAZQBjAGsAYgBvAHhGb250IGdlbmVyYXRlZCBieSBJY29Nb29uLgBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('truetype')}.ui.checkbox .box:after,.ui.checkbox label:after{font-family:Checkbox}.ui.checkbox input:checked~.box:after,.ui.checkbox input:checked~label:after{content:'\e800'}.ui.checkbox input:indeterminate~.box:after,.ui.checkbox input:indeterminate~label:after{font-size:12px;content:'\e801'}/*!
+ * # Semantic UI 2.4.2 - Dimmer
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.dimmable:not(body){position:relative}.ui.dimmer{display:none;position:absolute;top:0!important;left:0!important;width:100%;height:100%;text-align:center;vertical-align:middle;padding:1em;background-color:rgba(0,0,0,.85);opacity:0;line-height:1;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-transition:background-color .5s linear;transition:background-color .5s linear;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;will-change:opacity;z-index:1000}.ui.dimmer>.content{-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;color:#fff}.ui.segment>.ui.dimmer{border-radius:inherit!important}.ui.dimmer:not(.inverted)::-webkit-scrollbar-track{background:rgba(255,255,255,.1)}.ui.dimmer:not(.inverted)::-webkit-scrollbar-thumb{background:rgba(255,255,255,.25)}.ui.dimmer:not(.inverted)::-webkit-scrollbar-thumb:window-inactive{background:rgba(255,255,255,.15)}.ui.dimmer:not(.inverted)::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.35)}.animating.dimmable:not(body),.dimmed.dimmable:not(body){overflow:hidden}.dimmed.dimmable>.ui.animating.dimmer,.dimmed.dimmable>.ui.visible.dimmer,.ui.active.dimmer{display:-webkit-box;display:-ms-flexbox;display:flex;opacity:1}.ui.disabled.dimmer{width:0!important;height:0!important}.dimmed.dimmable>.ui.animating.legacy.dimmer,.dimmed.dimmable>.ui.visible.legacy.dimmer,.ui.active.legacy.dimmer{display:block}.ui[class*="top aligned"].dimmer{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.ui[class*="bottom aligned"].dimmer{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.ui.page.dimmer{position:fixed;-webkit-transform-style:'';transform-style:'';-webkit-perspective:2000px;perspective:2000px;-webkit-transform-origin:center center;transform-origin:center center}body.animating.in.dimmable,body.dimmed.dimmable{overflow:hidden}body.dimmable>.dimmer{position:fixed}.blurring.dimmable>:not(.dimmer){-webkit-filter:blur(0) grayscale(0);filter:blur(0) grayscale(0);-webkit-transition:.8s -webkit-filter ease;transition:.8s -webkit-filter ease;transition:.8s filter ease;transition:.8s filter ease,.8s -webkit-filter ease}.blurring.dimmed.dimmable>:not(.dimmer){-webkit-filter:blur(5px) grayscale(.7);filter:blur(5px) grayscale(.7)}.blurring.dimmable>.dimmer{background-color:rgba(0,0,0,.6)}.blurring.dimmable>.inverted.dimmer{background-color:rgba(255,255,255,.6)}.ui.dimmer>.top.aligned.content>*{vertical-align:top}.ui.dimmer>.bottom.aligned.content>*{vertical-align:bottom}.ui.inverted.dimmer{background-color:rgba(255,255,255,.85)}.ui.inverted.dimmer>.content>*{color:#fff}.ui.simple.dimmer{display:block;overflow:hidden;opacity:1;width:0%;height:0%;z-index:-100;background-color:rgba(0,0,0,0)}.dimmed.dimmable>.ui.simple.dimmer{overflow:visible;opacity:1;width:100%;height:100%;background-color:rgba(0,0,0,.85);z-index:1}.ui.simple.inverted.dimmer{background-color:rgba(255,255,255,0)}.dimmed.dimmable>.ui.simple.inverted.dimmer{background-color:rgba(255,255,255,.85)}/*!
+ * # Semantic UI 2.4.2 - Dropdown
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.dropdown{cursor:pointer;position:relative;display:inline-block;outline:0;text-align:left;-webkit-transition:width .1s ease,-webkit-box-shadow .1s ease;transition:width .1s ease,-webkit-box-shadow .1s ease;transition:box-shadow .1s ease,width .1s ease;transition:box-shadow .1s ease,width .1s ease,-webkit-box-shadow .1s ease;-webkit-tap-highlight-color:transparent}.ui.dropdown .menu{cursor:auto;position:absolute;display:none;outline:0;top:100%;min-width:-webkit-max-content;min-width:-moz-max-content;min-width:max-content;margin:0;padding:0 0;background:#fff;font-size:1em;text-shadow:none;text-align:left;-webkit-box-shadow:0 2px 3px 0 rgba(34,36,38,.15);box-shadow:0 2px 3px 0 rgba(34,36,38,.15);border:1px solid rgba(34,36,38,.15);border-radius:.28571429rem;-webkit-transition:opacity .1s ease;transition:opacity .1s ease;z-index:11;will-change:transform,opacity}.ui.dropdown .menu>*{white-space:nowrap}.ui.dropdown>input:not(.search):first-child,.ui.dropdown>select{display:none!important}.ui.dropdown>.dropdown.icon{position:relative;width:auto;font-size:.85714286em;margin:0 0 0 1em}.ui.dropdown .menu>.item .dropdown.icon{width:auto;float:right;margin:0 0 0 1em}.ui.dropdown .menu>.item .dropdown.icon+.text{margin-right:1em}.ui.dropdown>.text{display:inline-block;-webkit-transition:none;transition:none}.ui.dropdown .menu>.item{position:relative;cursor:pointer;display:block;border:none;height:auto;text-align:left;border-top:none;line-height:1em;color:rgba(0,0,0,.87);padding:.78571429rem 1.14285714rem!important;font-size:1rem;text-transform:none;font-weight:400;-webkit-box-shadow:none;box-shadow:none;-webkit-touch-callout:none}.ui.dropdown .menu>.item:first-child{border-top-width:0}.ui.dropdown .menu .item>[class*="right floated"],.ui.dropdown>.text>[class*="right floated"]{float:right!important;margin-right:0!important;margin-left:1em!important}.ui.dropdown .menu .item>[class*="left floated"],.ui.dropdown>.text>[class*="left floated"]{float:left!important;margin-left:0!important;margin-right:1em!important}.ui.dropdown .menu .item>.flag.floated,.ui.dropdown .menu .item>.icon.floated,.ui.dropdown .menu .item>.image.floated,.ui.dropdown .menu .item>img.floated{margin-top:0}.ui.dropdown .menu>.header{margin:1rem 0 .75rem;padding:0 1.14285714rem;color:rgba(0,0,0,.85);font-size:.78571429em;font-weight:700;text-transform:uppercase}.ui.dropdown .menu>.divider{border-top:1px solid rgba(34,36,38,.1);height:0;margin:.5em 0}.ui.dropdown.dropdown .menu>.input{width:auto;display:-webkit-box;display:-ms-flexbox;display:flex;margin:1.14285714rem .78571429rem;min-width:10rem}.ui.dropdown .menu>.header+.input{margin-top:0}.ui.dropdown .menu>.input:not(.transparent) input{padding:.5em 1em}.ui.dropdown .menu>.input:not(.transparent) .button,.ui.dropdown .menu>.input:not(.transparent) .icon,.ui.dropdown .menu>.input:not(.transparent) .label{padding-top:.5em;padding-bottom:.5em}.ui.dropdown .menu>.item>.description,.ui.dropdown>.text>.description{float:right;margin:0 0 0 1em;color:rgba(0,0,0,.4)}.ui.dropdown .menu>.message{padding:.78571429rem 1.14285714rem;font-weight:400}.ui.dropdown .menu>.message:not(.ui){color:rgba(0,0,0,.4)}.ui.dropdown .menu .menu{top:0!important;left:100%;right:auto;margin:0 0 0 -.5em!important;border-radius:.28571429rem!important;z-index:21!important}.ui.dropdown .menu .menu:after{display:none}.ui.dropdown>.text>.flag,.ui.dropdown>.text>.icon,.ui.dropdown>.text>.image,.ui.dropdown>.text>.label,.ui.dropdown>.text>img{margin-top:0}.ui.dropdown .menu>.item>.flag,.ui.dropdown .menu>.item>.icon,.ui.dropdown .menu>.item>.image,.ui.dropdown .menu>.item>.label,.ui.dropdown .menu>.item>img{margin-top:0}.ui.dropdown .menu>.item>.flag,.ui.dropdown .menu>.item>.icon,.ui.dropdown .menu>.item>.image,.ui.dropdown .menu>.item>.label,.ui.dropdown .menu>.item>img,.ui.dropdown>.text>.flag,.ui.dropdown>.text>.icon,.ui.dropdown>.text>.image,.ui.dropdown>.text>.label,.ui.dropdown>.text>img{margin-left:0;float:none;margin-right:.78571429rem}.ui.dropdown .menu>.item>.image,.ui.dropdown .menu>.item>img,.ui.dropdown>.text>.image,.ui.dropdown>.text>img{display:inline-block;vertical-align:top;width:auto;margin-top:-.5em;margin-bottom:-.5em;max-height:2em}.ui.dropdown .ui.menu>.item:before,.ui.menu .ui.dropdown .menu>.item:before{display:none}.ui.menu .ui.dropdown .menu .active.item{border-left:none}.ui.buttons>.ui.dropdown:last-child .menu,.ui.menu .right.dropdown.item .menu,.ui.menu .right.menu .dropdown:last-child .menu{left:auto;right:0}.ui.label.dropdown .menu{min-width:100%}.ui.dropdown.icon.button>.dropdown.icon{margin:0}.ui.button.dropdown .menu{min-width:100%}.ui.selection.dropdown{cursor:pointer;word-wrap:break-word;line-height:1em;white-space:normal;outline:0;-webkit-transform:rotateZ(0);transform:rotateZ(0);min-width:14em;min-height:2.71428571em;background:#fff;display:inline-block;padding:.78571429em 2.1em .78571429em 1em;color:rgba(0,0,0,.87);-webkit-box-shadow:none;box-shadow:none;border:1px solid rgba(34,36,38,.15);border-radius:.28571429rem;-webkit-transition:width .1s ease,-webkit-box-shadow .1s ease;transition:width .1s ease,-webkit-box-shadow .1s ease;transition:box-shadow .1s ease,width .1s ease;transition:box-shadow .1s ease,width .1s ease,-webkit-box-shadow .1s ease}.ui.selection.dropdown.active,.ui.selection.dropdown.visible{z-index:10}select.ui.dropdown{height:38px;padding:.5em;border:1px solid rgba(34,36,38,.15);visibility:visible}.ui.selection.dropdown>.delete.icon,.ui.selection.dropdown>.dropdown.icon,.ui.selection.dropdown>.search.icon{cursor:pointer;position:absolute;width:auto;height:auto;line-height:1.21428571em;top:.78571429em;right:1em;z-index:3;margin:-.78571429em;padding:.91666667em;opacity:.8;-webkit-transition:opacity .1s ease;transition:opacity .1s ease}.ui.compact.selection.dropdown{min-width:0}.ui.selection.dropdown .menu{overflow-x:hidden;overflow-y:auto;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-overflow-scrolling:touch;border-top-width:0!important;width:auto;outline:0;margin:0 -1px;min-width:calc(100% + 2px);width:calc(100% + 2px);border-radius:0 0 .28571429rem .28571429rem;-webkit-box-shadow:0 2px 3px 0 rgba(34,36,38,.15);box-shadow:0 2px 3px 0 rgba(34,36,38,.15);-webkit-transition:opacity .1s ease;transition:opacity .1s ease}.ui.selection.dropdown .menu:after,.ui.selection.dropdown .menu:before{display:none}.ui.selection.dropdown .menu>.message{padding:.78571429rem 1.14285714rem}@media only screen and (max-width:767px){.ui.selection.dropdown .menu{max-height:8.01428571rem}}@media only screen and (min-width:768px){.ui.selection.dropdown .menu{max-height:10.68571429rem}}@media only screen and (min-width:992px){.ui.selection.dropdown .menu{max-height:16.02857143rem}}@media only screen and (min-width:1920px){.ui.selection.dropdown .menu{max-height:21.37142857rem}}.ui.selection.dropdown .menu>.item{border-top:1px solid #fafafa;padding:.78571429rem 1.14285714rem!important;white-space:normal;word-wrap:normal}.ui.selection.dropdown .menu>.hidden.addition.item{display:none}.ui.selection.dropdown:hover{border-color:rgba(34,36,38,.35);-webkit-box-shadow:none;box-shadow:none}.ui.selection.active.dropdown{border-color:#96c8da;-webkit-box-shadow:0 2px 3px 0 rgba(34,36,38,.15);box-shadow:0 2px 3px 0 rgba(34,36,38,.15)}.ui.selection.active.dropdown .menu{border-color:#96c8da;-webkit-box-shadow:0 2px 3px 0 rgba(34,36,38,.15);box-shadow:0 2px 3px 0 rgba(34,36,38,.15)}.ui.selection.dropdown:focus{border-color:#96c8da;-webkit-box-shadow:none;box-shadow:none}.ui.selection.dropdown:focus .menu{border-color:#96c8da;-webkit-box-shadow:0 2px 3px 0 rgba(34,36,38,.15);box-shadow:0 2px 3px 0 rgba(34,36,38,.15)}.ui.selection.visible.dropdown>.text:not(.default){font-weight:400;color:rgba(0,0,0,.8)}.ui.selection.active.dropdown:hover{border-color:#96c8da;-webkit-box-shadow:0 2px 3px 0 rgba(34,36,38,.15);box-shadow:0 2px 3px 0 rgba(34,36,38,.15)}.ui.selection.active.dropdown:hover .menu{border-color:#96c8da;-webkit-box-shadow:0 2px 3px 0 rgba(34,36,38,.15);box-shadow:0 2px 3px 0 rgba(34,36,38,.15)}.ui.active.selection.dropdown>.dropdown.icon,.ui.visible.selection.dropdown>.dropdown.icon{opacity:'';z-index:3}.ui.active.selection.dropdown{border-bottom-left-radius:0!important;border-bottom-right-radius:0!important}.ui.active.empty.selection.dropdown{border-radius:.28571429rem!important;-webkit-box-shadow:none!important;box-shadow:none!important}.ui.active.empty.selection.dropdown .menu{border:none!important;-webkit-box-shadow:none!important;box-shadow:none!important}.ui.search.dropdown{min-width:''}.ui.search.dropdown>input.search{background:none transparent!important;border:none!important;-webkit-box-shadow:none!important;box-shadow:none!important;cursor:text;top:0;left:1px;width:100%;outline:0;-webkit-tap-highlight-color:rgba(255,255,255,0);padding:inherit}.ui.search.dropdown>input.search{position:absolute;z-index:2}.ui.search.dropdown>.text{cursor:text;position:relative;left:1px;z-index:3}.ui.search.selection.dropdown>input.search{line-height:1.21428571em;padding:.67857143em 2.1em .67857143em 1em}.ui.search.selection.dropdown>span.sizer{line-height:1.21428571em;padding:.67857143em 2.1em .67857143em 1em;display:none;white-space:pre}.ui.search.dropdown.active>input.search,.ui.search.dropdown.visible>input.search{cursor:auto}.ui.search.dropdown.active>.text,.ui.search.dropdown.visible>.text{pointer-events:none}.ui.active.search.dropdown input.search:focus+.text .flag,.ui.active.search.dropdown input.search:focus+.text .icon{opacity:.45}.ui.active.search.dropdown input.search:focus+.text{color:rgba(115,115,115,.87)!important}.ui.search.dropdown .menu{overflow-x:hidden;overflow-y:auto;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-overflow-scrolling:touch}@media only screen and (max-width:767px){.ui.search.dropdown .menu{max-height:8.01428571rem}}@media only screen and (min-width:768px){.ui.search.dropdown .menu{max-height:10.68571429rem}}@media only screen and (min-width:992px){.ui.search.dropdown .menu{max-height:16.02857143rem}}@media only screen and (min-width:1920px){.ui.search.dropdown .menu{max-height:21.37142857rem}}.ui.multiple.dropdown{padding:.22619048em 2.1em .22619048em .35714286em}.ui.multiple.dropdown .menu{cursor:auto}.ui.multiple.search.dropdown,.ui.multiple.search.dropdown>input.search{cursor:text}.ui.multiple.dropdown>.label{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:inline-block;vertical-align:top;white-space:normal;font-size:1em;padding:.35714286em .78571429em;margin:.14285714rem .28571429rem .14285714rem 0;-webkit-box-shadow:0 0 0 1px rgba(34,36,38,.15) inset;box-shadow:0 0 0 1px rgba(34,36,38,.15) inset}.ui.multiple.dropdown .dropdown.icon{margin:'';padding:''}.ui.multiple.dropdown>.text{position:static;padding:0;max-width:100%;margin:.45238095em 0 .45238095em .64285714em;line-height:1.21428571em}.ui.multiple.dropdown>.label~input.search{margin-left:.14285714em!important}.ui.multiple.dropdown>.label~.text{display:none}.ui.multiple.search.dropdown>.text{display:inline-block;position:absolute;top:0;left:0;padding:inherit;margin:.45238095em 0 .45238095em .64285714em;line-height:1.21428571em}.ui.multiple.search.dropdown>.label~.text{display:none}.ui.multiple.search.dropdown>input.search{position:static;padding:0;max-width:100%;margin:.45238095em 0 .45238095em .64285714em;width:2.2em;line-height:1.21428571em}.ui.inline.dropdown{cursor:pointer;display:inline-block;color:inherit}.ui.inline.dropdown .dropdown.icon{margin:0 .21428571em 0 .21428571em;vertical-align:baseline}.ui.inline.dropdown>.text{font-weight:700}.ui.inline.dropdown .menu{cursor:auto;margin-top:.21428571em;border-radius:.28571429rem}.ui.dropdown .menu .active.item{background:0 0;font-weight:700;color:rgba(0,0,0,.95);-webkit-box-shadow:none;box-shadow:none;z-index:12}.ui.dropdown .menu>.item:hover{background:rgba(0,0,0,.05);color:rgba(0,0,0,.95);z-index:13}.ui.loading.dropdown>i.icon{height:1em!important}.ui.loading.selection.dropdown>i.icon{padding:1.5em 1.28571429em!important}.ui.loading.dropdown>i.icon:before{position:absolute;content:'';top:50%;left:50%;margin:-.64285714em 0 0 -.64285714em;width:1.28571429em;height:1.28571429em;border-radius:500rem;border:.2em solid rgba(0,0,0,.1)}.ui.loading.dropdown>i.icon:after{position:absolute;content:'';top:50%;left:50%;-webkit-box-shadow:0 0 0 1px transparent;box-shadow:0 0 0 1px transparent;margin:-.64285714em 0 0 -.64285714em;width:1.28571429em;height:1.28571429em;-webkit-animation:dropdown-spin .6s linear;animation:dropdown-spin .6s linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;border-radius:500rem;border-color:#767676 transparent transparent;border-style:solid;border-width:.2em}.ui.loading.dropdown.button>i.icon:after,.ui.loading.dropdown.button>i.icon:before{display:none}@-webkit-keyframes dropdown-spin{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes dropdown-spin{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.ui.default.dropdown:not(.button)>.text,.ui.dropdown:not(.button)>.default.text{color:rgba(191,191,191,.87)}.ui.default.dropdown:not(.button)>input:focus~.text,.ui.dropdown:not(.button)>input:focus~.default.text{color:rgba(115,115,115,.87)}.ui.loading.dropdown>.text{-webkit-transition:none;transition:none}.ui.dropdown .loading.menu{display:block;visibility:hidden;z-index:-1}.ui.dropdown>.loading.menu{left:0!important;right:auto!important}.ui.dropdown>.menu .loading.menu{left:100%!important;right:auto!important}.ui.dropdown .menu .selected.item,.ui.dropdown.selected{background:rgba(0,0,0,.03);color:rgba(0,0,0,.95)}.ui.dropdown>.filtered.text{visibility:hidden}.ui.dropdown .filtered.item{display:none!important}.ui.dropdown.error,.ui.dropdown.error>.default.text,.ui.dropdown.error>.text{color:#9f3a38}.ui.selection.dropdown.error{background:#fff6f6;border-color:#e0b4b4}.ui.selection.dropdown.error:hover{border-color:#e0b4b4}.ui.dropdown.error>.menu,.ui.dropdown.error>.menu .menu{border-color:#e0b4b4}.ui.dropdown.error>.menu>.item{color:#9f3a38}.ui.multiple.selection.error.dropdown>.label{border-color:#e0b4b4}.ui.dropdown.error>.menu>.item:hover{background-color:#fff2f2}.ui.dropdown.error>.menu .active.item{background-color:#fdcfcf}.ui.dropdown>.clear.dropdown.icon{opacity:.8;-webkit-transition:opacity .1s ease;transition:opacity .1s ease}.ui.dropdown>.clear.dropdown.icon:hover{opacity:1}.ui.disabled.dropdown,.ui.dropdown .menu>.disabled.item{cursor:default;pointer-events:none;opacity:.45}.ui.dropdown .menu{left:0}.ui.dropdown .menu .right.menu,.ui.dropdown .right.menu>.menu{left:100%!important;right:auto!important;border-radius:.28571429rem!important}.ui.dropdown>.left.menu{left:auto!important;right:0!important}.ui.dropdown .menu .left.menu,.ui.dropdown>.left.menu .menu{left:auto;right:100%;margin:0 -.5em 0 0!important;border-radius:.28571429rem!important}.ui.dropdown .item .left.dropdown.icon,.ui.dropdown .left.menu .item .dropdown.icon{width:auto;float:left;margin:0}.ui.dropdown .item .left.dropdown.icon,.ui.dropdown .left.menu .item .dropdown.icon{width:auto;float:left;margin:0}.ui.dropdown .item .left.dropdown.icon+.text,.ui.dropdown .left.menu .item .dropdown.icon+.text{margin-left:1em;margin-right:0}.ui.upward.dropdown>.menu{top:auto;bottom:100%;-webkit-box-shadow:0 0 3px 0 rgba(0,0,0,.08);box-shadow:0 0 3px 0 rgba(0,0,0,.08);border-radius:.28571429rem .28571429rem 0 0}.ui.dropdown .upward.menu{top:auto!important;bottom:0!important}.ui.simple.upward.active.dropdown,.ui.simple.upward.dropdown:hover{border-radius:.28571429rem .28571429rem 0 0!important}.ui.upward.dropdown.button:not(.pointing):not(.floating).active{border-radius:.28571429rem .28571429rem 0 0}.ui.upward.selection.dropdown .menu{border-top-width:1px!important;border-bottom-width:0!important;-webkit-box-shadow:0 -2px 3px 0 rgba(0,0,0,.08);box-shadow:0 -2px 3px 0 rgba(0,0,0,.08)}.ui.upward.selection.dropdown:hover{-webkit-box-shadow:0 0 2px 0 rgba(0,0,0,.05);box-shadow:0 0 2px 0 rgba(0,0,0,.05)}.ui.active.upward.selection.dropdown{border-radius:0 0 .28571429rem .28571429rem!important}.ui.upward.selection.dropdown.visible{-webkit-box-shadow:0 0 3px 0 rgba(0,0,0,.08);box-shadow:0 0 3px 0 rgba(0,0,0,.08);border-radius:0 0 .28571429rem .28571429rem!important}.ui.upward.active.selection.dropdown:hover{-webkit-box-shadow:0 0 3px 0 rgba(0,0,0,.05);box-shadow:0 0 3px 0 rgba(0,0,0,.05)}.ui.upward.active.selection.dropdown:hover .menu{-webkit-box-shadow:0 -2px 3px 0 rgba(0,0,0,.08);box-shadow:0 -2px 3px 0 rgba(0,0,0,.08)}.ui.dropdown .scrolling.menu,.ui.scrolling.dropdown .menu{overflow-x:hidden;overflow-y:auto}.ui.scrolling.dropdown .menu{overflow-x:hidden;overflow-y:auto;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-overflow-scrolling:touch;min-width:100%!important;width:auto!important}.ui.dropdown .scrolling.menu{position:static;overflow-y:auto;border:none;-webkit-box-shadow:none!important;box-shadow:none!important;border-radius:0!important;margin:0!important;min-width:100%!important;width:auto!important;border-top:1px solid rgba(34,36,38,.15)}.ui.dropdown .scrolling.menu>.item.item.item,.ui.scrolling.dropdown .menu .item.item.item{border-top:none}.ui.dropdown .scrolling.menu .item:first-child,.ui.scrolling.dropdown .menu .item:first-child{border-top:none}.ui.dropdown>.animating.menu .scrolling.menu,.ui.dropdown>.visible.menu .scrolling.menu{display:block}@media all and (-ms-high-contrast:none){.ui.dropdown .scrolling.menu,.ui.scrolling.dropdown .menu{min-width:calc(100% - 17px)}}@media only screen and (max-width:767px){.ui.dropdown .scrolling.menu,.ui.scrolling.dropdown .menu{max-height:10.28571429rem}}@media only screen and (min-width:768px){.ui.dropdown .scrolling.menu,.ui.scrolling.dropdown .menu{max-height:15.42857143rem}}@media only screen and (min-width:992px){.ui.dropdown .scrolling.menu,.ui.scrolling.dropdown .menu{max-height:20.57142857rem}}@media only screen and (min-width:1920px){.ui.dropdown .scrolling.menu,.ui.scrolling.dropdown .menu{max-height:20.57142857rem}}.ui.simple.dropdown .menu:after,.ui.simple.dropdown .menu:before{display:none}.ui.simple.dropdown .menu{position:absolute;display:block;overflow:hidden;top:-9999px!important;opacity:0;width:0;height:0;-webkit-transition:opacity .1s ease;transition:opacity .1s ease}.ui.simple.active.dropdown,.ui.simple.dropdown:hover{border-bottom-left-radius:0!important;border-bottom-right-radius:0!important}.ui.simple.active.dropdown>.menu,.ui.simple.dropdown:hover>.menu{overflow:visible;width:auto;height:auto;top:100%!important;opacity:1}.ui.simple.dropdown:hover>.menu>.item:hover>.menu,.ui.simple.dropdown>.menu>.item:active>.menu{overflow:visible;width:auto;height:auto;top:0!important;left:100%!important;opacity:1}.ui.simple.disabled.dropdown:hover .menu{display:none;height:0;width:0;overflow:hidden}.ui.simple.visible.dropdown>.menu{display:block}.ui.fluid.dropdown{display:block;width:100%;min-width:0}.ui.fluid.dropdown>.dropdown.icon{float:right}.ui.floating.dropdown .menu{left:0;right:auto;-webkit-box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15)!important;box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15)!important;border-radius:.28571429rem!important}.ui.floating.dropdown>.menu{margin-top:.5em!important;border-radius:.28571429rem!important}.ui.pointing.dropdown>.menu{top:100%;margin-top:.78571429rem;border-radius:.28571429rem}.ui.pointing.dropdown>.menu:after{display:block;position:absolute;pointer-events:none;content:'';visibility:visible;-webkit-transform:rotate(45deg);transform:rotate(45deg);width:.5em;height:.5em;-webkit-box-shadow:-1px -1px 0 0 rgba(34,36,38,.15);box-shadow:-1px -1px 0 0 rgba(34,36,38,.15);background:#fff;z-index:2}.ui.pointing.dropdown>.menu:after{top:-.25em;left:50%;margin:0 0 0 -.25em}.ui.top.left.pointing.dropdown>.menu{top:100%;bottom:auto;left:0;right:auto;margin:1em 0 0}.ui.top.left.pointing.dropdown>.menu{top:100%;bottom:auto;left:0;right:auto;margin:1em 0 0}.ui.top.left.pointing.dropdown>.menu:after{top:-.25em;left:1em;right:auto;margin:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}.ui.top.right.pointing.dropdown>.menu{top:100%;bottom:auto;right:0;left:auto;margin:1em 0 0}.ui.top.pointing.dropdown>.left.menu:after,.ui.top.right.pointing.dropdown>.menu:after{top:-.25em;left:auto!important;right:1em!important;margin:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}.ui.left.pointing.dropdown>.menu{top:0;left:100%;right:auto;margin:0 0 0 1em}.ui.left.pointing.dropdown>.menu:after{top:1em;left:-.25em;margin:0;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.ui.left:not(.top):not(.bottom).pointing.dropdown>.left.menu{left:auto!important;right:100%!important;margin:0 1em 0 0}.ui.left:not(.top):not(.bottom).pointing.dropdown>.left.menu:after{top:1em;left:auto;right:-.25em;margin:0;-webkit-transform:rotate(135deg);transform:rotate(135deg)}.ui.right.pointing.dropdown>.menu{top:0;left:auto;right:100%;margin:0 1em 0 0}.ui.right.pointing.dropdown>.menu:after{top:1em;left:auto;right:-.25em;margin:0;-webkit-transform:rotate(135deg);transform:rotate(135deg)}.ui.bottom.pointing.dropdown>.menu{top:auto;bottom:100%;left:0;right:auto;margin:0 0 1em}.ui.bottom.pointing.dropdown>.menu:after{top:auto;bottom:-.25em;right:auto;margin:0;-webkit-transform:rotate(-135deg);transform:rotate(-135deg)}.ui.bottom.pointing.dropdown>.menu .menu{top:auto!important;bottom:0!important}.ui.bottom.left.pointing.dropdown>.menu{left:0;right:auto}.ui.bottom.left.pointing.dropdown>.menu:after{left:1em;right:auto}.ui.bottom.right.pointing.dropdown>.menu{right:0;left:auto}.ui.bottom.right.pointing.dropdown>.menu:after{left:auto;right:1em}.ui.pointing.upward.dropdown .menu,.ui.top.pointing.upward.dropdown .menu{top:auto!important;bottom:100%!important;margin:0 0 .78571429rem;border-radius:.28571429rem}.ui.pointing.upward.dropdown .menu:after,.ui.top.pointing.upward.dropdown .menu:after{top:100%!important;bottom:auto!important;-webkit-box-shadow:1px 1px 0 0 rgba(34,36,38,.15);box-shadow:1px 1px 0 0 rgba(34,36,38,.15);margin:-.25em 0 0}.ui.right.pointing.upward.dropdown:not(.top):not(.bottom) .menu{top:auto!important;bottom:0!important;margin:0 1em 0 0}.ui.right.pointing.upward.dropdown:not(.top):not(.bottom) .menu:after{top:auto!important;bottom:0!important;margin:0 0 1em 0;-webkit-box-shadow:-1px -1px 0 0 rgba(34,36,38,.15);box-shadow:-1px -1px 0 0 rgba(34,36,38,.15)}.ui.left.pointing.upward.dropdown:not(.top):not(.bottom) .menu{top:auto!important;bottom:0!important;margin:0 0 0 1em}.ui.left.pointing.upward.dropdown:not(.top):not(.bottom) .menu:after{top:auto!important;bottom:0!important;margin:0 0 1em 0;-webkit-box-shadow:-1px -1px 0 0 rgba(34,36,38,.15);box-shadow:-1px -1px 0 0 rgba(34,36,38,.15)}@font-face{font-family:Dropdown;src:url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAVgAA8AAAAACFAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABWAAAABwAAAAchGgaq0dERUYAAAF0AAAAHAAAAB4AJwAPT1MvMgAAAZAAAABDAAAAVnW4TJdjbWFwAAAB1AAAAEsAAAFS8CcaqmN2dCAAAAIgAAAABAAAAAQAEQFEZ2FzcAAAAiQAAAAIAAAACP//AANnbHlmAAACLAAAAQoAAAGkrRHP9WhlYWQAAAM4AAAAMAAAADYPK8YyaGhlYQAAA2gAAAAdAAAAJANCAb1obXR4AAADiAAAACIAAAAiCBkAOGxvY2EAAAOsAAAAFAAAABQBnAIybWF4cAAAA8AAAAAfAAAAIAEVAF5uYW1lAAAD4AAAATAAAAKMFGlj5HBvc3QAAAUQAAAARgAAAHJoedjqd2ViZgAABVgAAAAGAAAABrO7W5UAAAABAAAAANXulPUAAAAA1r4hgAAAAADXu2Q1eNpjYGRgYOABYjEgZmJgBEIOIGYB8xgAA/YAN3jaY2BktGOcwMDKwMI4jTGNgYHBHUp/ZZBkaGFgYGJgZWbACgLSXFMYHFT/fLjFeOD/AQY9xjMMbkBhRpAcAN48DQYAeNpjYGBgZoBgGQZGBhDwAfIYwXwWBgMgzQGETAwMqn8+8H649f8/lHX9//9b7Pzf+fWgusCAkY0BzmUE6gHpQwGMDMMeAACbxg7SAAARAUQAAAAB//8AAnjadZBPSsNAGMXfS+yMqYgOhpSuSlKadmUhiVEhEMQzFF22m17BbbvzCh5BXCUn6EG8gjeQ4DepwYo4i+/ffL95j4EDA+CFC7jQuKyIeVHrI3wkleq9F7XrSInKteOeHdda8bOoaeepSc00NWPz/LRec9G8GabyGtEdF7h19z033GAMTK7zbM42xNEZpzYof0RtQ5CUHAQJ73OtVyutc+3b7Ou//b8XNlsPx3jgjUifABdhEohKJJL5iM5p39uqc7X1+sRQSqmGrUVhlsJ4lpmEUVwyT8SUYtg0P9DyNzPADDs+tjrGV6KRCRfsui3eHcL4/p8ZXvfMlcnEU+CLv7hDykOP+AKTPTxbAAB42mNgZGBgAGKuf5KP4vltvjLIMzGAwLV9ig0g+vruFFMQzdjACOJzMIClARh0CTJ42mNgZGBgPPD/AJD8wgAEjA0MjAyogAMAbOQEAQAAAAC7ABEAAAAAAKoAAAH0AAABgAAAAUAACAFAAAgAwAAXAAAAAAAAACoAKgAqADIAbACGAKAAugDSeNpjYGRgYOBkUGFgYgABEMkFhAwM/xn0QAIADdUBdAB42qWQvUoDQRSFv3GjaISUQaymSmGxJoGAsRC0iPYLsU50Y6IxrvlRtPCJJKUPIBb+PIHv4EN4djKuKAqCDHfmu+feOdwZoMCUAJNbAlYUMzaUlM14jjxbngOq7HnOia89z1Pk1vMCa9x7ztPkzfMyJbPj+ZGi6Xp+omxuPD+zaD7meaFg7mb8GrBqHmhwxoAxlm0uiRkpP9X5m26pKRoMxTGR1D49Dv/Yb/91o6l8qL6eu5n2hZQzn68utR9m3FU2cB4t9cdSLG2utI+44Eh/P9bqKO+oJ/WxmXssj77YkrjasZQD6SFddythk3Wtzrf+UF2p076Udla1VNzsERP3kkjVRKel7mp1udXYcHtZSlV7RfmJe1GiFWveluaeKD5/MuJcSk8Tpm/vvwPIbmJleNpjYGKAAFYG7ICTgYGRiZGZkYWRlZGNkZ2Rg5GTLT2nsiDDEEIZsZfmZRqZujmDaDcDAxcI7WIOpS2gtCWUdgQAZkcSmQAAAAFblbO6AAA=) format('woff');font-weight:400;font-style:normal}.ui.dropdown>.dropdown.icon{font-family:Dropdown;line-height:1;height:1em;width:1.23em;-webkit-backface-visibility:hidden;backface-visibility:hidden;font-weight:400;font-style:normal;text-align:center}.ui.dropdown>.dropdown.icon{width:auto}.ui.dropdown>.dropdown.icon:before{content:'\f0d7'}.ui.dropdown .menu .item .dropdown.icon:before{content:'\f0da'}.ui.dropdown .item .left.dropdown.icon:before,.ui.dropdown .left.menu .item .dropdown.icon:before{content:"\f0d9"}.ui.vertical.menu .dropdown.item>.dropdown.icon:before{content:"\f0da"}.ui.dropdown>.clear.icon:before{content:"\f00d"}/*!
+ * # Semantic UI 2.4.2 - Video
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.embed{position:relative;max-width:100%;height:0;overflow:hidden;background:#dcddde;padding-bottom:56.25%}.ui.embed embed,.ui.embed iframe,.ui.embed object{position:absolute;border:none;width:100%;height:100%;top:0;left:0;margin:0;padding:0}.ui.embed>.embed{display:none}.ui.embed>.placeholder{position:absolute;cursor:pointer;top:0;left:0;display:block;width:100%;height:100%;background-color:radial-gradient(transparent 45%,rgba(0,0,0,.3))}.ui.embed>.icon{cursor:pointer;position:absolute;top:0;left:0;width:100%;height:100%;z-index:2}.ui.embed>.icon:after{position:absolute;top:0;left:0;width:100%;height:100%;z-index:3;content:'';background:-webkit-radial-gradient(transparent 45%,rgba(0,0,0,.3));background:radial-gradient(transparent 45%,rgba(0,0,0,.3));opacity:.5;-webkit-transition:opacity .5s ease;transition:opacity .5s ease}.ui.embed>.icon:before{position:absolute;top:50%;left:50%;z-index:4;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%);color:#fff;font-size:6rem;text-shadow:0 2px 10px rgba(34,36,38,.2);-webkit-transition:opacity .5s ease,color .5s ease;transition:opacity .5s ease,color .5s ease;z-index:10}.ui.embed .icon:hover:after{background:-webkit-radial-gradient(transparent 45%,rgba(0,0,0,.3));background:radial-gradient(transparent 45%,rgba(0,0,0,.3));opacity:1}.ui.embed .icon:hover:before{color:#fff}.ui.active.embed>.icon,.ui.active.embed>.placeholder{display:none}.ui.active.embed>.embed{display:block}.ui.square.embed{padding-bottom:100%}.ui[class*="4:3"].embed{padding-bottom:75%}.ui[class*="16:9"].embed{padding-bottom:56.25%}.ui[class*="21:9"].embed{padding-bottom:42.85714286%}/*!
+ * # Semantic UI 2.4.2 - Modal
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.modal{position:absolute;display:none;z-index:1001;text-align:left;background:#fff;border:none;-webkit-box-shadow:1px 3px 3px 0 rgba(0,0,0,.2),1px 3px 15px 2px rgba(0,0,0,.2);box-shadow:1px 3px 3px 0 rgba(0,0,0,.2),1px 3px 15px 2px rgba(0,0,0,.2);-webkit-transform-origin:50% 25%;transform-origin:50% 25%;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;border-radius:.28571429rem;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;will-change:top,left,margin,transform,opacity}.ui.modal>.icon:first-child+*,.ui.modal>:first-child:not(.icon){border-top-left-radius:.28571429rem;border-top-right-radius:.28571429rem}.ui.modal>:last-child{border-bottom-left-radius:.28571429rem;border-bottom-right-radius:.28571429rem}.ui.modal>.close{cursor:pointer;position:absolute;top:-2.5rem;right:-2.5rem;z-index:1;opacity:.8;font-size:1.25em;color:#fff;width:2.25rem;height:2.25rem;padding:.625rem 0 0 0}.ui.modal>.close:hover{opacity:1}.ui.modal>.header{display:block;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;background:#fff;margin:0;padding:1.25rem 1.5rem;-webkit-box-shadow:none;box-shadow:none;color:rgba(0,0,0,.85);border-bottom:1px solid rgba(34,36,38,.15)}.ui.modal>.header:not(.ui){font-size:1.42857143rem;line-height:1.28571429em;font-weight:700}.ui.modal>.content{display:block;width:100%;font-size:1em;line-height:1.4;padding:1.5rem;background:#fff}.ui.modal>.image.content{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.ui.modal>.content>.image{display:block;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;width:'';-ms-flex-item-align:top;align-self:top}.ui.modal>[class*="top aligned"]{-ms-flex-item-align:top;align-self:top}.ui.modal>[class*="middle aligned"]{-ms-flex-item-align:middle;align-self:middle}.ui.modal>[class*=stretched]{-ms-flex-item-align:stretch;align-self:stretch}.ui.modal>.content>.description{display:block;-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;min-width:0;-ms-flex-item-align:top;align-self:top}.ui.modal>.content>.icon+.description,.ui.modal>.content>.image+.description{-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;min-width:'';width:auto;padding-left:2em}.ui.modal>.content>.image>i.icon{margin:0;opacity:1;width:auto;line-height:1;font-size:8rem}.ui.modal>.actions{background:#f9fafb;padding:1rem 1rem;border-top:1px solid rgba(34,36,38,.15);text-align:right}.ui.modal .actions>.button{margin-left:.75em}@media only screen and (max-width:767px){.ui.modal{width:95%;margin:0}}@media only screen and (min-width:768px){.ui.modal{width:88%;margin:0}}@media only screen and (min-width:992px){.ui.modal{width:850px;margin:0}}@media only screen and (min-width:1200px){.ui.modal{width:900px;margin:0}}@media only screen and (min-width:1920px){.ui.modal{width:950px;margin:0}}@media only screen and (max-width:991px){.ui.modal>.header{padding-right:2.25rem}.ui.modal>.close{top:1.0535rem;right:1rem;color:rgba(0,0,0,.87)}}@media only screen and (max-width:767px){.ui.modal>.header{padding:.75rem 1rem!important;padding-right:2.25rem!important}.ui.modal>.content{display:block;padding:1rem!important}.ui.modal>.close{top:.5rem!important;right:.5rem!important}.ui.modal .image.content{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.ui.modal .content>.image{display:block;max-width:100%;margin:0 auto!important;text-align:center;padding:0 0 1rem!important}.ui.modal>.content>.image>i.icon{font-size:5rem;text-align:center}.ui.modal .content>.description{display:block;width:100%!important;margin:0!important;padding:1rem 0!important;-webkit-box-shadow:none;box-shadow:none}.ui.modal>.actions{padding:1rem 1rem 0!important}.ui.modal .actions>.button,.ui.modal .actions>.buttons{margin-bottom:1rem}}.ui.inverted.dimmer>.ui.modal{-webkit-box-shadow:1px 3px 10px 2px rgba(0,0,0,.2);box-shadow:1px 3px 10px 2px rgba(0,0,0,.2)}.ui.basic.modal{background-color:transparent;border:none;border-radius:0;-webkit-box-shadow:none!important;box-shadow:none!important;color:#fff}.ui.basic.modal>.actions,.ui.basic.modal>.content,.ui.basic.modal>.header{background-color:transparent}.ui.basic.modal>.header{color:#fff}.ui.basic.modal>.close{top:1rem;right:1.5rem}.ui.inverted.dimmer>.basic.modal{color:rgba(0,0,0,.87)}.ui.inverted.dimmer>.ui.basic.modal>.header{color:rgba(0,0,0,.85)}.ui.legacy.modal,.ui.legacy.page.dimmer>.ui.modal{top:50%;left:50%}.ui.legacy.page.dimmer>.ui.scrolling.modal,.ui.page.dimmer>.ui.scrolling.legacy.modal,.ui.top.aligned.dimmer>.ui.legacy.modal,.ui.top.aligned.legacy.page.dimmer>.ui.modal{top:auto}@media only screen and (max-width:991px){.ui.basic.modal>.close{color:#fff}}.ui.loading.modal{display:block;visibility:hidden;z-index:-1}.ui.active.modal{display:block}.modals.dimmer[class*="top aligned"] .modal{margin:5vh auto}@media only screen and (max-width:767px){.modals.dimmer[class*="top aligned"] .modal{margin:1rem auto}}.legacy.modals.dimmer[class*="top aligned"]{padding-top:5vh}@media only screen and (max-width:767px){.legacy.modals.dimmer[class*="top aligned"]{padding-top:1rem}}.scrolling.dimmable.dimmed{overflow:hidden}.scrolling.dimmable>.dimmer{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.scrolling.dimmable.dimmed>.dimmer{overflow:auto;-webkit-overflow-scrolling:touch}.scrolling.dimmable>.dimmer{position:fixed}.modals.dimmer .ui.scrolling.modal{margin:1rem auto}.scrolling.undetached.dimmable.dimmed{overflow:auto;-webkit-overflow-scrolling:touch}.scrolling.undetached.dimmable.dimmed>.dimmer{overflow:hidden}.scrolling.undetached.dimmable .ui.scrolling.modal{position:absolute;left:50%;margin-top:1rem!important}.ui.modal .scrolling.content{max-height:calc(80vh - 10em);overflow:auto}.ui.fullscreen.modal{width:95%!important;margin:1em auto}.ui.fullscreen.modal>.header{padding-right:2.25rem}.ui.fullscreen.modal>.close{top:1.0535rem;right:1rem;color:rgba(0,0,0,.87)}.ui.modal{font-size:1rem}.ui.mini.modal>.header:not(.ui){font-size:1.3em}@media only screen and (max-width:767px){.ui.mini.modal{width:95%;margin:0}}@media only screen and (min-width:768px){.ui.mini.modal{width:35.2%;margin:0}}@media only screen and (min-width:992px){.ui.mini.modal{width:340px;margin:0}}@media only screen and (min-width:1200px){.ui.mini.modal{width:360px;margin:0}}@media only screen and (min-width:1920px){.ui.mini.modal{width:380px;margin:0}}.ui.small.modal>.header:not(.ui){font-size:1.3em}@media only screen and (max-width:767px){.ui.tiny.modal{width:95%;margin:0}}@media only screen and (min-width:768px){.ui.tiny.modal{width:52.8%;margin:0}}@media only screen and (min-width:992px){.ui.tiny.modal{width:510px;margin:0}}@media only screen and (min-width:1200px){.ui.tiny.modal{width:540px;margin:0}}@media only screen and (min-width:1920px){.ui.tiny.modal{width:570px;margin:0}}.ui.small.modal>.header:not(.ui){font-size:1.3em}@media only screen and (max-width:767px){.ui.small.modal{width:95%;margin:0}}@media only screen and (min-width:768px){.ui.small.modal{width:70.4%;margin:0}}@media only screen and (min-width:992px){.ui.small.modal{width:680px;margin:0}}@media only screen and (min-width:1200px){.ui.small.modal{width:720px;margin:0}}@media only screen and (min-width:1920px){.ui.small.modal{width:760px;margin:0}}.ui.large.modal>.header{font-size:1.6em}@media only screen and (max-width:767px){.ui.large.modal{width:95%;margin:0}}@media only screen and (min-width:768px){.ui.large.modal{width:88%;margin:0}}@media only screen and (min-width:992px){.ui.large.modal{width:1020px;margin:0}}@media only screen and (min-width:1200px){.ui.large.modal{width:1080px;margin:0}}@media only screen and (min-width:1920px){.ui.large.modal{width:1140px;margin:0}}/*!
+ * # Semantic UI 2.4.2 - Nag
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.nag{display:none;opacity:.95;position:relative;top:0;left:0;z-index:999;min-height:0;width:100%;margin:0;padding:.75em 1em;background:#555;-webkit-box-shadow:0 1px 2px 0 rgba(0,0,0,.2);box-shadow:0 1px 2px 0 rgba(0,0,0,.2);font-size:1rem;text-align:center;color:rgba(0,0,0,.87);border-radius:0 0 .28571429rem .28571429rem;-webkit-transition:.2s background ease;transition:.2s background ease}a.ui.nag{cursor:pointer}.ui.nag>.title{display:inline-block;margin:0 .5em;color:#fff}.ui.nag>.close.icon{cursor:pointer;opacity:.4;position:absolute;top:50%;right:1em;font-size:1em;margin:-.5em 0 0;color:#fff;-webkit-transition:opacity .2s ease;transition:opacity .2s ease}.ui.nag:hover{background:#555;opacity:1}.ui.nag .close:hover{opacity:1}.ui.overlay.nag{position:absolute;display:block}.ui.fixed.nag{position:fixed}.ui.bottom.nag,.ui.bottom.nags{border-radius:.28571429rem .28571429rem 0 0;top:auto;bottom:0}.ui.inverted.nag,.ui.inverted.nags .nag{background-color:#f3f4f5;color:rgba(0,0,0,.85)}.ui.inverted.nag .close,.ui.inverted.nag .title,.ui.inverted.nags .nag .close,.ui.inverted.nags .nag .title{color:rgba(0,0,0,.4)}.ui.nags .nag{border-radius:0!important}.ui.nags .nag:last-child{border-radius:0 0 .28571429rem .28571429rem}.ui.bottom.nags .nag:last-child{border-radius:.28571429rem .28571429rem 0 0}/*!
+ * # Semantic UI 2.4.2 - Popup
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.popup{display:none;position:absolute;top:0;right:0;min-width:-webkit-min-content;min-width:-moz-min-content;min-width:min-content;z-index:1900;border:1px solid #d4d4d5;line-height:1.4285em;max-width:250px;background:#fff;padding:.833em 1em;font-weight:400;font-style:normal;color:rgba(0,0,0,.87);border-radius:.28571429rem;-webkit-box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15);box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15)}.ui.popup>.header{padding:0;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-size:1.14285714em;line-height:1.2;font-weight:700}.ui.popup>.header+.content{padding-top:.5em}.ui.popup:before{position:absolute;content:'';width:.71428571em;height:.71428571em;background:#fff;-webkit-transform:rotate(45deg);transform:rotate(45deg);z-index:2;-webkit-box-shadow:1px 1px 0 0 #bababc;box-shadow:1px 1px 0 0 #bababc}[data-tooltip]{position:relative}[data-tooltip]:before{pointer-events:none;position:absolute;content:'';font-size:1rem;width:.71428571em;height:.71428571em;background:#fff;-webkit-transform:rotate(45deg);transform:rotate(45deg);z-index:2;-webkit-box-shadow:1px 1px 0 0 #bababc;box-shadow:1px 1px 0 0 #bababc}[data-tooltip]:after{pointer-events:none;content:attr(data-tooltip);position:absolute;text-transform:none;text-align:left;white-space:nowrap;font-size:1rem;border:1px solid #d4d4d5;line-height:1.4285em;max-width:none;background:#fff;padding:.833em 1em;font-weight:400;font-style:normal;color:rgba(0,0,0,.87);border-radius:.28571429rem;-webkit-box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15);box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15);z-index:1}[data-tooltip]:not([data-position]):before{top:auto;right:auto;bottom:100%;left:50%;background:#fff;margin-left:-.07142857rem;margin-bottom:.14285714rem}[data-tooltip]:not([data-position]):after{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);bottom:100%;margin-bottom:.5em}[data-tooltip]:after,[data-tooltip]:before{pointer-events:none;visibility:hidden}[data-tooltip]:before{opacity:0;-webkit-transform:rotate(45deg) scale(0)!important;transform:rotate(45deg) scale(0)!important;-webkit-transform-origin:center top;transform-origin:center top;-webkit-transition:all .1s ease;transition:all .1s ease}[data-tooltip]:after{opacity:1;-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-transition:all .1s ease;transition:all .1s ease}[data-tooltip]:hover:after,[data-tooltip]:hover:before{visibility:visible;pointer-events:auto}[data-tooltip]:hover:before{-webkit-transform:rotate(45deg) scale(1)!important;transform:rotate(45deg) scale(1)!important;opacity:1}[data-tooltip]:after,[data-tooltip][data-position="bottom center"]:after,[data-tooltip][data-position="top center"]:after{-webkit-transform:translateX(-50%) scale(0)!important;transform:translateX(-50%) scale(0)!important}[data-tooltip]:hover:after,[data-tooltip][data-position="bottom center"]:hover:after{-webkit-transform:translateX(-50%) scale(1)!important;transform:translateX(-50%) scale(1)!important}[data-tooltip][data-position="left center"]:after,[data-tooltip][data-position="right center"]:after{-webkit-transform:translateY(-50%) scale(0)!important;transform:translateY(-50%) scale(0)!important}[data-tooltip][data-position="left center"]:hover:after,[data-tooltip][data-position="right center"]:hover:after{-webkit-transform:translateY(-50%) scale(1)!important;transform:translateY(-50%) scale(1)!important}[data-tooltip][data-position="bottom left"]:after,[data-tooltip][data-position="bottom right"]:after,[data-tooltip][data-position="top left"]:after,[data-tooltip][data-position="top right"]:after{-webkit-transform:scale(0)!important;transform:scale(0)!important}[data-tooltip][data-position="bottom left"]:hover:after,[data-tooltip][data-position="bottom right"]:hover:after,[data-tooltip][data-position="top left"]:hover:after,[data-tooltip][data-position="top right"]:hover:after{-webkit-transform:scale(1)!important;transform:scale(1)!important}[data-tooltip][data-inverted]:before{-webkit-box-shadow:none!important;box-shadow:none!important}[data-tooltip][data-inverted]:before{background:#1b1c1d}[data-tooltip][data-inverted]:after{background:#1b1c1d;color:#fff;border:none;-webkit-box-shadow:none;box-shadow:none}[data-tooltip][data-inverted]:after .header{background-color:none;color:#fff}[data-position="top center"][data-tooltip]:after{top:auto;right:auto;left:50%;bottom:100%;-webkit-transform:translateX(-50%);transform:translateX(-50%);margin-bottom:.5em}[data-position="top center"][data-tooltip]:before{top:auto;right:auto;bottom:100%;left:50%;background:#fff;margin-left:-.07142857rem;margin-bottom:.14285714rem}[data-position="top left"][data-tooltip]:after{top:auto;right:auto;left:0;bottom:100%;margin-bottom:.5em}[data-position="top left"][data-tooltip]:before{top:auto;right:auto;bottom:100%;left:1em;margin-left:-.07142857rem;margin-bottom:.14285714rem}[data-position="top right"][data-tooltip]:after{top:auto;left:auto;right:0;bottom:100%;margin-bottom:.5em}[data-position="top right"][data-tooltip]:before{top:auto;left:auto;bottom:100%;right:1em;margin-left:-.07142857rem;margin-bottom:.14285714rem}[data-position="bottom center"][data-tooltip]:after{bottom:auto;right:auto;left:50%;top:100%;-webkit-transform:translateX(-50%);transform:translateX(-50%);margin-top:.5em}[data-position="bottom center"][data-tooltip]:before{bottom:auto;right:auto;top:100%;left:50%;margin-left:-.07142857rem;margin-top:.14285714rem}[data-position="bottom left"][data-tooltip]:after{left:0;top:100%;margin-top:.5em}[data-position="bottom left"][data-tooltip]:before{bottom:auto;right:auto;top:100%;left:1em;margin-left:-.07142857rem;margin-top:.14285714rem}[data-position="bottom right"][data-tooltip]:after{right:0;top:100%;margin-top:.5em}[data-position="bottom right"][data-tooltip]:before{bottom:auto;left:auto;top:100%;right:1em;margin-left:-.14285714rem;margin-top:.07142857rem}[data-position="left center"][data-tooltip]:after{right:100%;top:50%;margin-right:.5em;-webkit-transform:translateY(-50%);transform:translateY(-50%)}[data-position="left center"][data-tooltip]:before{right:100%;top:50%;margin-top:-.14285714rem;margin-right:-.07142857rem}[data-position="right center"][data-tooltip]:after{left:100%;top:50%;margin-left:.5em;-webkit-transform:translateY(-50%);transform:translateY(-50%)}[data-position="right center"][data-tooltip]:before{left:100%;top:50%;margin-top:-.07142857rem;margin-left:.14285714rem}[data-position~=bottom][data-tooltip]:before{background:#fff;-webkit-box-shadow:-1px -1px 0 0 #bababc;box-shadow:-1px -1px 0 0 #bababc}[data-position="left center"][data-tooltip]:before{background:#fff;-webkit-box-shadow:1px -1px 0 0 #bababc;box-shadow:1px -1px 0 0 #bababc}[data-position="right center"][data-tooltip]:before{background:#fff;-webkit-box-shadow:-1px 1px 0 0 #bababc;box-shadow:-1px 1px 0 0 #bababc}[data-position~=top][data-tooltip]:before{background:#fff}[data-inverted][data-position~=bottom][data-tooltip]:before{background:#1b1c1d;-webkit-box-shadow:-1px -1px 0 0 #bababc;box-shadow:-1px -1px 0 0 #bababc}[data-inverted][data-position="left center"][data-tooltip]:before{background:#1b1c1d;-webkit-box-shadow:1px -1px 0 0 #bababc;box-shadow:1px -1px 0 0 #bababc}[data-inverted][data-position="right center"][data-tooltip]:before{background:#1b1c1d;-webkit-box-shadow:-1px 1px 0 0 #bababc;box-shadow:-1px 1px 0 0 #bababc}[data-inverted][data-position~=top][data-tooltip]:before{background:#1b1c1d}[data-position~=bottom][data-tooltip]:before{-webkit-transform-origin:center bottom;transform-origin:center bottom}[data-position~=bottom][data-tooltip]:after{-webkit-transform-origin:center top;transform-origin:center top}[data-position="left center"][data-tooltip]:before{-webkit-transform-origin:top center;transform-origin:top center}[data-position="left center"][data-tooltip]:after{-webkit-transform-origin:right center;transform-origin:right center}[data-position="right center"][data-tooltip]:before{-webkit-transform-origin:right center;transform-origin:right center}[data-position="right center"][data-tooltip]:after{-webkit-transform-origin:left center;transform-origin:left center}.ui.popup{margin:0}.ui.top.popup{margin:0 0 .71428571em}.ui.top.left.popup{-webkit-transform-origin:left bottom;transform-origin:left bottom}.ui.top.center.popup{-webkit-transform-origin:center bottom;transform-origin:center bottom}.ui.top.right.popup{-webkit-transform-origin:right bottom;transform-origin:right bottom}.ui.left.center.popup{margin:0 .71428571em 0 0;-webkit-transform-origin:right 50%;transform-origin:right 50%}.ui.right.center.popup{margin:0 0 0 .71428571em;-webkit-transform-origin:left 50%;transform-origin:left 50%}.ui.bottom.popup{margin:.71428571em 0 0}.ui.bottom.left.popup{-webkit-transform-origin:left top;transform-origin:left top}.ui.bottom.center.popup{-webkit-transform-origin:center top;transform-origin:center top}.ui.bottom.right.popup{-webkit-transform-origin:right top;transform-origin:right top}.ui.bottom.center.popup:before{margin-left:-.30714286em;top:-.30714286em;left:50%;right:auto;bottom:auto;-webkit-box-shadow:-1px -1px 0 0 #bababc;box-shadow:-1px -1px 0 0 #bababc}.ui.bottom.left.popup{margin-left:0}.ui.bottom.left.popup:before{top:-.30714286em;left:1em;right:auto;bottom:auto;margin-left:0;-webkit-box-shadow:-1px -1px 0 0 #bababc;box-shadow:-1px -1px 0 0 #bababc}.ui.bottom.right.popup{margin-right:0}.ui.bottom.right.popup:before{top:-.30714286em;right:1em;bottom:auto;left:auto;margin-left:0;-webkit-box-shadow:-1px -1px 0 0 #bababc;box-shadow:-1px -1px 0 0 #bababc}.ui.top.center.popup:before{top:auto;right:auto;bottom:-.30714286em;left:50%;margin-left:-.30714286em}.ui.top.left.popup{margin-left:0}.ui.top.left.popup:before{bottom:-.30714286em;left:1em;top:auto;right:auto;margin-left:0}.ui.top.right.popup{margin-right:0}.ui.top.right.popup:before{bottom:-.30714286em;right:1em;top:auto;left:auto;margin-left:0}.ui.left.center.popup:before{top:50%;right:-.30714286em;bottom:auto;left:auto;margin-top:-.30714286em;-webkit-box-shadow:1px -1px 0 0 #bababc;box-shadow:1px -1px 0 0 #bababc}.ui.right.center.popup:before{top:50%;left:-.30714286em;bottom:auto;right:auto;margin-top:-.30714286em;-webkit-box-shadow:-1px 1px 0 0 #bababc;box-shadow:-1px 1px 0 0 #bababc}.ui.bottom.popup:before{background:#fff}.ui.left.center.popup:before,.ui.right.center.popup:before{background:#fff}.ui.top.popup:before{background:#fff}.ui.inverted.bottom.popup:before{background:#1b1c1d}.ui.inverted.left.center.popup:before,.ui.inverted.right.center.popup:before{background:#1b1c1d}.ui.inverted.top.popup:before{background:#1b1c1d}.ui.popup>.ui.grid:not(.padded){width:calc(100% + 1.75rem);margin:-.7rem -.875rem}.ui.loading.popup{display:block;visibility:hidden;z-index:-1}.ui.animating.popup,.ui.visible.popup{display:block}.ui.visible.popup{-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden}.ui.basic.popup:before{display:none}.ui.wide.popup{max-width:350px}.ui[class*="very wide"].popup{max-width:550px}@media only screen and (max-width:767px){.ui.wide.popup,.ui[class*="very wide"].popup{max-width:250px}}.ui.fluid.popup{width:100%;max-width:none}.ui.inverted.popup{background:#1b1c1d;color:#fff;border:none;-webkit-box-shadow:none;box-shadow:none}.ui.inverted.popup .header{background-color:none;color:#fff}.ui.inverted.popup:before{background-color:#1b1c1d;-webkit-box-shadow:none!important;box-shadow:none!important}.ui.flowing.popup{max-width:none}.ui.mini.popup{font-size:.78571429rem}.ui.tiny.popup{font-size:.85714286rem}.ui.small.popup{font-size:.92857143rem}.ui.popup{font-size:1rem}.ui.large.popup{font-size:1.14285714rem}.ui.huge.popup{font-size:1.42857143rem}/*!
+ * # Semantic UI 2.4.2 - Progress Bar
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.progress{position:relative;display:block;max-width:100%;border:none;margin:1em 0 2.5em;-webkit-box-shadow:none;box-shadow:none;background:rgba(0,0,0,.1);padding:0;border-radius:.28571429rem}.ui.progress:first-child{margin:0 0 2.5em}.ui.progress:last-child{margin:0 0 1.5em}.ui.progress .bar{display:block;line-height:1;position:relative;width:0%;min-width:2em;background:#888;border-radius:.28571429rem;-webkit-transition:width .1s ease,background-color .1s ease;transition:width .1s ease,background-color .1s ease}.ui.progress .bar>.progress{white-space:nowrap;position:absolute;width:auto;font-size:.92857143em;top:50%;right:.5em;left:auto;bottom:auto;color:rgba(255,255,255,.7);text-shadow:none;margin-top:-.5em;font-weight:700;text-align:left}.ui.progress>.label{position:absolute;width:100%;font-size:1em;top:100%;right:auto;left:0;bottom:auto;color:rgba(0,0,0,.87);font-weight:700;text-shadow:none;margin-top:.2em;text-align:center;-webkit-transition:color .4s ease;transition:color .4s ease}.ui.indicating.progress[data-percent^="1"] .bar,.ui.indicating.progress[data-percent^="2"] .bar{background-color:#d95c5c}.ui.indicating.progress[data-percent^="3"] .bar{background-color:#efbc72}.ui.indicating.progress[data-percent^="4"] .bar,.ui.indicating.progress[data-percent^="5"] .bar{background-color:#e6bb48}.ui.indicating.progress[data-percent^="6"] .bar{background-color:#ddc928}.ui.indicating.progress[data-percent^="7"] .bar,.ui.indicating.progress[data-percent^="8"] .bar{background-color:#b4d95c}.ui.indicating.progress[data-percent^="100"] .bar,.ui.indicating.progress[data-percent^="9"] .bar{background-color:#66da81}.ui.indicating.progress[data-percent^="1"] .label,.ui.indicating.progress[data-percent^="2"] .label{color:rgba(0,0,0,.87)}.ui.indicating.progress[data-percent^="3"] .label{color:rgba(0,0,0,.87)}.ui.indicating.progress[data-percent^="4"] .label,.ui.indicating.progress[data-percent^="5"] .label{color:rgba(0,0,0,.87)}.ui.indicating.progress[data-percent^="6"] .label{color:rgba(0,0,0,.87)}.ui.indicating.progress[data-percent^="7"] .label,.ui.indicating.progress[data-percent^="8"] .label{color:rgba(0,0,0,.87)}.ui.indicating.progress[data-percent^="100"] .label,.ui.indicating.progress[data-percent^="9"] .label{color:rgba(0,0,0,.87)}.ui.indicating.progress[data-percent="1"] .bar,.ui.indicating.progress[data-percent="2"] .bar,.ui.indicating.progress[data-percent="3"] .bar,.ui.indicating.progress[data-percent="4"] .bar,.ui.indicating.progress[data-percent="5"] .bar,.ui.indicating.progress[data-percent="6"] .bar,.ui.indicating.progress[data-percent="7"] .bar,.ui.indicating.progress[data-percent="8"] .bar,.ui.indicating.progress[data-percent="9"] .bar{background-color:#d95c5c}.ui.indicating.progress[data-percent="1"] .label,.ui.indicating.progress[data-percent="2"] .label,.ui.indicating.progress[data-percent="3"] .label,.ui.indicating.progress[data-percent="4"] .label,.ui.indicating.progress[data-percent="5"] .label,.ui.indicating.progress[data-percent="6"] .label,.ui.indicating.progress[data-percent="7"] .label,.ui.indicating.progress[data-percent="8"] .label,.ui.indicating.progress[data-percent="9"] .label{color:rgba(0,0,0,.87)}.ui.indicating.progress.success .label{color:#1a531b}.ui.progress.success .bar{background-color:#21ba45!important}.ui.progress.success .bar,.ui.progress.success .bar::after{-webkit-animation:none!important;animation:none!important}.ui.progress.success>.label{color:#1a531b}.ui.progress.warning .bar{background-color:#f2c037!important}.ui.progress.warning .bar,.ui.progress.warning .bar::after{-webkit-animation:none!important;animation:none!important}.ui.progress.warning>.label{color:#794b02}.ui.progress.error .bar{background-color:#db2828!important}.ui.progress.error .bar,.ui.progress.error .bar::after{-webkit-animation:none!important;animation:none!important}.ui.progress.error>.label{color:#912d2b}.ui.active.progress .bar{position:relative;min-width:2em}.ui.active.progress .bar::after{content:'';opacity:0;position:absolute;top:0;left:0;right:0;bottom:0;background:#fff;border-radius:.28571429rem;-webkit-animation:progress-active 2s ease infinite;animation:progress-active 2s ease infinite}@-webkit-keyframes progress-active{0%{opacity:.3;width:0}100%{opacity:0;width:100%}}@keyframes progress-active{0%{opacity:.3;width:0}100%{opacity:0;width:100%}}.ui.disabled.progress{opacity:.35}.ui.disabled.progress .bar,.ui.disabled.progress .bar::after{-webkit-animation:none!important;animation:none!important}.ui.inverted.progress{background:rgba(255,255,255,.08);border:none}.ui.inverted.progress .bar{background:#888}.ui.inverted.progress .bar>.progress{color:#f9fafb}.ui.inverted.progress>.label{color:#fff}.ui.inverted.progress.success>.label{color:#21ba45}.ui.inverted.progress.warning>.label{color:#f2c037}.ui.inverted.progress.error>.label{color:#db2828}.ui.progress.attached{background:0 0;position:relative;border:none;margin:0}.ui.progress.attached,.ui.progress.attached .bar{display:block;height:.2rem;padding:0;overflow:hidden;border-radius:0 0 .28571429rem .28571429rem}.ui.progress.attached .bar{border-radius:0}.ui.progress.top.attached,.ui.progress.top.attached .bar{top:0;border-radius:.28571429rem .28571429rem 0 0}.ui.progress.top.attached .bar{border-radius:0}.ui.card>.ui.attached.progress,.ui.segment>.ui.attached.progress{position:absolute;top:auto;left:0;bottom:100%;width:100%}.ui.card>.ui.bottom.attached.progress,.ui.segment>.ui.bottom.attached.progress{top:100%;bottom:auto}.ui.red.progress .bar{background-color:#db2828}.ui.red.inverted.progress .bar{background-color:#ff695e}.ui.orange.progress .bar{background-color:#f2711c}.ui.orange.inverted.progress .bar{background-color:#ff851b}.ui.yellow.progress .bar{background-color:#fbbd08}.ui.yellow.inverted.progress .bar{background-color:#ffe21f}.ui.olive.progress .bar{background-color:#b5cc18}.ui.olive.inverted.progress .bar{background-color:#d9e778}.ui.green.progress .bar{background-color:#21ba45}.ui.green.inverted.progress .bar{background-color:#2ecc40}.ui.teal.progress .bar{background-color:#00b5ad}.ui.teal.inverted.progress .bar{background-color:#6dffff}.ui.blue.progress .bar{background-color:#2185d0}.ui.blue.inverted.progress .bar{background-color:#54c8ff}.ui.violet.progress .bar{background-color:#6435c9}.ui.violet.inverted.progress .bar{background-color:#a291fb}.ui.purple.progress .bar{background-color:#a333c8}.ui.purple.inverted.progress .bar{background-color:#dc73ff}.ui.pink.progress .bar{background-color:#e03997}.ui.pink.inverted.progress .bar{background-color:#ff8edf}.ui.brown.progress .bar{background-color:#a5673f}.ui.brown.inverted.progress .bar{background-color:#d67c1c}.ui.grey.progress .bar{background-color:#767676}.ui.grey.inverted.progress .bar{background-color:#dcddde}.ui.black.progress .bar{background-color:#1b1c1d}.ui.black.inverted.progress .bar{background-color:#545454}.ui.tiny.progress{font-size:.85714286rem}.ui.tiny.progress .bar{height:.5em}.ui.small.progress{font-size:.92857143rem}.ui.small.progress .bar{height:1em}.ui.progress{font-size:1rem}.ui.progress .bar{height:1.75em}.ui.large.progress{font-size:1.14285714rem}.ui.large.progress .bar{height:2.5em}.ui.big.progress{font-size:1.28571429rem}.ui.big.progress .bar{height:3.5em}/*!
+ * # Semantic UI 2.4.2 - Rating
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.rating{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;white-space:nowrap;vertical-align:baseline}.ui.rating:last-child{margin-right:0}.ui.rating .icon{padding:0;margin:0;text-align:center;font-weight:400;font-style:normal;-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;cursor:pointer;width:1.25em;height:auto;-webkit-transition:opacity .1s ease,background .1s ease,text-shadow .1s ease,color .1s ease;transition:opacity .1s ease,background .1s ease,text-shadow .1s ease,color .1s ease}.ui.rating .icon{background:0 0;color:rgba(0,0,0,.15)}.ui.rating .active.icon{background:0 0;color:rgba(0,0,0,.85)}.ui.rating .icon.selected,.ui.rating .icon.selected.active{background:0 0;color:rgba(0,0,0,.87)}.ui.star.rating .icon{width:1.25em;height:auto;background:0 0;color:rgba(0,0,0,.15);text-shadow:none}.ui.star.rating .active.icon{background:0 0!important;color:#ffe623!important;text-shadow:0 -1px 0 #ddc507,-1px 0 0 #ddc507,0 1px 0 #ddc507,1px 0 0 #ddc507!important}.ui.star.rating .icon.selected,.ui.star.rating .icon.selected.active{background:0 0!important;color:#fc0!important;text-shadow:0 -1px 0 #e6a200,-1px 0 0 #e6a200,0 1px 0 #e6a200,1px 0 0 #e6a200!important}.ui.heart.rating .icon{width:1.4em;height:auto;background:0 0;color:rgba(0,0,0,.15);text-shadow:none!important}.ui.heart.rating .active.icon{background:0 0!important;color:#ff6d75!important;text-shadow:0 -1px 0 #cd0707,-1px 0 0 #cd0707,0 1px 0 #cd0707,1px 0 0 #cd0707!important}.ui.heart.rating .icon.selected,.ui.heart.rating .icon.selected.active{background:0 0!important;color:#ff3000!important;text-shadow:0 -1px 0 #aa0101,-1px 0 0 #aa0101,0 1px 0 #aa0101,1px 0 0 #aa0101!important}.ui.disabled.rating .icon{cursor:default}.ui.rating.selected .active.icon{opacity:1}.ui.rating .icon.selected,.ui.rating.selected .icon.selected{opacity:1}.ui.mini.rating{font-size:.78571429rem}.ui.tiny.rating{font-size:.85714286rem}.ui.small.rating{font-size:.92857143rem}.ui.rating{font-size:1rem}.ui.large.rating{font-size:1.14285714rem}.ui.huge.rating{font-size:1.42857143rem}.ui.massive.rating{font-size:2rem}@font-face{font-family:Rating;src:url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMggjCBsAAAC8AAAAYGNtYXCj2pm8AAABHAAAAKRnYXNwAAAAEAAAAcAAAAAIZ2x5ZlJbXMYAAAHIAAARnGhlYWQBGAe5AAATZAAAADZoaGVhA+IB/QAAE5wAAAAkaG10eCzgAEMAABPAAAAAcGxvY2EwXCxOAAAUMAAAADptYXhwACIAnAAAFGwAAAAgbmFtZfC1n04AABSMAAABPHBvc3QAAwAAAAAVyAAAACAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADxZQHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEAJAAAAAgACAABAAAAAEAIOYF8AbwDfAj8C7wbvBw8Irwl/Cc8SPxZf/9//8AAAAAACDmAPAE8AzwI/Au8G7wcPCH8JfwnPEj8WT//f//AAH/4xoEEAYQAQ/sD+IPow+iD4wPgA98DvYOtgADAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAPAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAIAAP/tAgAB0wAKABUAAAEvAQ8BFwc3Fyc3BQc3Jz8BHwEHFycCALFPT7GAHp6eHoD/AHAWW304OH1bFnABGRqgoBp8sFNTsHyyOnxYEnFxElh8OgAAAAACAAD/7QIAAdMACgASAAABLwEPARcHNxcnNwUxER8BBxcnAgCxT0+xgB6enh6A/wA4fVsWcAEZGqCgGnywU1OwfLIBHXESWHw6AAAAAQAA/+0CAAHTAAoAAAEvAQ8BFwc3Fyc3AgCxT0+xgB6enh6AARkaoKAafLBTU7B8AAAAAAEAAAAAAgABwAArAAABFA4CBzEHDgMjIi4CLwEuAzU0PgIzMh4CFz4DMzIeAhUCAAcMEgugBgwMDAYGDAwMBqALEgwHFyg2HhAfGxkKChkbHxAeNigXAS0QHxsZCqAGCwkGBQkLBqAKGRsfEB42KBcHDBILCxIMBxcoNh4AAAAAAgAAAAACAAHAACsAWAAAATQuAiMiDgIHLgMjIg4CFRQeAhcxFx4DMzI+Aj8BPgM1DwEiFCIGMTAmIjQjJy4DNTQ+AjMyHgIfATc+AzMyHgIVFA4CBwIAFyg2HhAfGxkKChkbHxAeNigXBwwSC6AGDAwMBgYMDAwGoAsSDAdbogEBAQEBAaIGCgcEDRceEQkREA4GLy8GDhARCREeFw0EBwoGAS0eNigXBwwSCwsSDAcXKDYeEB8bGQqgBgsJBgUJCwagChkbHxA+ogEBAQGiBg4QEQkRHhcNBAcKBjQ0BgoHBA0XHhEJERAOBgABAAAAAAIAAcAAMQAAARQOAgcxBw4DIyIuAi8BLgM1ND4CMzIeAhcHFwc3Jzc+AzMyHgIVAgAHDBILoAYMDAwGBgwMDAagCxIMBxcoNh4KFRMSCC9wQLBwJwUJCgkFHjYoFwEtEB8bGQqgBgsJBgUJCwagChkbHxAeNigXAwUIBUtAoMBAOwECAQEXKDYeAAABAAAAAAIAAbcAKgAAEzQ3NjMyFxYXFhcWFzY3Njc2NzYzMhcWFRQPAQYjIi8BJicmJyYnJicmNQAkJUARExIQEAsMCgoMCxAQEhMRQCUkQbIGBwcGsgMFBQsKCQkGBwExPyMkBgYLCgkKCgoKCQoLBgYkIz8/QawFBawCBgUNDg4OFRQTAAAAAQAAAA0B2wHSACYAABM0PwI2FzYfAhYVFA8BFxQVFAcGByYvAQcGByYnJjU0PwEnJjUAEI9BBQkIBkCPEAdoGQMDBgUGgIEGBQYDAwEYaAcBIwsCFoEMAQEMgRYCCwYIZJABBQUFAwEBAkVFAgEBAwUFAwOQZAkFAAAAAAIAAAANAdsB0gAkAC4AABM0PwI2FzYfAhYVFA8BFxQVFAcmLwEHBgcmJyY1ND8BJyY1HwEHNxcnNy8BBwAQj0EFCQgGQI8QB2gZDAUGgIEGBQYDAwEYaAc/WBVsaxRXeDY2ASMLAhaBDAEBDIEWAgsGCGSQAQUNAQECRUUCAQEDBQUDA5BkCQURVXg4OHhVEW5uAAABACMAKQHdAXwAGgAANzQ/ATYXNh8BNzYXNh8BFhUUDwEGByYvASY1IwgmCAwLCFS8CAsMCCYICPUIDAsIjgjSCwkmCQEBCVS7CQEBCSYJCg0H9gcBAQePBwwAAAEAHwAfAXMBcwAsAAA3ND8BJyY1ND8BNjMyHwE3NjMyHwEWFRQPARcWFRQPAQYjIi8BBwYjIi8BJjUfCFRUCAgnCAwLCFRUCAwLCCcICFRUCAgnCAsMCFRUCAsMCCcIYgsIVFQIDAsIJwgIVFQICCcICwwIVFQICwwIJwgIVFQICCcIDAAAAAACAAAAJQFJAbcAHwArAAA3NTQ3NjsBNTQ3NjMyFxYdATMyFxYdARQHBiMhIicmNTczNTQnJiMiBwYdAQAICAsKJSY1NCYmCQsICAgIC/7tCwgIW5MWFR4fFRZApQsICDc0JiYmJjQ3CAgLpQsICAgIC8A3HhYVFRYeNwAAAQAAAAcBbgG3ACEAADcRNDc2NzYzITIXFhcWFREUBwYHBiMiLwEHBiMiJyYnJjUABgUKBgYBLAYGCgUGBgUKBQcOCn5+Cg4GBgoFBicBcAoICAMDAwMICAr+kAoICAQCCXl5CQIECAgKAAAAAwAAACUCAAFuABgAMQBKAAA3NDc2NzYzMhcWFxYVFAcGBwYjIicmJyY1MxYXFjMyNzY3JicWFRQHBiMiJyY1NDcGBzcUFxYzMjc2NTQ3NjMyNzY1NCcmIyIHBhUABihDREtLREMoBgYoQ0RLS0RDKAYlJjk5Q0M5OSYrQREmJTU1JSYRQSuEBAQGBgQEEREZBgQEBAQGJBkayQoKQSgoKChBCgoKCkEoJycoQQoKOiMjIyM6RCEeIjUmJSUmNSIeIUQlBgQEBAQGGBIRBAQGBgQEGhojAAAABQAAAAkCAAGJACwAOABRAGgAcAAANzQ3Njc2MzIXNzYzMhcWFxYXFhcWFxYVFDEGBwYPAQYjIicmNTQ3JicmJyY1MxYXNyYnJjU0NwYHNxQXFjMyNzY1NDc2MzI3NjU0JyYjIgcGFRc3Njc2NyYnNxYXFhcWFRQHBgcGBwYjPwEWFRQHBgcABitBQU0ZGhADBQEEBAUFBAUEBQEEHjw8Hg4DBQQiBQ0pIyIZBiUvSxYZDg4RQSuEBAQGBgQEEREZBgQEBAQGJBkaVxU9MzQiIDASGxkZEAYGCxQrODk/LlACFxYlyQsJQycnBRwEAgEDAwIDAwIBAwUCNmxsNhkFFAMFBBUTHh8nCQtKISgSHBsfIh4hRCUGBAQEBAYYEhEEBAYGBAQaGiPJJQUiIjYzISASGhkbCgoKChIXMRsbUZANCyghIA8AAAMAAAAAAbcB2wA5AEoAlAAANzU0NzY7ATY3Njc2NzY3Njc2MzIXFhcWFRQHMzIXFhUUBxYVFAcUFRQHFgcGKwEiJyYnJisBIicmNTcUFxYzMjc2NTQnJiMiBwYVFzMyFxYXFhcWFxYXFhcWOwEyNTQnNjc2NTQnNjU0JyYnNjc2NTQnJisBNDc2NTQnJiMGBwYHBgcGBwYHBgcGBwYHBgcGBwYrARUACwoQTgodEQ4GBAMFBgwLDxgTEwoKDjMdFhYOAgoRARkZKCUbGxsjIQZSEAoLJQUFCAcGBQUGBwgFBUkJBAUFBAQHBwMDBwcCPCUjNwIJBQUFDwMDBAkGBgsLDmUODgoJGwgDAwYFDAYQAQUGAwQGBgYFBgUGBgQJSbcPCwsGJhUPCBERExMMCgkJFBQhGxwWFR4ZFQoKFhMGBh0WKBcXBgcMDAoLDxIHBQYGBQcIBQYGBQgSAQEBAQICAQEDAgEULwgIBQoLCgsJDhQHCQkEAQ0NCg8LCxAdHREcDQ4IEBETEw0GFAEHBwUECAgFBQUFAgO3AAADAAD/2wG3AbcAPABNAJkAADc1NDc2OwEyNzY3NjsBMhcWBxUWFRQVFhUUBxYVFAcGKwEWFRQHBgcGIyInJicmJyYnJicmJyYnIyInJjU3FBcWMzI3NjU0JyYjIgcGFRczMhcWFxYXFhcWFxYXFhcWFxYXFhcWFzI3NjU0JyY1MzI3NjU0JyYjNjc2NTQnNjU0JyYnNjU0JyYrASIHIgcGBwYHBgcGIwYrARUACwoQUgYhJRsbHiAoGRkBEQoCDhYWHTMOCgoTExgPCwoFBgIBBAMFDhEdCk4QCgslBQUIBwYFBQYHCAUFSQkEBgYFBgUGBgYEAwYFARAGDAUGAwMIGwkKDg5lDgsLBgYJBAMDDwUFBQkCDg4ZJSU8AgcHAwMHBwQEBQUECbe3DwsKDAwHBhcWJwIWHQYGExYKChUZHhYVHRoiExQJCgsJDg4MDAwNBg4WJQcLCw+kBwUGBgUHCAUGBgUIpAMCBQYFBQcIBAUHBwITBwwTExERBw0OHBEdHRALCw8KDQ0FCQkHFA4JCwoLCgUICBgMCxUDAgEBAgMBAQG3AAAAAQAAAA0A7gHSABQAABM0PwI2FxEHBgcmJyY1ND8BJyY1ABCPQQUJgQYFBgMDARhoBwEjCwIWgQwB/oNFAgEBAwUFAwOQZAkFAAAAAAIAAAAAAgABtwAqAFkAABM0NzYzMhcWFxYXFhc2NzY3Njc2MzIXFhUUDwEGIyIvASYnJicmJyYnJjUzFB8BNzY1NCcmJyYnJicmIyIHBgcGBwYHBiMiJyYnJicmJyYjIgcGBwYHBgcGFQAkJUARExIQEAsMCgoMCxAQEhMRQCUkQbIGBwcGsgMFBQsKCQkGByU1pqY1BgYJCg4NDg0PDhIRDg8KCgcFCQkFBwoKDw4REg4PDQ4NDgoJBgYBMT8jJAYGCwoJCgoKCgkKCwYGJCM/P0GsBQWsAgYFDQ4ODhUUEzA1oJ82MBcSEgoLBgcCAgcHCwsKCQgHBwgJCgsLBwcCAgcGCwoSEhcAAAACAAAABwFuAbcAIQAoAAA3ETQ3Njc2MyEyFxYXFhURFAcGBwYjIi8BBwYjIicmJyY1PwEfAREhEQAGBQoGBgEsBgYKBQYGBQoFBw4Kfn4KDgYGCgUGJZIZef7cJwFwCggIAwMDAwgICv6QCggIBAIJeXkJAgQICAoIjRl0AWP+nQAAAAABAAAAJQHbAbcAMgAANzU0NzY7ATU0NzYzMhcWHQEUBwYrASInJj0BNCcmIyIHBh0BMzIXFh0BFAcGIyEiJyY1AAgIC8AmJjQ1JiUFBQgSCAUFFhUfHhUWHAsICAgIC/7tCwgIQKULCAg3NSUmJiU1SQgFBgYFCEkeFhUVFh43CAgLpQsICAgICwAAAAIAAQANAdsB0gAiAC0AABM2PwI2MzIfAhYXFg8BFxYHBiMiLwEHBiMiJyY/AScmNx8CLwE/AS8CEwEDDJBABggJBUGODgIDCmcYAgQCCAMIf4IFBgYEAgEZaQgC7hBbEgINSnkILgEBJggCFYILC4IVAggICWWPCgUFA0REAwUFCo9lCQipCTBmEw1HEhFc/u0AAAADAAAAAAHJAbcAFAAlAHkAADc1NDc2OwEyFxYdARQHBisBIicmNTcUFxYzMjc2NTQnJiMiBwYVFzU0NzYzNjc2NzY3Njc2NzY3Njc2NzY3NjMyFxYXFhcWFxYXFhUUFRQHBgcGBxQHBgcGBzMyFxYVFAcWFRYHFgcGBxYHBgcjIicmJyYnJiciJyY1AAUGB1MHBQYGBQdTBwYFJQUFCAcGBQUGBwgFBWQFBQgGDw8OFAkFBAQBAQMCAQIEBAYFBw4KCgcHBQQCAwEBAgMDAgYCAgIBAU8XEBAQBQEOBQUECwMREiYlExYXDAwWJAoHBQY3twcGBQUGB7cIBQUFBQgkBwYFBQYHCAUGBgUIJLcHBQYBEBATGQkFCQgGBQwLBgcICQUGAwMFBAcHBgYICQQEBwsLCwYGCgIDBAMCBBEQFhkSDAoVEhAREAsgFBUBBAUEBAcMAQUFCAAAAAADAAD/2wHJAZIAFAAlAHkAADcUFxYXNxY3Nj0BNCcmBycGBwYdATc0NzY3FhcWFRQHBicGJyY1FzU0NzY3Fjc2NzY3NjcXNhcWBxYXFgcWBxQHFhUUBwYHJxYXFhcWFRYXFhcWFRQVFAcGBwYHBgcGBwYnBicmJyYnJicmJyYnJicmJyYnJiciJyY1AAUGB1MHBQYGBQdTBwYFJQUFCAcGBQUGBwgFBWQGBQcKJBYMDBcWEyUmEhEDCwQFBQ4BBRAQEBdPAQECAgIGAgMDAgEBAwIEBQcHCgoOBwUGBAQCAQIDAQEEBAUJFA4PDwYIBQWlBwYFAQEBBwQJtQkEBwEBAQUGB7eTBwYEAQEEBgcJBAYBAQYECZS4BwYEAgENBwUCBgMBAQEXEyEJEhAREBcIDhAaFhEPAQEFAgQCBQELBQcKDAkIBAUHCgUGBwgDBgIEAQEHBQkIBwUMCwcECgcGCRoREQ8CBgQIAAAAAQAAAAEAAJth57dfDzz1AAsCAAAAAADP/GODAAAAAM/8Y4MAAP/bAgAB2wAAAAgAAgAAAAAAAAABAAAB4P/gAAACAAAAAAACAAABAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAdwAAAHcAAACAAAjAZMAHwFJAAABbgAAAgAAAAIAAAACAAAAAgAAAAEAAAACAAAAAW4AAAHcAAAB3AABAdwAAAHcAAAAAAAAAAoAFAAeAEoAcACKAMoBQAGIAcwCCgJUAoICxgMEAzoDpgRKBRgF7AYSBpgG2gcgB2oIGAjOAAAAAQAAABwAmgAFAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAwAAAABAAAAAAACAA4AQAABAAAAAAADAAwAIgABAAAAAAAEAAwATgABAAAAAAAFABYADAABAAAAAAAGAAYALgABAAAAAAAKADQAWgADAAEECQABAAwAAAADAAEECQACAA4AQAADAAEECQADAAwAIgADAAEECQAEAAwATgADAAEECQAFABYADAADAAEECQAGAAwANAADAAEECQAKADQAWgByAGEAdABpAG4AZwBWAGUAcgBzAGkAbwBuACAAMQAuADAAcgBhAHQAaQBuAGdyYXRpbmcAcgBhAHQAaQBuAGcAUgBlAGcAdQBsAGEAcgByAGEAdABpAG4AZwBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==) format('truetype'),url(data:application/font-woff;charset=utf-8;base64,d09GRk9UVE8AABcUAAoAAAAAFswAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAEuEAABLho6TvIE9TLzIAABPYAAAAYAAAAGAIIwgbY21hcAAAFDgAAACkAAAApKPambxnYXNwAAAU3AAAAAgAAAAIAAAAEGhlYWQAABTkAAAANgAAADYBGAe5aGhlYQAAFRwAAAAkAAAAJAPiAf1obXR4AAAVQAAAAHAAAABwLOAAQ21heHAAABWwAAAABgAAAAYAHFAAbmFtZQAAFbgAAAE8AAABPPC1n05wb3N0AAAW9AAAACAAAAAgAAMAAAEABAQAAQEBB3JhdGluZwABAgABADr4HAL4GwP4GAQeCgAZU/+Lix4KABlT/4uLDAeLZviU+HQFHQAAAP0PHQAAAQIRHQAAAAkdAAAS2BIAHQEBBw0PERQZHiMoLTI3PEFGS1BVWl9kaW5zeH2Ch4xyYXRpbmdyYXRpbmd1MHUxdTIwdUU2MDB1RTYwMXVFNjAydUU2MDN1RTYwNHVFNjA1dUYwMDR1RjAwNXVGMDA2dUYwMEN1RjAwRHVGMDIzdUYwMkV1RjA2RXVGMDcwdUYwODd1RjA4OHVGMDg5dUYwOEF1RjA5N3VGMDlDdUYxMjN1RjE2NHVGMTY1AAACAYkAGgAcAgABAAQABwAKAA0AVgCWAL0BAgGMAeQCbwLwA4cD5QR0BQMFdgZgB8MJkQtxC7oM2Q1jDggOmRAYEZr8lA78lA78lA77lA74lPetFftFpTz3NDz7NPtFcfcU+xBt+0T3Mt73Mjht90T3FPcQBfuU+0YV+wRRofcQMOP3EZ3D9wXD+wX3EXkwM6H7EPsExQUO+JT3rRX7RaU89zQ8+zT7RXH3FPsQbftE9zLe9zI4bfdE9xT3EAX7lPtGFYuLi/exw/sF9xF5MDOh+xD7BMUFDviU960V+0WlPPc0PPs0+0Vx9xT7EG37RPcy3vcyOG33RPcU9xAFDviU98EVi2B4ZG5wCIuL+zT7NAV7e3t7e4t7i3ube5sI+zT3NAVupniyi7aL3M3N3Iu2i7J4pm6mqLKetovci81JizoIDviU98EVi9xJzTqLYItkeHBucKhknmCLOotJSYs6i2CeZKhwCIuL9zT7NAWbe5t7m4ubi5ubm5sI9zT3NAWopp6yi7YIME0V+zb7NgWKioqKiouKi4qMiowI+zb3NgV6m4Ghi6OLubCwuYuji6GBm3oIule6vwWbnKGVo4u5i7Bmi12Lc4F1ensIDviU98EVi2B4ZG5wCIuL+zT7NAV7e3t7e4t7i3ube5sI+zT3NAVupniyi7aL3M3N3Iuni6WDoX4IXED3BEtL+zT3RPdU+wTLssYFl46YjZiL3IvNSYs6CA6L98UVi7WXrKOio6Otl7aLlouXiZiHl4eWhZaEloSUhZKFk4SShZKEkpKSkZOSkpGUkZaSCJaSlpGXj5iPl42Wi7aLrX+jc6N0l2qLYYthdWBgYAj7RvtABYeIh4mGi4aLh42Hjgj7RvdABYmNiY2Hj4iOhpGDlISUhZWFlIWVhpaHmYaYiZiLmAgOZ4v3txWLkpCPlo0I9yOgzPcWBY6SkI+Ri5CLkIePhAjL+xb3I3YFlomQh4uEi4aJh4aGCCMmpPsjBYuKi4mLiIuHioiJiImIiIqHi4iLh4yHjQj7FM/7FUcFh4mHioiLh4uIjImOiY6KjouPi4yLjYyOCKP3IyPwBYaQiZCLjwgOZ4v3txWLkpCPlo0I9yOgzPcWBY6SkI+Ri5CLkIePhAjL+xb3I3YFlomQh4uEi4aJh4aGCCMmpPsjBYuKi4mLiIuCh4aDi4iLh4yHjQj7FM/7FUcFh4mHioiLh4uIjImOiY6KjouPi4yLjYyOCKP3IyPwBYaQiZCLjwjKeRXjN3b7DfcAxPZSd/cN4t/7DJ1V9wFV+wEFDq73ZhWLk42RkZEIsbIFkZCRjpOLkouSiJCGCN8291D3UAWQkJKOkouTi5GIkYYIsWQFkYaNhIuEi4OJhYWFCPuJ+4kFhYWFiYOLhIuEjYaRCPsi9yIFhZCJkouSCA77AartFYuSjpKQkAjf3zffBYaQiJKLk4uSjpKQkAiysgWRkJGOk4uSi5KIkIYI3zff3wWQkJKOk4uSi5KIkIYIsmQFkIaOhIuEi4OIhIaGCDc33zcFkIaOhIuEi4OIhYaFCGRkBYaGhIiEi4OLhI6GkAg33zc3BYaGhIiEi4OLhY6FkAhksgWGkYiRi5MIDvtLi8sVi/c5BYuSjpKQkJCQko6SiwiVi4vCBYuul6mkpKSkqpiui66LqX6kcqRymG2LaAiLVJSLBZKLkoiQhpCGjoSLhAiL+zkFi4OIhYaGhoWEiYSLCPuniwWEi4SNhpGGkIiRi5MI5vdUFfcni4vCBYufhJx8mn2ZepJ3i3aLeoR9fX18g3qLdwiLVAUO+yaLshWL+AQFi5GNkY+RjpCQj5KNj42PjI+LCPfAiwWPi4+Kj4mRiZCHj4aPhY2Fi4UIi/wEBYuEiYWHhoeGhoeFiIiKhoqHi4GLhI6EkQj7EvcN+xL7DQWEhYOIgouHi4eLh42EjoaPiJCHkImRi5IIDov3XRWLko2Rj5Kltq+vuKW4pbuZvYu9i7t9uHG4ca9npWCPhI2Fi4SLhYmEh4RxYGdoXnAIXnFbflmLWYtbmF6lXqZnrnG2h5KJkouRCLCLFaRkq2yxdLF0tH+4i7iLtJexorGiq6qksm64Z61goZZ3kXaLdItnfm1ycnJybX9oiwhoi22XcqRypH6pi6+LopGglp9gdWdpbl4I9xiwFYuHjIiOiI6IjoqPi4+LjoyOjo2OjY6Lj4ubkJmXl5eWmZGbi4+LjoyOjo2OjY6LjwiLj4mOiY6IjYiNh4tzi3eCenp6eoJ3i3MIDov3XRWLko2Sj5GouK+utqW3pbqYvouci5yJnIgIm6cFjY6NjI+LjIuNi42JjYqOio+JjomOiY6KjomOiY6JjoqNioyKjomMiYuHi4qLiouLCHdnbVVjQ2NDbVV3Zwh9cgWJiIiJiIuJi36SdJiIjYmOi46LjY+UlJlvl3KcdJ90oHeie6WHkYmSi5IIsIsVqlq0Z711CKGzBXqXfpqCnoKdhp6LoIuikaCWn2B1Z2luXgj3GLAVi4eMiI6IjoiOio+Lj4uOjI6OjY6NjouPi5uQmZeXl5aZkZuLj4uOjI6OjY6NjouPCIuPiY6JjoiNiI2Hi3OLd4J6enp6gneLcwji+10VoLAFtI+wmK2hrqKnqKKvdq1wp2uhCJ2rBZ1/nHycepx6mHqWeY+EjYWLhIuEiYWHhIR/gH1+fG9qaXJmeWV5Y4Jhiwi53BXb9yQFjIKMg4uEi3CDc3x1fHV3fHOBCA6L1BWL90sFi5WPlJKSkpKTj5aLCNmLBZKPmJqepJaZlZeVlY+Qj5ONl42WjpeOmI+YkZWTk5OSk46Vi5uLmYiYhZiFlIGSfgiSfo55i3WLeYd5gXgIvosFn4uchJl8mn2Seot3i3qGfIJ9jYSLhYuEi3yIfoR+i4eLh4uHi3eGen99i3CDdnt8CHt8dYNwiwhmiwV5i3mNeY95kHeRc5N1k36Ph4sIOYsFgIuDjoSShJKHlIuVCLCdFYuGjIePiI+Hj4mQi5CLj42Pj46OjY+LkIuQiZCIjoePh42Gi4aLh4mHh4eIioaLhgjUeRWUiwWNi46Lj4qOi4+KjYqOi4+Kj4mQio6KjYqNio+Kj4mQio6KjIqzfquEpIsIrosFr4uemouri5CKkYqQkY6QkI6SjpKNkouSi5KJkoiRlZWQlouYi5CKkImRiZGJj4iOCJGMkI+PlI+UjZKLkouViJODk4SSgo+CiwgmiwWLlpCalJ6UnpCbi5aLnoiYhJSFlH+QeYuGhoeDiYCJf4h/h3+IfoWBg4KHh4SCgH4Ii4qIiYiGh4aIh4mIiIiIh4eGh4aHh4eHiIiHiIeHiIiHiIeKh4mIioiLCIKLi/tLBQ6L90sVi/dLBYuVj5OSk5KSk46WiwjdiwWPi5iPoZOkk6CRnZCdj56Nn4sIq4sFpougg5x8m3yTd4txCIuJBZd8kHuLd4uHi4eLh5J+jn6LfIuEi4SJhZR9kHyLeot3hHp8fH19eoR3iwhYiwWVeI95i3mLdIh6hH6EfoKBfoV+hX2He4uBi4OPg5KFkYaTh5SHlYiTipOKk4qTiJMIiZSIkYiPgZSBl4CaeKR+moSPCD2LBYCLg4+EkoSSh5SLlQiw9zgVi4aMh4+Ij4ePiZCLkIuPjY+Pjo6Nj4uQi5CJkIiOh4+HjYaLhouHiYeHh4iKhouGCNT7OBWUiwWOi46Kj4mPio+IjoiPh4+IjoePiI+Hj4aPho6HjoiNiI6Hj4aOho6Ii4qWfpKDj4YIk4ORgY5+j36OgI1/jYCPg5CGnYuXj5GUkpSOmYuei5aGmoKfgp6GmouWCPCLBZSLlI+SkpOTjpOLlYuSiZKHlIeUho+Fi46PjY+NkY2RjJCLkIuYhpaBlY6RjZKLkgiLkomSiJKIkoaQhY6MkIyRi5CLm4aXgpOBkn6Pe4sIZosFcotrhGN9iouIioaJh4qHiomKiYqIioaKh4mHioiKiYuHioiLh4qIi4mLCIKLi/tLBQ77lIv3txWLkpCPlo0I9yOgzPcWBY6SkI+RiwiL/BL7FUcFh4mHioiLh4uIjImOiY6KjouPi4yLjYyOCKP3IyPwBYaQiZCLjwgOi/fFFYu1l6yjoqOjrZe2i5aLl4mYh5eHloWWhJaElIWShZOEkoWShJKSkpGTkpKRlJGWkgiWkpaRl4+Yj5eNlou2i61/o3OjdJdqi2GLYXVgYGAI+0b7QAWHiIeJhouGi4eNh44I+0b3QAWJjYmNh4+IjoaRg5SElIWVhZSFlYaWh5mGmImYi5gIsIsVi2ucaa9oCPc6+zT3OvczBa+vnK2Lq4ubiZiHl4eXhpSFkoSSg5GCj4KQgo2CjYONgYuBi4KLgIl/hoCGgIWChAiBg4OFhISEhYaFhoaIhoaJhYuFi4aNiJCGkIaRhJGEkoORgZOCkoCRgJB/kICNgosIgYuBi4OJgomCiYKGgoeDhYSEhYSGgod/h3+Jfot7CA77JouyFYv4BAWLkY2Rj5GOkJCPko2PjY+Mj4sI98CLBY+Lj4qPiZGJkIePho+FjYWLhQiL/AQFi4SJhYeGh4aGh4WIiIqGioeLgYuEjoSRCPsS9w37EvsNBYSFg4iCi4eLh4uHjYSOho+IkIeQiZGLkgiwkxX3JvchpHL3DfsIi/f3+7iLi/v3BQ5ni8sVi/c5BYuSjpKQkJCQko6Siwj3VIuLwgWLrpippKSkpKmYrouvi6l+pHKkcpdti2gIi0IFi4aKhoeIh4eHiYaLCHmLBYaLh42Hj4eOipCLkAiL1AWLn4OcfZp9mXqSdot3i3qEfX18fIR6i3cIi1SniwWSi5KIkIaQho6Ei4QIi/s5BYuDiIWGhoaFhImEiwj7p4sFhIuEjYaRhpCIkYuTCA5njPe6FYyQkI6UjQj3I6DM9xYFj5KPj5GLkIuQh4+ECMv7FvcjdgWUiZCIjYaNhoiFhYUIIyak+yMFjIWKhomHiYiIiYaLiIuHjIeNCPsUz/sVRwWHiYeKiIuHi4eNiY6Jj4uQjJEIo/cjI/AFhZGJkY2QCPeB+z0VnILlW3rxiJ6ZmNTS+wydgpxe54v7pwUOZ4vCFYv3SwWLkI2Pjo+Pjo+NkIsI3osFkIuPiY6Ij4eNh4uGCIv7SwWLhomHh4eIh4eKhosIOIsFhouHjIePiI+Jj4uQCLCvFYuGjIePh46IkImQi5CLj42Pjo6PjY+LkIuQiZCIjoePh42Gi4aLhomIh4eIioaLhgjvZxWL90sFi5CNj46Oj4+PjZCLj4ySkJWWlZaVl5SXmJuVl5GRjo6OkI6RjZCNkIyPjI6MkY2TCIySjJGMj4yPjZCOkY6RjpCPjo6Pj42Qi5SLk4qSiZKJkYiPiJCIjoiPho6GjYeMhwiNh4yGjIaMhYuHi4iLiIuHi4eLg4uEiYSJhImFiYeJh4mFh4WLioqJiomJiIqJiokIi4qKiIqJCNqLBZqLmIWWgJaAkH+LfIt6hn2Af46DjYSLhIt9h36Cf4+Bi3+HgImAhYKEhI12hnmAfgh/fXiDcosIZosFfot+jHyOfI5/joOOg41/j32Qc5N8j4SMhouHjYiOh4+Jj4uQCA5ni/c5FYuGjYaOiI+Hj4mQiwjeiwWQi4+Njo+Pjo2Qi5AIi/dKBYuQiZCHjoiPh42Giwg4iwWGi4eJh4eIiImGi4YIi/tKBbD3JhWLkIyPj4+OjpCNkIuQi4+Jj4iOh42Hi4aLhomHiIeHh4eKhouGi4aMiI+Hj4qPi5AI7/snFYv3SwWLkI2Qj46Oj4+NkIuSi5qPo5OZkJePk46TjZeOmo6ajpiMmIsIsIsFpIueg5d9ln6Qeol1koSRgo2Aj4CLgIeAlH+Pfot9i4WJhIiCloCQfIt7i3yFfoGACICAfoZ8iwg8iwWMiIyJi4mMiYyJjYmMiIyKi4mPhI2GjYeNh42GjYOMhIyEi4SLhouHi4iLiYuGioYIioWKhomHioeJh4iGh4eIh4aIh4iFiISJhImDioKLhouHjYiPh4+Ij4iRiJGJkIqPCIqPipGKkomTipGKj4qOiZCJkYiQiJCIjoWSgZZ+nIKXgZaBloGWhJGHi4aLh42HjwiIjomQi48IDviUFPiUFYsMCgAAAAADAgABkAAFAAABTAFmAAAARwFMAWYAAAD1ABkAhAAAAAAAAAAAAAAAAAAAAAEQAAAAAAAAAAAAAAAAAAAAAEAAAPFlAeD/4P/gAeAAIAAAAAEAAAAAAAAAAAAAACAAAAAAAAIAAAADAAAAFAADAAEAAAAUAAQAkAAAACAAIAAEAAAAAQAg5gXwBvAN8CPwLvBu8HDwivCX8JzxI/Fl//3//wAAAAAAIOYA8ATwDPAj8C7wbvBw8Ifwl/Cc8SPxZP/9//8AAf/jGgQQBhABD+wP4g+jD6IPjA+AD3wO9g62AAMAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAEAAJrVlLJfDzz1AAsCAAAAAADP/GODAAAAAM/8Y4MAAP/bAgAB2wAAAAgAAgAAAAAAAAABAAAB4P/gAAACAAAAAAACAAABAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAdwAAAHcAAACAAAjAZMAHwFJAAABbgAAAgAAAAIAAAACAAAAAgAAAAEAAAACAAAAAW4AAAHcAAAB3AABAdwAAAHcAAAAAFAAABwAAAAAAA4ArgABAAAAAAABAAwAAAABAAAAAAACAA4AQAABAAAAAAADAAwAIgABAAAAAAAEAAwATgABAAAAAAAFABYADAABAAAAAAAGAAYALgABAAAAAAAKADQAWgADAAEECQABAAwAAAADAAEECQACAA4AQAADAAEECQADAAwAIgADAAEECQAEAAwATgADAAEECQAFABYADAADAAEECQAGAAwANAADAAEECQAKADQAWgByAGEAdABpAG4AZwBWAGUAcgBzAGkAbwBuACAAMQAuADAAcgBhAHQAaQBuAGdyYXRpbmcAcgBhAHQAaQBuAGcAUgBlAGcAdQBsAGEAcgByAGEAdABpAG4AZwBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==) format('woff');font-weight:400;font-style:normal}.ui.rating .icon{font-family:Rating;line-height:1;-webkit-backface-visibility:hidden;backface-visibility:hidden;font-weight:400;font-style:normal;text-align:center}.ui.rating .icon:before{content:'\f005'}.ui.rating .active.icon:before{content:'\f005'}.ui.star.rating .icon:before{content:'\f005'}.ui.star.rating .active.icon:before{content:'\f005'}.ui.star.rating .partial.icon:before{content:'\f006'}.ui.star.rating .partial.icon{content:'\f005'}.ui.heart.rating .icon:before{content:'\f004'}.ui.heart.rating .active.icon:before{content:'\f004'}/*!
+ * # Semantic UI 2.4.2 - Search
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.search{position:relative}.ui.search>.prompt{margin:0;outline:0;-webkit-appearance:none;-webkit-tap-highlight-color:rgba(255,255,255,0);text-shadow:none;font-style:normal;font-weight:400;line-height:1.21428571em;padding:.67857143em 1em;font-size:1em;background:#fff;border:1px solid rgba(34,36,38,.15);color:rgba(0,0,0,.87);-webkit-box-shadow:0 0 0 0 transparent inset;box-shadow:0 0 0 0 transparent inset;-webkit-transition:background-color .1s ease,color .1s ease,border-color .1s ease,-webkit-box-shadow .1s ease;transition:background-color .1s ease,color .1s ease,border-color .1s ease,-webkit-box-shadow .1s ease;transition:background-color .1s ease,color .1s ease,box-shadow .1s ease,border-color .1s ease;transition:background-color .1s ease,color .1s ease,box-shadow .1s ease,border-color .1s ease,-webkit-box-shadow .1s ease}.ui.search .prompt{border-radius:500rem}.ui.search .prompt~.search.icon{cursor:pointer}.ui.search>.results{display:none;position:absolute;top:100%;left:0;-webkit-transform-origin:center top;transform-origin:center top;white-space:normal;text-align:left;text-transform:none;background:#fff;margin-top:.5em;width:18em;border-radius:.28571429rem;-webkit-box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15);box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15);border:1px solid #d4d4d5;z-index:998}.ui.search>.results>:first-child{border-radius:.28571429rem .28571429rem 0 0}.ui.search>.results>:last-child{border-radius:0 0 .28571429rem .28571429rem}.ui.search>.results .result{cursor:pointer;display:block;overflow:hidden;font-size:1em;padding:.85714286em 1.14285714em;color:rgba(0,0,0,.87);line-height:1.33;border-bottom:1px solid rgba(34,36,38,.1)}.ui.search>.results .result:last-child{border-bottom:none!important}.ui.search>.results .result .image{float:right;overflow:hidden;background:0 0;width:5em;height:3em;border-radius:.25em}.ui.search>.results .result .image img{display:block;width:auto;height:100%}.ui.search>.results .result .image+.content{margin:0 6em 0 0}.ui.search>.results .result .title{margin:-.14285714em 0 0;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-weight:700;font-size:1em;color:rgba(0,0,0,.85)}.ui.search>.results .result .description{margin-top:0;font-size:.92857143em;color:rgba(0,0,0,.4)}.ui.search>.results .result .price{float:right;color:#21ba45}.ui.search>.results>.message{padding:1em 1em}.ui.search>.results>.message .header{font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-size:1rem;font-weight:700;color:rgba(0,0,0,.87)}.ui.search>.results>.message .description{margin-top:.25rem;font-size:1em;color:rgba(0,0,0,.87)}.ui.search>.results>.action{display:block;border-top:none;background:#f3f4f5;padding:.92857143em 1em;color:rgba(0,0,0,.87);font-weight:700;text-align:center}.ui.search>.prompt:focus{border-color:rgba(34,36,38,.35);background:#fff;color:rgba(0,0,0,.95)}.ui.loading.search .input>i.icon:before{position:absolute;content:'';top:50%;left:50%;margin:-.64285714em 0 0 -.64285714em;width:1.28571429em;height:1.28571429em;border-radius:500rem;border:.2em solid rgba(0,0,0,.1)}.ui.loading.search .input>i.icon:after{position:absolute;content:'';top:50%;left:50%;margin:-.64285714em 0 0 -.64285714em;width:1.28571429em;height:1.28571429em;-webkit-animation:button-spin .6s linear;animation:button-spin .6s linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;border-radius:500rem;border-color:#767676 transparent transparent;border-style:solid;border-width:.2em;-webkit-box-shadow:0 0 0 1px transparent;box-shadow:0 0 0 1px transparent}.ui.category.search>.results .category .result:hover,.ui.search>.results .result:hover{background:#f9fafb}.ui.search .action:hover{background:#e0e0e0}.ui.category.search>.results .category.active{background:#f3f4f5}.ui.category.search>.results .category.active>.name{color:rgba(0,0,0,.87)}.ui.category.search>.results .category .result.active,.ui.search>.results .result.active{position:relative;border-left-color:rgba(34,36,38,.1);background:#f3f4f5;-webkit-box-shadow:none;box-shadow:none}.ui.search>.results .result.active .title{color:rgba(0,0,0,.85)}.ui.search>.results .result.active .description{color:rgba(0,0,0,.85)}.ui.disabled.search{cursor:default;pointer-events:none;opacity:.45}.ui.search.selection .prompt{border-radius:.28571429rem}.ui.search.selection>.icon.input>.remove.icon{pointer-events:none;position:absolute;left:auto;opacity:0;color:'';top:0;right:0;-webkit-transition:color .1s ease,opacity .1s ease;transition:color .1s ease,opacity .1s ease}.ui.search.selection>.icon.input>.active.remove.icon{cursor:pointer;opacity:.8;pointer-events:auto}.ui.search.selection>.icon.input:not([class*="left icon"])>.icon~.remove.icon{right:1.85714em}.ui.search.selection>.icon.input>.remove.icon:hover{opacity:1;color:#db2828}.ui.category.search .results{width:28em}.ui.category.search .results.animating,.ui.category.search .results.visible{display:table}.ui.category.search>.results .category{display:table-row;background:#f3f4f5;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:background .1s ease,border-color .1s ease;transition:background .1s ease,border-color .1s ease}.ui.category.search>.results .category:last-child{border-bottom:none}.ui.category.search>.results .category:first-child .name+.result{border-radius:0 .28571429rem 0 0}.ui.category.search>.results .category:last-child .result:last-child{border-radius:0 0 .28571429rem 0}.ui.category.search>.results .category>.name{display:table-cell;text-overflow:ellipsis;width:100px;white-space:nowrap;background:0 0;font-family:Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;font-size:1em;padding:.4em 1em;font-weight:700;color:rgba(0,0,0,.4);border-bottom:1px solid rgba(34,36,38,.1)}.ui.category.search>.results .category .results{display:table-cell;background:#fff;border-left:1px solid rgba(34,36,38,.15);border-bottom:1px solid rgba(34,36,38,.1)}.ui.category.search>.results .category .result{border-bottom:1px solid rgba(34,36,38,.1);-webkit-transition:background .1s ease,border-color .1s ease;transition:background .1s ease,border-color .1s ease;padding:.85714286em 1.14285714em}.ui[class*="left aligned"].search>.results{right:auto;left:0}.ui[class*="right aligned"].search>.results{right:0;left:auto}.ui.fluid.search .results{width:100%}.ui.mini.search{font-size:.78571429em}.ui.small.search{font-size:.92857143em}.ui.search{font-size:1em}.ui.large.search{font-size:1.14285714em}.ui.big.search{font-size:1.28571429em}.ui.huge.search{font-size:1.42857143em}.ui.massive.search{font-size:1.71428571em}@media only screen and (max-width:767px){.ui.search .results{max-width:calc(100vw - 2rem)}}/*!
+ * # Semantic UI 2.4.2 - Shape
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.shape{position:relative;vertical-align:top;display:inline-block;-webkit-perspective:2000px;perspective:2000px;-webkit-transition:left .6s ease-in-out,width .6s ease-in-out,height .6s ease-in-out,-webkit-transform .6s ease-in-out;transition:left .6s ease-in-out,width .6s ease-in-out,height .6s ease-in-out,-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out,left .6s ease-in-out,width .6s ease-in-out,height .6s ease-in-out;transition:transform .6s ease-in-out,left .6s ease-in-out,width .6s ease-in-out,height .6s ease-in-out,-webkit-transform .6s ease-in-out}.ui.shape .sides{-webkit-transform-style:preserve-3d;transform-style:preserve-3d}.ui.shape .side{opacity:1;width:100%;margin:0!important;-webkit-backface-visibility:hidden;backface-visibility:hidden}.ui.shape .side{display:none}.ui.shape .side *{-webkit-backface-visibility:visible!important;backface-visibility:visible!important}.ui.cube.shape .side{min-width:15em;height:15em;padding:2em;background-color:#e6e6e6;color:rgba(0,0,0,.87);-webkit-box-shadow:0 0 2px rgba(0,0,0,.3);box-shadow:0 0 2px rgba(0,0,0,.3)}.ui.cube.shape .side>.content{width:100%;height:100%;display:table;text-align:center;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.ui.cube.shape .side>.content>div{display:table-cell;vertical-align:middle;font-size:2em}.ui.text.shape.animating .sides{position:static}.ui.text.shape .side{white-space:nowrap}.ui.text.shape .side>*{white-space:normal}.ui.loading.shape{position:absolute;top:-9999px;left:-9999px}.ui.shape .animating.side{position:absolute;top:0;left:0;display:block;z-index:100}.ui.shape .hidden.side{opacity:.6}.ui.shape.animating .sides{position:absolute}.ui.shape.animating .sides{-webkit-transition:left .6s ease-in-out,width .6s ease-in-out,height .6s ease-in-out,-webkit-transform .6s ease-in-out;transition:left .6s ease-in-out,width .6s ease-in-out,height .6s ease-in-out,-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out,left .6s ease-in-out,width .6s ease-in-out,height .6s ease-in-out;transition:transform .6s ease-in-out,left .6s ease-in-out,width .6s ease-in-out,height .6s ease-in-out,-webkit-transform .6s ease-in-out}.ui.shape.animating .side{-webkit-transition:opacity .6s ease-in-out;transition:opacity .6s ease-in-out}.ui.shape .active.side{display:block}/*!
+ * # Semantic UI 2.4.2 - Sidebar
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.sidebar{position:fixed;top:0;left:0;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transition:none;transition:none;will-change:transform;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);visibility:hidden;-webkit-overflow-scrolling:touch;height:100%!important;max-height:100%;border-radius:0!important;margin:0!important;overflow-y:auto!important;z-index:102}.ui.sidebar>*{-webkit-backface-visibility:hidden;backface-visibility:hidden}.ui.left.sidebar{right:auto;left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.ui.right.sidebar{right:0!important;left:auto!important;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.ui.bottom.sidebar,.ui.top.sidebar{width:100%!important;height:auto!important}.ui.top.sidebar{top:0!important;bottom:auto!important;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}.ui.bottom.sidebar{top:auto!important;bottom:0!important;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}.pushable{height:100%;overflow-x:hidden;padding:0!important}body.pushable{background:#545454!important}.pushable:not(body){-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.pushable:not(body)>.fixed,.pushable:not(body)>.pusher:after,.pushable:not(body)>.ui.sidebar{position:absolute}.pushable>.fixed{position:fixed;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease;will-change:transform;z-index:101}.pushable>.pusher{position:relative;-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden;min-height:100%;-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease;z-index:2}body.pushable>.pusher{background:#fff}.pushable>.pusher{background:inherit}.pushable>.pusher:after{position:fixed;top:0;right:0;content:'';background-color:rgba(0,0,0,.4);overflow:hidden;opacity:0;-webkit-transition:opacity .5s;transition:opacity .5s;will-change:opacity;z-index:1000}.ui.sidebar.menu .item{border-radius:0!important}.pushable>.pusher.dimmed:after{width:100%!important;height:100%!important;opacity:1!important}.ui.animating.sidebar{visibility:visible}.ui.visible.sidebar{visibility:visible;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.left.visible.sidebar,.ui.right.visible.sidebar{-webkit-box-shadow:0 0 20px rgba(34,36,38,.15);box-shadow:0 0 20px rgba(34,36,38,.15)}.ui.bottom.visible.sidebar,.ui.top.visible.sidebar{-webkit-box-shadow:0 0 20px rgba(34,36,38,.15);box-shadow:0 0 20px rgba(34,36,38,.15)}.ui.visible.left.sidebar~.fixed,.ui.visible.left.sidebar~.pusher{-webkit-transform:translate3d(260px,0,0);transform:translate3d(260px,0,0)}.ui.visible.right.sidebar~.fixed,.ui.visible.right.sidebar~.pusher{-webkit-transform:translate3d(-260px,0,0);transform:translate3d(-260px,0,0)}.ui.visible.top.sidebar~.fixed,.ui.visible.top.sidebar~.pusher{-webkit-transform:translate3d(0,36px,0);transform:translate3d(0,36px,0)}.ui.visible.bottom.sidebar~.fixed,.ui.visible.bottom.sidebar~.pusher{-webkit-transform:translate3d(0,-36px,0);transform:translate3d(0,-36px,0)}.ui.visible.left.sidebar~.ui.visible.right.sidebar~.fixed,.ui.visible.left.sidebar~.ui.visible.right.sidebar~.pusher,.ui.visible.right.sidebar~.ui.visible.left.sidebar~.fixed,.ui.visible.right.sidebar~.ui.visible.left.sidebar~.pusher{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.thin.left.sidebar,.ui.thin.right.sidebar{width:150px}.ui[class*="very thin"].left.sidebar,.ui[class*="very thin"].right.sidebar{width:60px}.ui.left.sidebar,.ui.right.sidebar{width:260px}.ui.wide.left.sidebar,.ui.wide.right.sidebar{width:350px}.ui[class*="very wide"].left.sidebar,.ui[class*="very wide"].right.sidebar{width:475px}.ui.visible.thin.left.sidebar~.fixed,.ui.visible.thin.left.sidebar~.pusher{-webkit-transform:translate3d(150px,0,0);transform:translate3d(150px,0,0)}.ui.visible[class*="very thin"].left.sidebar~.fixed,.ui.visible[class*="very thin"].left.sidebar~.pusher{-webkit-transform:translate3d(60px,0,0);transform:translate3d(60px,0,0)}.ui.visible.wide.left.sidebar~.fixed,.ui.visible.wide.left.sidebar~.pusher{-webkit-transform:translate3d(350px,0,0);transform:translate3d(350px,0,0)}.ui.visible[class*="very wide"].left.sidebar~.fixed,.ui.visible[class*="very wide"].left.sidebar~.pusher{-webkit-transform:translate3d(475px,0,0);transform:translate3d(475px,0,0)}.ui.visible.thin.right.sidebar~.fixed,.ui.visible.thin.right.sidebar~.pusher{-webkit-transform:translate3d(-150px,0,0);transform:translate3d(-150px,0,0)}.ui.visible[class*="very thin"].right.sidebar~.fixed,.ui.visible[class*="very thin"].right.sidebar~.pusher{-webkit-transform:translate3d(-60px,0,0);transform:translate3d(-60px,0,0)}.ui.visible.wide.right.sidebar~.fixed,.ui.visible.wide.right.sidebar~.pusher{-webkit-transform:translate3d(-350px,0,0);transform:translate3d(-350px,0,0)}.ui.visible[class*="very wide"].right.sidebar~.fixed,.ui.visible[class*="very wide"].right.sidebar~.pusher{-webkit-transform:translate3d(-475px,0,0);transform:translate3d(-475px,0,0)}.ui.overlay.sidebar{z-index:102}.ui.left.overlay.sidebar{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.ui.right.overlay.sidebar{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.ui.top.overlay.sidebar{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}.ui.bottom.overlay.sidebar{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}.animating.ui.overlay.sidebar,.ui.visible.overlay.sidebar{-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease}.ui.visible.left.overlay.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.visible.right.overlay.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.visible.top.overlay.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.visible.bottom.overlay.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.visible.overlay.sidebar~.fixed,.ui.visible.overlay.sidebar~.pusher{-webkit-transform:none!important;transform:none!important}.ui.push.sidebar{-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease;z-index:102}.ui.left.push.sidebar{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.ui.right.push.sidebar{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.ui.top.push.sidebar{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}.ui.bottom.push.sidebar{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}.ui.visible.push.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.uncover.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);z-index:1}.ui.visible.uncover.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease}.ui.slide.along.sidebar{z-index:1}.ui.left.slide.along.sidebar{-webkit-transform:translate3d(-50%,0,0);transform:translate3d(-50%,0,0)}.ui.right.slide.along.sidebar{-webkit-transform:translate3d(50%,0,0);transform:translate3d(50%,0,0)}.ui.top.slide.along.sidebar{-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0)}.ui.bottom.slide.along.sidebar{-webkit-transform:translate3d(0,50%,0);transform:translate3d(0,50%,0)}.ui.animating.slide.along.sidebar{-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease}.ui.visible.slide.along.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.slide.out.sidebar{z-index:1}.ui.left.slide.out.sidebar{-webkit-transform:translate3d(50%,0,0);transform:translate3d(50%,0,0)}.ui.right.slide.out.sidebar{-webkit-transform:translate3d(-50%,0,0);transform:translate3d(-50%,0,0)}.ui.top.slide.out.sidebar{-webkit-transform:translate3d(0,50%,0);transform:translate3d(0,50%,0)}.ui.bottom.slide.out.sidebar{-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0)}.ui.animating.slide.out.sidebar{-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease}.ui.visible.slide.out.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.scale.down.sidebar{-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease;z-index:102}.ui.left.scale.down.sidebar{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.ui.right.scale.down.sidebar{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.ui.top.scale.down.sidebar{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}.ui.bottom.scale.down.sidebar{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}.ui.scale.down.left.sidebar~.pusher{-webkit-transform-origin:75% 50%;transform-origin:75% 50%}.ui.scale.down.right.sidebar~.pusher{-webkit-transform-origin:25% 50%;transform-origin:25% 50%}.ui.scale.down.top.sidebar~.pusher{-webkit-transform-origin:50% 75%;transform-origin:50% 75%}.ui.scale.down.bottom.sidebar~.pusher{-webkit-transform-origin:50% 25%;transform-origin:50% 25%}.ui.animating.scale.down>.visible.ui.sidebar{-webkit-transition:-webkit-transform .5s ease;transition:-webkit-transform .5s ease;transition:transform .5s ease;transition:transform .5s ease,-webkit-transform .5s ease}.ui.animating.scale.down.sidebar~.pusher,.ui.visible.scale.down.sidebar~.pusher{display:block!important;width:100%;height:100%;overflow:hidden!important}.ui.visible.scale.down.sidebar{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.ui.visible.scale.down.sidebar~.pusher{-webkit-transform:scale(.75);transform:scale(.75)}/*!
+ * # Semantic UI 2.4.2 - Sticky
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.sticky{position:static;-webkit-transition:none;transition:none;z-index:800}.ui.sticky.bound{position:absolute;left:auto;right:auto}.ui.sticky.fixed{position:fixed;left:auto;right:auto}.ui.sticky.bound.top,.ui.sticky.fixed.top{top:0;bottom:auto}.ui.sticky.bound.bottom,.ui.sticky.fixed.bottom{top:auto;bottom:0}.ui.native.sticky{position:-webkit-sticky;position:-moz-sticky;position:-ms-sticky;position:-o-sticky;position:sticky}/*!
+ * # Semantic UI 2.4.2 - Tab
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.ui.tab{display:none}.ui.tab.active,.ui.tab.open{display:block}.ui.tab.loading{position:relative;overflow:hidden;display:block;min-height:250px}.ui.tab.loading *{position:relative!important;left:-10000px!important}.ui.tab.loading.segment:before,.ui.tab.loading:before{position:absolute;content:'';top:100px;left:50%;margin:-1.25em 0 0 -1.25em;width:2.5em;height:2.5em;border-radius:500rem;border:.2em solid rgba(0,0,0,.1)}.ui.tab.loading.segment:after,.ui.tab.loading:after{position:absolute;content:'';top:100px;left:50%;margin:-1.25em 0 0 -1.25em;width:2.5em;height:2.5em;-webkit-animation:button-spin .6s linear;animation:button-spin .6s linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;border-radius:500rem;border-color:#767676 transparent transparent;border-style:solid;border-width:.2em;-webkit-box-shadow:0 0 0 1px transparent;box-shadow:0 0 0 1px transparent}/*!
+ * # Semantic UI 2.4.2 - Transition
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */.transition{-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animating.transition{-webkit-backface-visibility:hidden;backface-visibility:hidden;visibility:visible!important}.loading.transition{position:absolute;top:-99999px;left:-99999px}.hidden.transition{display:none;visibility:hidden}.visible.transition{display:block!important;visibility:visible!important}.disabled.transition{-webkit-animation-play-state:paused;animation-play-state:paused}.looping.transition{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.transition.browse{-webkit-animation-duration:.5s;animation-duration:.5s}.transition.browse.in{-webkit-animation-name:browseIn;animation-name:browseIn}.transition.browse.left.out,.transition.browse.out{-webkit-animation-name:browseOutLeft;animation-name:browseOutLeft}.transition.browse.right.out{-webkit-animation-name:browseOutRight;animation-name:browseOutRight}@-webkit-keyframes browseIn{0%{-webkit-transform:scale(.8) translateZ(0);transform:scale(.8) translateZ(0);z-index:-1}10%{-webkit-transform:scale(.8) translateZ(0);transform:scale(.8) translateZ(0);z-index:-1;opacity:.7}80%{-webkit-transform:scale(1.05) translateZ(0);transform:scale(1.05) translateZ(0);opacity:1;z-index:999}100%{-webkit-transform:scale(1) translateZ(0);transform:scale(1) translateZ(0);z-index:999}}@keyframes browseIn{0%{-webkit-transform:scale(.8) translateZ(0);transform:scale(.8) translateZ(0);z-index:-1}10%{-webkit-transform:scale(.8) translateZ(0);transform:scale(.8) translateZ(0);z-index:-1;opacity:.7}80%{-webkit-transform:scale(1.05) translateZ(0);transform:scale(1.05) translateZ(0);opacity:1;z-index:999}100%{-webkit-transform:scale(1) translateZ(0);transform:scale(1) translateZ(0);z-index:999}}@-webkit-keyframes browseOutLeft{0%{z-index:999;-webkit-transform:translateX(0) rotateY(0) rotateX(0);transform:translateX(0) rotateY(0) rotateX(0)}50%{z-index:-1;-webkit-transform:translateX(-105%) rotateY(35deg) rotateX(10deg) translateZ(-10px);transform:translateX(-105%) rotateY(35deg) rotateX(10deg) translateZ(-10px)}80%{opacity:1}100%{z-index:-1;-webkit-transform:translateX(0) rotateY(0) rotateX(0) translateZ(-10px);transform:translateX(0) rotateY(0) rotateX(0) translateZ(-10px);opacity:0}}@keyframes browseOutLeft{0%{z-index:999;-webkit-transform:translateX(0) rotateY(0) rotateX(0);transform:translateX(0) rotateY(0) rotateX(0)}50%{z-index:-1;-webkit-transform:translateX(-105%) rotateY(35deg) rotateX(10deg) translateZ(-10px);transform:translateX(-105%) rotateY(35deg) rotateX(10deg) translateZ(-10px)}80%{opacity:1}100%{z-index:-1;-webkit-transform:translateX(0) rotateY(0) rotateX(0) translateZ(-10px);transform:translateX(0) rotateY(0) rotateX(0) translateZ(-10px);opacity:0}}@-webkit-keyframes browseOutRight{0%{z-index:999;-webkit-transform:translateX(0) rotateY(0) rotateX(0);transform:translateX(0) rotateY(0) rotateX(0)}50%{z-index:1;-webkit-transform:translateX(105%) rotateY(35deg) rotateX(10deg) translateZ(-10px);transform:translateX(105%) rotateY(35deg) rotateX(10deg) translateZ(-10px)}80%{opacity:1}100%{z-index:1;-webkit-transform:translateX(0) rotateY(0) rotateX(0) translateZ(-10px);transform:translateX(0) rotateY(0) rotateX(0) translateZ(-10px);opacity:0}}@keyframes browseOutRight{0%{z-index:999;-webkit-transform:translateX(0) rotateY(0) rotateX(0);transform:translateX(0) rotateY(0) rotateX(0)}50%{z-index:1;-webkit-transform:translateX(105%) rotateY(35deg) rotateX(10deg) translateZ(-10px);transform:translateX(105%) rotateY(35deg) rotateX(10deg) translateZ(-10px)}80%{opacity:1}100%{z-index:1;-webkit-transform:translateX(0) rotateY(0) rotateX(0) translateZ(-10px);transform:translateX(0) rotateY(0) rotateX(0) translateZ(-10px);opacity:0}}.drop.transition{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-duration:.4s;animation-duration:.4s;-webkit-animation-timing-function:cubic-bezier(.34,1.61,.7,1);animation-timing-function:cubic-bezier(.34,1.61,.7,1)}.drop.transition.in{-webkit-animation-name:dropIn;animation-name:dropIn}.drop.transition.out{-webkit-animation-name:dropOut;animation-name:dropOut}@-webkit-keyframes dropIn{0%{opacity:0;-webkit-transform:scale(0);transform:scale(0)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes dropIn{0%{opacity:0;-webkit-transform:scale(0);transform:scale(0)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes dropOut{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:0;-webkit-transform:scale(0);transform:scale(0)}}@keyframes dropOut{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:0;-webkit-transform:scale(0);transform:scale(0)}}.transition.fade.in{-webkit-animation-name:fadeIn;animation-name:fadeIn}.transition[class*="fade up"].in{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}.transition[class*="fade down"].in{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}.transition[class*="fade left"].in{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}.transition[class*="fade right"].in{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}.transition.fade.out{-webkit-animation-name:fadeOut;animation-name:fadeOut}.transition[class*="fade up"].out{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}.transition[class*="fade down"].out{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}.transition[class*="fade left"].out{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}.transition[class*="fade right"].out{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translateY(10%);transform:translateY(10%)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translateY(10%);transform:translateY(10%)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translateY(-10%);transform:translateY(-10%)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translateY(-10%);transform:translateY(-10%)}100%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translateX(10%);transform:translateX(10%)}100%{opacity:1;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translateX(10%);transform:translateX(10%)}100%{opacity:1;-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translateX(-10%);transform:translateX(-10%)}100%{opacity:1;-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translateX(-10%);transform:translateX(-10%)}100%{opacity:1;-webkit-transform:translateX(0);transform:translateX(0)}}@-webkit-keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@-webkit-keyframes fadeOutUp{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(5%);transform:translateY(5%)}}@keyframes fadeOutUp{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(5%);transform:translateY(5%)}}@-webkit-keyframes fadeOutDown{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(-5%);transform:translateY(-5%)}}@keyframes fadeOutDown{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(-5%);transform:translateY(-5%)}}@-webkit-keyframes fadeOutLeft{0%{opacity:1;-webkit-transform:translateX(0);transform:translateX(0)}100%{opacity:0;-webkit-transform:translateX(5%);transform:translateX(5%)}}@keyframes fadeOutLeft{0%{opacity:1;-webkit-transform:translateX(0);transform:translateX(0)}100%{opacity:0;-webkit-transform:translateX(5%);transform:translateX(5%)}}@-webkit-keyframes fadeOutRight{0%{opacity:1;-webkit-transform:translateX(0);transform:translateX(0)}100%{opacity:0;-webkit-transform:translateX(-5%);transform:translateX(-5%)}}@keyframes fadeOutRight{0%{opacity:1;-webkit-transform:translateX(0);transform:translateX(0)}100%{opacity:0;-webkit-transform:translateX(-5%);transform:translateX(-5%)}}.flip.transition.in,.flip.transition.out{-webkit-animation-duration:.6s;animation-duration:.6s}.horizontal.flip.transition.in{-webkit-animation-name:horizontalFlipIn;animation-name:horizontalFlipIn}.horizontal.flip.transition.out{-webkit-animation-name:horizontalFlipOut;animation-name:horizontalFlipOut}.vertical.flip.transition.in{-webkit-animation-name:verticalFlipIn;animation-name:verticalFlipIn}.vertical.flip.transition.out{-webkit-animation-name:verticalFlipOut;animation-name:verticalFlipOut}@-webkit-keyframes horizontalFlipIn{0%{-webkit-transform:perspective(2000px) rotateY(-90deg);transform:perspective(2000px) rotateY(-90deg);opacity:0}100%{-webkit-transform:perspective(2000px) rotateY(0);transform:perspective(2000px) rotateY(0);opacity:1}}@keyframes horizontalFlipIn{0%{-webkit-transform:perspective(2000px) rotateY(-90deg);transform:perspective(2000px) rotateY(-90deg);opacity:0}100%{-webkit-transform:perspective(2000px) rotateY(0);transform:perspective(2000px) rotateY(0);opacity:1}}@-webkit-keyframes verticalFlipIn{0%{-webkit-transform:perspective(2000px) rotateX(-90deg);transform:perspective(2000px) rotateX(-90deg);opacity:0}100%{-webkit-transform:perspective(2000px) rotateX(0);transform:perspective(2000px) rotateX(0);opacity:1}}@keyframes verticalFlipIn{0%{-webkit-transform:perspective(2000px) rotateX(-90deg);transform:perspective(2000px) rotateX(-90deg);opacity:0}100%{-webkit-transform:perspective(2000px) rotateX(0);transform:perspective(2000px) rotateX(0);opacity:1}}@-webkit-keyframes horizontalFlipOut{0%{-webkit-transform:perspective(2000px) rotateY(0);transform:perspective(2000px) rotateY(0);opacity:1}100%{-webkit-transform:perspective(2000px) rotateY(90deg);transform:perspective(2000px) rotateY(90deg);opacity:0}}@keyframes horizontalFlipOut{0%{-webkit-transform:perspective(2000px) rotateY(0);transform:perspective(2000px) rotateY(0);opacity:1}100%{-webkit-transform:perspective(2000px) rotateY(90deg);transform:perspective(2000px) rotateY(90deg);opacity:0}}@-webkit-keyframes verticalFlipOut{0%{-webkit-transform:perspective(2000px) rotateX(0);transform:perspective(2000px) rotateX(0);opacity:1}100%{-webkit-transform:perspective(2000px) rotateX(-90deg);transform:perspective(2000px) rotateX(-90deg);opacity:0}}@keyframes verticalFlipOut{0%{-webkit-transform:perspective(2000px) rotateX(0);transform:perspective(2000px) rotateX(0);opacity:1}100%{-webkit-transform:perspective(2000px) rotateX(-90deg);transform:perspective(2000px) rotateX(-90deg);opacity:0}}.scale.transition.in{-webkit-animation-name:scaleIn;animation-name:scaleIn}.scale.transition.out{-webkit-animation-name:scaleOut;animation-name:scaleOut}@-webkit-keyframes scaleIn{0%{opacity:0;-webkit-transform:scale(.8);transform:scale(.8)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes scaleIn{0%{opacity:0;-webkit-transform:scale(.8);transform:scale(.8)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes scaleOut{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:0;-webkit-transform:scale(.9);transform:scale(.9)}}@keyframes scaleOut{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:0;-webkit-transform:scale(.9);transform:scale(.9)}}.transition.fly{-webkit-animation-duration:.6s;animation-duration:.6s;-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1)}.transition.fly.in{-webkit-animation-name:flyIn;animation-name:flyIn}.transition[class*="fly up"].in{-webkit-animation-name:flyInUp;animation-name:flyInUp}.transition[class*="fly down"].in{-webkit-animation-name:flyInDown;animation-name:flyInDown}.transition[class*="fly left"].in{-webkit-animation-name:flyInLeft;animation-name:flyInLeft}.transition[class*="fly right"].in{-webkit-animation-name:flyInRight;animation-name:flyInRight}.transition.fly.out{-webkit-animation-name:flyOut;animation-name:flyOut}.transition[class*="fly up"].out{-webkit-animation-name:flyOutUp;animation-name:flyOutUp}.transition[class*="fly down"].out{-webkit-animation-name:flyOutDown;animation-name:flyOutDown}.transition[class*="fly left"].out{-webkit-animation-name:flyOutLeft;animation-name:flyOutLeft}.transition[class*="fly right"].out{-webkit-animation-name:flyOutRight;animation-name:flyOutRight}@-webkit-keyframes flyIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes flyIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@-webkit-keyframes flyInUp{0%{opacity:0;-webkit-transform:translate3d(0,1500px,0);transform:translate3d(0,1500px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}@keyframes flyInUp{0%{opacity:0;-webkit-transform:translate3d(0,1500px,0);transform:translate3d(0,1500px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}@-webkit-keyframes flyInDown{0%{opacity:0;-webkit-transform:translate3d(0,-1500px,0);transform:translate3d(0,-1500px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}100%{-webkit-transform:none;transform:none}}@keyframes flyInDown{0%{opacity:0;-webkit-transform:translate3d(0,-1500px,0);transform:translate3d(0,-1500px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}100%{-webkit-transform:none;transform:none}}@-webkit-keyframes flyInLeft{0%{opacity:0;-webkit-transform:translate3d(1500px,0,0);transform:translate3d(1500px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}100%{-webkit-transform:none;transform:none}}@keyframes flyInLeft{0%{opacity:0;-webkit-transform:translate3d(1500px,0,0);transform:translate3d(1500px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}100%{-webkit-transform:none;transform:none}}@-webkit-keyframes flyInRight{0%{opacity:0;-webkit-transform:translate3d(-1500px,0,0);transform:translate3d(-1500px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}100%{-webkit-transform:none;transform:none}}@keyframes flyInRight{0%{opacity:0;-webkit-transform:translate3d(-1500px,0,0);transform:translate3d(-1500px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}100%{-webkit-transform:none;transform:none}}@-webkit-keyframes flyOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}100%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes flyOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}100%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@-webkit-keyframes flyOutUp{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}100%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes flyOutUp{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}100%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@-webkit-keyframes flyOutDown{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}100%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes flyOutDown{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}100%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@-webkit-keyframes flyOutRight{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}100%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes flyOutRight{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}100%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@-webkit-keyframes flyOutLeft{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}100%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes flyOutLeft{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}100%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.transition.slide.in,.transition[class*="slide down"].in{-webkit-animation-name:slideInY;animation-name:slideInY;-webkit-transform-origin:top center;transform-origin:top center}.transition[class*="slide up"].in{-webkit-animation-name:slideInY;animation-name:slideInY;-webkit-transform-origin:bottom center;transform-origin:bottom center}.transition[class*="slide left"].in{-webkit-animation-name:slideInX;animation-name:slideInX;-webkit-transform-origin:center right;transform-origin:center right}.transition[class*="slide right"].in{-webkit-animation-name:slideInX;animation-name:slideInX;-webkit-transform-origin:center left;transform-origin:center left}.transition.slide.out,.transition[class*="slide down"].out{-webkit-animation-name:slideOutY;animation-name:slideOutY;-webkit-transform-origin:top center;transform-origin:top center}.transition[class*="slide up"].out{-webkit-animation-name:slideOutY;animation-name:slideOutY;-webkit-transform-origin:bottom center;transform-origin:bottom center}.transition[class*="slide left"].out{-webkit-animation-name:slideOutX;animation-name:slideOutX;-webkit-transform-origin:center right;transform-origin:center right}.transition[class*="slide right"].out{-webkit-animation-name:slideOutX;animation-name:slideOutX;-webkit-transform-origin:center left;transform-origin:center left}@-webkit-keyframes slideInY{0%{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}100%{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1)}}@keyframes slideInY{0%{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}100%{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1)}}@-webkit-keyframes slideInX{0%{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}100%{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes slideInX{0%{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}100%{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes slideOutY{0%{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1)}100%{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}}@keyframes slideOutY{0%{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1)}100%{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}}@-webkit-keyframes slideOutX{0%{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}100%{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}}@keyframes slideOutX{0%{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}100%{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}}.transition.swing{-webkit-animation-duration:.8s;animation-duration:.8s}.transition[class*="swing down"].in{-webkit-animation-name:swingInX;animation-name:swingInX;-webkit-transform-origin:top center;transform-origin:top center}.transition[class*="swing up"].in{-webkit-animation-name:swingInX;animation-name:swingInX;-webkit-transform-origin:bottom center;transform-origin:bottom center}.transition[class*="swing left"].in{-webkit-animation-name:swingInY;animation-name:swingInY;-webkit-transform-origin:center right;transform-origin:center right}.transition[class*="swing right"].in{-webkit-animation-name:swingInY;animation-name:swingInY;-webkit-transform-origin:center left;transform-origin:center left}.transition.swing.out,.transition[class*="swing down"].out{-webkit-animation-name:swingOutX;animation-name:swingOutX;-webkit-transform-origin:top center;transform-origin:top center}.transition[class*="swing up"].out{-webkit-animation-name:swingOutX;animation-name:swingOutX;-webkit-transform-origin:bottom center;transform-origin:bottom center}.transition[class*="swing left"].out{-webkit-animation-name:swingOutY;animation-name:swingOutY;-webkit-transform-origin:center right;transform-origin:center right}.transition[class*="swing right"].out{-webkit-animation-name:swingOutY;animation-name:swingOutY;-webkit-transform-origin:center left;transform-origin:center left}@-webkit-keyframes swingInX{0%{-webkit-transform:perspective(1000px) rotateX(90deg);transform:perspective(1000px) rotateX(90deg);opacity:0}40%{-webkit-transform:perspective(1000px) rotateX(-30deg);transform:perspective(1000px) rotateX(-30deg);opacity:1}60%{-webkit-transform:perspective(1000px) rotateX(15deg);transform:perspective(1000px) rotateX(15deg)}80%{-webkit-transform:perspective(1000px) rotateX(-7.5deg);transform:perspective(1000px) rotateX(-7.5deg)}100%{-webkit-transform:perspective(1000px) rotateX(0);transform:perspective(1000px) rotateX(0)}}@keyframes swingInX{0%{-webkit-transform:perspective(1000px) rotateX(90deg);transform:perspective(1000px) rotateX(90deg);opacity:0}40%{-webkit-transform:perspective(1000px) rotateX(-30deg);transform:perspective(1000px) rotateX(-30deg);opacity:1}60%{-webkit-transform:perspective(1000px) rotateX(15deg);transform:perspective(1000px) rotateX(15deg)}80%{-webkit-transform:perspective(1000px) rotateX(-7.5deg);transform:perspective(1000px) rotateX(-7.5deg)}100%{-webkit-transform:perspective(1000px) rotateX(0);transform:perspective(1000px) rotateX(0)}}@-webkit-keyframes swingInY{0%{-webkit-transform:perspective(1000px) rotateY(-90deg);transform:perspective(1000px) rotateY(-90deg);opacity:0}40%{-webkit-transform:perspective(1000px) rotateY(30deg);transform:perspective(1000px) rotateY(30deg);opacity:1}60%{-webkit-transform:perspective(1000px) rotateY(-17.5deg);transform:perspective(1000px) rotateY(-17.5deg)}80%{-webkit-transform:perspective(1000px) rotateY(7.5deg);transform:perspective(1000px) rotateY(7.5deg)}100%{-webkit-transform:perspective(1000px) rotateY(0);transform:perspective(1000px) rotateY(0)}}@keyframes swingInY{0%{-webkit-transform:perspective(1000px) rotateY(-90deg);transform:perspective(1000px) rotateY(-90deg);opacity:0}40%{-webkit-transform:perspective(1000px) rotateY(30deg);transform:perspective(1000px) rotateY(30deg);opacity:1}60%{-webkit-transform:perspective(1000px) rotateY(-17.5deg);transform:perspective(1000px) rotateY(-17.5deg)}80%{-webkit-transform:perspective(1000px) rotateY(7.5deg);transform:perspective(1000px) rotateY(7.5deg)}100%{-webkit-transform:perspective(1000px) rotateY(0);transform:perspective(1000px) rotateY(0)}}@-webkit-keyframes swingOutX{0%{-webkit-transform:perspective(1000px) rotateX(0);transform:perspective(1000px) rotateX(0)}40%{-webkit-transform:perspective(1000px) rotateX(-7.5deg);transform:perspective(1000px) rotateX(-7.5deg)}60%{-webkit-transform:perspective(1000px) rotateX(17.5deg);transform:perspective(1000px) rotateX(17.5deg)}80%{-webkit-transform:perspective(1000px) rotateX(-30deg);transform:perspective(1000px) rotateX(-30deg);opacity:1}100%{-webkit-transform:perspective(1000px) rotateX(90deg);transform:perspective(1000px) rotateX(90deg);opacity:0}}@keyframes swingOutX{0%{-webkit-transform:perspective(1000px) rotateX(0);transform:perspective(1000px) rotateX(0)}40%{-webkit-transform:perspective(1000px) rotateX(-7.5deg);transform:perspective(1000px) rotateX(-7.5deg)}60%{-webkit-transform:perspective(1000px) rotateX(17.5deg);transform:perspective(1000px) rotateX(17.5deg)}80%{-webkit-transform:perspective(1000px) rotateX(-30deg);transform:perspective(1000px) rotateX(-30deg);opacity:1}100%{-webkit-transform:perspective(1000px) rotateX(90deg);transform:perspective(1000px) rotateX(90deg);opacity:0}}@-webkit-keyframes swingOutY{0%{-webkit-transform:perspective(1000px) rotateY(0);transform:perspective(1000px) rotateY(0)}40%{-webkit-transform:perspective(1000px) rotateY(7.5deg);transform:perspective(1000px) rotateY(7.5deg)}60%{-webkit-transform:perspective(1000px) rotateY(-10deg);transform:perspective(1000px) rotateY(-10deg)}80%{-webkit-transform:perspective(1000px) rotateY(30deg);transform:perspective(1000px) rotateY(30deg);opacity:1}100%{-webkit-transform:perspective(1000px) rotateY(-90deg);transform:perspective(1000px) rotateY(-90deg);opacity:0}}@keyframes swingOutY{0%{-webkit-transform:perspective(1000px) rotateY(0);transform:perspective(1000px) rotateY(0)}40%{-webkit-transform:perspective(1000px) rotateY(7.5deg);transform:perspective(1000px) rotateY(7.5deg)}60%{-webkit-transform:perspective(1000px) rotateY(-10deg);transform:perspective(1000px) rotateY(-10deg)}80%{-webkit-transform:perspective(1000px) rotateY(30deg);transform:perspective(1000px) rotateY(30deg);opacity:1}100%{-webkit-transform:perspective(1000px) rotateY(-90deg);transform:perspective(1000px) rotateY(-90deg);opacity:0}}.transition.zoom.in{-webkit-animation-name:zoomIn;animation-name:zoomIn}.transition.zoom.out{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomIn{0%{opacity:1;-webkit-transform:scale(0);transform:scale(0)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes zoomIn{0%{opacity:1;-webkit-transform:scale(0);transform:scale(0)}100%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes zoomOut{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:1;-webkit-transform:scale(0);transform:scale(0)}}@keyframes zoomOut{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}100%{opacity:1;-webkit-transform:scale(0);transform:scale(0)}}.flash.transition{-webkit-animation-duration:750ms;animation-duration:750ms;-webkit-animation-name:flash;animation-name:flash}.shake.transition{-webkit-animation-duration:750ms;animation-duration:750ms;-webkit-animation-name:shake;animation-name:shake}.bounce.transition{-webkit-animation-duration:750ms;animation-duration:750ms;-webkit-animation-name:bounce;animation-name:bounce}.tada.transition{-webkit-animation-duration:750ms;animation-duration:750ms;-webkit-animation-name:tada;animation-name:tada}.pulse.transition{-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-name:pulse;animation-name:pulse}.jiggle.transition{-webkit-animation-duration:750ms;animation-duration:750ms;-webkit-animation-name:jiggle;animation-name:jiggle}.transition.glow{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:cubic-bezier(.19,1,.22,1);animation-timing-function:cubic-bezier(.19,1,.22,1)}.transition.glow{-webkit-animation-name:glow;animation-name:glow}@-webkit-keyframes flash{0%,100%,50%{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,100%,50%{opacity:1}25%,75%{opacity:0}}@-webkit-keyframes shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@keyframes shake{0%,100%{-webkit-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}@-webkit-keyframes bounce{0%,100%,20%,50%,80%{-webkit-transform:translateY(0);transform:translateY(0)}40%{-webkit-transform:translateY(-30px);transform:translateY(-30px)}60%{-webkit-transform:translateY(-15px);transform:translateY(-15px)}}@keyframes bounce{0%,100%,20%,50%,80%{-webkit-transform:translateY(0);transform:translateY(0)}40%{-webkit-transform:translateY(-30px);transform:translateY(-30px)}60%{-webkit-transform:translateY(-15px);transform:translateY(-15px)}}@-webkit-keyframes tada{0%{-webkit-transform:scale(1);transform:scale(1)}10%,20%{-webkit-transform:scale(.9) rotate(-3deg);transform:scale(.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale(1.1) rotate(3deg);transform:scale(1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale(1.1) rotate(-3deg);transform:scale(1.1) rotate(-3deg)}100%{-webkit-transform:scale(1) rotate(0);transform:scale(1) rotate(0)}}@keyframes tada{0%{-webkit-transform:scale(1);transform:scale(1)}10%,20%{-webkit-transform:scale(.9) rotate(-3deg);transform:scale(.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale(1.1) rotate(3deg);transform:scale(1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale(1.1) rotate(-3deg);transform:scale(1.1) rotate(-3deg)}100%{-webkit-transform:scale(1) rotate(0);transform:scale(1) rotate(0)}}@-webkit-keyframes pulse{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}50%{-webkit-transform:scale(.9);transform:scale(.9);opacity:.7}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes pulse{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}50%{-webkit-transform:scale(.9);transform:scale(.9);opacity:.7}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@-webkit-keyframes jiggle{0%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}100%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes jiggle{0%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}100%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@-webkit-keyframes glow{0%{background-color:#fcfcfd}30%{background-color:#fff6cd}100%{background-color:#fcfcfd}}@keyframes glow{0%{background-color:#fcfcfd}30%{background-color:#fff6cd}100%{background-color:#fcfcfd}}
\ No newline at end of file
diff --git a/main/solution/ui/src/css/themes/basic/assets/fonts/icons.eot b/main/solution/ui/src/css/themes/basic/assets/fonts/icons.eot
new file mode 100644
index 0000000000..25066de069
Binary files /dev/null and b/main/solution/ui/src/css/themes/basic/assets/fonts/icons.eot differ
diff --git a/main/solution/ui/src/css/themes/basic/assets/fonts/icons.svg b/main/solution/ui/src/css/themes/basic/assets/fonts/icons.svg
new file mode 100644
index 0000000000..b9c54d022b
--- /dev/null
+++ b/main/solution/ui/src/css/themes/basic/assets/fonts/icons.svg
@@ -0,0 +1,450 @@
+
+
+
+
+Created by FontForge 20100429 at Thu Sep 20 22:09:47 2012
+ By root
+Copyright (C) 2012 by original authors @ fontello.com
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/solution/ui/src/css/themes/basic/assets/fonts/icons.ttf b/main/solution/ui/src/css/themes/basic/assets/fonts/icons.ttf
new file mode 100644
index 0000000000..318a2643d3
Binary files /dev/null and b/main/solution/ui/src/css/themes/basic/assets/fonts/icons.ttf differ
diff --git a/main/solution/ui/src/css/themes/basic/assets/fonts/icons.woff b/main/solution/ui/src/css/themes/basic/assets/fonts/icons.woff
new file mode 100644
index 0000000000..baba1b5ebe
Binary files /dev/null and b/main/solution/ui/src/css/themes/basic/assets/fonts/icons.woff differ
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.eot b/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.eot
new file mode 100644
index 0000000000..0a1ef3f7ec
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.eot differ
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.svg b/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.svg
new file mode 100644
index 0000000000..4c237533e0
--- /dev/null
+++ b/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.svg
@@ -0,0 +1,1008 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.ttf b/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.ttf
new file mode 100644
index 0000000000..f99085132d
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.ttf differ
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.woff b/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.woff
new file mode 100644
index 0000000000..2e874012a9
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.woff differ
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.woff2 b/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.woff2
new file mode 100644
index 0000000000..0d575fd51a
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/fonts/brand-icons.woff2 differ
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/icons.eot b/main/solution/ui/src/css/themes/default/assets/fonts/icons.eot
new file mode 100644
index 0000000000..ef75106be9
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/fonts/icons.eot differ
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/icons.svg b/main/solution/ui/src/css/themes/default/assets/fonts/icons.svg
new file mode 100644
index 0000000000..0ae8e32980
--- /dev/null
+++ b/main/solution/ui/src/css/themes/default/assets/fonts/icons.svg
@@ -0,0 +1,1518 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/icons.ttf b/main/solution/ui/src/css/themes/default/assets/fonts/icons.ttf
new file mode 100644
index 0000000000..17bb6747a5
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/fonts/icons.ttf differ
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/icons.woff b/main/solution/ui/src/css/themes/default/assets/fonts/icons.woff
new file mode 100644
index 0000000000..4cf2a4fee4
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/fonts/icons.woff differ
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/icons.woff2 b/main/solution/ui/src/css/themes/default/assets/fonts/icons.woff2
new file mode 100644
index 0000000000..eea9aa2281
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/fonts/icons.woff2 differ
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.eot b/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.eot
new file mode 100644
index 0000000000..cda0a84cfb
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.eot differ
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.svg b/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.svg
new file mode 100644
index 0000000000..2875252ef2
--- /dev/null
+++ b/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.svg
@@ -0,0 +1,366 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.ttf b/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.ttf
new file mode 100644
index 0000000000..ee13f848ec
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.ttf differ
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.woff b/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.woff
new file mode 100644
index 0000000000..bcd834354e
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.woff differ
diff --git a/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.woff2 b/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.woff2
new file mode 100644
index 0000000000..35cc7b3b7a
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/fonts/outline-icons.woff2 differ
diff --git a/main/solution/ui/src/css/themes/default/assets/images/flags.png b/main/solution/ui/src/css/themes/default/assets/images/flags.png
new file mode 100644
index 0000000000..cdd33c3bc6
Binary files /dev/null and b/main/solution/ui/src/css/themes/default/assets/images/flags.png differ
diff --git a/main/solution/ui/src/css/themes/github/assets/fonts/octicons-local.ttf b/main/solution/ui/src/css/themes/github/assets/fonts/octicons-local.ttf
new file mode 100644
index 0000000000..d5f4e2ecaa
Binary files /dev/null and b/main/solution/ui/src/css/themes/github/assets/fonts/octicons-local.ttf differ
diff --git a/main/solution/ui/src/css/themes/github/assets/fonts/octicons.svg b/main/solution/ui/src/css/themes/github/assets/fonts/octicons.svg
new file mode 100644
index 0000000000..d3116a69b2
--- /dev/null
+++ b/main/solution/ui/src/css/themes/github/assets/fonts/octicons.svg
@@ -0,0 +1,200 @@
+
+
+
+
+(c) 2012-2015 GitHub
+
+When using the GitHub logos, be sure to follow the GitHub logo guidelines (https://github.com/logos)
+
+Font License: SIL OFL 1.1 (http://scripts.sil.org/OFL)
+Applies to all font files
+
+Code License: MIT (http://choosealicense.com/licenses/mit/)
+Applies to all other files
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/solution/ui/src/css/themes/github/assets/fonts/octicons.ttf b/main/solution/ui/src/css/themes/github/assets/fonts/octicons.ttf
new file mode 100644
index 0000000000..9e09105305
Binary files /dev/null and b/main/solution/ui/src/css/themes/github/assets/fonts/octicons.ttf differ
diff --git a/main/solution/ui/src/css/themes/github/assets/fonts/octicons.woff b/main/solution/ui/src/css/themes/github/assets/fonts/octicons.woff
new file mode 100644
index 0000000000..cc3c19f0df
Binary files /dev/null and b/main/solution/ui/src/css/themes/github/assets/fonts/octicons.woff differ
diff --git a/main/solution/ui/src/css/themes/material/assets/fonts/icons.eot b/main/solution/ui/src/css/themes/material/assets/fonts/icons.eot
new file mode 100644
index 0000000000..70508ebabc
Binary files /dev/null and b/main/solution/ui/src/css/themes/material/assets/fonts/icons.eot differ
diff --git a/main/solution/ui/src/css/themes/material/assets/fonts/icons.svg b/main/solution/ui/src/css/themes/material/assets/fonts/icons.svg
new file mode 100644
index 0000000000..a449327e22
--- /dev/null
+++ b/main/solution/ui/src/css/themes/material/assets/fonts/icons.svg
@@ -0,0 +1,2373 @@
+
+
+
+
+
+Created by FontForge 20151118 at Mon Feb 8 11:58:02 2016
+ By shyndman
+Copyright 2015 Google, Inc. All Rights Reserved.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/solution/ui/src/css/themes/material/assets/fonts/icons.ttf b/main/solution/ui/src/css/themes/material/assets/fonts/icons.ttf
new file mode 100644
index 0000000000..7015564ad1
Binary files /dev/null and b/main/solution/ui/src/css/themes/material/assets/fonts/icons.ttf differ
diff --git a/main/solution/ui/src/css/themes/material/assets/fonts/icons.woff b/main/solution/ui/src/css/themes/material/assets/fonts/icons.woff
new file mode 100644
index 0000000000..b648a3eea2
Binary files /dev/null and b/main/solution/ui/src/css/themes/material/assets/fonts/icons.woff differ
diff --git a/main/solution/ui/src/css/themes/material/assets/fonts/icons.woff2 b/main/solution/ui/src/css/themes/material/assets/fonts/icons.woff2
new file mode 100644
index 0000000000..9fa2112520
Binary files /dev/null and b/main/solution/ui/src/css/themes/material/assets/fonts/icons.woff2 differ
diff --git a/main/solution/ui/src/index.js b/main/solution/ui/src/index.js
new file mode 100644
index 0000000000..46665c0312
--- /dev/null
+++ b/main/solution/ui/src/index.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.
+ */
+
+import { renderAppContainer, renderError, renderProgress } from '@aws-ee/base-ui/dist/render-utils';
+import bootstrapApp from '@aws-ee/base-ui/dist/bootstrap-app';
+import pluginRegistry from './plugins/plugin-registry';
+
+import 'typeface-lato';
+import './css/basscss-important.css';
+import './css/semantic.min.css';
+import './css/animate.css';
+import 'toastr/build/toastr.css';
+import 'react-table/react-table.css';
+import './css/index.css';
+
+bootstrapApp({
+ renderAppContainer,
+ renderError,
+ renderProgress,
+ pluginRegistry,
+});
diff --git a/main/solution/ui/src/plugins/app-context-items-plugin.js b/main/solution/ui/src/plugins/app-context-items-plugin.js
new file mode 100644
index 0000000000..5c27e5cdd0
--- /dev/null
+++ b/main/solution/ui/src/plugins/app-context-items-plugin.js
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+/**
+ * Registers your app context items to the given appContext.
+ *
+ * @param appContext An instance of application context containing various application level objects such as various
+ * MobX stores. This is a live reference. Use this function to register context items by setting them directly on
+ * the given "appContext" reference. If you need access to the pluginRegistry, you can get it from appContext
+ */
+// eslint-disable-next-line no-unused-vars
+function registerAppContextItems(appContext) {
+ // This is where you can
+ // 1. register your app context items, such as your own MobX stores. To register your items
+ //
+ // myStore.registerAppContextItems(appContext); // The "myStore" needs to have a method "registerContextItems"
+ // // with implementation of adding itself to the given "appContext"
+ //
+ // 2. modify any existing items
+ //
+ // appContext.itemYouWantToModify = newItem;
+ //
+ // 3. delete any existing item,
+ //
+ // delete appContext.itemYouWantToRemoveFromContext; OR
+ // appContext.itemYouWantToRemoveFromContext = undefined;
+ //
+ // TODO: Register additional custom context items (such as your custom MobX stores) here
+}
+
+const plugin = {
+ registerAppContextItems,
+};
+
+export default plugin;
diff --git a/main/solution/ui/src/plugins/initialization-plugin.js b/main/solution/ui/src/plugins/initialization-plugin.js
new file mode 100644
index 0000000000..b0d2518997
--- /dev/null
+++ b/main/solution/ui/src/plugins/initialization-plugin.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+/**
+ * If you need to add some global UI initialization logic that is specific to the solution, you can do it here. However,
+ * It is unlikely that you will need to do so because the base-ui addon takes care of the most common initialization
+ * logic needed.
+ *
+ * @param payload A free form object. Use this object to add any properties that you need to pass to the App model
+ * when it is being initialized. The base-ui addon, makes a property named 'tokenInfo' available on this payload object.
+ * @param appContext An application context object containing various Mobx Stores, Models etc.
+ *
+ * @returns {Promise}
+ */
+// eslint-disable-next-line no-unused-vars
+async function init(payload, appContext) {
+ // Write any solution specific initialization logic.
+}
+
+// eslint-disable-next-line no-unused-vars
+async function postInit(payload, appContext) {
+ // Write any solution specific post initialization logic, such as loading stores.
+}
+
+const plugin = {
+ init,
+ postInit,
+};
+
+export default plugin;
diff --git a/main/solution/ui/src/plugins/menu-items-plugin.js b/main/solution/ui/src/plugins/menu-items-plugin.js
new file mode 100644
index 0000000000..42cb0a75b0
--- /dev/null
+++ b/main/solution/ui/src/plugins/menu-items-plugin.js
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+/**
+ * Adds your navigation menu items to the given itemsMap.
+ * This function is called last after adding navigation menu items to the itemsMap from all other installed addons.
+ *
+ * @param itemsMap A Map containing navigation menu items. This object is a Map that has route paths (urls) as
+ * keys and menu item object with the following shape
+ *
+ * {
+ * title: STRING, // Title for the navigation menu item
+ * icon: STRING, // semantic ui icon name fot the navigation menu item
+ * shouldShow: FUNCTION, // A function that returns a flag indicating whether to show the item or not (useful when showing menu items conditionally)
+ * render: OPTIONAL FUNCTION, // Optional function that returns rendered menu item component. Use this ONLY if you want to control full rendering of the menu item.
+ * }
+ *
+ * @param appContext An application context object containing all MobX store objects
+ *
+ * @returns Map<*> Returns A Map containing navigation menu items with the same shape as "itemsMap"
+ */
+// eslint-disable-next-line no-unused-vars
+function registerMenuItems(itemsMap, { location, appContext }) {
+ // This is where you can
+ // 1. register your navigation menu items, to register your items
+ //
+ // const items = new Map([
+ // ...itemsMap,
+ // // Add your navigation menu items here
+ // ['/your/menu/item1/url', {title:'my menu item1',icon:'some-icon-from-semantic-ui'}],
+ // ['/your/menu/item2/url', {title:'my menu item2',icon:'some-icon-from-semantic-ui',shouldShow:()=>true}],
+ // ['/your/menu/item3/url', {render:()=>{ // return rendered menu item here }] ]);
+ // return items;
+ //
+ // 2. modify any existing items
+ //
+ // items.set('the/menu/item/url/you/want/to/replace',{title:'my menu item1',icon:'some-icon-from-semantic-ui'});
+ // return routesMap;
+ //
+ // 3. delete any existing route, to delete existing route
+ //
+ // items.delete('the/menu/item/url/you/want/to/delete');
+ //
+
+ // TODO: Register additional custom navigation menu items here
+ const items = new Map([...itemsMap]);
+ // DO NOT forget to return items here. If you do not return any menu items here then the menu will not show any items
+ return items;
+}
+
+const plugin = {
+ registerMenuItems,
+};
+
+export default plugin;
diff --git a/main/solution/ui/src/plugins/plugin-registry.js b/main/solution/ui/src/plugins/plugin-registry.js
new file mode 100644
index 0000000000..dacc75f9a1
--- /dev/null
+++ b/main/solution/ui/src/plugins/plugin-registry.js
@@ -0,0 +1,62 @@
+/*
+ * 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 baseAppContextItemsPlugin from '@aws-ee/base-ui/dist/plugins/app-context-items-plugin';
+import baseInitializationPlugin from '@aws-ee/base-ui/dist/plugins/initialization-plugin';
+import baseAuthenticationPlugin from '@aws-ee/base-ui/dist/plugins/authentication-plugin';
+import baseAppComponentPlugin from '@aws-ee/base-ui/dist/plugins/app-component-plugin';
+import baseMenuItemsPlugin from '@aws-ee/base-ui/dist/plugins/menu-items-plugin';
+import baseRoutesPlugin from '@aws-ee/base-ui/dist/plugins/routes-plugin';
+import workflowAppContextItemsPlugin from '@aws-ee/base-workflow-ui/dist/plugins/app-context-items-plugin';
+import workflowMenuItemsPlugin from '@aws-ee/base-workflow-ui/dist/plugins/menu-items-plugin';
+import workflowRoutesPlugin from '@aws-ee/base-workflow-ui/dist/plugins/routes-plugin';
+import raasAppContextItemsPlugin from '@aws-ee/base-raas-ui/dist/plugins/app-context-items-plugin';
+import raasInitializationPlugin from '@aws-ee/base-raas-ui/dist/plugins/initialization-plugin';
+import raasAppComponentPlugin from '@aws-ee/base-raas-ui/dist/plugins/app-component-plugin';
+import raasMenuItemsPlugin from '@aws-ee/base-raas-ui/dist/plugins/menu-items-plugin';
+import raasRoutesPlugin from '@aws-ee/base-raas-ui/dist/plugins/routes-plugin';
+
+import appContextItemsPlugin from './app-context-items-plugin';
+import initializationPlugin from './initialization-plugin';
+import menuItemsPlugin from './menu-items-plugin';
+import routesPlugin from './routes-plugin';
+
+// baseAppContextItemsPlugin registers app context items (such as base MobX stores etc) provided by the base addon
+// baseInitializationPlugin registers the base initialization logic provided by the base ui addon
+// baseMenuItemsPlugin registers menu items provided by the base addon
+// baseRoutesPlugin registers base routes provided by the base addon
+const extensionPoints = {
+ 'app-context-items': [
+ baseAppContextItemsPlugin,
+ workflowAppContextItemsPlugin,
+ raasAppContextItemsPlugin,
+ appContextItemsPlugin,
+ ],
+ 'initialization': [baseInitializationPlugin, raasInitializationPlugin, initializationPlugin],
+ 'authentication': [baseAuthenticationPlugin],
+ 'app-component': [baseAppComponentPlugin, raasAppComponentPlugin],
+ 'menu-items': [baseMenuItemsPlugin, workflowMenuItemsPlugin, raasMenuItemsPlugin, menuItemsPlugin],
+ 'routes': [baseRoutesPlugin, workflowRoutesPlugin, raasRoutesPlugin, routesPlugin],
+};
+
+function getPlugins(extensionPoint) {
+ return extensionPoints[extensionPoint];
+}
+
+const registry = {
+ getPlugins,
+};
+
+export default registry;
diff --git a/main/solution/ui/src/plugins/routes-plugin.js b/main/solution/ui/src/plugins/routes-plugin.js
new file mode 100644
index 0000000000..24d297ee23
--- /dev/null
+++ b/main/solution/ui/src/plugins/routes-plugin.js
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+/**
+ * Adds your routes to the given routesMap.
+ * This function is called last after adding routes to the routesMap from all other installed addons.
+ *
+ * @param routesMap A Map containing routes. This object is a Map that has route paths as
+ * keys and React Component as value.
+ *
+ * @returns {Promise<*>} Returns a Map with the mapping of routes as keys and their React Component as values
+ */
+// eslint-disable-next-line no-unused-vars
+function registerRoutes(routesMap, { location, appContext }) {
+ // This is where you can
+ // 1. register your routes, to register your routes
+ //
+ // const routes = new Map([
+ // ...routesMap,
+ // // Add your routes here
+ // ['/your/routes', SomeReactComponent],
+ // ]);
+ // return routes;
+ //
+ // 2. modify any existing routes
+ //
+ // routesMap.set('the/route/you/want/to/replace',SomeReactComponent);
+ // return routesMap;
+ //
+ // 3. delete any existing route, to delete existing route
+ //
+ // routesMap.delete('the/route/you/want/to/delete');
+ //
+
+ // TODO: Register additional routes and their React Components as per your solution requirements
+
+ // DO NOT forget to return routes here. If you do not return here, no routes will be configured in React router
+ return routesMap;
+}
+
+// eslint-disable-next-line no-unused-vars
+function getDefaultRouteLocation({ location, appContext }) {
+ // If you want to override the default route location, do it here
+}
+
+const plugin = {
+ registerRoutes,
+ getDefaultRouteLocation,
+};
+
+export default plugin;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000000..ad5532d84c
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,19149 @@
+importers:
+ addons/addon-base-post-deployment/packages/base-post-deployment:
+ dependencies:
+ '@aws-ee/base-api-services': 'link:../../../addon-base-rest-api/packages/services'
+ '@aws-ee/base-services': 'link:../../../addon-base/packages/services'
+ '@aws-ee/base-services-container': 'link:../../../addon-base/packages/services-container'
+ aws-sdk: 2.656.0
+ generate-password: 1.5.1
+ lodash: 4.17.15
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-api-services': 'workspace:*'
+ '@aws-ee/base-services': 'workspace:*'
+ '@aws-ee/base-services-container': 'workspace:*'
+ aws-sdk: ^2.647.0
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ generate-password: ^1.5.0
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ source-map-support: ^0.5.16
+ addons/addon-base-raas-ui/packages/base-raas-ui:
+ dependencies:
+ '@auth0/auth0-spa-js': 1.6.5
+ '@aws-ee/base-ui': 'link:../../../addon-base-ui/packages/base-ui'
+ aws-sdk: 2.656.0
+ chart.js: 2.9.3
+ classnames: 2.2.6
+ crypto-browserify: 3.12.0
+ csvtojson: 2.0.10
+ is-cidr: 3.1.0
+ jwt-decode: 2.2.0
+ lodash: 4.17.15
+ mobx: 5.15.4
+ mobx-react: 6.2.2_mobx@5.15.4+react@16.13.1
+ mobx-react-form: 2.0.8_mobx@5.15.4
+ mobx-state-tree: 3.15.0_mobx@5.15.4
+ numeral: 2.0.6
+ pretty-bytes: 5.3.0
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-avatar: 3.9.2_prop-types@15.7.2+react@16.13.1
+ react-chartjs-2: 2.9.0_993042a9951b76ec634148f49ee6c836
+ react-copy-to-clipboard: 5.0.2_react@16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ react-dotdotdot: 1.3.1_eb0d650be231ffd0ace4a30b38162117
+ react-dropzone: 10.2.2_react@16.13.1
+ react-router-dom: 5.1.2_react@16.13.1
+ react-select: 3.1.0_react-dom@16.13.1+react@16.13.1
+ react-sparklines: 1.7.0_react-dom@16.13.1+react@16.13.1
+ react-syntax-highlighter: 11.0.2_react@16.13.1
+ react-table: 6.11.5_eb0d650be231ffd0ace4a30b38162117
+ react-timeago: 4.4.0_react@16.13.1
+ request: 2.88.2
+ semantic-ui-react: 0.88.2_react-dom@16.13.1+react@16.13.1
+ showdown: 1.9.1
+ toastr: 2.1.4
+ typeface-lato: 0.0.75
+ uuid: 3.4.0
+ validatorjs: 3.18.1
+ devDependencies:
+ '@babel/cli': 7.8.4_@babel+core@7.9.0
+ '@babel/core': 7.9.0
+ '@babel/plugin-proposal-class-properties': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-react-jsx': 7.9.4_@babel+core@7.9.0
+ '@babel/preset-env': 7.9.5_@babel+core@7.9.0
+ '@babel/preset-react': 7.9.4_@babel+core@7.9.0
+ babel-eslint: 10.1.0_eslint@6.8.0
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_7221e9efc3e1df952f9031babfc371af
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-jsx-a11y: 6.2.3_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ eslint-plugin-react: 7.19.0_eslint@6.8.0
+ eslint-plugin-react-hooks: 1.7.0_eslint@6.8.0
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ serverless: 1.67.3
+ serverless-deployment-bucket: 1.1.1
+ typescript: 3.8.3
+ webpack: 4.41.2_webpack@4.41.2
+ specifiers:
+ '@auth0/auth0-spa-js': ^1.2.3
+ '@aws-ee/base-ui': 'workspace:*'
+ '@babel/cli': ^7.8.4
+ '@babel/core': ^7.8.6
+ '@babel/plugin-proposal-class-properties': ^7.8.3
+ '@babel/plugin-transform-react-jsx': ^7.8.3
+ '@babel/preset-env': ^7.8.6
+ '@babel/preset-react': ^7.8.3
+ aws-sdk: ^2.647.0
+ babel-eslint: ^10.0.3
+ chart.js: ^2.9.3
+ classnames: ^2.2.6
+ crypto-browserify: ^3.12.0
+ csvtojson: ^2.0.10
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.0.1
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-jsx-a11y: ^6.2.3
+ eslint-plugin-prettier: ^3.1.2
+ eslint-plugin-react: ^7.18.3
+ eslint-plugin-react-hooks: ^1.7.0
+ husky: ^3.1.0
+ is-cidr: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ jwt-decode: ^2.2.0
+ lodash: ^4.17.15
+ mobx: ^5.15.4
+ mobx-react: ^6.1.7
+ mobx-react-form: ^2.0.8
+ mobx-state-tree: ^3.15.0
+ numeral: ^2.0.6
+ prettier: ^1.19.1
+ pretty-bytes: ^5.3.0
+ pretty-quick: ^1.11.1
+ prop-types: ^15.7.2
+ react: ^16.12.0
+ react-avatar: ^3.9.0
+ react-chartjs-2: ^2.9.0
+ react-copy-to-clipboard: ^5.0.2
+ react-dom: ^16.12.0
+ react-dotdotdot: ^1.3.1
+ react-dropzone: ^10.1.9
+ react-router-dom: ^5.1.2
+ react-select: ^3.0.8
+ react-sparklines: ^1.7.0
+ react-syntax-highlighter: ^11.0.2
+ react-table: ^6.11.5
+ react-timeago: ^4.4.0
+ request: ^2.34
+ semantic-ui-react: ^0.88.2
+ serverless: ^1.63.0
+ serverless-deployment-bucket: ^1.1.0
+ showdown: ^1.9.1
+ toastr: ^2.1.4
+ typeface-lato: 0.0.75
+ typescript: ^3.7.5
+ uuid: ^3.4.0
+ validatorjs: ^3.18.1
+ webpack: 4.41.2
+ addons/addon-base-raas/packages/base-raas-cfn-templates:
+ devDependencies:
+ '@babel/cli': 7.8.4_@babel+core@7.9.0
+ '@babel/core': 7.9.0
+ babel-plugin-inline-import: 3.0.0
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@babel/cli': ^7.8.4
+ '@babel/core': ^7.9.0
+ babel-plugin-inline-import: ^3.0.0
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.1.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ prettier: ^1.19.1
+ source-map-support: ^0.5.16
+ addons/addon-base-raas/packages/base-raas-post-deployment:
+ dependencies:
+ '@aws-ee/base-raas-services': 'link:../base-raas-services'
+ '@aws-ee/base-services': 'link:../../../addon-base/packages/services'
+ '@aws-ee/base-services-container': 'link:../../../addon-base/packages/services-container'
+ lodash: 4.17.15
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-raas-services': 'workspace:*'
+ '@aws-ee/base-services': 'workspace:*'
+ '@aws-ee/base-services-container': 'workspace:*'
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.1.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ source-map-support: ^0.5.16
+ addons/addon-base-raas/packages/base-raas-rest-api:
+ dependencies:
+ '@aws-ee/base-controllers': 'link:../../../addon-base-rest-api/packages/base-controllers'
+ '@aws-ee/base-raas-services': 'link:../base-raas-services'
+ '@aws-ee/base-services': 'link:../../../addon-base/packages/services'
+ lodash: 4.17.15
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-controllers': 'workspace:*'
+ '@aws-ee/base-raas-services': 'workspace:*'
+ '@aws-ee/base-services': 'workspace:*'
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ source-map-support: ^0.5.16
+ addons/addon-base-raas/packages/base-raas-services:
+ dependencies:
+ '@aws-ee/base-api-services': 'link:../../../addon-base-rest-api/packages/services'
+ '@aws-ee/base-services': 'link:../../../addon-base/packages/services'
+ '@aws-ee/base-services-container': 'link:../../../addon-base/packages/services-container'
+ lodash: 4.17.15
+ node-cache: 4.2.1
+ uuid: 3.4.0
+ xml: 1.0.1
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-api-services': 'workspace:*'
+ '@aws-ee/base-services': 'workspace:*'
+ '@aws-ee/base-services-container': 'workspace:*'
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ node-cache: ^4.2.1
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ source-map-support: ^0.5.16
+ uuid: ^3.4.0
+ xml: ^1.0.1
+ addons/addon-base-raas/packages/base-raas-workflow-steps:
+ dependencies:
+ '@aws-ee/base-workflow-core': 'link:../../../addon-base-workflow/packages/base-workflow-core'
+ lodash: 4.17.15
+ shortid: 2.2.15
+ slugify: 1.4.0
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-workflow-core': 'workspace:*'
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.1.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ shortid: ^2.2.15
+ slugify: ^1.4.0
+ source-map-support: ^0.5.16
+ addons/addon-base-raas/packages/base-raas-workflows:
+ dependencies:
+ '@aws-ee/base-workflow-core': 'link:../../../addon-base-workflow/packages/base-workflow-core'
+ lodash: 4.17.15
+ shortid: 2.2.15
+ slugify: 1.4.0
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-workflow-core': 'workspace:*'
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.1.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ shortid: ^2.2.15
+ slugify: ^1.4.0
+ source-map-support: ^0.5.16
+ addons/addon-base-raas/packages/serverless-packer:
+ dependencies:
+ chalk: 2.4.2
+ cross-spawn: 6.0.5
+ lodash: 4.17.15
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ chalk: ^2.4.2
+ cross-spawn: ^6.0.5
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ source-map-support: ^0.5.16
+ addons/addon-base-rest-api/packages/api-handler-factory:
+ dependencies:
+ '@aws-ee/base-services-container': 'link:../../../addon-base/packages/services-container'
+ body-parser: 1.19.0
+ compression: 1.7.4
+ cors: 2.8.5
+ express: 4.17.1
+ lodash: 4.17.15
+ serverless-http: 2.3.2
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-services-container': 'workspace:*'
+ body-parser: ^1.19.0
+ compression: ^1.7.4
+ cors: ^2.8.5
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ express: ^4.17.1
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ serverless-http: ^2.3.1
+ source-map-support: ^0.5.16
+ addons/addon-base-rest-api/packages/base-api-handler:
+ dependencies:
+ '@aws-ee/base-api-services': 'link:../services'
+ '@aws-ee/base-services': 'link:../../../addon-base/packages/services'
+ '@aws-ee/base-services-container': 'link:../../../addon-base/packages/services-container'
+ aws-sdk: 2.656.0
+ generate-password: 1.5.1
+ lodash: 4.17.15
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-api-services': 'workspace:*'
+ '@aws-ee/base-services': 'workspace:*'
+ '@aws-ee/base-services-container': 'workspace:*'
+ aws-sdk: ^2.647.0
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ generate-password: ^1.5.0
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ source-map-support: ^0.5.16
+ addons/addon-base-rest-api/packages/base-authn-handler:
+ dependencies:
+ '@aws-ee/base-api-services': 'link:../services'
+ '@aws-ee/base-services': 'link:../../../addon-base/packages/services'
+ '@aws-ee/base-services-container': 'link:../../../addon-base/packages/services-container'
+ aws-sdk: 2.656.0
+ generate-password: 1.5.1
+ lodash: 4.17.15
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-api-services': 'workspace:*'
+ '@aws-ee/base-services': 'workspace:*'
+ '@aws-ee/base-services-container': 'workspace:*'
+ aws-sdk: ^2.647.0
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ generate-password: ^1.5.0
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ source-map-support: ^0.5.16
+ addons/addon-base-rest-api/packages/base-controllers:
+ dependencies:
+ '@aws-ee/base-api-services': 'link:../services'
+ '@aws-ee/base-services': 'link:../../../addon-base/packages/services'
+ '@aws-ee/base-services-container': 'link:../../../addon-base/packages/services-container'
+ lodash: 4.17.15
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-api-services': 'workspace:*'
+ '@aws-ee/base-services': 'workspace:*'
+ '@aws-ee/base-services-container': 'workspace:*'
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ source-map-support: ^0.5.16
+ addons/addon-base-rest-api/packages/services:
+ dependencies:
+ '@aws-ee/base-services': 'link:../../../addon-base/packages/services'
+ '@aws-ee/base-services-container': 'link:../../../addon-base/packages/services-container'
+ ajv: 6.12.0
+ aws-sdk: 2.656.0
+ jsonwebtoken: 8.5.1
+ jwk-to-pem: 2.0.3
+ jwt-decode: 2.2.0
+ lodash: 4.17.15
+ request: 2.88.2
+ underscore: 1.10.2
+ uuid: 3.4.0
+ validatorjs: 3.18.1
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-services': 'workspace:*'
+ '@aws-ee/base-services-container': 'workspace:*'
+ ajv: ^6.11.0
+ aws-sdk: ^2.647.0
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ jsonwebtoken: ^8.5.1
+ jwk-to-pem: ^2.0.3
+ jwt-decode: ^2.2.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ request: ^2.88.2
+ source-map-support: ^0.5.16
+ underscore: ^1.9.2
+ uuid: ^3.4.0
+ validatorjs: ^3.18.1
+ addons/addon-base-ui/packages/base-ui:
+ dependencies:
+ aws-sdk: 2.656.0
+ chart.js: 2.9.3
+ classnames: 2.2.6
+ jwt-decode: 2.2.0
+ lodash: 4.17.15
+ mobx: 5.15.4
+ mobx-react: 6.2.2_mobx@5.15.4+react@16.13.1
+ mobx-react-form: 2.0.8_mobx@5.15.4
+ mobx-state-tree: 3.15.0_mobx@5.15.4
+ numeral: 2.0.6
+ react: 16.13.1
+ react-avatar: 3.9.2_prop-types@15.7.2+react@16.13.1
+ react-beautiful-dnd: 11.0.5_react-dom@16.13.1+react@16.13.1
+ react-chartjs-2: 2.9.0_993042a9951b76ec634148f49ee6c836
+ react-dom: 16.13.1_react@16.13.1
+ react-dotdotdot: 1.3.1_eb0d650be231ffd0ace4a30b38162117
+ react-idle-timer: 4.2.12_eb0d650be231ffd0ace4a30b38162117
+ react-responsive-carousel: 3.1.57
+ react-router-dom: 5.1.2_react@16.13.1
+ react-select: 3.1.0_react-dom@16.13.1+react@16.13.1
+ react-table: 6.11.5_eb0d650be231ffd0ace4a30b38162117
+ react-timeago: 4.4.0_react@16.13.1
+ semantic-ui-react: 0.88.2_react-dom@16.13.1+react@16.13.1
+ showdown: 1.9.1
+ toastr: 2.1.4
+ typeface-lato: 0.0.75
+ validatorjs: 3.18.1
+ devDependencies:
+ '@aws-ee/base-serverless-settings-helper': 'link:../../../addon-base/packages/serverless-settings-helper'
+ '@aws-ee/base-serverless-ui-tools': 'link:../serverless-ui-tools'
+ '@babel/cli': 7.8.4_@babel+core@7.9.0
+ '@babel/core': 7.9.0
+ '@babel/plugin-proposal-class-properties': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-react-jsx': 7.9.4_@babel+core@7.9.0
+ '@babel/preset-env': 7.9.5_@babel+core@7.9.0
+ '@babel/preset-react': 7.9.4_@babel+core@7.9.0
+ babel-eslint: 10.1.0_eslint@6.8.0
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_7221e9efc3e1df952f9031babfc371af
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-jsx-a11y: 6.2.3_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ eslint-plugin-react: 7.19.0_eslint@6.8.0
+ eslint-plugin-react-hooks: 1.7.0_eslint@6.8.0
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ prop-types: 15.7.2
+ serverless: 1.67.3
+ serverless-deployment-bucket: 1.1.1
+ typescript: 3.8.3
+ webpack: 4.41.2_webpack@4.41.2
+ specifiers:
+ '@aws-ee/base-serverless-settings-helper': 'workspace:*'
+ '@aws-ee/base-serverless-ui-tools': 'workspace:*'
+ '@babel/cli': ^7.8.4
+ '@babel/core': ^7.8.6
+ '@babel/plugin-proposal-class-properties': ^7.8.3
+ '@babel/plugin-transform-react-jsx': ^7.8.3
+ '@babel/preset-env': ^7.8.6
+ '@babel/preset-react': ^7.8.3
+ aws-sdk: ^2.647.0
+ babel-eslint: ^10.0.3
+ chart.js: ^2.9.3
+ classnames: ^2.2.6
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.0.1
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-jsx-a11y: ^6.2.3
+ eslint-plugin-prettier: ^3.1.2
+ eslint-plugin-react: ^7.18.3
+ eslint-plugin-react-hooks: ^1.7.0
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ jwt-decode: ^2.2.0
+ lodash: ^4.17.15
+ mobx: ^5.15.4
+ mobx-react: ^6.1.7
+ mobx-react-form: ^2.0.8
+ mobx-state-tree: ^3.15.0
+ numeral: ^2.0.6
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ prop-types: ^15.7.2
+ react: ^16.12.0
+ react-avatar: ^3.9.0
+ react-beautiful-dnd: ^11.0.5
+ react-chartjs-2: ^2.9.0
+ react-dom: ^16.12.0
+ react-dotdotdot: ^1.3.1
+ react-idle-timer: ^4.2.12
+ react-responsive-carousel: ^3.1.51
+ react-router-dom: ^5.1.2
+ react-select: ^3.0.8
+ react-table: ^6.11.5
+ react-timeago: ^4.4.0
+ semantic-ui-react: ^0.88.2
+ serverless: ^1.63.0
+ serverless-deployment-bucket: ^1.1.0
+ showdown: ^1.9.1
+ toastr: ^2.1.4
+ typeface-lato: 0.0.75
+ typescript: ^3.7.5
+ validatorjs: ^3.18.1
+ webpack: 4.41.2
+ addons/addon-base-ui/packages/serverless-ui-tools:
+ dependencies:
+ aws-sdk: 2.656.0
+ chalk: 2.4.2
+ cross-spawn: 7.0.2
+ lodash: 4.17.15
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ specifiers:
+ aws-sdk: ^2.647.0
+ chalk: ^2.4.2
+ cross-spawn: ^7.0.1
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ addons/addon-base-workflow-api/packages/base-worklfow-api:
+ dependencies:
+ '@aws-ee/base-controllers': 'link:../../../addon-base-rest-api/packages/base-controllers'
+ '@aws-ee/base-workflow-core': 'link:../../../addon-base-workflow/packages/base-workflow-core'
+ lodash: 4.17.15
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-controllers': 'workspace:*'
+ '@aws-ee/base-workflow-core': 'workspace:*'
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.1.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ source-map-support: ^0.5.16
+ addons/addon-base-workflow-ui/packages/base-workflow-ui:
+ dependencies:
+ '@aws-ee/base-ui': 'link:../../../addon-base-ui/packages/base-ui'
+ aws-sdk: 2.656.0
+ chart.js: 2.9.3
+ classnames: 2.2.6
+ jwt-decode: 2.2.0
+ lodash: 4.17.15
+ mobx: 5.15.4
+ mobx-react: 6.2.2_mobx@5.15.4+react@16.13.1
+ mobx-react-form: 2.0.8_mobx@5.15.4
+ mobx-state-tree: 3.15.0_mobx@5.15.4
+ numeral: 2.0.6
+ react: 16.13.1
+ react-avatar: 3.9.2_prop-types@15.7.2+react@16.13.1
+ react-beautiful-dnd: 11.0.5_react-dom@16.13.1+react@16.13.1
+ react-chartjs-2: 2.9.0_993042a9951b76ec634148f49ee6c836
+ react-dom: 16.13.1_react@16.13.1
+ react-dotdotdot: 1.3.1_eb0d650be231ffd0ace4a30b38162117
+ react-responsive-carousel: 3.1.57
+ react-router-dom: 5.1.2_react@16.13.1
+ react-select: 3.1.0_react-dom@16.13.1+react@16.13.1
+ react-table: 6.11.5_eb0d650be231ffd0ace4a30b38162117
+ react-timeago: 4.4.0_react@16.13.1
+ semantic-ui-react: 0.88.2_react-dom@16.13.1+react@16.13.1
+ showdown: 1.9.1
+ toastr: 2.1.4
+ typeface-lato: 0.0.75
+ validatorjs: 3.18.1
+ devDependencies:
+ '@babel/cli': 7.8.4_@babel+core@7.9.0
+ '@babel/core': 7.9.0
+ '@babel/plugin-proposal-class-properties': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-react-jsx': 7.9.4_@babel+core@7.9.0
+ '@babel/preset-env': 7.9.5_@babel+core@7.9.0
+ '@babel/preset-react': 7.9.4_@babel+core@7.9.0
+ babel-eslint: 10.1.0_eslint@6.8.0
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_7221e9efc3e1df952f9031babfc371af
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-jsx-a11y: 6.2.3_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ eslint-plugin-react: 7.19.0_eslint@6.8.0
+ eslint-plugin-react-hooks: 1.7.0_eslint@6.8.0
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ prop-types: 15.7.2
+ serverless: 1.67.3
+ serverless-deployment-bucket: 1.1.1
+ typescript: 3.8.3
+ webpack: 4.41.2_webpack@4.41.2
+ specifiers:
+ '@aws-ee/base-ui': 'workspace:*'
+ '@babel/cli': ^7.8.4
+ '@babel/core': ^7.8.6
+ '@babel/plugin-proposal-class-properties': ^7.8.3
+ '@babel/plugin-transform-react-jsx': ^7.8.3
+ '@babel/preset-env': ^7.8.6
+ '@babel/preset-react': ^7.8.3
+ aws-sdk: ^2.647.0
+ babel-eslint: ^10.0.3
+ chart.js: ^2.9.3
+ classnames: ^2.2.6
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.0.1
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-jsx-a11y: ^6.2.3
+ eslint-plugin-prettier: ^3.1.2
+ eslint-plugin-react: ^7.18.3
+ eslint-plugin-react-hooks: ^1.7.0
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ jwt-decode: ^2.2.0
+ lodash: ^4.17.15
+ mobx: ^5.15.4
+ mobx-react: ^6.1.7
+ mobx-react-form: ^2.0.8
+ mobx-state-tree: ^3.15.0
+ numeral: ^2.0.6
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ prop-types: ^15.7.2
+ react: ^16.12.0
+ react-avatar: ^3.9.0
+ react-beautiful-dnd: ^11.0.5
+ react-chartjs-2: ^2.9.0
+ react-dom: ^16.12.0
+ react-dotdotdot: ^1.3.1
+ react-responsive-carousel: ^3.1.51
+ react-router-dom: ^5.1.2
+ react-select: ^3.0.8
+ react-table: ^6.11.5
+ react-timeago: ^4.4.0
+ semantic-ui-react: ^0.88.2
+ serverless: ^1.63.0
+ serverless-deployment-bucket: ^1.1.0
+ showdown: ^1.9.1
+ toastr: ^2.1.4
+ typeface-lato: 0.0.75
+ typescript: ^3.7.5
+ validatorjs: ^3.18.1
+ webpack: 4.41.2
+ addons/addon-base-workflow/packages/base-workflow-core:
+ dependencies:
+ '@aws-ee/base-services': 'link:../../../addon-base/packages/services'
+ '@aws-ee/base-services-container': 'link:../../../addon-base/packages/services-container'
+ '@aws-ee/workflow-engine': 'link:../workflow-engine'
+ lodash: 4.17.15
+ shortid: 2.2.15
+ slugify: 1.4.0
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-services': 'workspace:*'
+ '@aws-ee/base-services-container': 'workspace:*'
+ '@aws-ee/workflow-engine': 'workspace:*'
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.1.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ shortid: ^2.2.15
+ slugify: ^1.4.0
+ source-map-support: ^0.5.16
+ addons/addon-base-workflow/packages/base-workflow-templates:
+ dependencies:
+ '@aws-ee/base-workflow-core': 'link:../base-workflow-core'
+ lodash: 4.17.15
+ shortid: 2.2.15
+ slugify: 1.4.0
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-workflow-core': 'workspace:*'
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.1.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ shortid: ^2.2.15
+ slugify: ^1.4.0
+ source-map-support: ^0.5.16
+ addons/addon-base-workflow/packages/base-worklfow-steps:
+ dependencies:
+ '@aws-ee/base-workflow-core': 'link:../base-workflow-core'
+ lodash: 4.17.15
+ shortid: 2.2.15
+ slugify: 1.4.0
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-workflow-core': 'workspace:*'
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.1.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ shortid: ^2.2.15
+ slugify: ^1.4.0
+ source-map-support: ^0.5.16
+ addons/addon-base-workflow/packages/workflow-engine:
+ dependencies:
+ lodash: 4.17.15
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.1.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ source-map-support: ^0.5.16
+ addons/addon-base/packages/serverless-backend-tools:
+ dependencies:
+ aws-sdk: 2.656.0
+ chalk: 2.4.2
+ fs-extra: 8.1.0
+ lodash: 4.17.15
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ specifiers:
+ aws-sdk: ^2.647.0
+ chalk: ^2.4.2
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ fs-extra: ^8.1.0
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ addons/addon-base/packages/serverless-settings-helper:
+ dependencies:
+ aws-sdk: 2.656.0
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_7221e9efc3e1df952f9031babfc371af
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-jsx-a11y: 6.2.3_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ eslint-plugin-react: 7.19.0_eslint@6.8.0
+ eslint-plugin-react-hooks: 1.7.0_eslint@6.8.0
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ serverless: 1.67.3
+ specifiers:
+ aws-sdk: ^2.647.0
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.0.1
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-jsx-a11y: ^6.2.3
+ eslint-plugin-prettier: ^3.1.2
+ eslint-plugin-react: ^7.18.3
+ eslint-plugin-react-hooks: ^1.7.0
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ serverless: ^1.63.0
+ addons/addon-base/packages/services:
+ dependencies:
+ '@aws-ee/base-services-container': 'link:../services-container'
+ ajv: 6.12.0
+ aws-sdk: 2.656.0
+ cycle: 1.0.3
+ jsonwebtoken: 8.5.1
+ jwk-to-pem: 2.0.3
+ jwt-decode: 2.2.0
+ lodash: 4.17.15
+ request: 2.88.2
+ underscore: 1.10.2
+ uuid: 3.4.0
+ validatorjs: 3.18.1
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-services-container': 'workspace:*'
+ ajv: ^6.11.0
+ aws-sdk: ^2.647.0
+ cycle: ^1.0.3
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ jsonwebtoken: ^8.5.1
+ jwk-to-pem: ^2.0.3
+ jwt-decode: ^2.2.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ request: ^2.88.2
+ source-map-support: ^0.5.16
+ underscore: ^1.9.2
+ uuid: ^3.4.0
+ validatorjs: ^3.18.1
+ addons/addon-base/packages/services-container:
+ dependencies:
+ aws-sdk: 2.656.0
+ toposort: 2.0.2
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ lodash: 4.17.15
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ aws-sdk: ^2.647.0
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ source-map-support: ^0.5.16
+ toposort: ^2.0.2
+ main/cicd/cicd-pipeline:
+ devDependencies:
+ '@aws-ee/base-serverless-settings-helper': 'link:../../../addons/addon-base/packages/serverless-settings-helper'
+ serverless: 1.67.3
+ serverless-deployment-bucket: 1.1.1
+ serverless-plugin-scripts: 1.0.2
+ optionalDependencies:
+ fsevents: 2.1.2
+ specifiers:
+ '@aws-ee/base-serverless-settings-helper': 'workspace:*'
+ fsevents: '*'
+ serverless: ^1.63.0
+ serverless-deployment-bucket: ^1.1.0
+ serverless-plugin-scripts: ^1.0.2
+ main/cicd/cicd-source:
+ devDependencies:
+ '@aws-ee/base-serverless-settings-helper': 'link:../../../addons/addon-base/packages/serverless-settings-helper'
+ aws-sdk: 2.656.0
+ serverless: 1.67.3
+ serverless-deployment-bucket: 1.1.1
+ optionalDependencies:
+ fsevents: 2.1.2
+ specifiers:
+ '@aws-ee/base-serverless-settings-helper': 'workspace:*'
+ aws-sdk: ^2.647.0
+ fsevents: '*'
+ serverless: ^1.63.0
+ serverless-deployment-bucket: ^1.1.0
+ main/integration-tests:
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_9bfaab310611d9c7b04264b84523c27a
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 23.8.2_eslint@6.8.0
+ eslint-plugin-jsx-a11y: 6.2.3_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 25.3.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ request: 2.88.2
+ request-promise-native: 1.0.8_request@2.88.2
+ specifiers:
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.0.1
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^23.7.0
+ eslint-plugin-jsx-a11y: ^6.2.3
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^25.1.0
+ jest-junit: ^10.0.0
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ request: ^2.88.0
+ request-promise-native: ^1.0.8
+ main/packages/services:
+ dependencies:
+ '@aws-ee/base-services': 'link:../../../addons/addon-base/packages/services'
+ '@aws-ee/base-services-container': 'link:../../../addons/addon-base/packages/services-container'
+ lodash: 4.17.15
+ devDependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ source-map-support: 0.5.16
+ specifiers:
+ '@aws-ee/base-services': 'workspace:*'
+ '@aws-ee/base-services-container': 'workspace:*'
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.1.0
+ eslint-config-airbnb-base: ^14.1.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ source-map-support: ^0.5.16
+ main/solution/backend:
+ dependencies:
+ '@aws-ee/base-api-handler': 'link:../../../addons/addon-base-rest-api/packages/base-api-handler'
+ '@aws-ee/base-api-handler-factory': 'link:../../../addons/addon-base-rest-api/packages/api-handler-factory'
+ '@aws-ee/base-api-services': 'link:../../../addons/addon-base-rest-api/packages/services'
+ '@aws-ee/base-authn-handler': 'link:../../../addons/addon-base-rest-api/packages/base-authn-handler'
+ '@aws-ee/base-controllers': 'link:../../../addons/addon-base-rest-api/packages/base-controllers'
+ '@aws-ee/base-raas-cfn-templates': 'link:../../../addons/addon-base-raas/packages/base-raas-cfn-templates'
+ '@aws-ee/base-raas-rest-api': 'link:../../../addons/addon-base-raas/packages/base-raas-rest-api'
+ '@aws-ee/base-raas-services': 'link:../../../addons/addon-base-raas/packages/base-raas-services'
+ '@aws-ee/base-raas-workflow-steps': 'link:../../../addons/addon-base-raas/packages/base-raas-workflow-steps'
+ '@aws-ee/base-raas-workflows': 'link:../../../addons/addon-base-raas/packages/base-raas-workflows'
+ '@aws-ee/base-services': 'link:../../../addons/addon-base/packages/services'
+ '@aws-ee/base-services-container': 'link:../../../addons/addon-base/packages/services-container'
+ '@aws-ee/base-workflow-api': 'link:../../../addons/addon-base-workflow-api/packages/base-worklfow-api'
+ '@aws-ee/base-workflow-core': 'link:../../../addons/addon-base-workflow/packages/base-workflow-core'
+ '@aws-ee/base-workflow-steps': 'link:../../../addons/addon-base-workflow/packages/base-worklfow-steps'
+ aws-sdk: 2.656.0
+ js-yaml: 3.13.1
+ jwt-decode: 2.2.0
+ lodash: 4.17.15
+ node-fetch: 2.6.0
+ services: 'link:../../packages/services'
+ devDependencies:
+ '@aws-ee/base-serverless-backend-tools': 'link:../../../addons/addon-base/packages/serverless-backend-tools'
+ '@aws-ee/base-serverless-settings-helper': 'link:../../../addons/addon-base/packages/serverless-settings-helper'
+ '@babel/core': 7.9.0
+ '@babel/plugin-transform-runtime': 7.9.0_@babel+core@7.9.0
+ '@babel/preset-env': 7.9.5_@babel+core@7.9.0
+ babel-jest: 24.9.0_@babel+core@7.9.0
+ babel-loader: 8.1.0_@babel+core@7.9.0+webpack@4.42.1
+ babel-plugin-source-map-support: 2.1.1
+ copy-webpack-plugin: 5.1.1_webpack@4.42.1
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ js-yaml-loader: 1.2.2
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ serverless: 1.67.3
+ serverless-deployment-bucket: 1.1.1
+ serverless-offline: 5.12.1_serverless@1.67.3
+ serverless-s3-sync: 1.12.0
+ serverless-webpack: 5.3.1_webpack@4.42.1
+ source-map-support: 0.5.16
+ webpack: 4.42.1_webpack@4.42.1
+ webpack-cli: 3.3.11_webpack@4.42.1
+ webpack-node-externals: 1.7.2
+ optionalDependencies:
+ fsevents: 2.1.2
+ specifiers:
+ '@aws-ee/base-api-handler': 'workspace:*'
+ '@aws-ee/base-api-handler-factory': 'workspace:*'
+ '@aws-ee/base-api-services': 'workspace:*'
+ '@aws-ee/base-authn-handler': 'workspace:*'
+ '@aws-ee/base-controllers': 'workspace:*'
+ '@aws-ee/base-raas-cfn-templates': 'workspace:*'
+ '@aws-ee/base-raas-rest-api': 'workspace:*'
+ '@aws-ee/base-raas-services': 'workspace:*'
+ '@aws-ee/base-raas-workflow-steps': 'workspace:*'
+ '@aws-ee/base-raas-workflows': 'workspace:*'
+ '@aws-ee/base-serverless-backend-tools': 'workspace:*'
+ '@aws-ee/base-serverless-settings-helper': 'workspace:*'
+ '@aws-ee/base-services': 'workspace:*'
+ '@aws-ee/base-services-container': 'workspace:*'
+ '@aws-ee/base-workflow-api': 'workspace:*'
+ '@aws-ee/base-workflow-core': 'workspace:*'
+ '@aws-ee/base-workflow-steps': 'workspace:*'
+ '@babel/core': ^7.8.4
+ '@babel/plugin-transform-runtime': ^7.8.3
+ '@babel/preset-env': ^7.8.4
+ aws-sdk: ^2.647.0
+ babel-jest: ^24.9.0
+ babel-loader: ^8.0.6
+ babel-plugin-source-map-support: ^2.1.1
+ copy-webpack-plugin: ^5.1.1
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ fsevents: '*'
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ js-yaml: ^3.13.1
+ js-yaml-loader: ^1.2.2
+ jwt-decode: ^2.2.0
+ lodash: ^4.17.15
+ node-fetch: ^2.6.0
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ serverless: ^1.63.0
+ serverless-deployment-bucket: ^1.1.0
+ serverless-offline: ^5.12.1
+ serverless-s3-sync: ^1.12.0
+ serverless-webpack: ^5.3.1
+ services: 'workspace:*'
+ source-map-support: ^0.5.16
+ webpack: ^4.41.5
+ webpack-cli: ^3.3.10
+ webpack-node-externals: ^1.7.2
+ main/solution/edge-lambda:
+ devDependencies:
+ '@aws-ee/base-serverless-settings-helper': 'link:../../../addons/addon-base/packages/serverless-settings-helper'
+ serverless: 1.67.3
+ serverless-deployment-bucket: 1.1.1
+ specifiers:
+ '@aws-ee/base-serverless-settings-helper': 'workspace:*'
+ serverless: ^1.63.0
+ serverless-deployment-bucket: ^1.1.0
+ main/solution/infrastructure:
+ devDependencies:
+ '@aws-ee/base-serverless-settings-helper': 'link:../../../addons/addon-base/packages/serverless-settings-helper'
+ serverless: 1.67.3
+ serverless-deployment-bucket: 1.1.1
+ specifiers:
+ '@aws-ee/base-serverless-settings-helper': 'workspace:*'
+ serverless: ^1.63.0
+ serverless-deployment-bucket: ^1.1.0
+ main/solution/machine-images:
+ devDependencies:
+ '@aws-ee/base-serverless-settings-helper': 'link:../../../addons/addon-base/packages/serverless-settings-helper'
+ '@aws-ee/serverless-packer': 'link:../../../addons/addon-base-raas/packages/serverless-packer'
+ serverless: 1.67.3
+ specifiers:
+ '@aws-ee/base-serverless-settings-helper': 'workspace:*'
+ '@aws-ee/serverless-packer': 'workspace:*'
+ serverless: ^1.63.0
+ main/solution/post-deployment:
+ dependencies:
+ '@aws-ee/base-post-deployment': 'link:../../../addons/addon-base-post-deployment/packages/base-post-deployment'
+ '@aws-ee/base-raas-post-deployment': 'link:../../../addons/addon-base-raas/packages/base-raas-post-deployment'
+ '@aws-ee/base-raas-rest-api': 'link:../../../addons/addon-base-raas/packages/base-raas-rest-api'
+ '@aws-ee/base-raas-services': 'link:../../../addons/addon-base-raas/packages/base-raas-services'
+ '@aws-ee/base-raas-workflow-steps': 'link:../../../addons/addon-base-raas/packages/base-raas-workflow-steps'
+ '@aws-ee/base-raas-workflows': 'link:../../../addons/addon-base-raas/packages/base-raas-workflows'
+ '@aws-ee/base-services': 'link:../../../addons/addon-base/packages/services'
+ '@aws-ee/base-services-container': 'link:../../../addons/addon-base/packages/services-container'
+ '@aws-ee/base-workflow-core': 'link:../../../addons/addon-base-workflow/packages/base-workflow-core'
+ '@aws-ee/base-workflow-steps': 'link:../../../addons/addon-base-workflow/packages/base-worklfow-steps'
+ '@aws-ee/base-workflow-templates': 'link:../../../addons/addon-base-workflow/packages/base-workflow-templates'
+ aws-sdk: 2.656.0
+ lodash: 4.17.15
+ devDependencies:
+ '@aws-ee/base-serverless-backend-tools': 'link:../../../addons/addon-base/packages/serverless-backend-tools'
+ '@aws-ee/base-serverless-settings-helper': 'link:../../../addons/addon-base/packages/serverless-settings-helper'
+ '@babel/core': 7.9.0
+ '@babel/preset-env': 7.9.5_@babel+core@7.9.0
+ babel-loader: 8.1.0_@babel+core@7.9.0+webpack@4.42.1
+ babel-plugin-source-map-support: 2.1.1
+ copy-webpack-plugin: 5.1.1_webpack@4.42.1
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ js-yaml-loader: 1.2.2
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ serverless: 1.67.3
+ serverless-deployment-bucket: 1.1.1
+ serverless-s3-sync: 1.12.0
+ serverless-webpack: 5.3.1_webpack@4.42.1
+ source-map-support: 0.5.16
+ webpack: 4.42.1_webpack@4.42.1
+ specifiers:
+ '@aws-ee/base-post-deployment': 'workspace:*'
+ '@aws-ee/base-raas-post-deployment': 'workspace:*'
+ '@aws-ee/base-raas-rest-api': 'workspace:*'
+ '@aws-ee/base-raas-services': 'workspace:*'
+ '@aws-ee/base-raas-workflow-steps': 'workspace:*'
+ '@aws-ee/base-raas-workflows': 'workspace:*'
+ '@aws-ee/base-serverless-backend-tools': 'workspace:*'
+ '@aws-ee/base-serverless-settings-helper': 'workspace:*'
+ '@aws-ee/base-services': 'workspace:*'
+ '@aws-ee/base-services-container': 'workspace:*'
+ '@aws-ee/base-workflow-core': 'workspace:*'
+ '@aws-ee/base-workflow-steps': 'workspace:*'
+ '@aws-ee/base-workflow-templates': 'workspace:*'
+ '@babel/core': ^7.8.4
+ '@babel/preset-env': ^7.8.4
+ aws-sdk: ^2.647.0
+ babel-loader: ^8.0.6
+ babel-plugin-source-map-support: ^2.1.1
+ copy-webpack-plugin: ^5.1.1
+ eslint: ^6.8.0
+ eslint-config-airbnb-base: ^14.0.0
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-prettier: ^3.1.2
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ js-yaml-loader: ^1.2.2
+ lodash: ^4.17.15
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ serverless: ^1.63.0
+ serverless-deployment-bucket: ^1.1.0
+ serverless-s3-sync: ^1.10.2
+ serverless-webpack: ^5.3.1
+ source-map-support: ^0.5.16
+ webpack: ^4.41.5
+ main/solution/prepare-master-acc:
+ devDependencies:
+ '@aws-ee/base-serverless-settings-helper': 'link:../../../addons/addon-base/packages/serverless-settings-helper'
+ serverless: 1.67.3
+ serverless-deployment-bucket: 1.1.1
+ specifiers:
+ '@aws-ee/base-serverless-settings-helper': 'workspace:*'
+ serverless: ^1.63.0
+ serverless-deployment-bucket: ^1.1.0
+ main/solution/ui:
+ dependencies:
+ '@aws-ee/base-raas-ui': 'link:../../../addons/addon-base-raas-ui/packages/base-raas-ui'
+ '@aws-ee/base-ui': 'link:../../../addons/addon-base-ui/packages/base-ui'
+ '@aws-ee/base-workflow-ui': 'link:../../../addons/addon-base-workflow-ui/packages/base-workflow-ui'
+ aws-sdk: 2.656.0
+ lodash: 4.17.15
+ mobx: 5.15.4
+ mobx-react: 6.2.2_mobx@5.15.4+react@16.13.1
+ mobx-react-form: 2.0.8_mobx@5.15.4
+ mobx-state-tree: 3.15.0_mobx@5.15.4
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-avatar: 3.9.2_prop-types@15.7.2+react@16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ react-router-dom: 5.1.2_react@16.13.1
+ react-table: 6.11.5_eb0d650be231ffd0ace4a30b38162117
+ semantic-ui-react: 0.88.2_react-dom@16.13.1+react@16.13.1
+ toastr: 2.1.4
+ typeface-lato: 0.0.75
+ uuid: 3.4.0
+ devDependencies:
+ '@aws-ee/base-serverless-settings-helper': 'link:../../../addons/addon-base/packages/serverless-settings-helper'
+ '@aws-ee/base-serverless-ui-tools': 'link:../../../addons/addon-base-ui/packages/serverless-ui-tools'
+ babel-eslint: 10.1.0_eslint@6.8.0
+ eslint: 6.8.0
+ eslint-config-airbnb: 18.1.0_93d707b3c4a28e806b96154710814f94
+ eslint-config-prettier: 6.10.1_eslint@6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jest: 22.21.0_eslint@6.8.0
+ eslint-plugin-jsx-a11y: 6.2.3_eslint@6.8.0
+ eslint-plugin-prettier: 3.1.2_eslint@6.8.0+prettier@1.19.1
+ eslint-plugin-react: 7.19.0_eslint@6.8.0
+ eslint-plugin-react-hooks: 2.5.1_eslint@6.8.0
+ husky: 3.1.0
+ jest: 24.9.0
+ jest-junit: 10.0.0
+ prettier: 1.19.1
+ pretty-quick: 1.11.1_prettier@1.19.1
+ react-scripts: 3.4.1
+ serverless: 1.67.3
+ serverless-deployment-bucket: 1.1.1
+ specifiers:
+ '@aws-ee/base-raas-ui': 'workspace:*'
+ '@aws-ee/base-serverless-settings-helper': 'workspace:*'
+ '@aws-ee/base-serverless-ui-tools': 'workspace:*'
+ '@aws-ee/base-ui': 'workspace:*'
+ '@aws-ee/base-workflow-ui': 'workspace:*'
+ aws-sdk: ^2.647.0
+ babel-eslint: ^10.0.3
+ eslint: ^6.8.0
+ eslint-config-airbnb: ^18.0.1
+ eslint-config-prettier: ^6.10.0
+ eslint-import-resolver-node: ^0.3.3
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jest: ^22.21.0
+ eslint-plugin-jsx-a11y: ^6.2.3
+ eslint-plugin-prettier: ^3.1.2
+ eslint-plugin-react: ^7.18.3
+ eslint-plugin-react-hooks: ^2.0.1
+ husky: ^3.1.0
+ jest: ^24.9.0
+ jest-junit: ^10.0.0
+ lodash: ^4.17.15
+ mobx: ^5.15.4
+ mobx-react: ^6.1.7
+ mobx-react-form: ^2.0.8
+ mobx-state-tree: ^3.15.0
+ prettier: ^1.19.1
+ pretty-quick: ^1.11.1
+ prop-types: ^15.7.2
+ react: ^16.12.0
+ react-avatar: ^3.9.0
+ react-dom: ^16.12.0
+ react-router-dom: ^5.1.2
+ react-scripts: ^3.3.1
+ react-table: ^6.11.5
+ semantic-ui-react: ^0.88.2
+ serverless: ^1.63.0
+ serverless-deployment-bucket: ^1.1.0
+ toastr: ^2.1.4
+ typeface-lato: 0.0.75
+ uuid: ^3.3.3
+lockfileVersion: 5.1
+packages:
+ /2-thenable/1.0.0:
+ dependencies:
+ d: 1.0.1
+ es5-ext: 0.10.53
+ dev: true
+ resolution:
+ integrity: sha512-HqiDzaLDFCXkcCO/SwoyhRwqYtINFHF7t9BDRq4x90TOKNAJpiqUt9X5lQ08bwxYzc067HUywDjGySpebHcUpw==
+ /@auth0/auth0-spa-js/1.6.5:
+ dependencies:
+ browser-tabs-lock: 1.2.8
+ core-js: 3.6.4
+ es-cookie: 1.3.2
+ fast-text-encoding: 1.0.1
+ promise-polyfill: 8.1.3
+ unfetch: 4.1.0
+ dev: false
+ resolution:
+ integrity: sha512-pS5jF5DAHXeDssN9cJwOqAbgLYhJaXD2EBgeXkjfB3rrNcd7bYC9rOGckRTqyS2k2A05/N2aaRFnju81AgSDgQ==
+ /@auth0/s3/1.0.0:
+ dependencies:
+ aws-sdk: 2.656.0
+ fd-slicer: 1.0.1
+ findit2: 2.2.3
+ graceful-fs: 4.1.15
+ mime: 2.4.4
+ mkdirp: 0.5.5
+ pend: 1.2.0
+ rimraf: 2.2.8
+ streamsink: 1.2.0
+ dev: true
+ engines:
+ node: '>=8.0.0'
+ resolution:
+ integrity: sha512-O8PTXJnA7n8ONBSwqlWa+aZ/vlOdZYnSCGQt25h87ALWNItY/Yij79TOnzIkMTJZ8aCpGXQPuIRziLmBliV++Q==
+ /@babel/cli/7.8.4_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ commander: 4.1.1
+ convert-source-map: 1.7.0
+ fs-readdir-recursive: 1.1.0
+ glob: 7.1.6
+ lodash: 4.17.15
+ make-dir: 2.1.0
+ slash: 2.0.0
+ source-map: 0.5.7
+ dev: true
+ hasBin: true
+ optionalDependencies:
+ chokidar: 2.1.8
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-XXLgAm6LBbaNxaGhMAznXXaxtCWfuv6PIDJ9Alsy9JYTOh+j2jJz+L/162kkfU1j/pTSxK1xGmlwI4pdIMkoag==
+ /@babel/code-frame/7.8.3:
+ dependencies:
+ '@babel/highlight': 7.9.0
+ resolution:
+ integrity: sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==
+ /@babel/compat-data/7.9.0:
+ dependencies:
+ browserslist: 4.11.1
+ invariant: 2.2.4
+ semver: 5.7.1
+ dev: true
+ resolution:
+ integrity: sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g==
+ /@babel/core/7.9.0:
+ dependencies:
+ '@babel/code-frame': 7.8.3
+ '@babel/generator': 7.9.5
+ '@babel/helper-module-transforms': 7.9.0
+ '@babel/helpers': 7.9.2
+ '@babel/parser': 7.9.4
+ '@babel/template': 7.8.6
+ '@babel/traverse': 7.9.5
+ '@babel/types': 7.9.5
+ convert-source-map: 1.7.0
+ debug: 4.1.1
+ gensync: 1.0.0-beta.1
+ json5: 2.1.3
+ lodash: 4.17.15
+ resolve: 1.15.1
+ semver: 5.7.1
+ source-map: 0.5.7
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==
+ /@babel/generator/7.9.5:
+ dependencies:
+ '@babel/types': 7.9.5
+ jsesc: 2.5.2
+ lodash: 4.17.15
+ source-map: 0.5.7
+ dev: true
+ resolution:
+ integrity: sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==
+ /@babel/helper-annotate-as-pure/7.8.3:
+ dependencies:
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==
+ /@babel/helper-builder-binary-assignment-operator-visitor/7.8.3:
+ dependencies:
+ '@babel/helper-explode-assignable-expression': 7.8.3
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw==
+ /@babel/helper-builder-react-jsx-experimental/7.9.5:
+ dependencies:
+ '@babel/helper-annotate-as-pure': 7.8.3
+ '@babel/helper-module-imports': 7.8.3
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-HAagjAC93tk748jcXpZ7oYRZH485RCq/+yEv9SIWezHRPv9moZArTnkUNciUNzvwHUABmiWKlcxJvMcu59UwTg==
+ /@babel/helper-builder-react-jsx/7.9.0:
+ dependencies:
+ '@babel/helper-annotate-as-pure': 7.8.3
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-weiIo4gaoGgnhff54GQ3P5wsUQmnSwpkvU0r6ZHq6TzoSzKy4JxHEgnxNytaKbov2a9z/CVNyzliuCOUPEX3Jw==
+ /@babel/helper-compilation-targets/7.8.7_@babel+core@7.9.0:
+ dependencies:
+ '@babel/compat-data': 7.9.0
+ '@babel/core': 7.9.0
+ browserslist: 4.11.1
+ invariant: 2.2.4
+ levenary: 1.1.1
+ semver: 5.7.1
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ resolution:
+ integrity: sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw==
+ /@babel/helper-create-class-features-plugin/7.9.5_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-function-name': 7.9.5
+ '@babel/helper-member-expression-to-functions': 7.8.3
+ '@babel/helper-optimise-call-expression': 7.8.3
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/helper-replace-supers': 7.8.6
+ '@babel/helper-split-export-declaration': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ resolution:
+ integrity: sha512-IipaxGaQmW4TfWoXdqjY0TzoXQ1HRS0kPpEgvjosb3u7Uedcq297xFqDQiCcQtRRwzIMif+N1MLVI8C5a4/PAA==
+ /@babel/helper-create-regexp-features-plugin/7.8.8_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-annotate-as-pure': 7.8.3
+ '@babel/helper-regex': 7.8.3
+ regexpu-core: 4.7.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ resolution:
+ integrity: sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg==
+ /@babel/helper-define-map/7.8.3:
+ dependencies:
+ '@babel/helper-function-name': 7.9.5
+ '@babel/types': 7.9.5
+ lodash: 4.17.15
+ dev: true
+ resolution:
+ integrity: sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g==
+ /@babel/helper-explode-assignable-expression/7.8.3:
+ dependencies:
+ '@babel/traverse': 7.9.5
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw==
+ /@babel/helper-function-name/7.9.5:
+ dependencies:
+ '@babel/helper-get-function-arity': 7.8.3
+ '@babel/template': 7.8.6
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==
+ /@babel/helper-get-function-arity/7.8.3:
+ dependencies:
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==
+ /@babel/helper-hoist-variables/7.8.3:
+ dependencies:
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg==
+ /@babel/helper-member-expression-to-functions/7.8.3:
+ dependencies:
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==
+ /@babel/helper-module-imports/7.8.3:
+ dependencies:
+ '@babel/types': 7.9.5
+ resolution:
+ integrity: sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==
+ /@babel/helper-module-transforms/7.9.0:
+ dependencies:
+ '@babel/helper-module-imports': 7.8.3
+ '@babel/helper-replace-supers': 7.8.6
+ '@babel/helper-simple-access': 7.8.3
+ '@babel/helper-split-export-declaration': 7.8.3
+ '@babel/template': 7.8.6
+ '@babel/types': 7.9.5
+ lodash: 4.17.15
+ dev: true
+ resolution:
+ integrity: sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==
+ /@babel/helper-optimise-call-expression/7.8.3:
+ dependencies:
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==
+ /@babel/helper-plugin-utils/7.8.3:
+ dev: true
+ resolution:
+ integrity: sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==
+ /@babel/helper-regex/7.8.3:
+ dependencies:
+ lodash: 4.17.15
+ dev: true
+ resolution:
+ integrity: sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ==
+ /@babel/helper-remap-async-to-generator/7.8.3:
+ dependencies:
+ '@babel/helper-annotate-as-pure': 7.8.3
+ '@babel/helper-wrap-function': 7.8.3
+ '@babel/template': 7.8.6
+ '@babel/traverse': 7.9.5
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA==
+ /@babel/helper-replace-supers/7.8.6:
+ dependencies:
+ '@babel/helper-member-expression-to-functions': 7.8.3
+ '@babel/helper-optimise-call-expression': 7.8.3
+ '@babel/traverse': 7.9.5
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==
+ /@babel/helper-simple-access/7.8.3:
+ dependencies:
+ '@babel/template': 7.8.6
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==
+ /@babel/helper-split-export-declaration/7.8.3:
+ dependencies:
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==
+ /@babel/helper-validator-identifier/7.9.5:
+ resolution:
+ integrity: sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==
+ /@babel/helper-wrap-function/7.8.3:
+ dependencies:
+ '@babel/helper-function-name': 7.9.5
+ '@babel/template': 7.8.6
+ '@babel/traverse': 7.9.5
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ==
+ /@babel/helpers/7.9.2:
+ dependencies:
+ '@babel/template': 7.8.6
+ '@babel/traverse': 7.9.5
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==
+ /@babel/highlight/7.9.0:
+ dependencies:
+ '@babel/helper-validator-identifier': 7.9.5
+ chalk: 2.4.2
+ js-tokens: 4.0.0
+ resolution:
+ integrity: sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==
+ /@babel/parser/7.9.4:
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==
+ /@babel/plugin-proposal-async-generator-functions/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/helper-remap-async-to-generator': 7.8.3
+ '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==
+ /@babel/plugin-proposal-class-properties/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-create-class-features-plugin': 7.9.5_@babel+core@7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==
+ /@babel/plugin-proposal-decorators/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-create-class-features-plugin': 7.9.5_@babel+core@7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-decorators': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-e3RvdvS4qPJVTe288DlXjwKflpfy1hr0j5dz5WpIYYeP7vQZg2WfAEIp8k5/Lwis/m5REXEteIz6rrcDtXXG7w==
+ /@babel/plugin-proposal-dynamic-import/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w==
+ /@babel/plugin-proposal-json-strings/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q==
+ /@babel/plugin-proposal-nullish-coalescing-operator/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw==
+ /@babel/plugin-proposal-numeric-separator/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-numeric-separator': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ==
+ /@babel/plugin-proposal-object-rest-spread/7.9.5_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-parameters': 7.9.5_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-VP2oXvAf7KCYTthbUHwBlewbl1Iq059f6seJGsxMizaCdgHIeczOr7FBqELhSqfkIl04Fi8okzWzl63UKbQmmg==
+ /@babel/plugin-proposal-optional-catch-binding/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw==
+ /@babel/plugin-proposal-optional-chaining/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==
+ /@babel/plugin-proposal-unicode-property-regex/7.8.8_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-create-regexp-features-plugin': 7.8.8_@babel+core@7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ engines:
+ node: '>=4'
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A==
+ /@babel/plugin-syntax-async-generators/7.8.4_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
+ /@babel/plugin-syntax-bigint/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==
+ /@babel/plugin-syntax-class-properties/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg==
+ /@babel/plugin-syntax-decorators/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-8Hg4dNNT9/LcA1zQlfwuKR8BUc/if7Q7NkTam9sGTcJphLwpf2g4S42uhspQrIrR+dpzE0dtTqBVFoHl8GtnnQ==
+ /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
+ /@babel/plugin-syntax-flow/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-innAx3bUbA0KSYj2E2MNFSn9hiCeowOFLxlsuhXzw8hMQnzkDomUr9QCD7E9VF60NmnG1sNTuuv6Qf4f8INYsg==
+ /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
+ /@babel/plugin-syntax-jsx/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-WxdW9xyLgBdefoo0Ynn3MRSkhe5tFVxxKNVdnZSh318WrG2e2jH+E9wd/++JsqcLJZPfz87njQJ8j2Upjm0M0A==
+ /@babel/plugin-syntax-logical-assignment-operators/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-Zpg2Sgc++37kuFl6ppq2Q7Awc6E6AIW671x5PY8E/f7MCIyPPGK/EoeZXvvY3P42exZ3Q4/t3YOzP/HiN79jDg==
+ /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
+ /@babel/plugin-syntax-numeric-separator/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw==
+ /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
+ /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
+ /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+ /@babel/plugin-syntax-top-level-await/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g==
+ /@babel/plugin-syntax-typescript/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-GO1MQ/SGGGoiEXY0e0bSpHimJvxqB7lktLLIq2pv8xG7WZ8IMEle74jIe1FhprHBWjwjZtXHkycDLZXIWM5Wfg==
+ /@babel/plugin-transform-arrow-functions/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==
+ /@babel/plugin-transform-async-to-generator/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-module-imports': 7.8.3
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/helper-remap-async-to-generator': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ==
+ /@babel/plugin-transform-block-scoped-functions/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==
+ /@babel/plugin-transform-block-scoping/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ lodash: 4.17.15
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==
+ /@babel/plugin-transform-classes/7.9.5_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-annotate-as-pure': 7.8.3
+ '@babel/helper-define-map': 7.8.3
+ '@babel/helper-function-name': 7.9.5
+ '@babel/helper-optimise-call-expression': 7.8.3
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/helper-replace-supers': 7.8.6
+ '@babel/helper-split-export-declaration': 7.8.3
+ globals: 11.12.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-x2kZoIuLC//O5iA7PEvecB105o7TLzZo8ofBVhP79N+DO3jaX+KYfww9TQcfBEZD0nikNyYcGB1IKtRq36rdmg==
+ /@babel/plugin-transform-computed-properties/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==
+ /@babel/plugin-transform-destructuring/7.9.5_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-j3OEsGel8nHL/iusv/mRd5fYZ3DrOxWC82x0ogmdN/vHfAP4MYw+AFKYanzWlktNwikKvlzUV//afBW5FTp17Q==
+ /@babel/plugin-transform-dotall-regex/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-create-regexp-features-plugin': 7.8.8_@babel+core@7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw==
+ /@babel/plugin-transform-duplicate-keys/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ==
+ /@babel/plugin-transform-exponentiation-operator/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-builder-binary-assignment-operator-visitor': 7.8.3
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ==
+ /@babel/plugin-transform-flow-strip-types/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-flow': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-7Qfg0lKQhEHs93FChxVLAvhBshOPQDtJUTVHr/ZwQNRccCm4O9D79r9tVSoV8iNwjP1YgfD+e/fgHcPkN1qEQg==
+ /@babel/plugin-transform-for-of/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ==
+ /@babel/plugin-transform-function-name/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-function-name': 7.9.5
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ==
+ /@babel/plugin-transform-literals/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==
+ /@babel/plugin-transform-member-expression-literals/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA==
+ /@babel/plugin-transform-modules-amd/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-module-transforms': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ babel-plugin-dynamic-import-node: 2.3.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q==
+ /@babel/plugin-transform-modules-commonjs/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-module-transforms': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/helper-simple-access': 7.8.3
+ babel-plugin-dynamic-import-node: 2.3.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g==
+ /@babel/plugin-transform-modules-systemjs/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-hoist-variables': 7.8.3
+ '@babel/helper-module-transforms': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ babel-plugin-dynamic-import-node: 2.3.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ==
+ /@babel/plugin-transform-modules-umd/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-module-transforms': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ==
+ /@babel/plugin-transform-named-capturing-groups-regex/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-create-regexp-features-plugin': 7.8.8_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ resolution:
+ integrity: sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw==
+ /@babel/plugin-transform-new-target/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw==
+ /@babel/plugin-transform-object-super/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/helper-replace-supers': 7.8.6
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ==
+ /@babel/plugin-transform-parameters/7.9.5_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-get-function-arity': 7.8.3
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-0+1FhHnMfj6lIIhVvS4KGQJeuhe1GI//h5uptK4PvLt+BGBxsoUJbd3/IW002yk//6sZPlFgsG1hY6OHLcy6kA==
+ /@babel/plugin-transform-property-literals/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg==
+ /@babel/plugin-transform-react-constant-elements/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-wXMXsToAUOxJuBBEHajqKLFWcCkOSLshTI2ChCFFj1zDd7od4IOxiwLCOObNUvOpkxLpjIuaIdBMmNt6ocCPAw==
+ /@babel/plugin-transform-react-display-name/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-3Jy/PCw8Fe6uBKtEgz3M82ljt+lTg+xJaM4og+eyu83qLT87ZUSckn0wy7r31jflURWLO83TW6Ylf7lyXj3m5A==
+ /@babel/plugin-transform-react-jsx-development/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-builder-react-jsx-experimental': 7.9.5
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-jsx': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-tK8hWKrQncVvrhvtOiPpKrQjfNX3DtkNLSX4ObuGcpS9p0QrGetKmlySIGR07y48Zft8WVgPakqd/bk46JrMSw==
+ /@babel/plugin-transform-react-jsx-self/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-jsx': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-K2ObbWPKT7KUTAoyjCsFilOkEgMvFG+y0FqOl6Lezd0/13kMkkjHskVsZvblRPj1PHA44PrToaZANrryppzTvQ==
+ /@babel/plugin-transform-react-jsx-source/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-jsx': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-K6m3LlSnTSfRkM6FcRk8saNEeaeyG5k7AVkBU2bZK3+1zdkSED3qNdsWrUgQBeTVD2Tp3VMmerxVO2yM5iITmw==
+ /@babel/plugin-transform-react-jsx/7.9.4_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-builder-react-jsx': 7.9.0
+ '@babel/helper-builder-react-jsx-experimental': 7.9.5
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-jsx': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-Mjqf3pZBNLt854CK0C/kRuXAnE6H/bo7xYojP+WGtX8glDGSibcwnsWwhwoSuRg0+EBnxPC1ouVnuetUIlPSAw==
+ /@babel/plugin-transform-regenerator/7.8.7_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ regenerator-transform: 0.14.4
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-TIg+gAl4Z0a3WmD3mbYSk+J9ZUH6n/Yc57rtKRnlA/7rcCvpekHXe0CMZHP1gYp7/KLe9GHTuIba0vXmls6drA==
+ /@babel/plugin-transform-reserved-words/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A==
+ /@babel/plugin-transform-runtime/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-module-imports': 7.8.3
+ '@babel/helper-plugin-utils': 7.8.3
+ resolve: 1.15.1
+ semver: 5.7.1
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-pUu9VSf3kI1OqbWINQ7MaugnitRss1z533436waNXp+0N3ur3zfut37sXiQMxkuCF4VUjwZucen/quskCh7NHw==
+ /@babel/plugin-transform-shorthand-properties/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==
+ /@babel/plugin-transform-spread/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==
+ /@babel/plugin-transform-sticky-regex/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/helper-regex': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw==
+ /@babel/plugin-transform-template-literals/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-annotate-as-pure': 7.8.3
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ==
+ /@babel/plugin-transform-typeof-symbol/7.8.4_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg==
+ /@babel/plugin-transform-typescript/7.9.4_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-create-class-features-plugin': 7.9.5_@babel+core@7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-syntax-typescript': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-yeWeUkKx2auDbSxRe8MusAG+n4m9BFY/v+lPjmQDgOFX5qnySkUY5oXzkp6FwPdsYqnKay6lorXYdC0n3bZO7w==
+ /@babel/plugin-transform-unicode-regex/7.8.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-create-regexp-features-plugin': 7.8.8_@babel+core@7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw==
+ /@babel/preset-env/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/compat-data': 7.9.0
+ '@babel/core': 7.9.0
+ '@babel/helper-compilation-targets': 7.8.7_@babel+core@7.9.0
+ '@babel/helper-module-imports': 7.8.3
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-proposal-async-generator-functions': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-dynamic-import': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-json-strings': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-nullish-coalescing-operator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-numeric-separator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-object-rest-spread': 7.9.5_@babel+core@7.9.0
+ '@babel/plugin-proposal-optional-catch-binding': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-optional-chaining': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-proposal-unicode-property-regex': 7.8.8_@babel+core@7.9.0
+ '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.9.0
+ '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-numeric-separator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-top-level-await': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-arrow-functions': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-async-to-generator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-block-scoped-functions': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-block-scoping': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-classes': 7.9.5_@babel+core@7.9.0
+ '@babel/plugin-transform-computed-properties': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-destructuring': 7.9.5_@babel+core@7.9.0
+ '@babel/plugin-transform-dotall-regex': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-duplicate-keys': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-exponentiation-operator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-for-of': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-function-name': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-literals': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-member-expression-literals': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-modules-amd': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-modules-commonjs': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-modules-systemjs': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-modules-umd': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-new-target': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-object-super': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-parameters': 7.9.5_@babel+core@7.9.0
+ '@babel/plugin-transform-property-literals': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-regenerator': 7.8.7_@babel+core@7.9.0
+ '@babel/plugin-transform-reserved-words': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-shorthand-properties': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-spread': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-sticky-regex': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-template-literals': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-typeof-symbol': 7.8.4_@babel+core@7.9.0
+ '@babel/plugin-transform-unicode-regex': 7.8.3_@babel+core@7.9.0
+ '@babel/preset-modules': 0.1.3_@babel+core@7.9.0
+ '@babel/types': 7.9.5
+ browserslist: 4.11.1
+ core-js-compat: 3.6.4
+ invariant: 2.2.4
+ levenary: 1.1.1
+ semver: 5.7.1
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ==
+ /@babel/preset-env/7.9.5_@babel+core@7.9.0:
+ dependencies:
+ '@babel/compat-data': 7.9.0
+ '@babel/core': 7.9.0
+ '@babel/helper-compilation-targets': 7.8.7_@babel+core@7.9.0
+ '@babel/helper-module-imports': 7.8.3
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-proposal-async-generator-functions': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-dynamic-import': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-json-strings': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-nullish-coalescing-operator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-numeric-separator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-object-rest-spread': 7.9.5_@babel+core@7.9.0
+ '@babel/plugin-proposal-optional-catch-binding': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-optional-chaining': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-proposal-unicode-property-regex': 7.8.8_@babel+core@7.9.0
+ '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.9.0
+ '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-numeric-separator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-top-level-await': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-arrow-functions': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-async-to-generator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-block-scoped-functions': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-block-scoping': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-classes': 7.9.5_@babel+core@7.9.0
+ '@babel/plugin-transform-computed-properties': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-destructuring': 7.9.5_@babel+core@7.9.0
+ '@babel/plugin-transform-dotall-regex': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-duplicate-keys': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-exponentiation-operator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-for-of': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-function-name': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-literals': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-member-expression-literals': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-modules-amd': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-modules-commonjs': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-modules-systemjs': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-modules-umd': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-new-target': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-object-super': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-parameters': 7.9.5_@babel+core@7.9.0
+ '@babel/plugin-transform-property-literals': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-regenerator': 7.8.7_@babel+core@7.9.0
+ '@babel/plugin-transform-reserved-words': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-shorthand-properties': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-spread': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-sticky-regex': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-template-literals': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-typeof-symbol': 7.8.4_@babel+core@7.9.0
+ '@babel/plugin-transform-unicode-regex': 7.8.3_@babel+core@7.9.0
+ '@babel/preset-modules': 0.1.3_@babel+core@7.9.0
+ '@babel/types': 7.9.5
+ browserslist: 4.11.1
+ core-js-compat: 3.6.4
+ invariant: 2.2.4
+ levenary: 1.1.1
+ semver: 5.7.1
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-eWGYeADTlPJH+wq1F0wNfPbVS1w1wtmMJiYk55Td5Yu28AsdR9AsC97sZ0Qq8fHqQuslVSIYSGJMcblr345GfQ==
+ /@babel/preset-modules/0.1.3_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-proposal-unicode-property-regex': 7.8.8_@babel+core@7.9.0
+ '@babel/plugin-transform-dotall-regex': 7.8.3_@babel+core@7.9.0
+ '@babel/types': 7.9.5
+ esutils: 2.0.3
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==
+ /@babel/preset-react/7.9.1_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-transform-react-display-name': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-react-jsx': 7.9.4_@babel+core@7.9.0
+ '@babel/plugin-transform-react-jsx-development': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-react-jsx-self': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-react-jsx-source': 7.9.0_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-aJBYF23MPj0RNdp/4bHnAP0NVqqZRr9kl0NAOP4nJCex6OYVio59+dnQzsAWFuogdLyeaKA1hmfUIVZkY5J+TQ==
+ /@babel/preset-react/7.9.4_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-transform-react-display-name': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-react-jsx': 7.9.4_@babel+core@7.9.0
+ '@babel/plugin-transform-react-jsx-development': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-react-jsx-self': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-react-jsx-source': 7.9.0_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-AxylVB3FXeOTQXNXyiuAQJSvss62FEotbX2Pzx3K/7c+MKJMdSg6Ose6QYllkdCFA8EInCJVw7M/o5QbLuA4ZQ==
+ /@babel/preset-typescript/7.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/helper-plugin-utils': 7.8.3
+ '@babel/plugin-transform-typescript': 7.9.4_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ resolution:
+ integrity: sha512-S4cueFnGrIbvYJgwsVFKdvOmpiL0XGw9MFW9D0vgRys5g36PBhZRL8NX8Gr2akz8XRtzq6HuDXPD/1nniagNUg==
+ /@babel/runtime-corejs2/7.9.2:
+ dependencies:
+ core-js: 2.6.11
+ regenerator-runtime: 0.13.5
+ dev: false
+ resolution:
+ integrity: sha512-ayjSOxuK2GaSDJFCtLgHnYjuMyIpViNujWrZo8GUpN60/n7juzJKK5yOo6RFVb0zdU9ACJFK+MsZrUnj3OmXMw==
+ /@babel/runtime-corejs3/7.9.2:
+ dependencies:
+ core-js-pure: 3.6.4
+ regenerator-runtime: 0.13.5
+ dev: true
+ resolution:
+ integrity: sha512-HHxmgxbIzOfFlZ+tdeRKtaxWOMUoCG5Mu3wKeUmOxjYrwb3AAHgnmtCUbPPK11/raIWLIBK250t8E2BPO0p7jA==
+ /@babel/runtime/7.9.0:
+ dependencies:
+ regenerator-runtime: 0.13.5
+ dev: true
+ resolution:
+ integrity: sha512-cTIudHnzuWLS56ik4DnRnqqNf8MkdUzV4iFFI1h7Jo9xvrpQROYaAnaSd2mHLQAzzZAPfATynX5ord6YlNYNMA==
+ /@babel/runtime/7.9.2:
+ dependencies:
+ regenerator-runtime: 0.13.5
+ resolution:
+ integrity: sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
+ /@babel/template/7.8.6:
+ dependencies:
+ '@babel/code-frame': 7.8.3
+ '@babel/parser': 7.9.4
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==
+ /@babel/traverse/7.9.5:
+ dependencies:
+ '@babel/code-frame': 7.8.3
+ '@babel/generator': 7.9.5
+ '@babel/helper-function-name': 7.9.5
+ '@babel/helper-split-export-declaration': 7.8.3
+ '@babel/parser': 7.9.4
+ '@babel/types': 7.9.5
+ debug: 4.1.1
+ globals: 11.12.0
+ lodash: 4.17.15
+ dev: true
+ resolution:
+ integrity: sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==
+ /@babel/types/7.9.5:
+ dependencies:
+ '@babel/helper-validator-identifier': 7.9.5
+ lodash: 4.17.15
+ to-fast-properties: 2.0.0
+ resolution:
+ integrity: sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==
+ /@bcoe/v8-coverage/0.2.3:
+ dev: true
+ resolution:
+ integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
+ /@cnakazawa/watch/1.0.4:
+ dependencies:
+ exec-sh: 0.3.4
+ minimist: 1.2.5
+ dev: true
+ engines:
+ node: '>=0.1.95'
+ hasBin: true
+ resolution:
+ integrity: sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==
+ /@csstools/convert-colors/1.4.0:
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ resolution:
+ integrity: sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
+ /@csstools/normalize.css/10.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
+ /@emotion/cache/10.0.29:
+ dependencies:
+ '@emotion/sheet': 0.9.4
+ '@emotion/stylis': 0.8.5
+ '@emotion/utils': 0.11.3
+ '@emotion/weak-memoize': 0.2.5
+ dev: false
+ resolution:
+ integrity: sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==
+ /@emotion/core/10.0.28_react@16.13.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ '@emotion/cache': 10.0.29
+ '@emotion/css': 10.0.27
+ '@emotion/serialize': 0.11.16
+ '@emotion/sheet': 0.9.4
+ '@emotion/utils': 0.11.3
+ react: 16.13.1
+ dev: false
+ peerDependencies:
+ react: '>=16.3.0'
+ resolution:
+ integrity: sha512-pH8UueKYO5jgg0Iq+AmCLxBsvuGtvlmiDCOuv8fGNYn3cowFpLN98L8zO56U0H1PjDIyAlXymgL3Wu7u7v6hbA==
+ /@emotion/css/10.0.27:
+ dependencies:
+ '@emotion/serialize': 0.11.16
+ '@emotion/utils': 0.11.3
+ babel-plugin-emotion: 10.0.33
+ dev: false
+ resolution:
+ integrity: sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==
+ /@emotion/hash/0.8.0:
+ dev: false
+ resolution:
+ integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
+ /@emotion/memoize/0.7.4:
+ dev: false
+ resolution:
+ integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
+ /@emotion/serialize/0.11.16:
+ dependencies:
+ '@emotion/hash': 0.8.0
+ '@emotion/memoize': 0.7.4
+ '@emotion/unitless': 0.7.5
+ '@emotion/utils': 0.11.3
+ csstype: 2.6.10
+ dev: false
+ resolution:
+ integrity: sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==
+ /@emotion/sheet/0.9.4:
+ dev: false
+ resolution:
+ integrity: sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==
+ /@emotion/stylis/0.8.5:
+ dev: false
+ resolution:
+ integrity: sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
+ /@emotion/unitless/0.7.5:
+ dev: false
+ resolution:
+ integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
+ /@emotion/utils/0.11.3:
+ dev: false
+ resolution:
+ integrity: sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==
+ /@emotion/weak-memoize/0.2.5:
+ dev: false
+ resolution:
+ integrity: sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
+ /@hapi/accept/3.2.4:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ '@hapi/hoek': 8.5.1
+ dev: true
+ resolution:
+ integrity: sha512-soThGB+QMgfxlh0Vzhzlf3ZOEOPk5biEwcOXhkF0Eedqx8VnhGiggL9UYHrIsOb1rUg3Be3K8kp0iDL2wbVSOQ==
+ /@hapi/address/2.1.4:
+ dev: true
+ resolution:
+ integrity: sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==
+ /@hapi/ammo/3.1.2:
+ dependencies:
+ '@hapi/hoek': 8.5.1
+ dev: true
+ resolution:
+ integrity: sha512-ej9OtFmiZv1qr45g1bxEZNGyaR4jRpyMxU6VhbxjaYThymvOwsyIsUKMZnP5Qw2tfYFuwqCJuIBHGpeIbdX9gQ==
+ /@hapi/b64/4.2.1:
+ dependencies:
+ '@hapi/hoek': 8.5.1
+ dev: true
+ resolution:
+ integrity: sha512-zqHpQuH5CBMw6hADzKfU/IGNrxq1Q+/wTYV+OiZRQN9F3tMyk+9BUMeBvFRMamduuqL8iSp62QAnJ+7ATiYLWA==
+ /@hapi/boom/7.4.11:
+ dependencies:
+ '@hapi/hoek': 8.5.1
+ dev: true
+ resolution:
+ integrity: sha512-VSU/Cnj1DXouukYxxkes4nNJonCnlogHvIff1v1RVoN4xzkKhMXX+GRmb3NyH1iar10I9WFPDv2JPwfH3GaV0A==
+ /@hapi/boom/9.0.0:
+ dependencies:
+ '@hapi/hoek': 9.0.3
+ dev: true
+ resolution:
+ integrity: sha512-D+Or4yahLq3L7D1Jf0fR1+Lgr+HPK1lej8tc6hS/fBLmK66XdpvTyKv8YUR5ls1GeQy+KGtbpKAs+ZxyzNtUyA==
+ /@hapi/bounce/1.3.2:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ '@hapi/hoek': 8.5.1
+ dev: true
+ resolution:
+ integrity: sha512-3bnb1AlcEByFZnpDIidxQyw1Gds81z/1rSqlx4bIEE+wUN0ATj0D49B5cE1wGocy90Rp/de4tv7GjsKd5RQeew==
+ /@hapi/bourne/1.3.2:
+ dev: true
+ resolution:
+ integrity: sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==
+ /@hapi/call/5.1.3:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ '@hapi/hoek': 8.5.1
+ dev: true
+ resolution:
+ integrity: sha512-5DfWpMk7qZiYhvBhM5oUiT4GQ/O8a2rFR121/PdwA/eZ2C1EsuD547ZggMKAR5bZ+FtxOf0fdM20zzcXzq2mZA==
+ /@hapi/catbox-memory/4.1.1:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ '@hapi/hoek': 8.5.1
+ dev: true
+ resolution:
+ integrity: sha512-T6Hdy8DExzG0jY7C8yYWZB4XHfc0v+p1EGkwxl2HoaPYAmW7I3E59M/IvmSVpis8RPcIoBp41ZpO2aZPBpM2Ww==
+ /@hapi/catbox/10.2.3:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ '@hapi/hoek': 8.5.1
+ '@hapi/joi': 16.1.8
+ '@hapi/podium': 3.4.3
+ dev: true
+ resolution:
+ integrity: sha512-kN9hXO4NYyOHW09CXiuj5qW1syc/0XeVOBsNNk0Tz89wWNQE5h21WF+VsfAw3uFR8swn/Wj3YEVBnWqo82m/JQ==
+ /@hapi/content/4.1.1:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ dev: true
+ resolution:
+ integrity: sha512-3TWvmwpVPxFSF3KBjKZ8yDqIKKZZIm7VurDSweYpXYENZrJH3C1hd1+qEQW9wQaUaI76pPBLGrXl6I3B7i3ipA==
+ /@hapi/cryptiles/4.2.1:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ dev: true
+ resolution:
+ integrity: sha512-XoqgKsHK0l/VpqPs+tr6j6vE+VQ3+2bkF2stvttmc7xAOf1oSAwHcJ0tlp/6MxMysktt1IEY0Csy3khKaP9/uQ==
+ /@hapi/file/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-Bsfp/+1Gyf70eGtnIgmScvrH8sSypO3TcK3Zf0QdHnzn/ACnAkI6KLtGACmNRPEzzIy+W7aJX5E+1fc9GwIABQ==
+ /@hapi/formula/1.2.0:
+ dev: true
+ resolution:
+ integrity: sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA==
+ /@hapi/h2o2/8.3.2:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ '@hapi/hoek': 8.5.1
+ '@hapi/joi': 16.1.8
+ '@hapi/wreck': 15.1.0
+ dev: true
+ resolution:
+ integrity: sha512-2WkZq+QAkvYHWGqnUuG0stcVeGyv9T7bopBYnCJSUEuvBZlUf2BTX2JCVSKxsnTLOxCYwoC/aI4Rr0ZSRd2oVg==
+ /@hapi/hapi/18.4.1:
+ dependencies:
+ '@hapi/accept': 3.2.4
+ '@hapi/ammo': 3.1.2
+ '@hapi/boom': 7.4.11
+ '@hapi/bounce': 1.3.2
+ '@hapi/call': 5.1.3
+ '@hapi/catbox': 10.2.3
+ '@hapi/catbox-memory': 4.1.1
+ '@hapi/heavy': 6.2.2
+ '@hapi/hoek': 8.5.1
+ '@hapi/joi': 15.1.1
+ '@hapi/mimos': 4.1.1
+ '@hapi/podium': 3.4.3
+ '@hapi/shot': 4.1.2
+ '@hapi/somever': 2.1.1
+ '@hapi/statehood': 6.1.2
+ '@hapi/subtext': 6.1.3
+ '@hapi/teamwork': 3.3.1
+ '@hapi/topo': 3.1.6
+ dev: true
+ resolution:
+ integrity: sha512-9HjVGa0Z4Qv9jk9AVoUdJMQLA+KuZ+liKWyEEkVBx3e3H1F0JM6aGbPkY9jRfwsITBWGBU2iXazn65SFKSi/tg==
+ /@hapi/heavy/6.2.2:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ '@hapi/hoek': 8.5.1
+ '@hapi/joi': 16.1.8
+ dev: true
+ resolution:
+ integrity: sha512-PY1dCCO6dsze7RlafIRhTaGeyTgVe49A/lSkxbhKGjQ7x46o/OFf7hLiRqTCDh3atcEKf6362EaB3+kTUbCsVA==
+ /@hapi/hoek/8.5.1:
+ dev: true
+ resolution:
+ integrity: sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==
+ /@hapi/hoek/9.0.3:
+ dev: true
+ resolution:
+ integrity: sha512-jKtjLLDiH95b002sJVc5c74PE6KKYftuyVdVmsuYId5stTaWcRFqE+5ukZI4gDUKjGn8wv2C3zPn3/nyjEI7gg==
+ /@hapi/iron/5.1.4:
+ dependencies:
+ '@hapi/b64': 4.2.1
+ '@hapi/boom': 7.4.11
+ '@hapi/bourne': 1.3.2
+ '@hapi/cryptiles': 4.2.1
+ '@hapi/hoek': 8.5.1
+ dev: true
+ resolution:
+ integrity: sha512-+ElC+OCiwWLjlJBmm8ZEWjlfzTMQTdgPnU/TsoU5QsktspIWmWi9IU4kU83nH+X/SSya8TP8h8P11Wr5L7dkQQ==
+ /@hapi/joi/15.1.1:
+ dependencies:
+ '@hapi/address': 2.1.4
+ '@hapi/bourne': 1.3.2
+ '@hapi/hoek': 8.5.1
+ '@hapi/topo': 3.1.6
+ dev: true
+ resolution:
+ integrity: sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==
+ /@hapi/joi/16.1.8:
+ dependencies:
+ '@hapi/address': 2.1.4
+ '@hapi/formula': 1.2.0
+ '@hapi/hoek': 8.5.1
+ '@hapi/pinpoint': 1.0.2
+ '@hapi/topo': 3.1.6
+ dev: true
+ resolution:
+ integrity: sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg==
+ /@hapi/mimos/4.1.1:
+ dependencies:
+ '@hapi/hoek': 8.5.1
+ mime-db: 1.43.0
+ dev: true
+ resolution:
+ integrity: sha512-CXoi/zfcTWfKYX756eEea8rXJRIb9sR4d7VwyAH9d3BkDyNgAesZxvqIdm55npQc6S9mU3FExinMAQVlIkz0eA==
+ /@hapi/nigel/3.1.1:
+ dependencies:
+ '@hapi/hoek': 8.5.1
+ '@hapi/vise': 3.1.1
+ dev: true
+ resolution:
+ integrity: sha512-R9YWx4S8yu0gcCBrMUDCiEFm1SQT895dMlYoeNBp8I6YhF1BFF1iYPueKA2Kkp9BvyHdjmvrxCOns7GMmpl+Fw==
+ /@hapi/pez/4.1.2:
+ dependencies:
+ '@hapi/b64': 4.2.1
+ '@hapi/boom': 7.4.11
+ '@hapi/content': 4.1.1
+ '@hapi/hoek': 8.5.1
+ '@hapi/nigel': 3.1.1
+ dev: true
+ resolution:
+ integrity: sha512-8zSdJ8cZrJLFldTgwjU9Fb1JebID+aBCrCsycgqKYe0OZtM2r3Yv3aAwW5z97VsZWCROC1Vx6Mdn4rujh5Ktcg==
+ /@hapi/pinpoint/1.0.2:
+ dev: true
+ resolution:
+ integrity: sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ==
+ /@hapi/podium/3.4.3:
+ dependencies:
+ '@hapi/hoek': 8.5.1
+ '@hapi/joi': 16.1.8
+ dev: true
+ resolution:
+ integrity: sha512-QJlnYLEYZWlKQ9fSOtuUcpANyoVGwT68GA9P0iQQCAetBK0fI+nbRBt58+aMixoifczWZUthuGkNjqKxgPh/CQ==
+ /@hapi/shot/4.1.2:
+ dependencies:
+ '@hapi/hoek': 8.5.1
+ '@hapi/joi': 16.1.8
+ dev: true
+ resolution:
+ integrity: sha512-6LeHLjvsq/bQ0R+fhEyr7mqExRGguNTrxFZf5DyKe3CK6pNabiGgYO4JVFaRrLZ3JyuhkS0fo8iiRE2Ql2oA/A==
+ /@hapi/somever/2.1.1:
+ dependencies:
+ '@hapi/bounce': 1.3.2
+ '@hapi/hoek': 8.5.1
+ dev: true
+ resolution:
+ integrity: sha512-cic5Sto4KGd9B0oQSdKTokju+rYhCbdpzbMb0EBnrH5Oc1z048hY8PaZ1lx2vBD7I/XIfTQVQetBH57fU51XRA==
+ /@hapi/statehood/6.1.2:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ '@hapi/bounce': 1.3.2
+ '@hapi/bourne': 1.3.2
+ '@hapi/cryptiles': 4.2.1
+ '@hapi/hoek': 8.5.1
+ '@hapi/iron': 5.1.4
+ '@hapi/joi': 16.1.8
+ dev: true
+ resolution:
+ integrity: sha512-pYXw1x6npz/UfmtcpUhuMvdK5kuOGTKcJNfLqdNptzietK2UZH5RzNJSlv5bDHeSmordFM3kGItcuQWX2lj2nQ==
+ /@hapi/subtext/6.1.3:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ '@hapi/bourne': 1.3.2
+ '@hapi/content': 4.1.1
+ '@hapi/file': 1.0.0
+ '@hapi/hoek': 8.5.1
+ '@hapi/pez': 4.1.2
+ '@hapi/wreck': 15.1.0
+ dev: true
+ resolution:
+ integrity: sha512-qWN6NbiHNzohVcJMeAlpku/vzbyH4zIpnnMPMPioQMwIxbPFKeNViDCNI6fVBbMPBiw/xB4FjqiJkRG5P9eWWg==
+ /@hapi/teamwork/3.3.1:
+ dev: true
+ resolution:
+ integrity: sha512-61tiqWCYvMKP7fCTXy0M4VE6uNIwA0qvgFoiDubgfj7uqJ0fdHJFQNnVPGrxhLWlwz0uBPWrQlBH7r8y9vFITQ==
+ /@hapi/topo/3.1.6:
+ dependencies:
+ '@hapi/hoek': 8.5.1
+ dev: true
+ resolution:
+ integrity: sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==
+ /@hapi/vise/3.1.1:
+ dependencies:
+ '@hapi/hoek': 8.5.1
+ dev: true
+ resolution:
+ integrity: sha512-OXarbiCSadvtg+bSdVPqu31Z1JoBL+FwNYz3cYoBKQ5xq1/Cr4A3IkGpAZbAuxU5y4NL5pZFZG3d2a3ZGm/dOQ==
+ /@hapi/wreck/15.1.0:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ '@hapi/bourne': 1.3.2
+ '@hapi/hoek': 8.5.1
+ dev: true
+ resolution:
+ integrity: sha512-tQczYRTTeYBmvhsek/D49En/5khcShaBEmzrAaDjMrFXKJRuF8xA8+tlq1ETLBFwGd6Do6g2OC74rt11kzawzg==
+ /@istanbuljs/load-nyc-config/1.0.0:
+ dependencies:
+ camelcase: 5.3.1
+ find-up: 4.1.0
+ js-yaml: 3.13.1
+ resolve-from: 5.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==
+ /@istanbuljs/schema/0.1.2:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
+ /@jest/console/24.9.0:
+ dependencies:
+ '@jest/source-map': 24.9.0
+ chalk: 2.4.2
+ slash: 2.0.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==
+ /@jest/console/25.3.0:
+ dependencies:
+ '@jest/source-map': 25.2.6
+ chalk: 3.0.0
+ jest-util: 25.3.0
+ slash: 3.0.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-LvSDNqpmZIZyweFaEQ6wKY7CbexPitlsLHGJtcooNECo0An/w49rFhjCJzu6efeb6+a3ee946xss1Jcd9r03UQ==
+ /@jest/core/24.9.0:
+ dependencies:
+ '@jest/console': 24.9.0
+ '@jest/reporters': 24.9.0
+ '@jest/test-result': 24.9.0
+ '@jest/transform': 24.9.0
+ '@jest/types': 24.9.0
+ ansi-escapes: 3.2.0
+ chalk: 2.4.2
+ exit: 0.1.2
+ graceful-fs: 4.2.3
+ jest-changed-files: 24.9.0
+ jest-config: 24.9.0
+ jest-haste-map: 24.9.0
+ jest-message-util: 24.9.0
+ jest-regex-util: 24.9.0
+ jest-resolve: 24.9.0_jest-resolve@24.9.0
+ jest-resolve-dependencies: 24.9.0
+ jest-runner: 24.9.0
+ jest-runtime: 24.9.0
+ jest-snapshot: 24.9.0
+ jest-util: 24.9.0
+ jest-validate: 24.9.0
+ jest-watcher: 24.9.0
+ micromatch: 3.1.10
+ p-each-series: 1.0.0
+ realpath-native: 1.1.0
+ rimraf: 2.7.1
+ slash: 2.0.0
+ strip-ansi: 5.2.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==
+ /@jest/core/25.3.0:
+ dependencies:
+ '@jest/console': 25.3.0
+ '@jest/reporters': 25.3.0
+ '@jest/test-result': 25.3.0
+ '@jest/transform': 25.3.0
+ '@jest/types': 25.3.0
+ ansi-escapes: 4.3.1
+ chalk: 3.0.0
+ exit: 0.1.2
+ graceful-fs: 4.2.3
+ jest-changed-files: 25.3.0
+ jest-config: 25.3.0
+ jest-haste-map: 25.3.0
+ jest-message-util: 25.3.0
+ jest-regex-util: 25.2.6
+ jest-resolve: 25.3.0_jest-resolve@25.3.0
+ jest-resolve-dependencies: 25.3.0
+ jest-runner: 25.3.0
+ jest-runtime: 25.3.0
+ jest-snapshot: 25.3.0
+ jest-util: 25.3.0
+ jest-validate: 25.3.0
+ jest-watcher: 25.3.0
+ micromatch: 4.0.2
+ p-each-series: 2.1.0
+ realpath-native: 2.0.0
+ rimraf: 3.0.2
+ slash: 3.0.0
+ strip-ansi: 6.0.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-+D5a/tFf6pA/Gqft2DLBp/yeSRgXhlJ+Wpst0X/ZkfTRP54qDR3C61VfHwaex+GzZBiTcE9vQeoZ2v5T10+Mqw==
+ /@jest/environment/24.9.0:
+ dependencies:
+ '@jest/fake-timers': 24.9.0
+ '@jest/transform': 24.9.0
+ '@jest/types': 24.9.0
+ jest-mock: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==
+ /@jest/environment/25.3.0:
+ dependencies:
+ '@jest/fake-timers': 25.3.0
+ '@jest/types': 25.3.0
+ jest-mock: 25.3.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-vgooqwJTHLLak4fE+TaCGeYP7Tz1Y3CKOsNxR1sE0V3nx3KRUHn3NUnt+wbcfd5yQWKZQKAfW6wqbuwQLrXo3g==
+ /@jest/fake-timers/24.9.0:
+ dependencies:
+ '@jest/types': 24.9.0
+ jest-message-util: 24.9.0
+ jest-mock: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==
+ /@jest/fake-timers/25.3.0:
+ dependencies:
+ '@jest/types': 25.3.0
+ jest-message-util: 25.3.0
+ jest-mock: 25.3.0
+ jest-util: 25.3.0
+ lolex: 5.1.2
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-NHAj7WbsyR3qBJPpBwSwqaq2WluIvUQsyzpJTN7XDVk7VnlC/y1BAnaYZL3vbPIP8Nhm0Ae5DJe0KExr/SdMJQ==
+ /@jest/reporters/24.9.0:
+ dependencies:
+ '@jest/environment': 24.9.0
+ '@jest/test-result': 24.9.0
+ '@jest/transform': 24.9.0
+ '@jest/types': 24.9.0
+ chalk: 2.4.2
+ exit: 0.1.2
+ glob: 7.1.6
+ istanbul-lib-coverage: 2.0.5
+ istanbul-lib-instrument: 3.3.0
+ istanbul-lib-report: 2.0.8
+ istanbul-lib-source-maps: 3.0.6
+ istanbul-reports: 2.2.7
+ jest-haste-map: 24.9.0
+ jest-resolve: 24.9.0_jest-resolve@24.9.0
+ jest-runtime: 24.9.0
+ jest-util: 24.9.0
+ jest-worker: 24.9.0
+ node-notifier: 5.4.3
+ slash: 2.0.0
+ source-map: 0.6.1
+ string-length: 2.0.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==
+ /@jest/reporters/25.3.0:
+ dependencies:
+ '@bcoe/v8-coverage': 0.2.3
+ '@jest/console': 25.3.0
+ '@jest/test-result': 25.3.0
+ '@jest/transform': 25.3.0
+ '@jest/types': 25.3.0
+ chalk: 3.0.0
+ collect-v8-coverage: 1.0.1
+ exit: 0.1.2
+ glob: 7.1.6
+ istanbul-lib-coverage: 3.0.0
+ istanbul-lib-instrument: 4.0.1
+ istanbul-lib-report: 3.0.0
+ istanbul-lib-source-maps: 4.0.0
+ istanbul-reports: 3.0.2
+ jest-haste-map: 25.3.0
+ jest-resolve: 25.3.0_jest-resolve@25.3.0
+ jest-util: 25.3.0
+ jest-worker: 25.2.6
+ slash: 3.0.0
+ source-map: 0.6.1
+ string-length: 3.1.0
+ terminal-link: 2.1.1
+ v8-to-istanbul: 4.1.3
+ dev: true
+ engines:
+ node: '>= 8.3'
+ optionalDependencies:
+ node-notifier: 6.0.0
+ resolution:
+ integrity: sha512-1u0ZBygs0C9DhdYgLCrRfZfNKQa+9+J7Uo+Z9z0RWLHzgsxhoG32lrmMOtUw48yR6bLNELdvzormwUqSk4H4Vg==
+ /@jest/source-map/24.9.0:
+ dependencies:
+ callsites: 3.1.0
+ graceful-fs: 4.2.3
+ source-map: 0.6.1
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==
+ /@jest/source-map/25.2.6:
+ dependencies:
+ callsites: 3.1.0
+ graceful-fs: 4.2.3
+ source-map: 0.6.1
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-VuIRZF8M2zxYFGTEhkNSvQkUKafQro4y+mwUxy5ewRqs5N/ynSFUODYp3fy1zCnbCMy1pz3k+u57uCqx8QRSQQ==
+ /@jest/test-result/24.9.0:
+ dependencies:
+ '@jest/console': 24.9.0
+ '@jest/types': 24.9.0
+ '@types/istanbul-lib-coverage': 2.0.1
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==
+ /@jest/test-result/25.3.0:
+ dependencies:
+ '@jest/console': 25.3.0
+ '@jest/types': 25.3.0
+ '@types/istanbul-lib-coverage': 2.0.1
+ collect-v8-coverage: 1.0.1
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-mqrGuiiPXl1ap09Mydg4O782F3ouDQfsKqtQzIjitpwv3t1cHDwCto21jThw6WRRE+dKcWQvLG70GpyLJICfGw==
+ /@jest/test-sequencer/24.9.0:
+ dependencies:
+ '@jest/test-result': 24.9.0
+ jest-haste-map: 24.9.0
+ jest-runner: 24.9.0
+ jest-runtime: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==
+ /@jest/test-sequencer/25.3.0:
+ dependencies:
+ '@jest/test-result': 25.3.0
+ jest-haste-map: 25.3.0
+ jest-runner: 25.3.0
+ jest-runtime: 25.3.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-Xvns3xbji7JCvVcDGvqJ/pf4IpmohPODumoPEZJ0/VgC5gI4XaNVIBET2Dq5Czu6Gk3xFcmhtthh/MBOTljdNg==
+ /@jest/transform/24.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@jest/types': 24.9.0
+ babel-plugin-istanbul: 5.2.0
+ chalk: 2.4.2
+ convert-source-map: 1.7.0
+ fast-json-stable-stringify: 2.1.0
+ graceful-fs: 4.2.3
+ jest-haste-map: 24.9.0
+ jest-regex-util: 24.9.0
+ jest-util: 24.9.0
+ micromatch: 3.1.10
+ pirates: 4.0.1
+ realpath-native: 1.1.0
+ slash: 2.0.0
+ source-map: 0.6.1
+ write-file-atomic: 2.4.1
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==
+ /@jest/transform/25.3.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@jest/types': 25.3.0
+ babel-plugin-istanbul: 6.0.0
+ chalk: 3.0.0
+ convert-source-map: 1.7.0
+ fast-json-stable-stringify: 2.1.0
+ graceful-fs: 4.2.3
+ jest-haste-map: 25.3.0
+ jest-regex-util: 25.2.6
+ jest-util: 25.3.0
+ micromatch: 4.0.2
+ pirates: 4.0.1
+ realpath-native: 2.0.0
+ slash: 3.0.0
+ source-map: 0.6.1
+ write-file-atomic: 3.0.3
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-W01p8kTDvvEX6kd0tJc7Y5VdYyFaKwNWy1HQz6Jqlhu48z/8Gxp+yFCDVj+H8Rc7ezl3Mg0hDaGuFVkmHOqirg==
+ /@jest/types/24.9.0:
+ dependencies:
+ '@types/istanbul-lib-coverage': 2.0.1
+ '@types/istanbul-reports': 1.1.1
+ '@types/yargs': 13.0.8
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==
+ /@jest/types/25.3.0:
+ dependencies:
+ '@types/istanbul-lib-coverage': 2.0.1
+ '@types/istanbul-reports': 1.1.1
+ '@types/yargs': 15.0.4
+ chalk: 3.0.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-UkaDNewdqXAmCDbN2GlUM6amDKS78eCqiw/UmF5nE0mmLTd6moJkiZJML/X52Ke3LH7Swhw883IRXq8o9nWjVw==
+ /@mrmlnc/readdir-enhanced/2.2.1:
+ dependencies:
+ call-me-maybe: 1.0.1
+ glob-to-regexp: 0.3.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==
+ /@nodelib/fs.scandir/2.1.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.3
+ run-parallel: 1.1.9
+ dev: true
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==
+ /@nodelib/fs.stat/1.1.3:
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
+ /@nodelib/fs.stat/2.0.3:
+ dev: true
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==
+ /@nodelib/fs.walk/1.2.4:
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.3
+ fastq: 1.7.0
+ dev: true
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==
+ /@semantic-ui-react/event-stack/3.1.1_react-dom@16.13.1+react@16.13.1:
+ dependencies:
+ exenv: 1.2.2
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ dev: false
+ peerDependencies:
+ react: ^16.0.0
+ react-dom: ^16.0.0
+ resolution:
+ integrity: sha512-SA7VOu/tY3OkooR++mm9voeQrJpYXjJaMHO1aFCcSouS2xhqMR9Gnz0LEGLOR0h9ueWPBKaQzKIrx3FTTJZmUQ==
+ /@serverless/cli/1.4.0:
+ dependencies:
+ '@serverless/core': 1.1.2
+ '@serverless/template': 1.1.3
+ ansi-escapes: 4.3.1
+ chalk: 2.4.2
+ chokidar: 3.3.1
+ dotenv: 8.2.0
+ figures: 3.2.0
+ minimist: 1.2.5
+ prettyoutput: 1.2.0
+ strip-ansi: 5.2.0
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-YqlCiYmRFeGksw6XJaXbigIDlktc7OfRuVpyPB7IZgkCJ9mUlBmvyWdwqJEQdkUz0xPTGsd4Jd/XSrwyiw1Brg==
+ /@serverless/component-metrics/1.0.8:
+ dependencies:
+ node-fetch: 2.6.0
+ shortid: 2.2.15
+ dev: true
+ resolution:
+ integrity: sha512-lOUyRopNTKJYVEU9T6stp2irwlTDsYMmUKBOUjnMcwGveuUfIJqrCOtFLtIPPj3XJlbZy5F68l4KP9rZ8Ipang==
+ /@serverless/components/2.29.0:
+ dependencies:
+ '@serverless/inquirer': 1.1.0
+ '@serverless/platform-client': 0.25.4
+ '@serverless/platform-sdk': 2.3.0
+ '@serverless/tencent-platform-client': 0.25.10
+ adm-zip: 0.4.14
+ ansi-escapes: 4.3.1
+ axios: 0.19.2
+ chalk: 2.4.2
+ chokidar: 3.3.1
+ dotenv: 8.2.0
+ figures: 3.2.0
+ fs-extra: 8.1.0
+ globby: 10.0.2
+ js-yaml: 3.13.1
+ minimist: 1.2.5
+ moment: 2.24.0
+ open: 7.0.3
+ prettyoutput: 1.2.0
+ ramda: 0.26.1
+ strip-ansi: 5.2.0
+ traverse: 0.6.6
+ uuid: 3.4.0
+ ws: 7.2.3
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-dVAp2OTLPAFuQm4NJBfBAsTqfpVqaCMmeV9VH88/22G9aIdW5RfoT0BqXoXN7ljZiF9L4pHXj8FlS9/Yx9NWKA==
+ /@serverless/core/1.1.2:
+ dependencies:
+ fs-extra: 7.0.1
+ js-yaml: 3.13.1
+ package-json: 6.5.0
+ ramda: 0.26.1
+ semver: 6.3.0
+ dev: true
+ resolution:
+ integrity: sha512-PY7gH+7aQ+MltcUD7SRDuQODJ9Sav9HhFJsgOiyf8IVo7XVD6FxZIsSnpMI6paSkptOB7n+0Jz03gNlEkKetQQ==
+ /@serverless/enterprise-plugin/3.6.6:
+ dependencies:
+ '@serverless/event-mocks': 1.1.1
+ '@serverless/platform-client': 0.24.0
+ '@serverless/platform-sdk': 2.3.0
+ chalk: 2.4.2
+ child-process-ext: 2.1.1
+ chokidar: 3.3.1
+ cli-color: 2.0.0
+ dependency-tree: 7.2.1
+ find-process: 1.4.3
+ flat: 5.0.0
+ fs-extra: 8.1.0
+ iso8601-duration: 1.2.0
+ isomorphic-fetch: 2.2.1
+ js-yaml: 3.13.1
+ jsonata: 1.8.2
+ jszip: 3.3.0
+ lodash: 4.17.15
+ memoizee: 0.4.14
+ moment: 2.24.0
+ node-dir: 0.1.17
+ node-fetch: 2.6.0
+ regenerator-runtime: 0.13.5
+ semver: 6.3.0
+ simple-git: 1.132.0
+ source-map-support: 0.5.16
+ update-notifier: 2.5.0
+ uuid: 3.4.0
+ yamljs: 0.3.0
+ dev: true
+ engines:
+ node: '>=6.0'
+ resolution:
+ integrity: sha512-ZkzHp8WVOQv2opdXSYES39uorZV3m61+QDPK5W2PtV6InddYlYNTVuhH8vIynNYFrK8tZ95ZjpPi0BQkQ8q2EQ==
+ /@serverless/event-mocks/1.1.1:
+ dependencies:
+ '@types/lodash': 4.14.149
+ lodash: 4.17.15
+ dev: true
+ resolution:
+ integrity: sha512-YAV5V/y+XIOfd+HEVeXfPWZb8C6QLruFk9tBivoX2roQLWVq145s4uxf8D0QioCueuRzkukHUS4JIj+KVoS34A==
+ /@serverless/inquirer/1.1.0:
+ dependencies:
+ chalk: 3.0.0
+ inquirer: 7.1.0
+ ncjsm: 4.0.1
+ dev: true
+ resolution:
+ integrity: sha512-MpNMmV0uADfmGF8jVQ3Vmw+cdh7vAc8Ga/N9LHDhlbWh+EVHkqlpTi6bb3Xv6WsaPlWrx55Wo389IwpbhA0nPQ==
+ /@serverless/platform-client/0.24.0:
+ dependencies:
+ adm-zip: 0.4.14
+ axios: 0.19.2
+ https-proxy-agent: 5.0.0
+ isomorphic-ws: 4.0.1_ws@7.2.3
+ js-yaml: 3.13.1
+ jwt-decode: 2.2.0
+ querystring: 0.2.0
+ traverse: 0.6.6
+ ws: 7.2.3
+ dev: true
+ resolution:
+ integrity: sha512-ppxR5wONzzxNSmt/9agfSzC0F4yrkHZWAR5IPLm4yj+dMxb+768XrbqBU6vnOfCcmjb89OX5Bk0GvyQh+T5gLw==
+ /@serverless/platform-client/0.25.4:
+ dependencies:
+ adm-zip: 0.4.14
+ axios: 0.19.2
+ https-proxy-agent: 5.0.0
+ isomorphic-ws: 4.0.1_ws@7.2.3
+ js-yaml: 3.13.1
+ jwt-decode: 2.2.0
+ querystring: 0.2.0
+ traverse: 0.6.6
+ ws: 7.2.3
+ dev: true
+ resolution:
+ integrity: sha512-Q0aumXXyx+tyyvo30Ni1crE/Z0bKd1RrL7aFmPk9QULwvCX4mEJcebjlu2RvSHjz4A5+yRqqshKybdlDug/hDA==
+ /@serverless/platform-sdk/2.3.0:
+ dependencies:
+ chalk: 2.4.2
+ https-proxy-agent: 4.0.0
+ is-docker: 1.1.0
+ isomorphic-fetch: 2.2.1
+ jwt-decode: 2.2.0
+ opn: 5.5.0
+ querystring: 0.2.0
+ ramda: 0.25.0
+ rc: 1.2.8
+ regenerator-runtime: 0.13.5
+ source-map-support: 0.5.16
+ uuid: 3.4.0
+ write-file-atomic: 2.4.3
+ ws: 6.2.1
+ dev: true
+ resolution:
+ integrity: sha512-+9TiMYDVKJOyDWg9p/k0kmGVZ3+rjB8DXpACDxxyUChDSsRS55CTJnt321Yx7APfHctNRSnv3ubYmx7oGSTETQ==
+ /@serverless/template/1.1.3:
+ dependencies:
+ '@serverless/component-metrics': 1.0.8
+ '@serverless/core': 1.1.2
+ graphlib: 2.1.8
+ traverse: 0.6.6
+ dev: true
+ resolution:
+ integrity: sha512-hcMiX523rkp6kHeKnM1x6/dXEY+d1UFSr901yVKeeCgpFy4u33UI9vlKaPweAZCF6Ahzqywf01IsFTuBVadCrQ==
+ /@serverless/tencent-platform-client/0.25.10:
+ dependencies:
+ adm-zip: 0.4.14
+ axios: 0.19.2
+ dotenv: 8.2.0
+ https-proxy-agent: 5.0.0
+ isomorphic-ws: 4.0.1_ws@7.2.3
+ js-yaml: 3.13.1
+ jwt-decode: 2.2.0
+ querystring: 0.2.0
+ serverless-tencent-tools: 1.0.14
+ traverse: 0.6.6
+ urlencode: 1.1.0
+ ws: 7.2.3
+ dev: true
+ resolution:
+ integrity: sha512-HdifFh+2PNndRcaeXnrNoqdH7hiozlNH7Rk5enaTCSqAhD5YynJTiEOZMqWmo6eQRTOQJ30/xen8YJetGzMDPg==
+ /@sindresorhus/is/0.14.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
+ /@sindresorhus/is/0.7.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
+ /@sinonjs/commons/1.7.2:
+ dependencies:
+ type-detect: 4.0.8
+ dev: true
+ resolution:
+ integrity: sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw==
+ /@stardust-ui/react-component-event-listener/0.38.0_react-dom@16.13.1+react@16.13.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ dev: false
+ peerDependencies:
+ react: ^16.8.0
+ react-dom: ^16.8.0
+ resolution:
+ integrity: sha512-sIP/e0dyOrrlb8K7KWumfMxj/gAifswTBC4o68Aa+C/GA73ccRp/6W1VlHvF/dlOR4KLsA+5SKnhjH36xzPsWg==
+ /@stardust-ui/react-component-ref/0.38.0_react-dom@16.13.1+react@16.13.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ react-is: 16.13.1
+ dev: false
+ peerDependencies:
+ react: ^16.8.0
+ react-dom: ^16.8.0
+ resolution:
+ integrity: sha512-xjs6WnvJVueSIXMWw0C3oWIgAPpcD03qw43oGOjUXqFktvpNkB73JoKIhS4sCrtQxBdct75qqr4ZL6JiyPcESw==
+ /@svgr/babel-plugin-add-jsx-attribute/4.2.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-j7KnilGyZzYr/jhcrSYS3FGWMZVaqyCG0vzMCwzvei0coIkczuYMcniK07nI0aHJINciujjH11T72ICW5eL5Ig==
+ /@svgr/babel-plugin-remove-jsx-attribute/4.2.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-3XHLtJ+HbRCH4n28S7y/yZoEQnRpl0tvTZQsHqvaeNXPra+6vE5tbRliH3ox1yZYPCxrlqaJT/Mg+75GpDKlvQ==
+ /@svgr/babel-plugin-remove-jsx-empty-expression/4.2.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-yTr2iLdf6oEuUE9MsRdvt0NmdpMBAkgK8Bjhl6epb+eQWk6abBaX3d65UZ3E3FWaOwePyUgNyNCMVG61gGCQ7w==
+ /@svgr/babel-plugin-replace-jsx-attribute-value/4.2.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-U9m870Kqm0ko8beHawRXLGLvSi/ZMrl89gJ5BNcT452fAjtF2p4uRzXkdzvGJJJYBgx7BmqlDjBN/eCp5AAX2w==
+ /@svgr/babel-plugin-svg-dynamic-title/4.3.3:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-w3Be6xUNdwgParsvxkkeZb545VhXEwjGMwExMVBIdPQJeyMQHqm9Msnb2a1teHBqUYL66qtwfhNkbj1iarCG7w==
+ /@svgr/babel-plugin-svg-em-dimensions/4.2.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-C0Uy+BHolCHGOZ8Dnr1zXy/KgpBOkEUYY9kI/HseHVPeMbluaX3CijJr7D4C5uR8zrc1T64nnq/k63ydQuGt4w==
+ /@svgr/babel-plugin-transform-react-native-svg/4.2.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-7YvynOpZDpCOUoIVlaaOUU87J4Z6RdD6spYN4eUb5tfPoKGSF9OG2NuhgYnq4jSkAxcpMaXWPf1cePkzmqTPNw==
+ /@svgr/babel-plugin-transform-svg-component/4.2.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-hYfYuZhQPCBVotABsXKSCfel2slf/yvJY8heTVX1PCTaq/IgASq1IyxPPKJ0chWREEKewIU/JMSsIGBtK1KKxw==
+ /@svgr/babel-preset/4.3.3:
+ dependencies:
+ '@svgr/babel-plugin-add-jsx-attribute': 4.2.0
+ '@svgr/babel-plugin-remove-jsx-attribute': 4.2.0
+ '@svgr/babel-plugin-remove-jsx-empty-expression': 4.2.0
+ '@svgr/babel-plugin-replace-jsx-attribute-value': 4.2.0
+ '@svgr/babel-plugin-svg-dynamic-title': 4.3.3
+ '@svgr/babel-plugin-svg-em-dimensions': 4.2.0
+ '@svgr/babel-plugin-transform-react-native-svg': 4.2.0
+ '@svgr/babel-plugin-transform-svg-component': 4.2.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-6PG80tdz4eAlYUN3g5GZiUjg2FMcp+Wn6rtnz5WJG9ITGEF1pmFdzq02597Hn0OmnQuCVaBYQE1OVFAnwOl+0A==
+ /@svgr/core/4.3.3:
+ dependencies:
+ '@svgr/plugin-jsx': 4.3.3
+ camelcase: 5.3.1
+ cosmiconfig: 5.2.1
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-qNuGF1QON1626UCaZamWt5yedpgOytvLj5BQZe2j1k1B8DUG4OyugZyfEwBeXozCUwhLEpsrgPrE+eCu4fY17w==
+ /@svgr/hast-util-to-babel-ast/4.3.2:
+ dependencies:
+ '@babel/types': 7.9.5
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-JioXclZGhFIDL3ddn4Kiq8qEqYM2PyDKV0aYno8+IXTLuYt6TOgHUbUAAFvqtb0Xn37NwP0BTHglejFoYr8RZg==
+ /@svgr/plugin-jsx/4.3.3:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@svgr/babel-preset': 4.3.3
+ '@svgr/hast-util-to-babel-ast': 4.3.2
+ svg-parser: 2.0.4
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-cLOCSpNWQnDB1/v+SUENHH7a0XY09bfuMKdq9+gYvtuwzC2rU4I0wKGFEp1i24holdQdwodCtDQdFtJiTCWc+w==
+ /@svgr/plugin-svgo/4.3.1:
+ dependencies:
+ cosmiconfig: 5.2.1
+ merge-deep: 3.0.2
+ svgo: 1.3.2
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-PrMtEDUWjX3Ea65JsVCwTIXuSqa3CG9px+DluF1/eo9mlDrgrtFE7NE/DjdhjJgSM9wenlVBzkzneSIUgfUI/w==
+ /@svgr/webpack/4.3.3:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/plugin-transform-react-constant-elements': 7.9.0_@babel+core@7.9.0
+ '@babel/preset-env': 7.9.5_@babel+core@7.9.0
+ '@babel/preset-react': 7.9.4_@babel+core@7.9.0
+ '@svgr/core': 4.3.3
+ '@svgr/plugin-jsx': 4.3.3
+ '@svgr/plugin-svgo': 4.3.1
+ loader-utils: 1.4.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-bjnWolZ6KVsHhgyCoYRFmbd26p8XVbulCzSG53BDQqAr+JOAderYK7CuYrB3bDjHJuF6LJ7Wrr42+goLRV9qIg==
+ /@szmarczak/http-timer/1.1.2:
+ dependencies:
+ defer-to-connect: 1.1.3
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==
+ /@tencent-sdk/capi/0.2.15:
+ dependencies:
+ object-assign: 4.1.1
+ querystring: 0.2.0
+ request: 2.88.2
+ request-promise-native: 1.0.8_request@2.88.2
+ dev: true
+ resolution:
+ integrity: sha512-5t94Mo/+Kdvr60tJR/+pylUCwIM+ipcBIkUi4M7dtV0yCpuykOXV4GYT1aWg/iWMXyIPnfOUk4Pr6OwDoAVehw==
+ /@types/aws-lambda/8.10.48:
+ dev: false
+ optional: true
+ resolution:
+ integrity: sha512-+qFDcssXvrdXIxBbKCJp0atg94TJVJSt5sx3Cu6LOQX/EV2mbInjgxGeKuLmFFBjoxD7G6fSytZoeC6A9fzTuw==
+ /@types/babel__core/7.1.7:
+ dependencies:
+ '@babel/parser': 7.9.4
+ '@babel/types': 7.9.5
+ '@types/babel__generator': 7.6.1
+ '@types/babel__template': 7.0.2
+ '@types/babel__traverse': 7.0.10
+ dev: true
+ resolution:
+ integrity: sha512-RL62NqSFPCDK2FM1pSDH0scHpJvsXtZNiYlMB73DgPBaG1E38ZYVL+ei5EkWRbr+KC4YNiAUNBnRj+bgwpgjMw==
+ /@types/babel__generator/7.6.1:
+ dependencies:
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==
+ /@types/babel__template/7.0.2:
+ dependencies:
+ '@babel/parser': 7.9.4
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==
+ /@types/babel__traverse/7.0.10:
+ dependencies:
+ '@babel/types': 7.9.5
+ dev: true
+ resolution:
+ integrity: sha512-74fNdUGrWsgIB/V9kTO5FGHPWYY6Eqn+3Z7L6Hc4e/BxjYV7puvBqp5HwsVYYfLm6iURYBNCx4Ut37OF9yitCw==
+ /@types/color-name/1.1.1:
+ dev: true
+ resolution:
+ integrity: sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
+ /@types/eslint-visitor-keys/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==
+ /@types/events/3.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
+ /@types/glob/7.1.1:
+ dependencies:
+ '@types/events': 3.0.0
+ '@types/minimatch': 3.0.3
+ '@types/node': 13.11.1
+ dev: true
+ resolution:
+ integrity: sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
+ /@types/istanbul-lib-coverage/2.0.1:
+ dev: true
+ resolution:
+ integrity: sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==
+ /@types/istanbul-lib-report/3.0.0:
+ dependencies:
+ '@types/istanbul-lib-coverage': 2.0.1
+ dev: true
+ resolution:
+ integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==
+ /@types/istanbul-reports/1.1.1:
+ dependencies:
+ '@types/istanbul-lib-coverage': 2.0.1
+ '@types/istanbul-lib-report': 3.0.0
+ dev: true
+ resolution:
+ integrity: sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==
+ /@types/json-schema/7.0.4:
+ dev: true
+ resolution:
+ integrity: sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
+ /@types/lodash/4.14.149:
+ dev: true
+ resolution:
+ integrity: sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==
+ /@types/minimatch/3.0.3:
+ dev: true
+ resolution:
+ integrity: sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
+ /@types/node/13.11.1:
+ dev: true
+ resolution:
+ integrity: sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==
+ /@types/normalize-package-data/2.4.0:
+ dev: true
+ resolution:
+ integrity: sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
+ /@types/parse-json/4.0.0:
+ resolution:
+ integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
+ /@types/prettier/1.19.1:
+ dev: true
+ resolution:
+ integrity: sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==
+ /@types/prop-types/15.7.3:
+ dev: false
+ resolution:
+ integrity: sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
+ /@types/q/1.5.2:
+ dev: true
+ resolution:
+ integrity: sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
+ /@types/react-table/6.8.7:
+ dependencies:
+ '@types/react': 16.9.33
+ dev: false
+ resolution:
+ integrity: sha512-1U0xl47jk0BzE+HNHgxZYSLvtybSvnlLhOpW9Mfqf9iuRm/fGqgRab3TKivPCY6Tl7WPFM2hWEJ1GnsuSFc9AQ==
+ /@types/react/16.9.33:
+ dependencies:
+ '@types/prop-types': 15.7.3
+ csstype: 2.6.10
+ dev: false
+ resolution:
+ integrity: sha512-ovgoy7p9999HDzwv8Sewhl8GJjn/r0GRsFrM9UMwp1uodh0kQ0pwIHLQ6LNfqGSyjNzJ8II/HIg0BL7Yn/B9yA==
+ /@types/stack-utils/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
+ /@types/yargs-parser/15.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==
+ /@types/yargs/13.0.8:
+ dependencies:
+ '@types/yargs-parser': 15.0.0
+ dev: true
+ resolution:
+ integrity: sha512-XAvHLwG7UQ+8M4caKIH0ZozIOYay5fQkAgyIXegXT9jPtdIGdhga+sUEdAr1CiG46aB+c64xQEYyEzlwWVTNzA==
+ /@types/yargs/15.0.4:
+ dependencies:
+ '@types/yargs-parser': 15.0.0
+ dev: true
+ resolution:
+ integrity: sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==
+ /@typescript-eslint/eslint-plugin/2.27.0_9e31f0f459c1656d0a7ef30429cc70f8:
+ dependencies:
+ '@typescript-eslint/experimental-utils': 2.27.0_eslint@6.8.0
+ '@typescript-eslint/parser': 2.27.0_eslint@6.8.0
+ eslint: 6.8.0
+ functional-red-black-tree: 1.0.1
+ regexpp: 3.1.0
+ tsutils: 3.17.1
+ dev: true
+ engines:
+ node: ^8.10.0 || ^10.13.0 || >=11.10.1
+ peerDependencies:
+ '@typescript-eslint/parser': ^2.0.0
+ eslint: ^5.0.0 || ^6.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ resolution:
+ integrity: sha512-/my+vVHRN7zYgcp0n4z5A6HAK7bvKGBiswaM5zIlOQczsxj/aiD7RcgD+dvVFuwFaGh5+kM7XA6Q6PN0bvb1tw==
+ /@typescript-eslint/experimental-utils/1.13.0_eslint@6.8.0:
+ dependencies:
+ '@types/json-schema': 7.0.4
+ '@typescript-eslint/typescript-estree': 1.13.0
+ eslint: 6.8.0
+ eslint-scope: 4.0.3
+ dev: true
+ engines:
+ node: ^6.14.0 || ^8.10.0 || >=9.10.0
+ peerDependencies:
+ eslint: '*'
+ resolution:
+ integrity: sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==
+ /@typescript-eslint/experimental-utils/2.27.0_eslint@6.8.0:
+ dependencies:
+ '@types/json-schema': 7.0.4
+ '@typescript-eslint/typescript-estree': 2.27.0
+ eslint: 6.8.0
+ eslint-scope: 5.0.0
+ eslint-utils: 2.0.0
+ dev: true
+ engines:
+ node: ^8.10.0 || ^10.13.0 || >=11.10.1
+ peerDependencies:
+ eslint: '*'
+ resolution:
+ integrity: sha512-vOsYzjwJlY6E0NJRXPTeCGqjv5OHgRU1kzxHKWJVPjDYGbPgLudBXjIlc+OD1hDBZ4l1DLbOc5VjofKahsu9Jw==
+ /@typescript-eslint/parser/2.27.0_eslint@6.8.0:
+ dependencies:
+ '@types/eslint-visitor-keys': 1.0.0
+ '@typescript-eslint/experimental-utils': 2.27.0_eslint@6.8.0
+ '@typescript-eslint/typescript-estree': 2.27.0
+ eslint: 6.8.0
+ eslint-visitor-keys: 1.1.0
+ dev: true
+ engines:
+ node: ^8.10.0 || ^10.13.0 || >=11.10.1
+ peerDependencies:
+ eslint: ^5.0.0 || ^6.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ resolution:
+ integrity: sha512-HFUXZY+EdwrJXZo31DW4IS1ujQW3krzlRjBrFRrJcMDh0zCu107/nRfhk/uBasO8m0NVDbBF5WZKcIUMRO7vPg==
+ /@typescript-eslint/typescript-estree/1.13.0:
+ dependencies:
+ lodash.unescape: 4.0.1
+ semver: 5.5.0
+ dev: true
+ engines:
+ node: '>=6.14.0'
+ resolution:
+ integrity: sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==
+ /@typescript-eslint/typescript-estree/2.27.0:
+ dependencies:
+ debug: 4.1.1
+ eslint-visitor-keys: 1.1.0
+ glob: 7.1.6
+ is-glob: 4.0.1
+ lodash: 4.17.15
+ semver: 6.3.0
+ tsutils: 3.17.1
+ dev: true
+ engines:
+ node: ^8.10.0 || ^10.13.0 || >=11.10.1
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ resolution:
+ integrity: sha512-t2miCCJIb/FU8yArjAvxllxbTiyNqaXJag7UOpB5DVoM3+xnjeOngtqlJkLRnMtzaRcJhe3CIR9RmL40omubhg==
+ /@typescript-eslint/typescript-estree/2.27.0_typescript@3.8.3:
+ dependencies:
+ debug: 4.1.1
+ eslint-visitor-keys: 1.1.0
+ glob: 7.1.6
+ is-glob: 4.0.1
+ lodash: 4.17.15
+ semver: 6.3.0
+ tsutils: 3.17.1_typescript@3.8.3
+ typescript: 3.8.3
+ dev: true
+ engines:
+ node: ^8.10.0 || ^10.13.0 || >=11.10.1
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ resolution:
+ integrity: sha512-t2miCCJIb/FU8yArjAvxllxbTiyNqaXJag7UOpB5DVoM3+xnjeOngtqlJkLRnMtzaRcJhe3CIR9RmL40omubhg==
+ /@webassemblyjs/ast/1.8.5:
+ dependencies:
+ '@webassemblyjs/helper-module-context': 1.8.5
+ '@webassemblyjs/helper-wasm-bytecode': 1.8.5
+ '@webassemblyjs/wast-parser': 1.8.5
+ dev: true
+ resolution:
+ integrity: sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==
+ /@webassemblyjs/ast/1.9.0:
+ dependencies:
+ '@webassemblyjs/helper-module-context': 1.9.0
+ '@webassemblyjs/helper-wasm-bytecode': 1.9.0
+ '@webassemblyjs/wast-parser': 1.9.0
+ dev: true
+ resolution:
+ integrity: sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==
+ /@webassemblyjs/floating-point-hex-parser/1.8.5:
+ dev: true
+ resolution:
+ integrity: sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==
+ /@webassemblyjs/floating-point-hex-parser/1.9.0:
+ dev: true
+ resolution:
+ integrity: sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==
+ /@webassemblyjs/helper-api-error/1.8.5:
+ dev: true
+ resolution:
+ integrity: sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==
+ /@webassemblyjs/helper-api-error/1.9.0:
+ dev: true
+ resolution:
+ integrity: sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==
+ /@webassemblyjs/helper-buffer/1.8.5:
+ dev: true
+ resolution:
+ integrity: sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==
+ /@webassemblyjs/helper-buffer/1.9.0:
+ dev: true
+ resolution:
+ integrity: sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==
+ /@webassemblyjs/helper-code-frame/1.8.5:
+ dependencies:
+ '@webassemblyjs/wast-printer': 1.8.5
+ dev: true
+ resolution:
+ integrity: sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==
+ /@webassemblyjs/helper-code-frame/1.9.0:
+ dependencies:
+ '@webassemblyjs/wast-printer': 1.9.0
+ dev: true
+ resolution:
+ integrity: sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==
+ /@webassemblyjs/helper-fsm/1.8.5:
+ dev: true
+ resolution:
+ integrity: sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==
+ /@webassemblyjs/helper-fsm/1.9.0:
+ dev: true
+ resolution:
+ integrity: sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==
+ /@webassemblyjs/helper-module-context/1.8.5:
+ dependencies:
+ '@webassemblyjs/ast': 1.8.5
+ mamacro: 0.0.3
+ dev: true
+ resolution:
+ integrity: sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==
+ /@webassemblyjs/helper-module-context/1.9.0:
+ dependencies:
+ '@webassemblyjs/ast': 1.9.0
+ dev: true
+ resolution:
+ integrity: sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==
+ /@webassemblyjs/helper-wasm-bytecode/1.8.5:
+ dev: true
+ resolution:
+ integrity: sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==
+ /@webassemblyjs/helper-wasm-bytecode/1.9.0:
+ dev: true
+ resolution:
+ integrity: sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==
+ /@webassemblyjs/helper-wasm-section/1.8.5:
+ dependencies:
+ '@webassemblyjs/ast': 1.8.5
+ '@webassemblyjs/helper-buffer': 1.8.5
+ '@webassemblyjs/helper-wasm-bytecode': 1.8.5
+ '@webassemblyjs/wasm-gen': 1.8.5
+ dev: true
+ resolution:
+ integrity: sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==
+ /@webassemblyjs/helper-wasm-section/1.9.0:
+ dependencies:
+ '@webassemblyjs/ast': 1.9.0
+ '@webassemblyjs/helper-buffer': 1.9.0
+ '@webassemblyjs/helper-wasm-bytecode': 1.9.0
+ '@webassemblyjs/wasm-gen': 1.9.0
+ dev: true
+ resolution:
+ integrity: sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==
+ /@webassemblyjs/ieee754/1.8.5:
+ dependencies:
+ '@xtuc/ieee754': 1.2.0
+ dev: true
+ resolution:
+ integrity: sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==
+ /@webassemblyjs/ieee754/1.9.0:
+ dependencies:
+ '@xtuc/ieee754': 1.2.0
+ dev: true
+ resolution:
+ integrity: sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==
+ /@webassemblyjs/leb128/1.8.5:
+ dependencies:
+ '@xtuc/long': 4.2.2
+ dev: true
+ resolution:
+ integrity: sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==
+ /@webassemblyjs/leb128/1.9.0:
+ dependencies:
+ '@xtuc/long': 4.2.2
+ dev: true
+ resolution:
+ integrity: sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==
+ /@webassemblyjs/utf8/1.8.5:
+ dev: true
+ resolution:
+ integrity: sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==
+ /@webassemblyjs/utf8/1.9.0:
+ dev: true
+ resolution:
+ integrity: sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==
+ /@webassemblyjs/wasm-edit/1.8.5:
+ dependencies:
+ '@webassemblyjs/ast': 1.8.5
+ '@webassemblyjs/helper-buffer': 1.8.5
+ '@webassemblyjs/helper-wasm-bytecode': 1.8.5
+ '@webassemblyjs/helper-wasm-section': 1.8.5
+ '@webassemblyjs/wasm-gen': 1.8.5
+ '@webassemblyjs/wasm-opt': 1.8.5
+ '@webassemblyjs/wasm-parser': 1.8.5
+ '@webassemblyjs/wast-printer': 1.8.5
+ dev: true
+ resolution:
+ integrity: sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==
+ /@webassemblyjs/wasm-edit/1.9.0:
+ dependencies:
+ '@webassemblyjs/ast': 1.9.0
+ '@webassemblyjs/helper-buffer': 1.9.0
+ '@webassemblyjs/helper-wasm-bytecode': 1.9.0
+ '@webassemblyjs/helper-wasm-section': 1.9.0
+ '@webassemblyjs/wasm-gen': 1.9.0
+ '@webassemblyjs/wasm-opt': 1.9.0
+ '@webassemblyjs/wasm-parser': 1.9.0
+ '@webassemblyjs/wast-printer': 1.9.0
+ dev: true
+ resolution:
+ integrity: sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==
+ /@webassemblyjs/wasm-gen/1.8.5:
+ dependencies:
+ '@webassemblyjs/ast': 1.8.5
+ '@webassemblyjs/helper-wasm-bytecode': 1.8.5
+ '@webassemblyjs/ieee754': 1.8.5
+ '@webassemblyjs/leb128': 1.8.5
+ '@webassemblyjs/utf8': 1.8.5
+ dev: true
+ resolution:
+ integrity: sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==
+ /@webassemblyjs/wasm-gen/1.9.0:
+ dependencies:
+ '@webassemblyjs/ast': 1.9.0
+ '@webassemblyjs/helper-wasm-bytecode': 1.9.0
+ '@webassemblyjs/ieee754': 1.9.0
+ '@webassemblyjs/leb128': 1.9.0
+ '@webassemblyjs/utf8': 1.9.0
+ dev: true
+ resolution:
+ integrity: sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==
+ /@webassemblyjs/wasm-opt/1.8.5:
+ dependencies:
+ '@webassemblyjs/ast': 1.8.5
+ '@webassemblyjs/helper-buffer': 1.8.5
+ '@webassemblyjs/wasm-gen': 1.8.5
+ '@webassemblyjs/wasm-parser': 1.8.5
+ dev: true
+ resolution:
+ integrity: sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==
+ /@webassemblyjs/wasm-opt/1.9.0:
+ dependencies:
+ '@webassemblyjs/ast': 1.9.0
+ '@webassemblyjs/helper-buffer': 1.9.0
+ '@webassemblyjs/wasm-gen': 1.9.0
+ '@webassemblyjs/wasm-parser': 1.9.0
+ dev: true
+ resolution:
+ integrity: sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==
+ /@webassemblyjs/wasm-parser/1.8.5:
+ dependencies:
+ '@webassemblyjs/ast': 1.8.5
+ '@webassemblyjs/helper-api-error': 1.8.5
+ '@webassemblyjs/helper-wasm-bytecode': 1.8.5
+ '@webassemblyjs/ieee754': 1.8.5
+ '@webassemblyjs/leb128': 1.8.5
+ '@webassemblyjs/utf8': 1.8.5
+ dev: true
+ resolution:
+ integrity: sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==
+ /@webassemblyjs/wasm-parser/1.9.0:
+ dependencies:
+ '@webassemblyjs/ast': 1.9.0
+ '@webassemblyjs/helper-api-error': 1.9.0
+ '@webassemblyjs/helper-wasm-bytecode': 1.9.0
+ '@webassemblyjs/ieee754': 1.9.0
+ '@webassemblyjs/leb128': 1.9.0
+ '@webassemblyjs/utf8': 1.9.0
+ dev: true
+ resolution:
+ integrity: sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==
+ /@webassemblyjs/wast-parser/1.8.5:
+ dependencies:
+ '@webassemblyjs/ast': 1.8.5
+ '@webassemblyjs/floating-point-hex-parser': 1.8.5
+ '@webassemblyjs/helper-api-error': 1.8.5
+ '@webassemblyjs/helper-code-frame': 1.8.5
+ '@webassemblyjs/helper-fsm': 1.8.5
+ '@xtuc/long': 4.2.2
+ dev: true
+ resolution:
+ integrity: sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==
+ /@webassemblyjs/wast-parser/1.9.0:
+ dependencies:
+ '@webassemblyjs/ast': 1.9.0
+ '@webassemblyjs/floating-point-hex-parser': 1.9.0
+ '@webassemblyjs/helper-api-error': 1.9.0
+ '@webassemblyjs/helper-code-frame': 1.9.0
+ '@webassemblyjs/helper-fsm': 1.9.0
+ '@xtuc/long': 4.2.2
+ dev: true
+ resolution:
+ integrity: sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==
+ /@webassemblyjs/wast-printer/1.8.5:
+ dependencies:
+ '@webassemblyjs/ast': 1.8.5
+ '@webassemblyjs/wast-parser': 1.8.5
+ '@xtuc/long': 4.2.2
+ dev: true
+ resolution:
+ integrity: sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==
+ /@webassemblyjs/wast-printer/1.9.0:
+ dependencies:
+ '@webassemblyjs/ast': 1.9.0
+ '@webassemblyjs/wast-parser': 1.9.0
+ '@xtuc/long': 4.2.2
+ dev: true
+ resolution:
+ integrity: sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==
+ /@xtuc/ieee754/1.2.0:
+ dev: true
+ resolution:
+ integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+ /@xtuc/long/4.2.2:
+ dev: true
+ resolution:
+ integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+ /abab/2.0.3:
+ dev: true
+ resolution:
+ integrity: sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==
+ /accepts/1.3.7:
+ dependencies:
+ mime-types: 2.1.26
+ negotiator: 0.6.2
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
+ /acorn-globals/4.3.4:
+ dependencies:
+ acorn: 6.4.1
+ acorn-walk: 6.2.0
+ dev: true
+ resolution:
+ integrity: sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==
+ /acorn-jsx/5.2.0_acorn@7.1.1:
+ dependencies:
+ acorn: 7.1.1
+ dev: true
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0
+ resolution:
+ integrity: sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==
+ /acorn-walk/6.2.0:
+ dev: true
+ engines:
+ node: '>=0.4.0'
+ resolution:
+ integrity: sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
+ /acorn/5.7.4:
+ dev: true
+ engines:
+ node: '>=0.4.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
+ /acorn/6.4.1:
+ dev: true
+ engines:
+ node: '>=0.4.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
+ /acorn/7.1.1:
+ dev: true
+ engines:
+ node: '>=0.4.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==
+ /address/1.1.2:
+ dev: true
+ engines:
+ node: '>= 0.12.0'
+ resolution:
+ integrity: sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==
+ /adjust-sourcemap-loader/2.0.0:
+ dependencies:
+ assert: 1.4.1
+ camelcase: 5.0.0
+ loader-utils: 1.2.3
+ object-path: 0.11.4
+ regex-parser: 2.2.10
+ dev: true
+ resolution:
+ integrity: sha512-4hFsTsn58+YjrU9qKzML2JSSDqKvN8mUGQ0nNIrfPi8hmIONT4L3uUaT6MKdMsZ9AjsU6D2xDkZxCkbQPxChrA==
+ /adm-zip/0.4.14:
+ dev: true
+ engines:
+ node: '>=0.3.0'
+ resolution:
+ integrity: sha512-/9aQCnQHF+0IiCl0qhXoK7qs//SwYE7zX8lsr/DNk1BRAHYxeLZPL4pguwK29gUEqasYQjqPtEpDRSWEkdHn9g==
+ /after/0.8.2:
+ dev: true
+ resolution:
+ integrity: sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
+ /agent-base/5.1.1:
+ dev: true
+ engines:
+ node: '>= 6.0.0'
+ resolution:
+ integrity: sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==
+ /agent-base/6.0.0:
+ dependencies:
+ debug: 4.1.1
+ dev: true
+ engines:
+ node: '>= 6.0.0'
+ resolution:
+ integrity: sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==
+ /aggregate-error/3.0.1:
+ dependencies:
+ clean-stack: 2.2.0
+ indent-string: 4.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==
+ /ajv-errors/1.0.1_ajv@6.12.0:
+ dependencies:
+ ajv: 6.12.0
+ dev: true
+ peerDependencies:
+ ajv: '>=5.0.0'
+ resolution:
+ integrity: sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==
+ /ajv-keywords/3.4.1_ajv@6.12.0:
+ dependencies:
+ ajv: 6.12.0
+ dev: true
+ peerDependencies:
+ ajv: ^6.9.1
+ resolution:
+ integrity: sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
+ /ajv/6.12.0:
+ dependencies:
+ fast-deep-equal: 3.1.1
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.2.2
+ resolution:
+ integrity: sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==
+ /alphanum-sort/1.0.2:
+ dev: true
+ resolution:
+ integrity: sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
+ /ansi-align/2.0.0:
+ dependencies:
+ string-width: 2.1.1
+ dev: true
+ resolution:
+ integrity: sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=
+ /ansi-align/3.0.0:
+ dependencies:
+ string-width: 3.1.0
+ dev: true
+ resolution:
+ integrity: sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==
+ /ansi-colors/3.2.4:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==
+ /ansi-escapes/3.2.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
+ /ansi-escapes/4.3.1:
+ dependencies:
+ type-fest: 0.11.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==
+ /ansi-html/0.0.7:
+ dev: true
+ engines:
+ '0': node >= 0.8.0
+ hasBin: true
+ resolution:
+ integrity: sha1-gTWEAhliqenm/QOflA0S9WynhZ4=
+ /ansi-regex/2.1.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
+ /ansi-regex/3.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+ /ansi-regex/4.1.0:
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+ /ansi-regex/5.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
+ /ansi-styles/2.2.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
+ /ansi-styles/3.2.1:
+ dependencies:
+ color-convert: 1.9.3
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ /ansi-styles/4.2.1:
+ dependencies:
+ '@types/color-name': 1.1.1
+ color-convert: 2.0.1
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
+ /anymatch/2.0.0:
+ dependencies:
+ micromatch: 3.1.10
+ normalize-path: 2.1.1
+ dev: true
+ resolution:
+ integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+ /anymatch/3.1.1:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.2.2
+ dev: true
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
+ /app-module-path/2.2.0:
+ dev: true
+ resolution:
+ integrity: sha1-ZBqlXft9am8KgUHEucCqULbCTdU=
+ /aproba/1.2.0:
+ dev: true
+ resolution:
+ integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+ /archive-type/4.0.0:
+ dependencies:
+ file-type: 4.4.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=
+ /archiver-utils/1.3.0:
+ dependencies:
+ glob: 7.1.6
+ graceful-fs: 4.2.3
+ lazystream: 1.0.0
+ lodash: 4.17.15
+ normalize-path: 2.1.1
+ readable-stream: 2.3.7
+ dev: true
+ engines:
+ node: '>= 0.10.0'
+ resolution:
+ integrity: sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=
+ /archiver/1.3.0:
+ dependencies:
+ archiver-utils: 1.3.0
+ async: 2.6.3
+ buffer-crc32: 0.2.13
+ glob: 7.1.6
+ lodash: 4.17.15
+ readable-stream: 2.3.7
+ tar-stream: 1.6.2
+ walkdir: 0.0.11
+ zip-stream: 1.2.0
+ dev: true
+ engines:
+ node: '>= 0.10.0'
+ resolution:
+ integrity: sha1-TyGU1tj5nfP1MeaIHxTxXVX6ryI=
+ /archiver/2.1.1:
+ dependencies:
+ archiver-utils: 1.3.0
+ async: 2.6.3
+ buffer-crc32: 0.2.13
+ glob: 7.1.6
+ lodash: 4.17.15
+ readable-stream: 2.3.7
+ tar-stream: 1.6.2
+ zip-stream: 1.2.0
+ dev: true
+ engines:
+ node: '>= 4'
+ resolution:
+ integrity: sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw=
+ /argparse/1.0.10:
+ dependencies:
+ sprintf-js: 1.0.3
+ resolution:
+ integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+ /aria-query/3.0.0:
+ dependencies:
+ ast-types-flow: 0.0.7
+ commander: 2.20.3
+ dev: true
+ resolution:
+ integrity: sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=
+ /arity-n/1.0.4:
+ dev: true
+ resolution:
+ integrity: sha1-2edrEXM+CFacCEeuezmyhgswt0U=
+ /arr-diff/4.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+ /arr-flatten/1.1.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+ /arr-union/3.1.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+ /array-differ/2.1.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w==
+ /array-equal/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
+ /array-flatten/1.1.1:
+ resolution:
+ integrity: sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
+ /array-flatten/2.1.2:
+ dev: true
+ resolution:
+ integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
+ /array-includes/3.1.1:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ is-string: 1.0.5
+ dev: true
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==
+ /array-union/1.0.2:
+ dependencies:
+ array-uniq: 1.0.3
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=
+ /array-union/2.1.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+ /array-uniq/1.0.3:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
+ /array-unique/0.3.2:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+ /array.prototype.flat/1.2.3:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ dev: true
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==
+ /arraybuffer.slice/0.0.7:
+ dev: true
+ resolution:
+ integrity: sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
+ /arrify/1.0.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
+ /asap/2.0.6:
+ dev: true
+ resolution:
+ integrity: sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
+ /asn1.js/4.10.1:
+ dependencies:
+ bn.js: 4.11.8
+ inherits: 2.0.4
+ minimalistic-assert: 1.0.1
+ resolution:
+ integrity: sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==
+ /asn1.js/5.3.0:
+ dependencies:
+ bn.js: 4.11.8
+ inherits: 2.0.4
+ minimalistic-assert: 1.0.1
+ safer-buffer: 2.1.2
+ dev: false
+ resolution:
+ integrity: sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA==
+ /asn1/0.2.4:
+ dependencies:
+ safer-buffer: 2.1.2
+ resolution:
+ integrity: sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+ /assert-plus/1.0.0:
+ engines:
+ node: '>=0.8'
+ resolution:
+ integrity: sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+ /assert/1.4.1:
+ dependencies:
+ util: 0.10.3
+ dev: true
+ resolution:
+ integrity: sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=
+ /assert/1.5.0:
+ dependencies:
+ object-assign: 4.1.1
+ util: 0.10.3
+ dev: true
+ resolution:
+ integrity: sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==
+ /assign-symbols/1.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+ /ast-module-types/2.6.0:
+ dev: true
+ resolution:
+ integrity: sha512-zXSoVaMrf2R+r+ISid5/9a8SXm1LLdkhHzh6pSRhj9jklzruOOl1hva1YmFT33wAstg/f9ZndJAlq1BSrFLSGA==
+ /ast-types-flow/0.0.7:
+ dev: true
+ resolution:
+ integrity: sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
+ /astral-regex/1.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
+ /async-each/1.0.3:
+ dev: true
+ resolution:
+ integrity: sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
+ /async-limiter/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+ /async/1.5.2:
+ dev: true
+ resolution:
+ integrity: sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=
+ /async/2.6.3:
+ dependencies:
+ lodash: 4.17.15
+ dev: true
+ resolution:
+ integrity: sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
+ /asynckit/0.4.0:
+ resolution:
+ integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=
+ /atob/2.1.2:
+ dev: true
+ engines:
+ node: '>= 4.5.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+ /attr-accept/2.1.0:
+ dev: false
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-sLzVM3zCCmmDtDNhI0i96k6PUztkotSOXqE4kDGQt/6iDi5M+H0srjeF+QC6jN581l4X/Zq3Zu/tgcErEssavg==
+ /autoprefixer/9.7.6:
+ dependencies:
+ browserslist: 4.11.1
+ caniuse-lite: 1.0.30001039
+ chalk: 2.4.2
+ normalize-range: 0.1.2
+ num2fraction: 1.2.2
+ postcss: 7.0.27
+ postcss-value-parser: 4.0.3
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-F7cYpbN7uVVhACZTeeIeealwdGM6wMtfWARVLTy5xmKtgVdBNJvbDRoCK3YO1orcs7gv/KwYlb3iXwu9Ug9BkQ==
+ /aws-sdk/2.656.0:
+ dependencies:
+ buffer: 4.9.1
+ events: 1.1.1
+ ieee754: 1.1.13
+ jmespath: 0.15.0
+ querystring: 0.2.0
+ sax: 1.2.1
+ url: 0.10.3
+ uuid: 3.3.2
+ xml2js: 0.4.19
+ engines:
+ node: '>= 0.8.0'
+ resolution:
+ integrity: sha512-UzqDvvt6i7gpuzEdK0GT/JOfBJcsCPranzZWdQ9HR4+5E0m5kf5gybZ6OX+UseIAE2/WND6Dv0aHgiI21AKenw==
+ /aws-sign2/0.7.0:
+ resolution:
+ integrity: sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+ /aws4/1.9.1:
+ resolution:
+ integrity: sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
+ /axios/0.19.2:
+ dependencies:
+ follow-redirects: 1.5.10
+ dev: true
+ resolution:
+ integrity: sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
+ /axobject-query/2.1.2:
+ dev: true
+ resolution:
+ integrity: sha512-ICt34ZmrVt8UQnvPl6TVyDTkmhXmAyAT4Jh5ugfGUX4MOrZ+U/ZY6/sdylRw3qGNr9Ub5AJsaHeDMzNLehRdOQ==
+ /babel-code-frame/6.26.0:
+ dependencies:
+ chalk: 1.1.3
+ esutils: 2.0.3
+ js-tokens: 3.0.2
+ dev: true
+ resolution:
+ integrity: sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=
+ /babel-eslint/10.1.0_eslint@6.8.0:
+ dependencies:
+ '@babel/code-frame': 7.8.3
+ '@babel/parser': 7.9.4
+ '@babel/traverse': 7.9.5
+ '@babel/types': 7.9.5
+ eslint: 6.8.0
+ eslint-visitor-keys: 1.1.0
+ resolve: 1.15.1
+ dev: true
+ engines:
+ node: '>=6'
+ peerDependencies:
+ eslint: '>= 4.12.1'
+ resolution:
+ integrity: sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==
+ /babel-extract-comments/1.0.0:
+ dependencies:
+ babylon: 6.18.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-qWWzi4TlddohA91bFwgt6zO/J0X+io7Qp184Fw0m2JYRSTZnJbFR8+07KmzudHCZgOiKRCrjhylwv9Xd8gfhVQ==
+ /babel-jest/24.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@jest/transform': 24.9.0
+ '@jest/types': 24.9.0
+ '@types/babel__core': 7.1.7
+ babel-plugin-istanbul: 5.2.0
+ babel-preset-jest: 24.9.0_@babel+core@7.9.0
+ chalk: 2.4.2
+ slash: 2.0.0
+ dev: true
+ engines:
+ node: '>= 6'
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ resolution:
+ integrity: sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==
+ /babel-jest/25.3.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@jest/transform': 25.3.0
+ '@jest/types': 25.3.0
+ '@types/babel__core': 7.1.7
+ babel-plugin-istanbul: 6.0.0
+ babel-preset-jest: 25.3.0_@babel+core@7.9.0
+ chalk: 3.0.0
+ slash: 3.0.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ resolution:
+ integrity: sha512-qiXeX1Cmw4JZ5yQ4H57WpkO0MZ61Qj+YnsVUwAMnDV5ls+yHon11XjarDdgP7H8lTmiEi6biiZA8y3Tmvx6pCg==
+ /babel-loader/8.1.0_@babel+core@7.9.0+webpack@4.42.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ find-cache-dir: 2.1.0
+ loader-utils: 1.4.0
+ mkdirp: 0.5.5
+ pify: 4.0.1
+ schema-utils: 2.6.5
+ webpack: 4.42.0_webpack@4.42.0
+ dev: true
+ engines:
+ node: '>= 6.9'
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ webpack: '>=2'
+ resolution:
+ integrity: sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==
+ /babel-loader/8.1.0_@babel+core@7.9.0+webpack@4.42.1:
+ dependencies:
+ '@babel/core': 7.9.0
+ find-cache-dir: 2.1.0
+ loader-utils: 1.4.0
+ mkdirp: 0.5.5
+ pify: 4.0.1
+ schema-utils: 2.6.5
+ webpack: 4.42.1_webpack@4.42.1
+ dev: true
+ engines:
+ node: '>= 6.9'
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ webpack: '>=2'
+ resolution:
+ integrity: sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==
+ /babel-plugin-dynamic-import-node/2.3.0:
+ dependencies:
+ object.assign: 4.1.0
+ dev: true
+ resolution:
+ integrity: sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==
+ /babel-plugin-emotion/10.0.33:
+ dependencies:
+ '@babel/helper-module-imports': 7.8.3
+ '@emotion/hash': 0.8.0
+ '@emotion/memoize': 0.7.4
+ '@emotion/serialize': 0.11.16
+ babel-plugin-macros: 2.8.0
+ babel-plugin-syntax-jsx: 6.18.0
+ convert-source-map: 1.7.0
+ escape-string-regexp: 1.0.5
+ find-root: 1.1.0
+ source-map: 0.5.7
+ dev: false
+ resolution:
+ integrity: sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==
+ /babel-plugin-inline-import/3.0.0:
+ dependencies:
+ require-resolve: 0.0.2
+ dev: true
+ resolution:
+ integrity: sha512-thnykl4FMb8QjMjVCuZoUmAM7r2mnTn5qJwrryCvDv6rugbJlTHZMctdjDtEgD0WBAXJOLJSGXN3loooEwx7UQ==
+ /babel-plugin-istanbul/5.2.0:
+ dependencies:
+ '@babel/helper-plugin-utils': 7.8.3
+ find-up: 3.0.0
+ istanbul-lib-instrument: 3.3.0
+ test-exclude: 5.2.3
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==
+ /babel-plugin-istanbul/6.0.0:
+ dependencies:
+ '@babel/helper-plugin-utils': 7.8.3
+ '@istanbuljs/load-nyc-config': 1.0.0
+ '@istanbuljs/schema': 0.1.2
+ istanbul-lib-instrument: 4.0.1
+ test-exclude: 6.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==
+ /babel-plugin-jest-hoist/24.9.0:
+ dependencies:
+ '@types/babel__traverse': 7.0.10
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==
+ /babel-plugin-jest-hoist/25.2.6:
+ dependencies:
+ '@types/babel__traverse': 7.0.10
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-qE2xjMathybYxjiGFJg0mLFrz0qNp83aNZycWDY/SuHiZNq+vQfRQtuINqyXyue1ELd8Rd+1OhFSLjms8msMbw==
+ /babel-plugin-macros/2.8.0:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ cosmiconfig: 6.0.0
+ resolve: 1.15.1
+ resolution:
+ integrity: sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==
+ /babel-plugin-named-asset-import/0.3.6_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.1.0
+ resolution:
+ integrity: sha512-1aGDUfL1qOOIoqk9QKGIo2lANk+C7ko/fqH0uIyC71x3PEGz0uVP8ISgfEsFuG+FKmjHTvFK/nNM8dowpmUxLA==
+ /babel-plugin-source-map-support/2.1.1:
+ dependencies:
+ '@babel/helper-module-imports': 7.8.3
+ dev: true
+ resolution:
+ integrity: sha512-Ce0r4iXS/1JX8gjzZcfzw17Pooh7zIkbLFTljuhWPTneNWQ9RauomiutInvz5kmd8tYrZ9axgGq9dm0hml2+Lg==
+ /babel-plugin-syntax-jsx/6.18.0:
+ dev: false
+ resolution:
+ integrity: sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=
+ /babel-plugin-syntax-object-rest-spread/6.13.0:
+ dev: true
+ resolution:
+ integrity: sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=
+ /babel-plugin-transform-object-rest-spread/6.26.0:
+ dependencies:
+ babel-plugin-syntax-object-rest-spread: 6.13.0
+ babel-runtime: 6.26.0
+ dev: true
+ resolution:
+ integrity: sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=
+ /babel-plugin-transform-react-remove-prop-types/0.4.24:
+ dev: true
+ resolution:
+ integrity: sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==
+ /babel-preset-current-node-syntax/0.1.2_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.9.0
+ '@babel/plugin-syntax-bigint': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-class-properties': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-logical-assignment-operators': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-numeric-separator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.9.0
+ dev: true
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ resolution:
+ integrity: sha512-u/8cS+dEiK1SFILbOC8/rUI3ml9lboKuuMvZ/4aQnQmhecQAgPw5ew066C1ObnEAUmlx7dv/s2z52psWEtLNiw==
+ /babel-preset-jest/24.9.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.9.0
+ babel-plugin-jest-hoist: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ resolution:
+ integrity: sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==
+ /babel-preset-jest/25.3.0_@babel+core@7.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ babel-plugin-jest-hoist: 25.2.6
+ babel-preset-current-node-syntax: 0.1.2_@babel+core@7.9.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ peerDependencies:
+ '@babel/core': ^7.0.0
+ resolution:
+ integrity: sha512-tjdvLKNMwDI9r+QWz9sZUQGTq1dpoxjUqFUpEasAc7MOtHg9XuLT2fx0udFG+k1nvMV0WvHHVAN7VmCZ+1Zxbw==
+ /babel-preset-react-app/9.1.2:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/plugin-proposal-class-properties': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-decorators': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-nullish-coalescing-operator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-numeric-separator': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-proposal-optional-chaining': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-flow-strip-types': 7.9.0_@babel+core@7.9.0
+ '@babel/plugin-transform-react-display-name': 7.8.3_@babel+core@7.9.0
+ '@babel/plugin-transform-runtime': 7.9.0_@babel+core@7.9.0
+ '@babel/preset-env': 7.9.0_@babel+core@7.9.0
+ '@babel/preset-react': 7.9.1_@babel+core@7.9.0
+ '@babel/preset-typescript': 7.9.0_@babel+core@7.9.0
+ '@babel/runtime': 7.9.0
+ babel-plugin-macros: 2.8.0
+ babel-plugin-transform-react-remove-prop-types: 0.4.24
+ dev: true
+ resolution:
+ integrity: sha512-k58RtQOKH21NyKtzptoAvtAODuAJJs3ZhqBMl456/GnXEQ/0La92pNmwgWoMn5pBTrsvk3YYXdY7zpY4e3UIxA==
+ /babel-runtime/6.26.0:
+ dependencies:
+ core-js: 2.6.11
+ regenerator-runtime: 0.11.1
+ dev: true
+ resolution:
+ integrity: sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
+ /babylon/6.18.0:
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
+ /backo2/1.0.2:
+ dev: true
+ resolution:
+ integrity: sha1-MasayLEpNjRj41s+u2n038+6eUc=
+ /balanced-match/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+ /base/0.11.2:
+ dependencies:
+ cache-base: 1.0.1
+ class-utils: 0.3.6
+ component-emitter: 1.3.0
+ define-property: 1.0.0
+ isobject: 3.0.1
+ mixin-deep: 1.3.2
+ pascalcase: 0.1.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+ /base64-arraybuffer/0.1.5:
+ dev: true
+ engines:
+ node: '>= 0.6.0'
+ resolution:
+ integrity: sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
+ /base64-js/1.3.1:
+ resolution:
+ integrity: sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
+ /batch/0.6.1:
+ dev: true
+ resolution:
+ integrity: sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=
+ /bcrypt-pbkdf/1.0.2:
+ dependencies:
+ tweetnacl: 0.14.5
+ resolution:
+ integrity: sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+ /better-assert/1.0.2:
+ dependencies:
+ callsite: 1.0.0
+ dev: true
+ resolution:
+ integrity: sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=
+ /big.js/5.2.2:
+ dev: true
+ resolution:
+ integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+ /bignumber.js/9.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==
+ /binary-extensions/1.13.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
+ /binary-extensions/2.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
+ /bindings/1.5.0:
+ dependencies:
+ file-uri-to-path: 1.0.0
+ dev: true
+ optional: true
+ resolution:
+ integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
+ /bl/1.2.2:
+ dependencies:
+ readable-stream: 2.3.7
+ safe-buffer: 5.2.0
+ dev: true
+ resolution:
+ integrity: sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==
+ /blob/0.0.5:
+ dev: true
+ resolution:
+ integrity: sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
+ /bluebird/3.7.2:
+ resolution:
+ integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+ /bn.js/4.11.8:
+ resolution:
+ integrity: sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
+ /body-parser/1.19.0:
+ dependencies:
+ bytes: 3.1.0
+ content-type: 1.0.4
+ debug: 2.6.9
+ depd: 1.1.2
+ http-errors: 1.7.2
+ iconv-lite: 0.4.24
+ on-finished: 2.3.0
+ qs: 6.7.0
+ raw-body: 2.4.0
+ type-is: 1.6.18
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
+ /bonjour/3.5.0:
+ dependencies:
+ array-flatten: 2.1.2
+ deep-equal: 1.1.1
+ dns-equal: 1.0.0
+ dns-txt: 2.0.2
+ multicast-dns: 6.2.3
+ multicast-dns-service-types: 1.1.0
+ dev: true
+ resolution:
+ integrity: sha1-jokKGD2O6aI5OzhExpGkK897yfU=
+ /boolbase/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+ /boxen/1.3.0:
+ dependencies:
+ ansi-align: 2.0.0
+ camelcase: 4.1.0
+ chalk: 2.4.2
+ cli-boxes: 1.0.0
+ string-width: 2.1.1
+ term-size: 1.2.0
+ widest-line: 2.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==
+ /boxen/3.2.0:
+ dependencies:
+ ansi-align: 3.0.0
+ camelcase: 5.3.1
+ chalk: 2.4.2
+ cli-boxes: 2.2.0
+ string-width: 3.1.0
+ term-size: 1.2.0
+ type-fest: 0.3.1
+ widest-line: 2.0.1
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-cU4J/+NodM3IHdSL2yN8bqYqnmlBTidDR4RC7nJs61ZmtGz8VZzM3HLQX0zY5mrSmPtR3xWwsq2jOUQqFZN8+A==
+ /brace-expansion/1.1.11:
+ dependencies:
+ balanced-match: 1.0.0
+ concat-map: 0.0.1
+ dev: true
+ resolution:
+ integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ /braces/2.3.2:
+ dependencies:
+ arr-flatten: 1.1.0
+ array-unique: 0.3.2
+ extend-shallow: 2.0.1
+ fill-range: 4.0.0
+ isobject: 3.0.1
+ repeat-element: 1.1.3
+ snapdragon: 0.8.2
+ snapdragon-node: 2.1.1
+ split-string: 3.1.0
+ to-regex: 3.0.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+ /braces/3.0.2:
+ dependencies:
+ fill-range: 7.0.1
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+ /brorand/1.1.0:
+ resolution:
+ integrity: sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
+ /browser-process-hrtime/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
+ /browser-resolve/1.11.3:
+ dependencies:
+ resolve: 1.1.7
+ dev: true
+ resolution:
+ integrity: sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==
+ /browser-tabs-lock/1.2.8:
+ dev: false
+ requiresBuild: true
+ resolution:
+ integrity: sha512-Xrj33YUTltPDoGrD1KnaAn5ZuxnnlJFcIW9srVTPHbMNPd9MlcnBCWaGV0STlvGKu8Ok0ad5qxyx5sIwFTr/Ig==
+ /browserify-aes/1.2.0:
+ dependencies:
+ buffer-xor: 1.0.3
+ cipher-base: 1.0.4
+ create-hash: 1.2.0
+ evp_bytestokey: 1.0.3
+ inherits: 2.0.4
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
+ /browserify-cipher/1.0.1:
+ dependencies:
+ browserify-aes: 1.2.0
+ browserify-des: 1.0.2
+ evp_bytestokey: 1.0.3
+ resolution:
+ integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==
+ /browserify-des/1.0.2:
+ dependencies:
+ cipher-base: 1.0.4
+ des.js: 1.0.1
+ inherits: 2.0.4
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==
+ /browserify-rsa/4.0.1:
+ dependencies:
+ bn.js: 4.11.8
+ randombytes: 2.1.0
+ resolution:
+ integrity: sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=
+ /browserify-sign/4.0.4:
+ dependencies:
+ bn.js: 4.11.8
+ browserify-rsa: 4.0.1
+ create-hash: 1.2.0
+ create-hmac: 1.1.7
+ elliptic: 6.5.2
+ inherits: 2.0.4
+ parse-asn1: 5.1.5
+ resolution:
+ integrity: sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=
+ /browserify-zlib/0.2.0:
+ dependencies:
+ pako: 1.0.11
+ dev: true
+ resolution:
+ integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
+ /browserslist/4.10.0:
+ dependencies:
+ caniuse-lite: 1.0.30001039
+ electron-to-chromium: 1.3.399
+ node-releases: 1.1.53
+ pkg-up: 3.1.0
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-TpfK0TDgv71dzuTsEAlQiHeWQ/tiPqgNZVdv046fvNtBZrjbv2O3TsWCDU0AWGJJKCF/KsjNdLzR9hXOsh/CfA==
+ /browserslist/4.11.1:
+ dependencies:
+ caniuse-lite: 1.0.30001039
+ electron-to-chromium: 1.3.399
+ node-releases: 1.1.53
+ pkg-up: 2.0.0
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-DCTr3kDrKEYNw6Jb9HFxVLQNaue8z+0ZfRBRjmCunKDEXEBajKDj2Y+Uelg+Pi29OnvaSGwjOsnRyNEkXzHg5g==
+ /bser/2.1.1:
+ dependencies:
+ node-int64: 0.4.0
+ dev: true
+ resolution:
+ integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==
+ /buffer-alloc-unsafe/1.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
+ /buffer-alloc/1.2.0:
+ dependencies:
+ buffer-alloc-unsafe: 1.1.0
+ buffer-fill: 1.0.0
+ dev: true
+ resolution:
+ integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
+ /buffer-crc32/0.2.13:
+ dev: true
+ resolution:
+ integrity: sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
+ /buffer-equal-constant-time/1.0.1:
+ resolution:
+ integrity: sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
+ /buffer-fill/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-+PeLdniYiO858gXNY39o5wISKyw=
+ /buffer-from/1.1.1:
+ dev: true
+ resolution:
+ integrity: sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+ /buffer-indexof/1.1.1:
+ dev: true
+ resolution:
+ integrity: sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==
+ /buffer-xor/1.0.3:
+ resolution:
+ integrity: sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
+ /buffer/4.9.1:
+ dependencies:
+ base64-js: 1.3.1
+ ieee754: 1.1.13
+ isarray: 1.0.0
+ resolution:
+ integrity: sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=
+ /buffer/4.9.2:
+ dependencies:
+ base64-js: 1.3.1
+ ieee754: 1.1.13
+ isarray: 1.0.0
+ dev: true
+ resolution:
+ integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
+ /buffer/5.5.0:
+ dependencies:
+ base64-js: 1.3.1
+ ieee754: 1.1.13
+ dev: true
+ resolution:
+ integrity: sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==
+ /builtin-modules/1.1.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
+ /builtin-modules/3.1.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==
+ /builtin-status-codes/3.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
+ /bytes/3.0.0:
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
+ /bytes/3.1.0:
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+ /cacache/12.0.4:
+ dependencies:
+ bluebird: 3.7.2
+ chownr: 1.1.4
+ figgy-pudding: 3.5.2
+ glob: 7.1.6
+ graceful-fs: 4.2.3
+ infer-owner: 1.0.4
+ lru-cache: 5.1.1
+ mississippi: 3.0.0
+ mkdirp: 0.5.5
+ move-concurrently: 1.0.1
+ promise-inflight: 1.0.1
+ rimraf: 2.7.1
+ ssri: 6.0.1
+ unique-filename: 1.1.1
+ y18n: 4.0.0
+ dev: true
+ resolution:
+ integrity: sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==
+ /cacache/13.0.1:
+ dependencies:
+ chownr: 1.1.4
+ figgy-pudding: 3.5.2
+ fs-minipass: 2.1.0
+ glob: 7.1.6
+ graceful-fs: 4.2.3
+ infer-owner: 1.0.4
+ lru-cache: 5.1.1
+ minipass: 3.1.1
+ minipass-collect: 1.0.2
+ minipass-flush: 1.0.5
+ minipass-pipeline: 1.2.2
+ mkdirp: 0.5.5
+ move-concurrently: 1.0.1
+ p-map: 3.0.0
+ promise-inflight: 1.0.1
+ rimraf: 2.7.1
+ ssri: 7.1.0
+ unique-filename: 1.1.1
+ dev: true
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==
+ /cache-base/1.0.1:
+ dependencies:
+ collection-visit: 1.0.0
+ component-emitter: 1.3.0
+ get-value: 2.0.6
+ has-value: 1.0.0
+ isobject: 3.0.1
+ set-value: 2.0.1
+ to-object-path: 0.3.0
+ union-value: 1.0.1
+ unset-value: 1.0.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+ /cacheable-request/2.1.4:
+ dependencies:
+ clone-response: 1.0.2
+ get-stream: 3.0.0
+ http-cache-semantics: 3.8.1
+ keyv: 3.0.0
+ lowercase-keys: 1.0.0
+ normalize-url: 2.0.1
+ responselike: 1.0.2
+ dev: true
+ resolution:
+ integrity: sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=
+ /cacheable-request/6.1.0:
+ dependencies:
+ clone-response: 1.0.2
+ get-stream: 5.1.0
+ http-cache-semantics: 4.1.0
+ keyv: 3.1.0
+ lowercase-keys: 2.0.0
+ normalize-url: 4.5.0
+ responselike: 1.0.2
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==
+ /cachedir/2.3.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==
+ /call-me-maybe/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-JtII6onje1y95gJQoV8DHBak1ms=
+ /caller-callsite/2.0.0:
+ dependencies:
+ callsites: 2.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=
+ /caller-path/2.0.0:
+ dependencies:
+ caller-callsite: 2.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=
+ /callsite/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
+ /callsites/2.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=
+ /callsites/3.1.0:
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+ /camel-case/4.1.1:
+ dependencies:
+ pascal-case: 3.1.1
+ tslib: 1.11.1
+ dev: true
+ resolution:
+ integrity: sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==
+ /camelcase/4.1.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
+ /camelcase/5.0.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
+ /camelcase/5.3.1:
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+ /caniuse-api/3.0.0:
+ dependencies:
+ browserslist: 4.11.1
+ caniuse-lite: 1.0.30001039
+ lodash.memoize: 4.1.2
+ lodash.uniq: 4.5.0
+ dev: true
+ resolution:
+ integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+ /caniuse-lite/1.0.30001039:
+ dev: true
+ resolution:
+ integrity: sha512-SezbWCTT34eyFoWHgx8UWso7YtvtM7oosmFoXbCkdC6qJzRfBTeTgE9REtKtiuKXuMwWTZEvdnFNGAyVMorv8Q==
+ /capture-exit/2.0.0:
+ dependencies:
+ rsvp: 4.8.5
+ dev: true
+ engines:
+ node: 6.* || 8.* || >= 10.*
+ resolution:
+ integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==
+ /capture-stack-trace/1.0.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==
+ /case-sensitive-paths-webpack-plugin/2.3.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-/4YgnZS8y1UXXmC02xD5rRrBEu6T5ub+mQHLNRj0fzTRbgdBYhsNo2V5EqwgqrExjxsjtF/OpAKAMkKsxbD5XQ==
+ /caseless/0.12.0:
+ resolution:
+ integrity: sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+ /caw/2.0.1:
+ dependencies:
+ get-proxy: 2.1.0
+ isurl: 1.0.0
+ tunnel-agent: 0.6.0
+ url-to-options: 1.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==
+ /cbor-js/0.1.0:
+ dev: true
+ resolution:
+ integrity: sha1-yAzmEg84fo+qdDcN/aIdlluPx/k=
+ /cbor/5.0.1:
+ dependencies:
+ bignumber.js: 9.0.0
+ nofilter: 1.0.3
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-l4ghwqioCyuAaD3LvY4ONwv8NMuERz62xjbMHGdWBqERJPygVmoFER1b4+VS6iW0rXwoVGuKZPPPTofwWOg3YQ==
+ /chalk/1.1.3:
+ dependencies:
+ ansi-styles: 2.2.1
+ escape-string-regexp: 1.0.5
+ has-ansi: 2.0.0
+ strip-ansi: 3.0.1
+ supports-color: 2.0.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
+ /chalk/2.4.2:
+ dependencies:
+ ansi-styles: 3.2.1
+ escape-string-regexp: 1.0.5
+ supports-color: 5.5.0
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ /chalk/3.0.0:
+ dependencies:
+ ansi-styles: 4.2.1
+ supports-color: 7.1.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
+ /character-entities-legacy/1.1.4:
+ dev: false
+ resolution:
+ integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==
+ /character-entities/1.2.4:
+ dev: false
+ resolution:
+ integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==
+ /character-reference-invalid/1.1.4:
+ dev: false
+ resolution:
+ integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
+ /chardet/0.7.0:
+ dev: true
+ resolution:
+ integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+ /charenc/0.0.2:
+ dev: false
+ resolution:
+ integrity: sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
+ /chart.js/2.9.3:
+ dependencies:
+ chartjs-color: 2.4.1
+ moment: 2.24.0
+ dev: false
+ resolution:
+ integrity: sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==
+ /chartjs-color-string/0.6.0:
+ dependencies:
+ color-name: 1.1.4
+ dev: false
+ resolution:
+ integrity: sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
+ /chartjs-color/2.4.1:
+ dependencies:
+ chartjs-color-string: 0.6.0
+ color-convert: 1.9.3
+ dev: false
+ resolution:
+ integrity: sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
+ /child-process-ext/2.1.1:
+ dependencies:
+ cross-spawn: 6.0.5
+ es5-ext: 0.10.53
+ log: 6.0.0
+ split2: 3.1.1
+ stream-promise: 3.2.0
+ dev: true
+ resolution:
+ integrity: sha512-0UQ55f51JBkOFa+fvR76ywRzxiPwQS3Xe8oe5bZRphpv+dIMeerW5Zn5e4cUy4COJwVtJyU0R79RMnw+aCqmGA==
+ /chokidar/2.1.8:
+ dependencies:
+ anymatch: 2.0.0
+ async-each: 1.0.3
+ braces: 2.3.2
+ glob-parent: 3.1.0
+ inherits: 2.0.4
+ is-binary-path: 1.0.1
+ is-glob: 4.0.1
+ normalize-path: 3.0.0
+ path-is-absolute: 1.0.1
+ readdirp: 2.2.1
+ upath: 1.2.0
+ dev: true
+ optionalDependencies:
+ fsevents: 1.2.12
+ resolution:
+ integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==
+ /chokidar/3.3.1:
+ dependencies:
+ anymatch: 3.1.1
+ braces: 3.0.2
+ glob-parent: 5.1.1
+ is-binary-path: 2.1.0
+ is-glob: 4.0.1
+ normalize-path: 3.0.0
+ readdirp: 3.3.0
+ dev: true
+ engines:
+ node: '>= 8.10.0'
+ optionalDependencies:
+ fsevents: 2.1.2
+ resolution:
+ integrity: sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==
+ /chownr/1.1.4:
+ dev: true
+ resolution:
+ integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+ /chrome-trace-event/1.0.2:
+ dependencies:
+ tslib: 1.11.1
+ dev: true
+ engines:
+ node: '>=6.0'
+ resolution:
+ integrity: sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==
+ /ci-info/1.6.0:
+ dev: true
+ resolution:
+ integrity: sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==
+ /ci-info/2.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
+ /cidr-regex/2.0.10:
+ dependencies:
+ ip-regex: 2.1.0
+ dev: false
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-sB3ogMQXWvreNPbJUZMRApxuRYd+KoIo4RGQ81VatjmMW6WJPo+IJZ2846FGItr9VzKo5w7DXzijPLGtSd0N3Q==
+ /cipher-base/1.0.4:
+ dependencies:
+ inherits: 2.0.4
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
+ /class-utils/0.3.6:
+ dependencies:
+ arr-union: 3.1.0
+ define-property: 0.2.5
+ isobject: 3.0.1
+ static-extend: 0.1.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+ /classnames/2.2.6:
+ dev: false
+ resolution:
+ integrity: sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
+ /clean-css/4.2.3:
+ dependencies:
+ source-map: 0.6.1
+ dev: true
+ engines:
+ node: '>= 4.0'
+ resolution:
+ integrity: sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==
+ /clean-stack/2.2.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
+ /cli-boxes/1.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-T6kXw+WclKAEzWH47lCdplFocUM=
+ /cli-boxes/2.2.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==
+ /cli-color/2.0.0:
+ dependencies:
+ ansi-regex: 2.1.1
+ d: 1.0.1
+ es5-ext: 0.10.53
+ es6-iterator: 2.0.3
+ memoizee: 0.4.14
+ timers-ext: 0.1.7
+ dev: true
+ resolution:
+ integrity: sha512-a0VZ8LeraW0jTuCkuAGMNufareGHhyZU9z8OGsW0gXd1hZGi1SRuNRXdbGkraBBKnhyUhyebFWnRbp+dIn0f0A==
+ /cli-cursor/2.1.0:
+ dependencies:
+ restore-cursor: 2.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
+ /cli-cursor/3.1.0:
+ dependencies:
+ restore-cursor: 3.1.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
+ /cli-width/2.2.0:
+ dev: true
+ resolution:
+ integrity: sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
+ /clipboard/2.0.6:
+ dependencies:
+ good-listener: 1.2.2
+ select: 1.1.2
+ tiny-emitter: 2.1.0
+ dev: false
+ optional: true
+ resolution:
+ integrity: sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==
+ /cliui/4.1.0:
+ dependencies:
+ string-width: 2.1.1
+ strip-ansi: 4.0.0
+ wrap-ansi: 2.1.0
+ dev: true
+ resolution:
+ integrity: sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==
+ /cliui/5.0.0:
+ dependencies:
+ string-width: 3.1.0
+ strip-ansi: 5.2.0
+ wrap-ansi: 5.1.0
+ resolution:
+ integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
+ /cliui/6.0.0:
+ dependencies:
+ string-width: 4.2.0
+ strip-ansi: 6.0.0
+ wrap-ansi: 6.2.0
+ dev: true
+ resolution:
+ integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
+ /clone-deep/0.2.4:
+ dependencies:
+ for-own: 0.1.5
+ is-plain-object: 2.0.4
+ kind-of: 3.2.2
+ lazy-cache: 1.0.4
+ shallow-clone: 0.1.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=
+ /clone-deep/4.0.1:
+ dependencies:
+ is-plain-object: 2.0.4
+ kind-of: 6.0.3
+ shallow-clone: 3.0.1
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+ /clone-response/1.0.2:
+ dependencies:
+ mimic-response: 1.0.1
+ dev: true
+ resolution:
+ integrity: sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
+ /clone/2.1.2:
+ dev: false
+ engines:
+ node: '>=0.8'
+ resolution:
+ integrity: sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
+ /co/4.6.0:
+ dev: true
+ engines:
+ iojs: '>= 1.0.0'
+ node: '>= 0.12.0'
+ resolution:
+ integrity: sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+ /coa/2.0.2:
+ dependencies:
+ '@types/q': 1.5.2
+ chalk: 2.4.2
+ q: 1.5.1
+ dev: true
+ engines:
+ node: '>= 4.0'
+ resolution:
+ integrity: sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==
+ /code-point-at/1.1.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+ /collect-v8-coverage/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==
+ /collection-visit/1.0.0:
+ dependencies:
+ map-visit: 1.0.0
+ object-visit: 1.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+ /color-convert/1.9.3:
+ dependencies:
+ color-name: 1.1.3
+ resolution:
+ integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ /color-convert/2.0.1:
+ dependencies:
+ color-name: 1.1.4
+ dev: true
+ engines:
+ node: '>=7.0.0'
+ resolution:
+ integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ /color-name/1.1.3:
+ resolution:
+ integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+ /color-name/1.1.4:
+ resolution:
+ integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+ /color-string/1.5.3:
+ dependencies:
+ color-name: 1.1.4
+ simple-swizzle: 0.2.2
+ dev: true
+ resolution:
+ integrity: sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
+ /color/3.0.0:
+ dependencies:
+ color-convert: 1.9.3
+ color-string: 1.5.3
+ dev: true
+ resolution:
+ integrity: sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==
+ /color/3.1.2:
+ dependencies:
+ color-convert: 1.9.3
+ color-string: 1.5.3
+ dev: true
+ resolution:
+ integrity: sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==
+ /colornames/1.1.1:
+ dev: true
+ resolution:
+ integrity: sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=
+ /colors/1.3.3:
+ dev: true
+ engines:
+ node: '>=0.1.90'
+ resolution:
+ integrity: sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==
+ /colors/1.4.0:
+ dev: true
+ engines:
+ node: '>=0.1.90'
+ resolution:
+ integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
+ /colorspace/1.1.2:
+ dependencies:
+ color: 3.0.0
+ text-hex: 1.0.0
+ dev: true
+ resolution:
+ integrity: sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==
+ /combined-stream/1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ /comma-separated-tokens/1.0.8:
+ dev: false
+ resolution:
+ integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
+ /commander/2.19.0:
+ dev: true
+ resolution:
+ integrity: sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
+ /commander/2.20.3:
+ dev: true
+ resolution:
+ integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+ /commander/2.8.1:
+ dependencies:
+ graceful-readlink: 1.0.1
+ dev: true
+ engines:
+ node: '>= 0.6.x'
+ resolution:
+ integrity: sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=
+ /commander/4.1.1:
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
+ /common-tags/1.8.0:
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ resolution:
+ integrity: sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==
+ /commondir/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
+ /component-bind/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=
+ /component-emitter/1.2.1:
+ dev: true
+ resolution:
+ integrity: sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
+ /component-emitter/1.3.0:
+ dev: true
+ resolution:
+ integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+ /component-inherit/0.0.3:
+ dev: true
+ resolution:
+ integrity: sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=
+ /compose-function/3.0.3:
+ dependencies:
+ arity-n: 1.0.4
+ dev: true
+ resolution:
+ integrity: sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=
+ /compress-commons/1.2.2:
+ dependencies:
+ buffer-crc32: 0.2.13
+ crc32-stream: 2.0.0
+ normalize-path: 2.1.1
+ readable-stream: 2.3.7
+ dev: true
+ engines:
+ node: '>= 0.10.0'
+ resolution:
+ integrity: sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=
+ /compressible/2.0.18:
+ dependencies:
+ mime-db: 1.43.0
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
+ /compression/1.7.4:
+ dependencies:
+ accepts: 1.3.7
+ bytes: 3.0.0
+ compressible: 2.0.18
+ debug: 2.6.9
+ on-headers: 1.0.2
+ safe-buffer: 5.1.2
+ vary: 1.1.2
+ engines:
+ node: '>= 0.8.0'
+ resolution:
+ integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
+ /concat-map/0.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+ /concat-stream/1.6.2:
+ dependencies:
+ buffer-from: 1.1.1
+ inherits: 2.0.4
+ readable-stream: 2.3.7
+ typedarray: 0.0.6
+ dev: true
+ engines:
+ '0': node >= 0.8
+ resolution:
+ integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
+ /config-chain/1.1.12:
+ dependencies:
+ ini: 1.3.5
+ proto-list: 1.2.4
+ dev: true
+ resolution:
+ integrity: sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==
+ /configstore/3.1.2:
+ dependencies:
+ dot-prop: 4.2.0
+ graceful-fs: 4.2.3
+ make-dir: 1.3.0
+ unique-string: 1.0.0
+ write-file-atomic: 2.4.3
+ xdg-basedir: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==
+ /configstore/4.0.0:
+ dependencies:
+ dot-prop: 4.2.0
+ graceful-fs: 4.2.3
+ make-dir: 1.3.0
+ unique-string: 1.0.0
+ write-file-atomic: 2.4.3
+ xdg-basedir: 3.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-CmquAXFBocrzaSM8mtGPMM/HiWmyIpr4CcJl/rgY2uCObZ/S7cKU0silxslqJejl+t/T9HS8E0PUNQD81JGUEQ==
+ /confusing-browser-globals/1.0.9:
+ dev: true
+ resolution:
+ integrity: sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==
+ /connect-history-api-fallback/1.6.0:
+ dev: true
+ engines:
+ node: '>=0.8'
+ resolution:
+ integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
+ /console-browserify/1.2.0:
+ dev: true
+ resolution:
+ integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
+ /constants-browserify/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=
+ /contains-path/0.1.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=
+ /content-disposition/0.5.3:
+ dependencies:
+ safe-buffer: 5.1.2
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
+ /content-type/1.0.4:
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+ /convert-source-map/0.3.5:
+ dev: true
+ resolution:
+ integrity: sha1-8dgClQr33SYxof6+BZZVDIarMZA=
+ /convert-source-map/1.7.0:
+ dependencies:
+ safe-buffer: 5.1.2
+ resolution:
+ integrity: sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
+ /cookie-signature/1.0.6:
+ resolution:
+ integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
+ /cookie/0.3.1:
+ dev: true
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
+ /cookie/0.4.0:
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
+ /cookiejar/2.1.2:
+ dev: true
+ resolution:
+ integrity: sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==
+ /copy-concurrently/1.0.5:
+ dependencies:
+ aproba: 1.2.0
+ fs-write-stream-atomic: 1.0.10
+ iferr: 0.1.5
+ mkdirp: 0.5.5
+ rimraf: 2.7.1
+ run-queue: 1.0.3
+ dev: true
+ resolution:
+ integrity: sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==
+ /copy-descriptor/0.1.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+ /copy-to-clipboard/3.3.1:
+ dependencies:
+ toggle-selection: 1.0.6
+ dev: false
+ resolution:
+ integrity: sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==
+ /copy-webpack-plugin/5.1.1_webpack@4.42.1:
+ dependencies:
+ cacache: 12.0.4
+ find-cache-dir: 2.1.0
+ glob-parent: 3.1.0
+ globby: 7.1.1
+ is-glob: 4.0.1
+ loader-utils: 1.4.0
+ minimatch: 3.0.4
+ normalize-path: 3.0.0
+ p-limit: 2.3.0
+ schema-utils: 1.0.0
+ serialize-javascript: 2.1.2
+ webpack: 4.42.1_webpack@4.42.1
+ webpack-log: 2.0.0
+ dev: true
+ engines:
+ node: '>= 6.9.0'
+ peerDependencies:
+ webpack: ^4.0.0 || ^5.0.0
+ resolution:
+ integrity: sha512-P15M5ZC8dyCjQHWwd4Ia/dm0SgVvZJMYeykVIVYXbGyqO4dWB5oyPHp9i7wjwo5LhtlhKbiBCdS2NvM07Wlybg==
+ /core-js-compat/3.6.4:
+ dependencies:
+ browserslist: 4.11.1
+ semver: 7.0.0
+ dev: true
+ resolution:
+ integrity: sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA==
+ /core-js-pure/3.6.4:
+ dev: true
+ requiresBuild: true
+ resolution:
+ integrity: sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
+ /core-js/2.6.11:
+ deprecated: 'core-js@<3 is no longer maintained and not recommended for usage due to the number of issues. Please, upgrade your dependencies to the actual version of core-js@3.'
+ requiresBuild: true
+ resolution:
+ integrity: sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
+ /core-js/3.6.4:
+ requiresBuild: true
+ resolution:
+ integrity: sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==
+ /core-util-is/1.0.2:
+ resolution:
+ integrity: sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+ /cors/2.8.5:
+ dependencies:
+ object-assign: 4.1.1
+ vary: 1.1.2
+ dev: false
+ engines:
+ node: '>= 0.10'
+ resolution:
+ integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
+ /cosmiconfig/5.2.1:
+ dependencies:
+ import-fresh: 2.0.0
+ is-directory: 0.3.1
+ js-yaml: 3.13.1
+ parse-json: 4.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==
+ /cosmiconfig/6.0.0:
+ dependencies:
+ '@types/parse-json': 4.0.0
+ import-fresh: 3.2.1
+ parse-json: 5.0.0
+ path-type: 4.0.0
+ yaml: 1.8.3
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==
+ /crc/3.8.0:
+ dependencies:
+ buffer: 5.5.0
+ dev: true
+ resolution:
+ integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==
+ /crc32-stream/2.0.0:
+ dependencies:
+ crc: 3.8.0
+ readable-stream: 2.3.7
+ dev: true
+ engines:
+ node: '>= 0.10.0'
+ resolution:
+ integrity: sha1-483TtN8xaN10494/u8t7KX/pCPQ=
+ /create-ecdh/4.0.3:
+ dependencies:
+ bn.js: 4.11.8
+ elliptic: 6.5.2
+ resolution:
+ integrity: sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==
+ /create-error-class/3.0.2:
+ dependencies:
+ capture-stack-trace: 1.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=
+ /create-hash/1.2.0:
+ dependencies:
+ cipher-base: 1.0.4
+ inherits: 2.0.4
+ md5.js: 1.3.5
+ ripemd160: 2.0.2
+ sha.js: 2.4.11
+ resolution:
+ integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
+ /create-hmac/1.1.7:
+ dependencies:
+ cipher-base: 1.0.4
+ create-hash: 1.2.0
+ inherits: 2.0.4
+ ripemd160: 2.0.2
+ safe-buffer: 5.2.0
+ sha.js: 2.4.11
+ resolution:
+ integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
+ /create-react-context/0.3.0_prop-types@15.7.2+react@16.13.1:
+ dependencies:
+ gud: 1.0.0
+ prop-types: 15.7.2
+ react: 16.13.1
+ warning: 4.0.3
+ dev: false
+ peerDependencies:
+ prop-types: ^15.0.0
+ react: ^0.14.0 || ^15.0.0 || ^16.0.0
+ resolution:
+ integrity: sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==
+ /cross-spawn/5.1.0:
+ dependencies:
+ lru-cache: 4.1.5
+ shebang-command: 1.2.0
+ which: 1.3.1
+ dev: true
+ resolution:
+ integrity: sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=
+ /cross-spawn/6.0.5:
+ dependencies:
+ nice-try: 1.0.5
+ path-key: 2.0.1
+ semver: 5.7.1
+ shebang-command: 1.2.0
+ which: 1.3.1
+ engines:
+ node: '>=4.8'
+ resolution:
+ integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+ /cross-spawn/7.0.1:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+ dev: true
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==
+ /cross-spawn/7.0.2:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==
+ /crypt/0.0.2:
+ dev: false
+ resolution:
+ integrity: sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=
+ /crypto-browserify/3.12.0:
+ dependencies:
+ browserify-cipher: 1.0.1
+ browserify-sign: 4.0.4
+ create-ecdh: 4.0.3
+ create-hash: 1.2.0
+ create-hmac: 1.1.7
+ diffie-hellman: 5.0.3
+ inherits: 2.0.4
+ pbkdf2: 3.0.17
+ public-encrypt: 4.0.3
+ randombytes: 2.1.0
+ randomfill: 1.0.4
+ resolution:
+ integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==
+ /crypto-random-string/1.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
+ /css-blank-pseudo/0.1.4:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w==
+ /css-box-model/1.2.0:
+ dependencies:
+ tiny-invariant: 1.1.0
+ dev: false
+ resolution:
+ integrity: sha512-lri0br+jSNV0kkkiGEp9y9y3Njq2PmpqbeGWRFQJuZteZzY9iC9GZhQ8Y4WpPwM/2YocjHePxy14igJY7YKzkA==
+ /css-color-names/0.0.4:
+ dev: true
+ resolution:
+ integrity: sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=
+ /css-declaration-sorter/4.0.1:
+ dependencies:
+ postcss: 7.0.27
+ timsort: 0.3.0
+ dev: true
+ engines:
+ node: '>4'
+ resolution:
+ integrity: sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==
+ /css-has-pseudo/0.10.0:
+ dependencies:
+ postcss: 7.0.27
+ postcss-selector-parser: 5.0.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ==
+ /css-loader/3.4.2_webpack@4.42.0:
+ dependencies:
+ camelcase: 5.3.1
+ cssesc: 3.0.0
+ icss-utils: 4.1.1
+ loader-utils: 1.4.0
+ normalize-path: 3.0.0
+ postcss: 7.0.27
+ postcss-modules-extract-imports: 2.0.0
+ postcss-modules-local-by-default: 3.0.2
+ postcss-modules-scope: 2.2.0
+ postcss-modules-values: 3.0.0
+ postcss-value-parser: 4.0.3
+ schema-utils: 2.6.5
+ webpack: 4.42.0_webpack@4.42.0
+ dev: true
+ engines:
+ node: '>= 8.9.0'
+ peerDependencies:
+ webpack: ^4.0.0 || ^5.0.0
+ resolution:
+ integrity: sha512-jYq4zdZT0oS0Iykt+fqnzVLRIeiPWhka+7BqPn+oSIpWJAHak5tmB/WZrJ2a21JhCeFyNnnlroSl8c+MtVndzA==
+ /css-prefers-color-scheme/3.1.1:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==
+ /css-select-base-adapter/0.1.1:
+ dev: true
+ resolution:
+ integrity: sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
+ /css-select/1.2.0:
+ dependencies:
+ boolbase: 1.0.0
+ css-what: 2.1.3
+ domutils: 1.5.1
+ nth-check: 1.0.2
+ dev: true
+ resolution:
+ integrity: sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+ /css-select/2.1.0:
+ dependencies:
+ boolbase: 1.0.0
+ css-what: 3.2.1
+ domutils: 1.7.0
+ nth-check: 1.0.2
+ dev: true
+ resolution:
+ integrity: sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==
+ /css-tree/1.0.0-alpha.37:
+ dependencies:
+ mdn-data: 2.0.4
+ source-map: 0.6.1
+ dev: true
+ engines:
+ node: '>=8.0.0'
+ resolution:
+ integrity: sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==
+ /css-tree/1.0.0-alpha.39:
+ dependencies:
+ mdn-data: 2.0.6
+ source-map: 0.6.1
+ dev: true
+ engines:
+ node: '>=8.0.0'
+ resolution:
+ integrity: sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==
+ /css-what/2.1.3:
+ dev: true
+ resolution:
+ integrity: sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
+ /css-what/3.2.1:
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==
+ /css/2.2.4:
+ dependencies:
+ inherits: 2.0.4
+ source-map: 0.6.1
+ source-map-resolve: 0.5.3
+ urix: 0.1.0
+ dev: true
+ resolution:
+ integrity: sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==
+ /cssdb/4.4.0:
+ dev: true
+ resolution:
+ integrity: sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ==
+ /cssesc/2.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ hasBin: true
+ resolution:
+ integrity: sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==
+ /cssesc/3.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ hasBin: true
+ resolution:
+ integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+ /cssnano-preset-default/4.0.7:
+ dependencies:
+ css-declaration-sorter: 4.0.1
+ cssnano-util-raw-cache: 4.0.1
+ postcss: 7.0.27
+ postcss-calc: 7.0.2
+ postcss-colormin: 4.0.3
+ postcss-convert-values: 4.0.1
+ postcss-discard-comments: 4.0.2
+ postcss-discard-duplicates: 4.0.2
+ postcss-discard-empty: 4.0.1
+ postcss-discard-overridden: 4.0.1
+ postcss-merge-longhand: 4.0.11
+ postcss-merge-rules: 4.0.3
+ postcss-minify-font-values: 4.0.2
+ postcss-minify-gradients: 4.0.2
+ postcss-minify-params: 4.0.2
+ postcss-minify-selectors: 4.0.2
+ postcss-normalize-charset: 4.0.1
+ postcss-normalize-display-values: 4.0.2
+ postcss-normalize-positions: 4.0.2
+ postcss-normalize-repeat-style: 4.0.2
+ postcss-normalize-string: 4.0.2
+ postcss-normalize-timing-functions: 4.0.2
+ postcss-normalize-unicode: 4.0.1
+ postcss-normalize-url: 4.0.1
+ postcss-normalize-whitespace: 4.0.2
+ postcss-ordered-values: 4.1.2
+ postcss-reduce-initial: 4.0.3
+ postcss-reduce-transforms: 4.0.2
+ postcss-svgo: 4.0.2
+ postcss-unique-selectors: 4.0.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==
+ /cssnano-util-get-arguments/4.0.0:
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=
+ /cssnano-util-get-match/4.0.0:
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=
+ /cssnano-util-raw-cache/4.0.1:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==
+ /cssnano-util-same-parent/4.0.1:
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==
+ /cssnano/4.1.10:
+ dependencies:
+ cosmiconfig: 5.2.1
+ cssnano-preset-default: 4.0.7
+ is-resolvable: 1.1.0
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==
+ /csso/4.0.3:
+ dependencies:
+ css-tree: 1.0.0-alpha.39
+ dev: true
+ engines:
+ node: '>=8.0.0'
+ resolution:
+ integrity: sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==
+ /cssom/0.3.8:
+ dev: true
+ resolution:
+ integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
+ /cssom/0.4.4:
+ dev: true
+ resolution:
+ integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
+ /cssstyle/1.4.0:
+ dependencies:
+ cssom: 0.3.8
+ dev: true
+ resolution:
+ integrity: sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==
+ /cssstyle/2.2.0:
+ dependencies:
+ cssom: 0.3.8
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-sEb3XFPx3jNnCAMtqrXPDeSgQr+jojtCeNf8cvMNMh1cG970+lljssvQDzPq6lmmJu2Vhqood/gtEomBiHOGnA==
+ /csstype/2.6.10:
+ dev: false
+ resolution:
+ integrity: sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==
+ /csvtojson/2.0.10:
+ dependencies:
+ bluebird: 3.7.2
+ lodash: 4.17.15
+ strip-bom: 2.0.0
+ dev: false
+ engines:
+ node: '>=4.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-lUWFxGKyhraKCW8Qghz6Z0f2l/PqB1W3AO0HKJzGIQ5JRSlR651ekJDiGJbBT4sRNNv5ddnSGVEnsxP9XRCVpQ==
+ /cuid/2.1.8:
+ dev: true
+ resolution:
+ integrity: sha512-xiEMER6E7TlTPnDxrM4eRiC6TRgjNX9xzEZ5U/Se2YJKr7Mq4pJn/2XEHjl3STcSh96GmkHPcBXLES8M29wyyg==
+ /cycle/1.0.3:
+ dev: false
+ engines:
+ node: '>=0.4.0'
+ resolution:
+ integrity: sha1-IegLK+hYD5i0aPN5QwZisEbDStI=
+ /cyclist/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
+ /d/1.0.1:
+ dependencies:
+ es5-ext: 0.10.53
+ type: 1.2.0
+ dev: true
+ resolution:
+ integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
+ /damerau-levenshtein/1.0.6:
+ dev: true
+ resolution:
+ integrity: sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==
+ /dashdash/1.14.1:
+ dependencies:
+ assert-plus: 1.0.0
+ engines:
+ node: '>=0.10'
+ resolution:
+ integrity: sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+ /data-urls/1.1.0:
+ dependencies:
+ abab: 2.0.3
+ whatwg-mimetype: 2.3.0
+ whatwg-url: 7.1.0
+ dev: true
+ resolution:
+ integrity: sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==
+ /date-fns/2.1.0:
+ dev: false
+ resolution:
+ integrity: sha512-eKeLk3sLCnxB/0PN4t1+zqDtSs4jb4mXRSTZ2okmx/myfWyDqeO4r5nnmA5LClJiCwpuTMeK2v5UQPuE4uMaxA==
+ /dayjs/1.8.23:
+ dev: true
+ resolution:
+ integrity: sha512-NmYHMFONftoZbeOhVz6jfiXI4zSiPN6NoVWJgC0aZQfYVwzy/ZpESPHuCcI0B8BUMpSJQ08zenHDbofOLKq8hQ==
+ /debug/2.2.0:
+ dependencies:
+ ms: 0.7.1
+ dev: true
+ resolution:
+ integrity: sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=
+ /debug/2.6.9:
+ dependencies:
+ ms: 2.0.0
+ resolution:
+ integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ /debug/3.1.0:
+ dependencies:
+ ms: 2.0.0
+ dev: true
+ resolution:
+ integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+ /debug/3.2.6:
+ dependencies:
+ ms: 2.1.2
+ dev: true
+ resolution:
+ integrity: sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+ /debug/4.1.1:
+ dependencies:
+ ms: 2.1.2
+ dev: true
+ resolution:
+ integrity: sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+ /decamelize/1.2.0:
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+ /decode-uri-component/0.2.0:
+ dev: true
+ engines:
+ node: '>=0.10'
+ resolution:
+ integrity: sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+ /decomment/0.9.2:
+ dependencies:
+ esprima: 4.0.1
+ dev: true
+ engines:
+ node: '>=6.4'
+ npm: '>=2.15'
+ resolution:
+ integrity: sha512-sblyUmOJZxiL7oJ2ogJS6jtl/67+CTOW87SrYE/96u3PhDYikYoLCdLzcnceToiQejOLlqNnLCkaxx/+nE/ehg==
+ /decompress-response/3.3.0:
+ dependencies:
+ mimic-response: 1.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
+ /decompress-tar/4.1.1:
+ dependencies:
+ file-type: 5.2.0
+ is-stream: 1.1.0
+ tar-stream: 1.6.2
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==
+ /decompress-tarbz2/4.1.1:
+ dependencies:
+ decompress-tar: 4.1.1
+ file-type: 6.2.0
+ is-stream: 1.1.0
+ seek-bzip: 1.0.5
+ unbzip2-stream: 1.4.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==
+ /decompress-targz/4.1.1:
+ dependencies:
+ decompress-tar: 4.1.1
+ file-type: 5.2.0
+ is-stream: 1.1.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==
+ /decompress-unzip/4.0.1:
+ dependencies:
+ file-type: 3.9.0
+ get-stream: 2.3.1
+ pify: 2.3.0
+ yauzl: 2.10.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-3qrM39FK6vhVePczroIQ+bSEj2k=
+ /decompress/4.2.1:
+ dependencies:
+ decompress-tar: 4.1.1
+ decompress-tarbz2: 4.1.1
+ decompress-targz: 4.1.1
+ decompress-unzip: 4.0.1
+ graceful-fs: 4.2.3
+ make-dir: 1.3.0
+ pify: 2.3.0
+ strip-dirs: 2.1.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==
+ /deep-equal/1.1.1:
+ dependencies:
+ is-arguments: 1.0.4
+ is-date-object: 1.0.2
+ is-regex: 1.0.5
+ object-is: 1.0.2
+ object-keys: 1.1.1
+ regexp.prototype.flags: 1.3.0
+ resolution:
+ integrity: sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
+ /deep-extend/0.6.0:
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ resolution:
+ integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+ /deep-is/0.1.3:
+ dev: true
+ resolution:
+ integrity: sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+ /deepmerge/4.2.2:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+ /default-gateway/4.2.0:
+ dependencies:
+ execa: 1.0.0
+ ip-regex: 2.1.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==
+ /defer-to-connect/1.1.3:
+ dev: true
+ resolution:
+ integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
+ /deferred/0.7.11:
+ dependencies:
+ d: 1.0.1
+ es5-ext: 0.10.53
+ event-emitter: 0.3.5
+ next-tick: 1.1.0
+ timers-ext: 0.1.7
+ dev: true
+ resolution:
+ integrity: sha512-8eluCl/Blx4YOGwMapBvXRKxHXhA8ejDXYzEaK8+/gtcm8hRMhSLmXSqDmNUKNc/C8HNSmuyyp/hflhqDAvK2A==
+ /define-properties/1.1.3:
+ dependencies:
+ object-keys: 1.1.1
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ /define-property/0.2.5:
+ dependencies:
+ is-descriptor: 0.1.6
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+ /define-property/1.0.0:
+ dependencies:
+ is-descriptor: 1.0.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+ /define-property/2.0.2:
+ dependencies:
+ is-descriptor: 1.0.2
+ isobject: 3.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+ /del/4.1.1:
+ dependencies:
+ '@types/glob': 7.1.1
+ globby: 6.1.0
+ is-path-cwd: 2.2.0
+ is-path-in-cwd: 2.1.0
+ p-map: 2.1.0
+ pify: 4.0.1
+ rimraf: 2.7.1
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==
+ /delayed-stream/1.0.0:
+ engines:
+ node: '>=0.4.0'
+ resolution:
+ integrity: sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+ /delegate/3.2.0:
+ dev: false
+ optional: true
+ resolution:
+ integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
+ /depd/1.1.2:
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+ /dependency-tree/7.2.1:
+ dependencies:
+ commander: 2.20.3
+ debug: 4.1.1
+ filing-cabinet: 2.5.1
+ precinct: 6.2.0
+ typescript: 3.8.3
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-nBxnjkqDW4LqAzBazy60V4lE0mAtIQ+oers/GIIvVvGYVdCD9+RNNd4G9jjstyz7ZFVg/j/OiYCvK5MjoVqA2w==
+ /des.js/1.0.1:
+ dependencies:
+ inherits: 2.0.4
+ minimalistic-assert: 1.0.1
+ resolution:
+ integrity: sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==
+ /destroy/1.0.4:
+ resolution:
+ integrity: sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+ /detect-file/1.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
+ /detect-newline/2.1.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=
+ /detect-newline/3.1.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
+ /detect-node/2.0.4:
+ dev: true
+ resolution:
+ integrity: sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
+ /detect-port-alt/1.1.6:
+ dependencies:
+ address: 1.1.2
+ debug: 2.6.9
+ dev: true
+ engines:
+ node: '>= 4.2.1'
+ hasBin: true
+ resolution:
+ integrity: sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==
+ /detective-amd/3.0.0:
+ dependencies:
+ ast-module-types: 2.6.0
+ escodegen: 1.14.1
+ get-amd-module-type: 3.0.0
+ node-source-walk: 4.2.0
+ dev: true
+ engines:
+ node: '>= 6.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-kOpKHyabdSKF9kj7PqYHLeHPw+TJT8q2u48tZYMkIcas28el1CYeLEJ42Nm+563/Fq060T5WknfwDhdX9+kkBQ==
+ /detective-cjs/3.1.1:
+ dependencies:
+ ast-module-types: 2.6.0
+ node-source-walk: 4.2.0
+ dev: true
+ engines:
+ node: '>= 6.0'
+ resolution:
+ integrity: sha512-JQtNTBgFY6h8uT6pgph5QpV3IyxDv+z3qPk/FZRDT9TlFfm5dnRtpH39WtQEr1khqsUxVqXzKjZHpdoQvQbllg==
+ /detective-es6/2.1.0:
+ dependencies:
+ node-source-walk: 4.2.0
+ dev: true
+ engines:
+ node: '>= 6.0'
+ resolution:
+ integrity: sha512-QSHqKGOp/YBIfmIqKXaXeq2rlL+bp3bcIQMfZ+0PvKzRlELSOSZxKRvpxVcxlLuocQv4QnOfuWGniGrmPbz8MQ==
+ /detective-less/1.0.2:
+ dependencies:
+ debug: 4.1.1
+ gonzales-pe: 4.3.0
+ node-source-walk: 4.2.0
+ dev: true
+ engines:
+ node: '>= 6.0'
+ resolution:
+ integrity: sha512-Rps1xDkEEBSq3kLdsdnHZL1x2S4NGDcbrjmd4q+PykK5aJwDdP5MBgrJw1Xo+kyUHuv3JEzPqxr+Dj9ryeDRTA==
+ /detective-postcss/3.0.1:
+ dependencies:
+ debug: 4.1.1
+ is-url: 1.2.4
+ postcss: 7.0.27
+ postcss-values-parser: 1.5.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-tfTS2GdpUal5NY0aCqI4dpEy8Xfr88AehYKB0iBIZvo8y2g3UsrcDnrp9PR2FbzoW7xD5Rip3NJW7eCSvtqdUw==
+ /detective-sass/3.0.1:
+ dependencies:
+ debug: 4.1.1
+ gonzales-pe: 4.3.0
+ node-source-walk: 4.2.0
+ dev: true
+ engines:
+ node: '>= 6.0'
+ resolution:
+ integrity: sha512-oSbrBozRjJ+QFF4WJFbjPQKeakoaY1GiR380NPqwdbWYd5wfl5cLWv0l6LsJVqrgWfFN1bjFqSeo32Nxza8Lbw==
+ /detective-scss/2.0.1:
+ dependencies:
+ debug: 4.1.1
+ gonzales-pe: 4.3.0
+ node-source-walk: 4.2.0
+ dev: true
+ engines:
+ node: '>= 6.0'
+ resolution:
+ integrity: sha512-VveyXW4WQE04s05KlJ8K0bG34jtHQVgTc9InspqoQxvnelj/rdgSAy7i2DXAazyQNFKlWSWbS+Ro2DWKFOKTPQ==
+ /detective-stylus/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-UK7n24uruZA4HwEMY/q7pbWOVM0=
+ /detective-typescript/5.7.0:
+ dependencies:
+ '@typescript-eslint/typescript-estree': 2.27.0_typescript@3.8.3
+ ast-module-types: 2.6.0
+ node-source-walk: 4.2.0
+ typescript: 3.8.3
+ dev: true
+ engines:
+ node: '>=6.0'
+ resolution:
+ integrity: sha512-4SQeACXWAjIOsd2kJykPL8gWC9nVA+z8w7KtAdtd/7BCpDfrpI2ZA7pdhsmHv/zxf3ofeqpYi72vCkZ65bAjtA==
+ /diagnostics/1.1.1:
+ dependencies:
+ colorspace: 1.1.2
+ enabled: 1.0.2
+ kuler: 1.0.1
+ dev: true
+ resolution:
+ integrity: sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==
+ /diff-sequences/24.9.0:
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
+ /diff-sequences/25.2.6:
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==
+ /diff/3.5.0:
+ dev: true
+ engines:
+ node: '>=0.3.1'
+ resolution:
+ integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
+ /diffie-hellman/5.0.3:
+ dependencies:
+ bn.js: 4.11.8
+ miller-rabin: 4.0.1
+ randombytes: 2.1.0
+ resolution:
+ integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==
+ /dijkstrajs/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-082BIh4+pAdCz83lVtTpnpjdxxs=
+ /dir-glob/2.0.0:
+ dependencies:
+ arrify: 1.0.1
+ path-type: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==
+ /dir-glob/2.2.2:
+ dependencies:
+ path-type: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==
+ /dir-glob/3.0.1:
+ dependencies:
+ path-type: 4.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+ /dns-equal/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-s55/HabrCnW6nBcySzR1PEfgZU0=
+ /dns-packet/1.3.1:
+ dependencies:
+ ip: 1.1.5
+ safe-buffer: 5.2.0
+ dev: true
+ resolution:
+ integrity: sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==
+ /dns-txt/2.0.2:
+ dependencies:
+ buffer-indexof: 1.1.1
+ dev: true
+ resolution:
+ integrity: sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=
+ /doctrine/1.5.0:
+ dependencies:
+ esutils: 2.0.3
+ isarray: 1.0.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=
+ /doctrine/2.1.0:
+ dependencies:
+ esutils: 2.0.3
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
+ /doctrine/3.0.0:
+ dependencies:
+ esutils: 2.0.3
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+ /dom-converter/0.2.0:
+ dependencies:
+ utila: 0.4.0
+ dev: true
+ resolution:
+ integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==
+ /dom-helpers/5.1.4:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ csstype: 2.6.10
+ dev: false
+ resolution:
+ integrity: sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==
+ /dom-serializer/0.2.2:
+ dependencies:
+ domelementtype: 2.0.1
+ entities: 2.0.0
+ dev: true
+ resolution:
+ integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
+ /domain-browser/1.2.0:
+ dev: true
+ engines:
+ node: '>=0.4'
+ npm: '>=1.2'
+ resolution:
+ integrity: sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
+ /domelementtype/1.3.1:
+ dev: true
+ resolution:
+ integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+ /domelementtype/2.0.1:
+ dev: true
+ resolution:
+ integrity: sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
+ /domexception/1.0.1:
+ dependencies:
+ webidl-conversions: 4.0.2
+ dev: true
+ resolution:
+ integrity: sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
+ /domhandler/2.4.2:
+ dependencies:
+ domelementtype: 1.3.1
+ dev: true
+ resolution:
+ integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+ /domutils/1.5.1:
+ dependencies:
+ dom-serializer: 0.2.2
+ domelementtype: 1.3.1
+ dev: true
+ resolution:
+ integrity: sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+ /domutils/1.7.0:
+ dependencies:
+ dom-serializer: 0.2.2
+ domelementtype: 1.3.1
+ dev: true
+ resolution:
+ integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+ /dot-case/3.0.3:
+ dependencies:
+ no-case: 3.0.3
+ tslib: 1.11.1
+ dev: true
+ resolution:
+ integrity: sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA==
+ /dot-prop/4.2.0:
+ dependencies:
+ is-obj: 1.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==
+ /dot-prop/5.2.0:
+ dependencies:
+ is-obj: 2.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==
+ /dot-qs/0.2.0:
+ dev: true
+ resolution:
+ integrity: sha1-02UX/iS3zaYfznpQJqACSvr1pDk=
+ /dotenv-expand/5.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
+ /dotenv/8.2.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
+ /download/7.1.0:
+ dependencies:
+ archive-type: 4.0.0
+ caw: 2.0.1
+ content-disposition: 0.5.3
+ decompress: 4.2.1
+ ext-name: 5.0.0
+ file-type: 8.1.0
+ filenamify: 2.1.0
+ get-stream: 3.0.0
+ got: 8.3.2
+ make-dir: 1.3.0
+ p-event: 2.3.1
+ pify: 3.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==
+ /duplexer/0.1.1:
+ dev: true
+ resolution:
+ integrity: sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=
+ /duplexer3/0.1.4:
+ dev: true
+ resolution:
+ integrity: sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
+ /duplexify/3.7.1:
+ dependencies:
+ end-of-stream: 1.4.4
+ inherits: 2.0.4
+ readable-stream: 2.3.7
+ stream-shift: 1.0.1
+ dev: true
+ resolution:
+ integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==
+ /duplexify/4.1.1:
+ dependencies:
+ end-of-stream: 1.4.4
+ inherits: 2.0.4
+ readable-stream: 3.6.0
+ stream-shift: 1.0.1
+ dev: true
+ resolution:
+ integrity: sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==
+ /duration/0.2.2:
+ dependencies:
+ d: 1.0.1
+ es5-ext: 0.10.53
+ dev: true
+ resolution:
+ integrity: sha512-06kgtea+bGreF5eKYgI/36A6pLXggY7oR4p1pq4SmdFBn1ReOL5D8RhG64VrqfTTKNucqqtBAwEj8aB88mcqrg==
+ /ecc-jsbn/0.1.2:
+ dependencies:
+ jsbn: 0.1.1
+ safer-buffer: 2.1.2
+ resolution:
+ integrity: sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+ /ecdsa-sig-formatter/1.0.11:
+ dependencies:
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
+ /ee-first/1.1.1:
+ resolution:
+ integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+ /electron-to-chromium/1.3.399:
+ dev: true
+ resolution:
+ integrity: sha512-+NBhK0/v17pls7CSh3Cx5Ir3tsGmtLPMMAO4Nz272bre2wzdykLEsev5wjOd3rYMt2/kSS681ufFT7Dywxq1sw==
+ /elliptic/6.5.2:
+ dependencies:
+ bn.js: 4.11.8
+ brorand: 1.1.0
+ hash.js: 1.1.7
+ hmac-drbg: 1.0.1
+ inherits: 2.0.4
+ minimalistic-assert: 1.0.1
+ minimalistic-crypto-utils: 1.0.1
+ resolution:
+ integrity: sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==
+ /emoji-regex/7.0.3:
+ resolution:
+ integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
+ /emoji-regex/8.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+ /emojis-list/2.1.0:
+ dev: true
+ engines:
+ node: '>= 0.10'
+ resolution:
+ integrity: sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
+ /emojis-list/3.0.0:
+ dev: true
+ engines:
+ node: '>= 4'
+ resolution:
+ integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
+ /enabled/1.0.2:
+ dependencies:
+ env-variable: 0.0.6
+ dev: true
+ resolution:
+ integrity: sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=
+ /encodeurl/1.0.2:
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+ /encoding/0.1.12:
+ dependencies:
+ iconv-lite: 0.4.24
+ dev: true
+ resolution:
+ integrity: sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=
+ /encodr/1.2.2:
+ dependencies:
+ cbor: 5.0.1
+ cbor-js: 0.1.0
+ msgpack-lite: 0.1.26
+ utf8: 3.0.0
+ dev: true
+ engines:
+ node: '>=8.0.0'
+ resolution:
+ integrity: sha512-MQ5cDilH5al566/2KXvsyrti6t67Oso5oe2RLfgdcNKaMcqEPfbTxpFOa+41QkPAu1+bEVksWL8JK3Owa6Ow+g==
+ /end-of-stream/1.4.4:
+ dependencies:
+ once: 1.4.0
+ dev: true
+ resolution:
+ integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+ /engine.io-client/3.4.0:
+ dependencies:
+ component-emitter: 1.2.1
+ component-inherit: 0.0.3
+ debug: 4.1.1
+ engine.io-parser: 2.2.0
+ has-cors: 1.1.0
+ indexof: 0.0.1
+ parseqs: 0.0.5
+ parseuri: 0.0.5
+ ws: 6.1.4
+ xmlhttprequest-ssl: 1.5.5
+ yeast: 0.1.2
+ dev: true
+ resolution:
+ integrity: sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA==
+ /engine.io-parser/2.2.0:
+ dependencies:
+ after: 0.8.2
+ arraybuffer.slice: 0.0.7
+ base64-arraybuffer: 0.1.5
+ blob: 0.0.5
+ has-binary2: 1.0.3
+ dev: true
+ resolution:
+ integrity: sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==
+ /enhanced-resolve/4.1.0:
+ dependencies:
+ graceful-fs: 4.2.3
+ memory-fs: 0.4.1
+ tapable: 1.1.3
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==
+ /enhanced-resolve/4.1.1:
+ dependencies:
+ graceful-fs: 4.2.3
+ memory-fs: 0.5.0
+ tapable: 1.1.3
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==
+ /entities/1.1.2:
+ dev: true
+ resolution:
+ integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+ /entities/2.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
+ /env-variable/0.0.6:
+ dev: true
+ resolution:
+ integrity: sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==
+ /errno/0.1.7:
+ dependencies:
+ prr: 1.0.1
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
+ /error-ex/1.3.2:
+ dependencies:
+ is-arrayish: 0.2.1
+ resolution:
+ integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+ /es-abstract/1.17.5:
+ dependencies:
+ es-to-primitive: 1.2.1
+ function-bind: 1.1.1
+ has: 1.0.3
+ has-symbols: 1.0.1
+ is-callable: 1.1.5
+ is-regex: 1.0.5
+ object-inspect: 1.7.0
+ object-keys: 1.1.1
+ object.assign: 4.1.0
+ string.prototype.trimleft: 2.1.2
+ string.prototype.trimright: 2.1.2
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
+ /es-cookie/1.3.2:
+ dev: false
+ resolution:
+ integrity: sha512-UTlYYhXGLOy05P/vKVT2Ui7WtC7NiRzGtJyAKKn32g5Gvcjn7KAClLPWlipCtxIus934dFg9o9jXiBL0nP+t9Q==
+ /es-to-primitive/1.2.1:
+ dependencies:
+ is-callable: 1.1.5
+ is-date-object: 1.0.2
+ is-symbol: 1.0.3
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+ /es5-ext/0.10.53:
+ dependencies:
+ es6-iterator: 2.0.3
+ es6-symbol: 3.1.3
+ next-tick: 1.0.0
+ dev: true
+ resolution:
+ integrity: sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
+ /es6-iterator/2.0.3:
+ dependencies:
+ d: 1.0.1
+ es5-ext: 0.10.53
+ es6-symbol: 3.1.3
+ dev: true
+ resolution:
+ integrity: sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
+ /es6-promisify/6.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-jCsk2fpfEFusVv1MDkF4Uf0hAzIKNDMgR6LyOIw6a3jwkN1sCgWzuwgnsHY9YSQ8n8P31HoncvE0LC44cpWTrw==
+ /es6-set/0.1.5:
+ dependencies:
+ d: 1.0.1
+ es5-ext: 0.10.53
+ es6-iterator: 2.0.3
+ es6-symbol: 3.1.1
+ event-emitter: 0.3.5
+ dev: true
+ resolution:
+ integrity: sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=
+ /es6-symbol/3.1.1:
+ dependencies:
+ d: 1.0.1
+ es5-ext: 0.10.53
+ dev: true
+ resolution:
+ integrity: sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=
+ /es6-symbol/3.1.3:
+ dependencies:
+ d: 1.0.1
+ ext: 1.4.0
+ dev: true
+ resolution:
+ integrity: sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
+ /es6-weak-map/2.0.3:
+ dependencies:
+ d: 1.0.1
+ es5-ext: 0.10.53
+ es6-iterator: 2.0.3
+ es6-symbol: 3.1.3
+ dev: true
+ resolution:
+ integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==
+ /escape-html/1.0.3:
+ resolution:
+ integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+ /escape-string-regexp/1.0.5:
+ engines:
+ node: '>=0.8.0'
+ resolution:
+ integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+ /escape-string-regexp/2.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
+ /escodegen/1.14.1:
+ dependencies:
+ esprima: 4.0.1
+ estraverse: 4.3.0
+ esutils: 2.0.3
+ optionator: 0.8.3
+ dev: true
+ engines:
+ node: '>=4.0'
+ hasBin: true
+ optionalDependencies:
+ source-map: 0.6.1
+ resolution:
+ integrity: sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==
+ /eslint-config-airbnb-base/14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3:
+ dependencies:
+ confusing-browser-globals: 1.0.9
+ eslint: 6.8.0
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ object.assign: 4.1.0
+ object.entries: 1.1.1
+ dev: true
+ engines:
+ node: '>= 6'
+ peerDependencies:
+ eslint: ^5.16.0 || ^6.8.0
+ eslint-plugin-import: ^2.20.1
+ resolution:
+ integrity: sha512-+XCcfGyCnbzOnktDVhwsCAx+9DmrzEmuwxyHUJpw+kqBVT744OUBrB09khgFKlK1lshVww6qXGsYPZpavoNjJw==
+ /eslint-config-airbnb/18.1.0_7221e9efc3e1df952f9031babfc371af:
+ dependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jsx-a11y: 6.2.3_eslint@6.8.0
+ eslint-plugin-react: 7.19.0_eslint@6.8.0
+ eslint-plugin-react-hooks: 1.7.0_eslint@6.8.0
+ object.assign: 4.1.0
+ object.entries: 1.1.1
+ dev: true
+ engines:
+ node: '>= 6'
+ peerDependencies:
+ eslint: ^5.16.0 || ^6.8.0
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jsx-a11y: ^6.2.3
+ eslint-plugin-react: ^7.19.0
+ eslint-plugin-react-hooks: ^2.5.0 || ^1.7.0
+ resolution:
+ integrity: sha512-kZFuQC/MPnH7KJp6v95xsLBf63G/w7YqdPfQ0MUanxQ7zcKUNG8j+sSY860g3NwCBOa62apw16J6pRN+AOgXzw==
+ /eslint-config-airbnb/18.1.0_8cdb6d8c18c3319a1365bd5afa0063a3:
+ dependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ object.assign: 4.1.0
+ object.entries: 1.1.1
+ dev: true
+ engines:
+ node: '>= 6'
+ peerDependencies:
+ eslint: ^5.16.0 || ^6.8.0
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jsx-a11y: ^6.2.3
+ eslint-plugin-react: ^7.19.0
+ eslint-plugin-react-hooks: ^2.5.0 || ^1.7.0
+ resolution:
+ integrity: sha512-kZFuQC/MPnH7KJp6v95xsLBf63G/w7YqdPfQ0MUanxQ7zcKUNG8j+sSY860g3NwCBOa62apw16J6pRN+AOgXzw==
+ /eslint-config-airbnb/18.1.0_93d707b3c4a28e806b96154710814f94:
+ dependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jsx-a11y: 6.2.3_eslint@6.8.0
+ eslint-plugin-react: 7.19.0_eslint@6.8.0
+ eslint-plugin-react-hooks: 2.5.1_eslint@6.8.0
+ object.assign: 4.1.0
+ object.entries: 1.1.1
+ dev: true
+ engines:
+ node: '>= 6'
+ peerDependencies:
+ eslint: ^5.16.0 || ^6.8.0
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jsx-a11y: ^6.2.3
+ eslint-plugin-react: ^7.19.0
+ eslint-plugin-react-hooks: ^2.5.0 || ^1.7.0
+ resolution:
+ integrity: sha512-kZFuQC/MPnH7KJp6v95xsLBf63G/w7YqdPfQ0MUanxQ7zcKUNG8j+sSY860g3NwCBOa62apw16J6pRN+AOgXzw==
+ /eslint-config-airbnb/18.1.0_9bfaab310611d9c7b04264b84523c27a:
+ dependencies:
+ eslint: 6.8.0
+ eslint-config-airbnb-base: 14.1.0_8cdb6d8c18c3319a1365bd5afa0063a3
+ eslint-plugin-import: 2.20.2_eslint@6.8.0
+ eslint-plugin-jsx-a11y: 6.2.3_eslint@6.8.0
+ object.assign: 4.1.0
+ object.entries: 1.1.1
+ dev: true
+ engines:
+ node: '>= 6'
+ peerDependencies:
+ eslint: ^5.16.0 || ^6.8.0
+ eslint-plugin-import: ^2.20.1
+ eslint-plugin-jsx-a11y: ^6.2.3
+ eslint-plugin-react: ^7.19.0
+ eslint-plugin-react-hooks: ^2.5.0 || ^1.7.0
+ resolution:
+ integrity: sha512-kZFuQC/MPnH7KJp6v95xsLBf63G/w7YqdPfQ0MUanxQ7zcKUNG8j+sSY860g3NwCBOa62apw16J6pRN+AOgXzw==
+ /eslint-config-prettier/6.10.1_eslint@6.8.0:
+ dependencies:
+ eslint: 6.8.0
+ get-stdin: 6.0.0
+ dev: true
+ hasBin: true
+ peerDependencies:
+ eslint: '>=3.14.1'
+ resolution:
+ integrity: sha512-svTy6zh1ecQojvpbJSgH3aei/Rt7C6i090l5f2WQ4aB05lYHeZIR1qL4wZyyILTbtmnbHP5Yn8MrsOJMGa8RkQ==
+ /eslint-config-react-app/5.2.1_c14ecc97ba42c4e073f7e6502a3f179f:
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 2.27.0_9e31f0f459c1656d0a7ef30429cc70f8
+ '@typescript-eslint/parser': 2.27.0_eslint@6.8.0
+ babel-eslint: 10.1.0_eslint@6.8.0
+ confusing-browser-globals: 1.0.9
+ eslint: 6.8.0
+ eslint-plugin-flowtype: 4.6.0_eslint@6.8.0
+ eslint-plugin-import: 2.20.1_eslint@6.8.0
+ eslint-plugin-jsx-a11y: 6.2.3_eslint@6.8.0
+ eslint-plugin-react: 7.19.0_eslint@6.8.0
+ eslint-plugin-react-hooks: 1.7.0_eslint@6.8.0
+ dev: true
+ peerDependencies:
+ '@typescript-eslint/eslint-plugin': 2.x
+ '@typescript-eslint/parser': 2.x
+ babel-eslint: 10.x
+ eslint: 6.x
+ eslint-plugin-flowtype: 3.x || 4.x
+ eslint-plugin-import: 2.x
+ eslint-plugin-jsx-a11y: 6.x
+ eslint-plugin-react: 7.x
+ eslint-plugin-react-hooks: 1.x || 2.x
+ resolution:
+ integrity: sha512-pGIZ8t0mFLcV+6ZirRgYK6RVqUIKRIi9MmgzUEmrIknsn3AdO0I32asO86dJgloHq+9ZPl8UIg8mYrvgP5u2wQ==
+ /eslint-import-resolver-node/0.3.3:
+ dependencies:
+ debug: 2.6.9
+ resolve: 1.15.1
+ dev: true
+ resolution:
+ integrity: sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==
+ /eslint-loader/3.0.3_eslint@6.8.0+webpack@4.42.0:
+ dependencies:
+ eslint: 6.8.0
+ fs-extra: 8.1.0
+ loader-fs-cache: 1.0.3
+ loader-utils: 1.4.0
+ object-hash: 2.0.3
+ schema-utils: 2.6.5
+ webpack: 4.42.0_webpack@4.42.0
+ dev: true
+ engines:
+ node: '>= 8.9.0'
+ peerDependencies:
+ eslint: ^5.0.0 || ^6.0.0
+ webpack: ^4.0.0 || ^5.0.0
+ resolution:
+ integrity: sha512-+YRqB95PnNvxNp1HEjQmvf9KNvCin5HXYYseOXVC2U0KEcw4IkQ2IQEBG46j7+gW39bMzeu0GsUhVbBY3Votpw==
+ /eslint-module-utils/2.6.0:
+ dependencies:
+ debug: 2.6.9
+ pkg-dir: 2.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==
+ /eslint-plugin-flowtype/4.6.0_eslint@6.8.0:
+ dependencies:
+ eslint: 6.8.0
+ lodash: 4.17.15
+ dev: true
+ engines:
+ node: '>=4'
+ peerDependencies:
+ eslint: '>=6.1.0'
+ resolution:
+ integrity: sha512-W5hLjpFfZyZsXfo5anlu7HM970JBDqbEshAJUkeczP6BFCIfJXuiIBQXyberLRtOStT0OGPF8efeTbxlHk4LpQ==
+ /eslint-plugin-import/2.20.1_eslint@6.8.0:
+ dependencies:
+ array-includes: 3.1.1
+ array.prototype.flat: 1.2.3
+ contains-path: 0.1.0
+ debug: 2.6.9
+ doctrine: 1.5.0
+ eslint: 6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-module-utils: 2.6.0
+ has: 1.0.3
+ minimatch: 3.0.4
+ object.values: 1.1.1
+ read-pkg-up: 2.0.0
+ resolve: 1.15.1
+ dev: true
+ engines:
+ node: '>=4'
+ peerDependencies:
+ eslint: 2.x - 6.x
+ resolution:
+ integrity: sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==
+ /eslint-plugin-import/2.20.2_eslint@6.8.0:
+ dependencies:
+ array-includes: 3.1.1
+ array.prototype.flat: 1.2.3
+ contains-path: 0.1.0
+ debug: 2.6.9
+ doctrine: 1.5.0
+ eslint: 6.8.0
+ eslint-import-resolver-node: 0.3.3
+ eslint-module-utils: 2.6.0
+ has: 1.0.3
+ minimatch: 3.0.4
+ object.values: 1.1.1
+ read-pkg-up: 2.0.0
+ resolve: 1.15.1
+ dev: true
+ engines:
+ node: '>=4'
+ peerDependencies:
+ eslint: 2.x - 6.x
+ resolution:
+ integrity: sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg==
+ /eslint-plugin-jest/22.21.0_eslint@6.8.0:
+ dependencies:
+ '@typescript-eslint/experimental-utils': 1.13.0_eslint@6.8.0
+ eslint: 6.8.0
+ dev: true
+ engines:
+ node: '>=6'
+ peerDependencies:
+ eslint: '>=5'
+ resolution:
+ integrity: sha512-OaqnSS7uBgcGiqXUiEnjoqxPNKvR4JWG5mSRkzVoR6+vDwlqqp11beeql1hYs0HTbdhiwrxWLxbX0Vx7roG3Ew==
+ /eslint-plugin-jest/23.8.2_eslint@6.8.0:
+ dependencies:
+ '@typescript-eslint/experimental-utils': 2.27.0_eslint@6.8.0
+ eslint: 6.8.0
+ dev: true
+ engines:
+ node: '>=8'
+ peerDependencies:
+ eslint: '>=5'
+ resolution:
+ integrity: sha512-xwbnvOsotSV27MtAe7s8uGWOori0nUsrXh2f1EnpmXua8sDfY6VZhHAhHg2sqK7HBNycRQExF074XSZ7DvfoFg==
+ /eslint-plugin-jsx-a11y/6.2.3_eslint@6.8.0:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ aria-query: 3.0.0
+ array-includes: 3.1.1
+ ast-types-flow: 0.0.7
+ axobject-query: 2.1.2
+ damerau-levenshtein: 1.0.6
+ emoji-regex: 7.0.3
+ eslint: 6.8.0
+ has: 1.0.3
+ jsx-ast-utils: 2.2.3
+ dev: true
+ engines:
+ node: '>=4.0'
+ peerDependencies:
+ eslint: ^3 || ^4 || ^5 || ^6
+ resolution:
+ integrity: sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==
+ /eslint-plugin-prettier/3.1.2_eslint@6.8.0+prettier@1.19.1:
+ dependencies:
+ eslint: 6.8.0
+ prettier: 1.19.1
+ prettier-linter-helpers: 1.0.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ peerDependencies:
+ eslint: '>= 5.0.0'
+ prettier: '>= 1.13.0'
+ resolution:
+ integrity: sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==
+ /eslint-plugin-react-hooks/1.7.0_eslint@6.8.0:
+ dependencies:
+ eslint: 6.8.0
+ dev: true
+ engines:
+ node: '>=7'
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
+ resolution:
+ integrity: sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==
+ /eslint-plugin-react-hooks/2.5.1_eslint@6.8.0:
+ dependencies:
+ eslint: 6.8.0
+ dev: true
+ engines:
+ node: '>=7'
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
+ resolution:
+ integrity: sha512-Y2c4b55R+6ZzwtTppKwSmK/Kar8AdLiC2f9NADCuxbcTgPPg41Gyqa6b9GppgXSvCtkRw43ZE86CT5sejKC6/g==
+ /eslint-plugin-react/7.19.0_eslint@6.8.0:
+ dependencies:
+ array-includes: 3.1.1
+ doctrine: 2.1.0
+ eslint: 6.8.0
+ has: 1.0.3
+ jsx-ast-utils: 2.2.3
+ object.entries: 1.1.1
+ object.fromentries: 2.0.2
+ object.values: 1.1.1
+ prop-types: 15.7.2
+ resolve: 1.15.1
+ semver: 6.3.0
+ string.prototype.matchall: 4.0.2
+ xregexp: 4.3.0
+ dev: true
+ engines:
+ node: '>=4'
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
+ resolution:
+ integrity: sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ==
+ /eslint-scope/4.0.3:
+ dependencies:
+ esrecurse: 4.2.1
+ estraverse: 4.3.0
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ resolution:
+ integrity: sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==
+ /eslint-scope/5.0.0:
+ dependencies:
+ esrecurse: 4.2.1
+ estraverse: 4.3.0
+ dev: true
+ engines:
+ node: '>=8.0.0'
+ resolution:
+ integrity: sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==
+ /eslint-utils/1.4.3:
+ dependencies:
+ eslint-visitor-keys: 1.1.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
+ /eslint-utils/2.0.0:
+ dependencies:
+ eslint-visitor-keys: 1.1.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==
+ /eslint-visitor-keys/1.1.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
+ /eslint/6.8.0:
+ dependencies:
+ '@babel/code-frame': 7.8.3
+ ajv: 6.12.0
+ chalk: 2.4.2
+ cross-spawn: 6.0.5
+ debug: 4.1.1
+ doctrine: 3.0.0
+ eslint-scope: 5.0.0
+ eslint-utils: 1.4.3
+ eslint-visitor-keys: 1.1.0
+ espree: 6.2.1
+ esquery: 1.2.0
+ esutils: 2.0.3
+ file-entry-cache: 5.0.1
+ functional-red-black-tree: 1.0.1
+ glob-parent: 5.1.1
+ globals: 12.4.0
+ ignore: 4.0.6
+ import-fresh: 3.2.1
+ imurmurhash: 0.1.4
+ inquirer: 7.1.0
+ is-glob: 4.0.1
+ js-yaml: 3.13.1
+ json-stable-stringify-without-jsonify: 1.0.1
+ levn: 0.3.0
+ lodash: 4.17.15
+ minimatch: 3.0.4
+ mkdirp: 0.5.5
+ natural-compare: 1.4.0
+ optionator: 0.8.3
+ progress: 2.0.3
+ regexpp: 2.0.1
+ semver: 6.3.0
+ strip-ansi: 5.2.0
+ strip-json-comments: 3.1.0
+ table: 5.4.6
+ text-table: 0.2.0
+ v8-compile-cache: 2.1.0
+ dev: true
+ engines:
+ node: ^8.10.0 || ^10.13.0 || >=11.10.1
+ hasBin: true
+ resolution:
+ integrity: sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
+ /esniff/1.1.0:
+ dependencies:
+ d: 1.0.1
+ es5-ext: 0.10.53
+ dev: true
+ resolution:
+ integrity: sha1-xmhJIp+RRk3t4uDUAgHtar9l8qw=
+ /espree/6.2.1:
+ dependencies:
+ acorn: 7.1.1
+ acorn-jsx: 5.2.0_acorn@7.1.1
+ eslint-visitor-keys: 1.1.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==
+ /esprima/4.0.1:
+ engines:
+ node: '>=4'
+ hasBin: true
+ resolution:
+ integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+ /esquery/1.2.0:
+ dependencies:
+ estraverse: 5.0.0
+ dev: true
+ engines:
+ node: '>=8.0'
+ resolution:
+ integrity: sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q==
+ /esrecurse/4.2.1:
+ dependencies:
+ estraverse: 4.3.0
+ dev: true
+ engines:
+ node: '>=4.0'
+ resolution:
+ integrity: sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==
+ /essentials/1.1.1:
+ dev: true
+ resolution:
+ integrity: sha512-SmaxoAdVu86XkZQM/u6TYSu96ZlFGwhvSk1l9zAkznFuQkMb9mRDS2iq/XWDow7R8OwBwdYH8nLyDKznMD+GWw==
+ /estraverse/4.3.0:
+ dev: true
+ engines:
+ node: '>=4.0'
+ resolution:
+ integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+ /estraverse/5.0.0:
+ dev: true
+ engines:
+ node: '>=4.0'
+ resolution:
+ integrity: sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A==
+ /esutils/2.0.3:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+ /etag/1.8.1:
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+ /event-emitter/0.3.5:
+ dependencies:
+ d: 1.0.1
+ es5-ext: 0.10.53
+ dev: true
+ resolution:
+ integrity: sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=
+ /event-lite/0.1.2:
+ dev: true
+ resolution:
+ integrity: sha512-HnSYx1BsJ87/p6swwzv+2v6B4X+uxUteoDfRxsAb1S1BePzQqOLevVmkdA15GHJVd9A9Ok6wygUR18Hu0YeV9g==
+ /eventemitter3/4.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==
+ /events/1.1.1:
+ engines:
+ node: '>=0.4.x'
+ resolution:
+ integrity: sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
+ /events/3.1.0:
+ dev: true
+ engines:
+ node: '>=0.8.x'
+ resolution:
+ integrity: sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==
+ /eventsource/1.0.7:
+ dependencies:
+ original: 1.0.2
+ dev: true
+ engines:
+ node: '>=0.12.0'
+ resolution:
+ integrity: sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==
+ /evp_bytestokey/1.0.3:
+ dependencies:
+ md5.js: 1.3.5
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
+ /exec-sh/0.3.4:
+ dev: true
+ resolution:
+ integrity: sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==
+ /execa/0.7.0:
+ dependencies:
+ cross-spawn: 5.1.0
+ get-stream: 3.0.0
+ is-stream: 1.1.0
+ npm-run-path: 2.0.2
+ p-finally: 1.0.0
+ signal-exit: 3.0.3
+ strip-eof: 1.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=
+ /execa/0.8.0:
+ dependencies:
+ cross-spawn: 5.1.0
+ get-stream: 3.0.0
+ is-stream: 1.1.0
+ npm-run-path: 2.0.2
+ p-finally: 1.0.0
+ signal-exit: 3.0.3
+ strip-eof: 1.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=
+ /execa/1.0.0:
+ dependencies:
+ cross-spawn: 6.0.5
+ get-stream: 4.1.0
+ is-stream: 1.1.0
+ npm-run-path: 2.0.2
+ p-finally: 1.0.0
+ signal-exit: 3.0.3
+ strip-eof: 1.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+ /execa/3.4.0:
+ dependencies:
+ cross-spawn: 7.0.2
+ get-stream: 5.1.0
+ human-signals: 1.1.1
+ is-stream: 2.0.0
+ merge-stream: 2.0.0
+ npm-run-path: 4.0.1
+ onetime: 5.1.0
+ p-finally: 2.0.1
+ signal-exit: 3.0.3
+ strip-final-newline: 2.0.0
+ dev: true
+ engines:
+ node: ^8.12.0 || >=9.7.0
+ resolution:
+ integrity: sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==
+ /exenv/1.2.2:
+ dev: false
+ resolution:
+ integrity: sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
+ /exit/0.1.2:
+ dev: true
+ engines:
+ node: '>= 0.8.0'
+ resolution:
+ integrity: sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=
+ /expand-brackets/2.1.4:
+ dependencies:
+ debug: 2.6.9
+ define-property: 0.2.5
+ extend-shallow: 2.0.1
+ posix-character-classes: 0.1.1
+ regex-not: 1.0.2
+ snapdragon: 0.8.2
+ to-regex: 3.0.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+ /expand-tilde/2.0.2:
+ dependencies:
+ homedir-polyfill: 1.0.3
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=
+ /expect/24.9.0:
+ dependencies:
+ '@jest/types': 24.9.0
+ ansi-styles: 3.2.1
+ jest-get-type: 24.9.0
+ jest-matcher-utils: 24.9.0
+ jest-message-util: 24.9.0
+ jest-regex-util: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==
+ /expect/25.3.0:
+ dependencies:
+ '@jest/types': 25.3.0
+ ansi-styles: 4.2.1
+ jest-get-type: 25.2.6
+ jest-matcher-utils: 25.3.0
+ jest-message-util: 25.3.0
+ jest-regex-util: 25.2.6
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-buboTXML2h/L0Kh44Ys2Cx49mX20ISc5KDirkxIs3Q9AJv0kazweUAbukegr+nHDOvFRKmxdojjIHCjqAceYfg==
+ /express/4.17.1:
+ dependencies:
+ accepts: 1.3.7
+ array-flatten: 1.1.1
+ body-parser: 1.19.0
+ content-disposition: 0.5.3
+ content-type: 1.0.4
+ cookie: 0.4.0
+ cookie-signature: 1.0.6
+ debug: 2.6.9
+ depd: 1.1.2
+ encodeurl: 1.0.2
+ escape-html: 1.0.3
+ etag: 1.8.1
+ finalhandler: 1.1.2
+ fresh: 0.5.2
+ merge-descriptors: 1.0.1
+ methods: 1.1.2
+ on-finished: 2.3.0
+ parseurl: 1.3.3
+ path-to-regexp: 0.1.7
+ proxy-addr: 2.0.6
+ qs: 6.7.0
+ range-parser: 1.2.1
+ safe-buffer: 5.1.2
+ send: 0.17.1
+ serve-static: 1.14.1
+ setprototypeof: 1.1.1
+ statuses: 1.5.0
+ type-is: 1.6.18
+ utils-merge: 1.0.1
+ vary: 1.1.2
+ engines:
+ node: '>= 0.10.0'
+ resolution:
+ integrity: sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
+ /ext-list/2.2.2:
+ dependencies:
+ mime-db: 1.43.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==
+ /ext-name/5.0.0:
+ dependencies:
+ ext-list: 2.2.2
+ sort-keys-length: 1.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==
+ /ext/1.4.0:
+ dependencies:
+ type: 2.0.0
+ dev: true
+ resolution:
+ integrity: sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
+ /extend-shallow/2.0.1:
+ dependencies:
+ is-extendable: 0.1.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+ /extend-shallow/3.0.2:
+ dependencies:
+ assign-symbols: 1.0.0
+ is-extendable: 1.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+ /extend/3.0.2:
+ resolution:
+ integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+ /external-editor/3.1.0:
+ dependencies:
+ chardet: 0.7.0
+ iconv-lite: 0.4.24
+ tmp: 0.0.33
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
+ /extglob/2.0.4:
+ dependencies:
+ array-unique: 0.3.2
+ define-property: 1.0.0
+ expand-brackets: 2.1.4
+ extend-shallow: 2.0.1
+ fragment-cache: 0.2.1
+ regex-not: 1.0.2
+ snapdragon: 0.8.2
+ to-regex: 3.0.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+ /extsprintf/1.3.0:
+ engines:
+ '0': node >=0.6.0
+ resolution:
+ integrity: sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+ /fast-deep-equal/3.1.1:
+ resolution:
+ integrity: sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
+ /fast-diff/1.2.0:
+ dev: true
+ resolution:
+ integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
+ /fast-glob/2.2.7:
+ dependencies:
+ '@mrmlnc/readdir-enhanced': 2.2.1
+ '@nodelib/fs.stat': 1.1.3
+ glob-parent: 3.1.0
+ is-glob: 4.0.1
+ merge2: 1.3.0
+ micromatch: 3.1.10
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ resolution:
+ integrity: sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==
+ /fast-glob/3.2.2:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.3
+ '@nodelib/fs.walk': 1.2.4
+ glob-parent: 5.1.1
+ merge2: 1.3.0
+ micromatch: 4.0.2
+ picomatch: 2.2.2
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A==
+ /fast-json-stable-stringify/2.1.0:
+ resolution:
+ integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+ /fast-levenshtein/2.0.6:
+ dev: true
+ resolution:
+ integrity: sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+ /fast-safe-stringify/2.0.7:
+ dev: true
+ resolution:
+ integrity: sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
+ /fast-text-encoding/1.0.1:
+ dev: false
+ resolution:
+ integrity: sha512-x4FEgaz3zNRtJfLFqJmHWxkMDDvXVtaznj2V9jiP8ACUJrUgist4bP9FmDL2Vew2Y9mEQI/tG4GqabaitYp9CQ==
+ /fastq/1.7.0:
+ dependencies:
+ reusify: 1.0.4
+ dev: true
+ resolution:
+ integrity: sha512-YOadQRnHd5q6PogvAR/x62BGituF2ufiEA6s8aavQANw5YKHERI4AREboX6KotzP8oX2klxYF2wcV/7bn1clfQ==
+ /fault/1.0.4:
+ dependencies:
+ format: 0.2.2
+ dev: false
+ resolution:
+ integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==
+ /faye-websocket/0.10.0:
+ dependencies:
+ websocket-driver: 0.7.3
+ dev: true
+ engines:
+ node: '>=0.4.0'
+ resolution:
+ integrity: sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=
+ /faye-websocket/0.11.3:
+ dependencies:
+ websocket-driver: 0.7.3
+ dev: true
+ engines:
+ node: '>=0.8.0'
+ resolution:
+ integrity: sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==
+ /fb-watchman/2.0.1:
+ dependencies:
+ bser: 2.1.1
+ dev: true
+ resolution:
+ integrity: sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==
+ /fd-slicer/1.0.1:
+ dependencies:
+ pend: 1.2.0
+ dev: true
+ resolution:
+ integrity: sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=
+ /fd-slicer/1.1.0:
+ dependencies:
+ pend: 1.2.0
+ dev: true
+ resolution:
+ integrity: sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
+ /fecha/2.3.3:
+ dev: true
+ resolution:
+ integrity: sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==
+ /figgy-pudding/3.5.2:
+ dev: true
+ resolution:
+ integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
+ /figures/2.0.0:
+ dependencies:
+ escape-string-regexp: 1.0.5
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=
+ /figures/3.2.0:
+ dependencies:
+ escape-string-regexp: 1.0.5
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
+ /file-entry-cache/5.0.1:
+ dependencies:
+ flat-cache: 2.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
+ /file-exists-dazinatorfork/1.0.2:
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-r70c72ln2YHzQINNfxDp02hAhbGkt1HffZ+Du8oetWDLjDtFja/Lm10lUaSh9e+wD+7VDvPee0b0C9SAy8pWZg==
+ /file-loader/4.3.0_webpack@4.42.0:
+ dependencies:
+ loader-utils: 1.4.0
+ schema-utils: 2.6.5
+ webpack: 4.42.0_webpack@4.42.0
+ dev: true
+ engines:
+ node: '>= 8.9.0'
+ peerDependencies:
+ webpack: ^4.0.0
+ resolution:
+ integrity: sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA==
+ /file-selector/0.1.12:
+ dependencies:
+ tslib: 1.11.1
+ dev: false
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==
+ /file-type/3.9.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-JXoHg4TR24CHvESdEH1SpSZyuek=
+ /file-type/4.4.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-G2AOX8ofvcboDApwxxyNul95BsU=
+ /file-type/5.2.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-LdvqfHP/42No365J3DOMBYwritY=
+ /file-type/6.2.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==
+ /file-type/8.1.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==
+ /file-uri-to-path/1.0.0:
+ dev: true
+ optional: true
+ resolution:
+ integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+ /filename-reserved-regex/2.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-q/c9+rc10EVECr/qLZHzieu/oik=
+ /filenamify/2.1.0:
+ dependencies:
+ filename-reserved-regex: 2.0.0
+ strip-outer: 1.0.1
+ trim-repeated: 1.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==
+ /filesize/3.6.1:
+ dev: true
+ engines:
+ node: '>= 0.4.0'
+ resolution:
+ integrity: sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==
+ /filesize/6.0.1:
+ dev: true
+ engines:
+ node: '>= 0.4.0'
+ resolution:
+ integrity: sha512-u4AYWPgbI5GBhs6id1KdImZWn5yfyFrrQ8OWZdN7ZMfA8Bf4HcO0BGo9bmUIEV8yrp8I1xVfJ/dn90GtFNNJcg==
+ /filing-cabinet/2.5.1:
+ dependencies:
+ app-module-path: 2.2.0
+ commander: 2.20.3
+ debug: 4.1.1
+ decomment: 0.9.2
+ enhanced-resolve: 4.1.1
+ is-relative-path: 1.0.2
+ module-definition: 3.3.0
+ module-lookup-amd: 6.2.0
+ resolve: 1.15.1
+ resolve-dependency-path: 2.0.0
+ sass-lookup: 3.0.0
+ stylus-lookup: 3.0.2
+ typescript: 3.8.3
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-GWOdObzou2L0HrJUk8MpJa01q0ZOwuTwTssM2+P+ABJWEGlVWd6ueEatANFdin94/3rdkVSdqpH14VqCNqp3RA==
+ /fill-range/4.0.0:
+ dependencies:
+ extend-shallow: 2.0.1
+ is-number: 3.0.0
+ repeat-string: 1.6.1
+ to-regex-range: 2.1.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+ /fill-range/7.0.1:
+ dependencies:
+ to-regex-range: 5.0.1
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+ /finalhandler/1.1.2:
+ dependencies:
+ debug: 2.6.9
+ encodeurl: 1.0.2
+ escape-html: 1.0.3
+ on-finished: 2.3.0
+ parseurl: 1.3.3
+ statuses: 1.5.0
+ unpipe: 1.0.0
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
+ /find-cache-dir/0.1.1:
+ dependencies:
+ commondir: 1.0.1
+ mkdirp: 0.5.5
+ pkg-dir: 1.0.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-yN765XyKUqinhPnjHFfHQumToLk=
+ /find-cache-dir/2.1.0:
+ dependencies:
+ commondir: 1.0.1
+ make-dir: 2.1.0
+ pkg-dir: 3.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==
+ /find-cache-dir/3.3.1:
+ dependencies:
+ commondir: 1.0.1
+ make-dir: 3.0.2
+ pkg-dir: 4.2.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==
+ /find-process/1.4.3:
+ dependencies:
+ chalk: 2.4.2
+ commander: 2.20.3
+ debug: 2.6.9
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-+IA+AUsQCf3uucawyTwMWcY+2M3FXq3BRvw3S+j5Jvydjk31f/+NPWpYZOJs+JUs2GvxH4Yfr6Wham0ZtRLlPA==
+ /find-requires/1.0.0:
+ dependencies:
+ es5-ext: 0.10.53
+ esniff: 1.1.0
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-UME7hNwBfzeISSFQcBEDemEEskpOjI/shPrpJM5PI4DSdn6hX0dmz+2dL70blZER2z8tSnTRL+2rfzlYgtbBoQ==
+ /find-root/1.1.0:
+ dev: false
+ resolution:
+ integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
+ /find-up/1.1.2:
+ dependencies:
+ path-exists: 2.1.0
+ pinkie-promise: 2.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=
+ /find-up/2.1.0:
+ dependencies:
+ locate-path: 2.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
+ /find-up/3.0.0:
+ dependencies:
+ locate-path: 3.0.0
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+ /find-up/4.1.0:
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ /find/0.3.0:
+ dependencies:
+ traverse-chain: 0.1.0
+ dev: true
+ resolution:
+ integrity: sha512-iSd+O4OEYV/I36Zl8MdYJO0xD82wH528SaCieTVHhclgiYNe9y+yPKSwK+A7/WsmHL1EZ+pYUJBXWTL5qofksw==
+ /findit2/2.2.3:
+ dev: true
+ engines:
+ node: '>=0.8.22'
+ resolution:
+ integrity: sha1-WKRmaX34piBc39vzlVNri9d3pfY=
+ /findup-sync/3.0.0:
+ dependencies:
+ detect-file: 1.0.0
+ is-glob: 4.0.1
+ micromatch: 3.1.10
+ resolve-dir: 1.0.1
+ dev: true
+ engines:
+ node: '>= 0.10'
+ resolution:
+ integrity: sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==
+ /flat-cache/2.0.1:
+ dependencies:
+ flatted: 2.0.2
+ rimraf: 2.6.3
+ write: 1.0.3
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
+ /flat/5.0.0:
+ dependencies:
+ is-buffer: 2.0.4
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-6KSMM+cHHzXC/hpldXApL2S8Uz+QZv+tq5o/L0KQYleoG+GcwrnIJhTWC7tCOiKQp8D/fIvryINU1OZCCwevjA==
+ /flatted/2.0.2:
+ dev: true
+ resolution:
+ integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
+ /flatten/1.0.3:
+ dev: true
+ resolution:
+ integrity: sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==
+ /flush-write-stream/1.1.1:
+ dependencies:
+ inherits: 2.0.4
+ readable-stream: 2.3.7
+ dev: true
+ resolution:
+ integrity: sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==
+ /follow-redirects/1.11.0:
+ dependencies:
+ debug: 3.2.6
+ dev: true
+ engines:
+ node: '>=4.0'
+ resolution:
+ integrity: sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==
+ /follow-redirects/1.5.10:
+ dependencies:
+ debug: 3.1.0
+ dev: true
+ engines:
+ node: '>=4.0'
+ resolution:
+ integrity: sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
+ /for-in/0.1.8:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=
+ /for-in/1.0.2:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+ /for-own/0.1.5:
+ dependencies:
+ for-in: 1.0.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=
+ /forever-agent/0.6.1:
+ resolution:
+ integrity: sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+ /fork-ts-checker-webpack-plugin/3.1.1:
+ dependencies:
+ babel-code-frame: 6.26.0
+ chalk: 2.4.2
+ chokidar: 3.3.1
+ micromatch: 3.1.10
+ minimatch: 3.0.4
+ semver: 5.7.1
+ tapable: 1.1.3
+ worker-rpc: 0.1.1
+ dev: true
+ engines:
+ node: '>=6.11.5'
+ yarn: '>=1.0.0'
+ resolution:
+ integrity: sha512-DuVkPNrM12jR41KM2e+N+styka0EgLkTnXmNcXdgOM37vtGeY+oCBK/Jx0hzSeEU6memFCtWb4htrHPMDfwwUQ==
+ /form-data/2.3.3:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ mime-types: 2.1.26
+ engines:
+ node: '>= 0.12'
+ resolution:
+ integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+ /form-data/2.5.1:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ mime-types: 2.1.26
+ dev: true
+ engines:
+ node: '>= 0.12'
+ resolution:
+ integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
+ /format/0.2.2:
+ dev: false
+ engines:
+ node: '>=0.4.x'
+ resolution:
+ integrity: sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=
+ /formidable/1.2.2:
+ dev: true
+ resolution:
+ integrity: sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==
+ /forwarded/0.1.2:
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
+ /fragment-cache/0.2.1:
+ dependencies:
+ map-cache: 0.2.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+ /fresh/0.5.2:
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+ /from2/2.3.0:
+ dependencies:
+ inherits: 2.0.4
+ readable-stream: 2.3.7
+ dev: true
+ resolution:
+ integrity: sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=
+ /fs-constants/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+ /fs-extra/0.30.0:
+ dependencies:
+ graceful-fs: 4.2.3
+ jsonfile: 2.4.0
+ klaw: 1.3.1
+ path-is-absolute: 1.0.1
+ rimraf: 2.7.1
+ dev: true
+ resolution:
+ integrity: sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=
+ /fs-extra/4.0.3:
+ dependencies:
+ graceful-fs: 4.2.3
+ jsonfile: 4.0.0
+ universalify: 0.1.2
+ dev: true
+ resolution:
+ integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==
+ /fs-extra/7.0.1:
+ dependencies:
+ graceful-fs: 4.2.3
+ jsonfile: 4.0.0
+ universalify: 0.1.2
+ dev: true
+ engines:
+ node: '>=6 <7 || >=8'
+ resolution:
+ integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
+ /fs-extra/8.1.0:
+ dependencies:
+ graceful-fs: 4.2.3
+ jsonfile: 4.0.0
+ universalify: 0.1.2
+ engines:
+ node: '>=6 <7 || >=8'
+ resolution:
+ integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+ /fs-minipass/2.1.0:
+ dependencies:
+ minipass: 3.1.1
+ dev: true
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
+ /fs-readdir-recursive/1.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==
+ /fs-write-stream-atomic/1.0.10:
+ dependencies:
+ graceful-fs: 4.2.3
+ iferr: 0.1.5
+ imurmurhash: 0.1.4
+ readable-stream: 2.3.7
+ dev: true
+ resolution:
+ integrity: sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=
+ /fs.realpath/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+ /fs2/0.3.7:
+ dependencies:
+ d: 1.0.1
+ deferred: 0.7.11
+ es5-ext: 0.10.53
+ event-emitter: 0.3.5
+ ignore: 5.1.4
+ memoizee: 0.4.14
+ type: 1.2.0
+ dev: true
+ engines:
+ node: '>=0.8'
+ resolution:
+ integrity: sha512-fwfd9MBI/fnXtR/ClVTyeuPXJ+oI5WNyXvBQPmc4btgqLYTKOuBRTRUVjmVpDUri0C88HLwMlc5ESg48fEAGjw==
+ /fsevents/1.2.12:
+ bundledDependencies:
+ - node-pre-gyp
+ dependencies:
+ bindings: 1.5.0
+ nan: 2.14.0
+ dev: true
+ engines:
+ node: '>= 4.0'
+ optional: true
+ os:
+ - darwin
+ requiresBuild: true
+ resolution:
+ integrity: sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==
+ /fsevents/2.1.2:
+ dev: true
+ engines:
+ node: ^8.16.0 || ^10.6.0 || >=11.0.0
+ optional: true
+ os:
+ - darwin
+ resolution:
+ integrity: sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==
+ /function-bind/1.1.1:
+ resolution:
+ integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+ /functional-red-black-tree/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+ /generate-password/1.5.1:
+ dev: false
+ resolution:
+ integrity: sha512-XdsyfiF4mKoOEuzA44w9jSNav50zOurdWOV3V8DbA7SJIxR3Xm9ob14HKYTnMQOPX3ylqiJMnQF0wEa8gXZIMw==
+ /gensync/1.0.0-beta.1:
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
+ /get-amd-module-type/3.0.0:
+ dependencies:
+ ast-module-types: 2.6.0
+ node-source-walk: 4.2.0
+ dev: true
+ engines:
+ node: '>=6.0'
+ resolution:
+ integrity: sha512-99Q7COuACPfVt18zH9N4VAMyb81S6TUgJm2NgV6ERtkh9VIkAaByZkW530wl3lLN5KTtSrK9jVLxYsoP5hQKsw==
+ /get-caller-file/1.0.3:
+ dev: true
+ resolution:
+ integrity: sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
+ /get-caller-file/2.0.5:
+ engines:
+ node: 6.* || 8.* || >= 10.*
+ resolution:
+ integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+ /get-own-enumerable-property-symbols/3.0.2:
+ dev: true
+ resolution:
+ integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==
+ /get-proxy/2.1.0:
+ dependencies:
+ npm-conf: 1.1.3
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==
+ /get-stdin/5.0.1:
+ dev: true
+ engines:
+ node: '>=0.12.0'
+ resolution:
+ integrity: sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=
+ /get-stdin/6.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==
+ /get-stdin/7.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==
+ /get-stream/2.3.1:
+ dependencies:
+ object-assign: 4.1.1
+ pinkie-promise: 2.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=
+ /get-stream/3.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
+ /get-stream/4.1.0:
+ dependencies:
+ pump: 3.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+ /get-stream/5.1.0:
+ dependencies:
+ pump: 3.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==
+ /get-value/2.0.6:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+ /getpass/0.1.7:
+ dependencies:
+ assert-plus: 1.0.0
+ resolution:
+ integrity: sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+ /glob-parent/3.1.0:
+ dependencies:
+ is-glob: 3.1.0
+ path-dirname: 1.0.2
+ dev: true
+ resolution:
+ integrity: sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
+ /glob-parent/5.1.1:
+ dependencies:
+ is-glob: 4.0.1
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+ /glob-to-regexp/0.3.0:
+ dev: true
+ resolution:
+ integrity: sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
+ /glob/7.1.6:
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.0.4
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+ dev: true
+ resolution:
+ integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+ /global-dirs/0.1.1:
+ dependencies:
+ ini: 1.3.5
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=
+ /global-modules/1.0.0:
+ dependencies:
+ global-prefix: 1.0.2
+ is-windows: 1.0.2
+ resolve-dir: 1.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==
+ /global-modules/2.0.0:
+ dependencies:
+ global-prefix: 3.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==
+ /global-prefix/1.0.2:
+ dependencies:
+ expand-tilde: 2.0.2
+ homedir-polyfill: 1.0.3
+ ini: 1.3.5
+ is-windows: 1.0.2
+ which: 1.3.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=
+ /global-prefix/3.0.0:
+ dependencies:
+ ini: 1.3.5
+ kind-of: 6.0.3
+ which: 1.3.1
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==
+ /globals/11.12.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+ /globals/12.4.0:
+ dependencies:
+ type-fest: 0.8.1
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==
+ /globby/10.0.2:
+ dependencies:
+ '@types/glob': 7.1.1
+ array-union: 2.1.0
+ dir-glob: 3.0.1
+ fast-glob: 3.2.2
+ glob: 7.1.6
+ ignore: 5.1.4
+ merge2: 1.3.0
+ slash: 3.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==
+ /globby/6.1.0:
+ dependencies:
+ array-union: 1.0.2
+ glob: 7.1.6
+ object-assign: 4.1.1
+ pify: 2.3.0
+ pinkie-promise: 2.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=
+ /globby/7.1.1:
+ dependencies:
+ array-union: 1.0.2
+ dir-glob: 2.2.2
+ glob: 7.1.6
+ ignore: 3.3.10
+ pify: 3.0.0
+ slash: 1.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-+yzP+UAfhgCUXfral0QMypcrhoA=
+ /globby/8.0.2:
+ dependencies:
+ array-union: 1.0.2
+ dir-glob: 2.0.0
+ fast-glob: 2.2.7
+ glob: 7.1.6
+ ignore: 3.3.10
+ pify: 3.0.0
+ slash: 1.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==
+ /gonzales-pe/4.3.0:
+ dependencies:
+ minimist: 1.2.5
+ dev: true
+ engines:
+ node: '>=0.6.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==
+ /good-listener/1.2.2:
+ dependencies:
+ delegate: 3.2.0
+ dev: false
+ optional: true
+ resolution:
+ integrity: sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=
+ /got/6.7.1:
+ dependencies:
+ create-error-class: 3.0.2
+ duplexer3: 0.1.4
+ get-stream: 3.0.0
+ is-redirect: 1.0.0
+ is-retry-allowed: 1.2.0
+ is-stream: 1.1.0
+ lowercase-keys: 1.0.1
+ safe-buffer: 5.2.0
+ timed-out: 4.0.1
+ unzip-response: 2.0.1
+ url-parse-lax: 1.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=
+ /got/8.3.2:
+ dependencies:
+ '@sindresorhus/is': 0.7.0
+ cacheable-request: 2.1.4
+ decompress-response: 3.3.0
+ duplexer3: 0.1.4
+ get-stream: 3.0.0
+ into-stream: 3.1.0
+ is-retry-allowed: 1.2.0
+ isurl: 1.0.0
+ lowercase-keys: 1.0.1
+ mimic-response: 1.0.1
+ p-cancelable: 0.4.1
+ p-timeout: 2.0.1
+ pify: 3.0.0
+ safe-buffer: 5.2.0
+ timed-out: 4.0.1
+ url-parse-lax: 3.0.0
+ url-to-options: 1.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==
+ /got/9.6.0:
+ dependencies:
+ '@sindresorhus/is': 0.14.0
+ '@szmarczak/http-timer': 1.1.2
+ cacheable-request: 6.1.0
+ decompress-response: 3.3.0
+ duplexer3: 0.1.4
+ get-stream: 4.1.0
+ lowercase-keys: 1.0.1
+ mimic-response: 1.0.1
+ p-cancelable: 1.1.0
+ to-readable-stream: 1.0.0
+ url-parse-lax: 3.0.0
+ dev: true
+ engines:
+ node: '>=8.6'
+ resolution:
+ integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==
+ /graceful-fs/4.1.15:
+ dev: true
+ resolution:
+ integrity: sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
+ /graceful-fs/4.2.3:
+ resolution:
+ integrity: sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
+ /graceful-readlink/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
+ /graphlib/2.1.8:
+ dependencies:
+ lodash: 4.17.15
+ dev: true
+ resolution:
+ integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
+ /growly/1.3.0:
+ dev: true
+ resolution:
+ integrity: sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
+ /gud/1.0.0:
+ dev: false
+ resolution:
+ integrity: sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
+ /gzip-size/5.1.1:
+ dependencies:
+ duplexer: 0.1.1
+ pify: 4.0.1
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==
+ /handle-thing/2.0.1:
+ dev: true
+ resolution:
+ integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==
+ /hapi-plugin-websocket/2.3.0_@hapi+hapi@18.4.1:
+ dependencies:
+ '@hapi/boom': 9.0.0
+ '@hapi/hapi': 18.4.1
+ '@hapi/hoek': 9.0.3
+ urijs: 1.19.2
+ websocket-framed: 1.2.2
+ ws: 7.2.1
+ dev: true
+ engines:
+ node: '>=8.0.0'
+ peerDependencies:
+ '@hapi/hapi': '>=18.0.0'
+ resolution:
+ integrity: sha512-bm+K5opYruM7/u9q+2YnP48PaMZqYo4NiQMtPfg6eHfXgkFjmQ6WUSr5NQkTzl3Gn/aMiYgEM3AX3pR30TZhwA==
+ /har-schema/2.0.0:
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+ /har-validator/5.1.3:
+ dependencies:
+ ajv: 6.12.0
+ har-schema: 2.0.0
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+ /harmony-reflect/1.6.1:
+ dev: true
+ resolution:
+ integrity: sha512-WJTeyp0JzGtHcuMsi7rw2VwtkvLa+JyfEKJCFyfcS0+CDkjQ5lHPu7zEhFZP+PDSRrEgXa5Ah0l1MbgbE41XjA==
+ /has-ansi/2.0.0:
+ dependencies:
+ ansi-regex: 2.1.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=
+ /has-binary2/1.0.3:
+ dependencies:
+ isarray: 2.0.1
+ dev: true
+ resolution:
+ integrity: sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==
+ /has-cors/1.1.0:
+ dev: true
+ resolution:
+ integrity: sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=
+ /has-flag/3.0.0:
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+ /has-flag/4.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+ /has-symbol-support-x/1.4.2:
+ dev: true
+ resolution:
+ integrity: sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==
+ /has-symbols/1.0.1:
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+ /has-to-string-tag-x/1.4.1:
+ dependencies:
+ has-symbol-support-x: 1.4.2
+ dev: true
+ resolution:
+ integrity: sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==
+ /has-value/0.3.1:
+ dependencies:
+ get-value: 2.0.6
+ has-values: 0.1.4
+ isobject: 2.1.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+ /has-value/1.0.0:
+ dependencies:
+ get-value: 2.0.6
+ has-values: 1.0.0
+ isobject: 3.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+ /has-values/0.1.4:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+ /has-values/1.0.0:
+ dependencies:
+ is-number: 3.0.0
+ kind-of: 4.0.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+ /has-yarn/2.1.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==
+ /has/1.0.3:
+ dependencies:
+ function-bind: 1.1.1
+ engines:
+ node: '>= 0.4.0'
+ resolution:
+ integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ /hash-base/3.0.4:
+ dependencies:
+ inherits: 2.0.4
+ safe-buffer: 5.2.0
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=
+ /hash.js/1.1.7:
+ dependencies:
+ inherits: 2.0.4
+ minimalistic-assert: 1.0.1
+ resolution:
+ integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
+ /hast-util-parse-selector/2.2.4:
+ dev: false
+ resolution:
+ integrity: sha512-gW3sxfynIvZApL4L07wryYF4+C9VvH3AUi7LAnVXV4MneGEgwOByXvFo18BgmTWnm7oHAe874jKbIB1YhHSIzA==
+ /hastscript/5.1.2:
+ dependencies:
+ comma-separated-tokens: 1.0.8
+ hast-util-parse-selector: 2.2.4
+ property-information: 5.4.0
+ space-separated-tokens: 1.1.5
+ dev: false
+ resolution:
+ integrity: sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ==
+ /he/1.2.0:
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+ /hex-color-regex/1.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
+ /highlight.js/9.13.1:
+ dev: false
+ resolution:
+ integrity: sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==
+ /history/4.10.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ loose-envify: 1.4.0
+ resolve-pathname: 3.0.0
+ tiny-invariant: 1.1.0
+ tiny-warning: 1.0.3
+ value-equal: 1.0.1
+ dev: false
+ resolution:
+ integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
+ /hmac-drbg/1.0.1:
+ dependencies:
+ hash.js: 1.1.7
+ minimalistic-assert: 1.0.1
+ minimalistic-crypto-utils: 1.0.1
+ resolution:
+ integrity: sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
+ /hoist-non-react-statics/3.3.2:
+ dependencies:
+ react-is: 16.13.1
+ dev: false
+ resolution:
+ integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+ /homedir-polyfill/1.0.3:
+ dependencies:
+ parse-passwd: 1.0.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
+ /hosted-git-info/2.8.8:
+ dev: true
+ resolution:
+ integrity: sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
+ /hpack.js/2.1.6:
+ dependencies:
+ inherits: 2.0.4
+ obuf: 1.1.2
+ readable-stream: 2.3.7
+ wbuf: 1.7.3
+ dev: true
+ resolution:
+ integrity: sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=
+ /hsl-regex/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=
+ /hsla-regex/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
+ /html-comment-regex/1.1.2:
+ dev: true
+ resolution:
+ integrity: sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
+ /html-encoding-sniffer/1.0.2:
+ dependencies:
+ whatwg-encoding: 1.0.5
+ dev: true
+ resolution:
+ integrity: sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==
+ /html-entities/1.2.1:
+ dev: true
+ engines:
+ '0': node >= 0.4.0
+ resolution:
+ integrity: sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=
+ /html-escaper/2.0.2:
+ dev: true
+ resolution:
+ integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
+ /html-minifier-terser/5.0.5:
+ dependencies:
+ camel-case: 4.1.1
+ clean-css: 4.2.3
+ commander: 4.1.1
+ he: 1.2.0
+ param-case: 3.0.3
+ relateurl: 0.2.7
+ terser: 4.6.11
+ dev: true
+ engines:
+ node: '>=6'
+ hasBin: true
+ resolution:
+ integrity: sha512-cBSFFghQh/uHcfSiL42KxxIRMF7A144+3E44xdlctIjxEmkEfCvouxNyFH2wysXk1fCGBPwtcr3hDWlGTfkDew==
+ /html-webpack-plugin/4.0.0-beta.11_webpack@4.42.0:
+ dependencies:
+ html-minifier-terser: 5.0.5
+ loader-utils: 1.4.0
+ lodash: 4.17.15
+ pretty-error: 2.1.1
+ tapable: 1.1.3
+ util.promisify: 1.0.0
+ webpack: 4.42.0_webpack@4.42.0
+ dev: true
+ engines:
+ node: '>=6.9'
+ peerDependencies:
+ webpack: ^4.0.0
+ resolution:
+ integrity: sha512-4Xzepf0qWxf8CGg7/WQM5qBB2Lc/NFI7MhU59eUDTkuQp3skZczH4UA1d6oQyDEIoMDgERVhRyTdtUPZ5s5HBg==
+ /htmlparser2/3.10.1:
+ dependencies:
+ domelementtype: 1.3.1
+ domhandler: 2.4.2
+ domutils: 1.7.0
+ entities: 1.1.2
+ inherits: 2.0.4
+ readable-stream: 3.6.0
+ dev: true
+ resolution:
+ integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+ /http-cache-semantics/3.8.1:
+ dev: true
+ resolution:
+ integrity: sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==
+ /http-cache-semantics/4.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
+ /http-deceiver/1.2.7:
+ dev: true
+ resolution:
+ integrity: sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=
+ /http-errors/1.6.3:
+ dependencies:
+ depd: 1.1.2
+ inherits: 2.0.3
+ setprototypeof: 1.1.0
+ statuses: 1.5.0
+ dev: true
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=
+ /http-errors/1.7.2:
+ dependencies:
+ depd: 1.1.2
+ inherits: 2.0.3
+ setprototypeof: 1.1.1
+ statuses: 1.5.0
+ toidentifier: 1.0.0
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
+ /http-errors/1.7.3:
+ dependencies:
+ depd: 1.1.2
+ inherits: 2.0.4
+ setprototypeof: 1.1.1
+ statuses: 1.5.0
+ toidentifier: 1.0.0
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+ /http-parser-js/0.4.10:
+ dev: true
+ resolution:
+ integrity: sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=
+ /http-proxy-middleware/0.19.1:
+ dependencies:
+ http-proxy: 1.18.0
+ is-glob: 4.0.1
+ lodash: 4.17.15
+ micromatch: 3.1.10
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ resolution:
+ integrity: sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==
+ /http-proxy/1.18.0:
+ dependencies:
+ eventemitter3: 4.0.0
+ follow-redirects: 1.11.0
+ requires-port: 1.0.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==
+ /http-signature/1.2.0:
+ dependencies:
+ assert-plus: 1.0.0
+ jsprim: 1.4.1
+ sshpk: 1.16.1
+ engines:
+ node: '>=0.8'
+ npm: '>=1.3.7'
+ resolution:
+ integrity: sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+ /https-browserify/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
+ /https-proxy-agent/4.0.0:
+ dependencies:
+ agent-base: 5.1.1
+ debug: 4.1.1
+ dev: true
+ engines:
+ node: '>= 6.0.0'
+ resolution:
+ integrity: sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==
+ /https-proxy-agent/5.0.0:
+ dependencies:
+ agent-base: 6.0.0
+ debug: 4.1.1
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
+ /human-signals/1.1.1:
+ dev: true
+ engines:
+ node: '>=8.12.0'
+ resolution:
+ integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
+ /husky/3.1.0:
+ dependencies:
+ chalk: 2.4.2
+ ci-info: 2.0.0
+ cosmiconfig: 5.2.1
+ execa: 1.0.0
+ get-stdin: 7.0.0
+ opencollective-postinstall: 2.0.2
+ pkg-dir: 4.2.0
+ please-upgrade-node: 3.2.0
+ read-pkg: 5.2.0
+ run-node: 1.0.0
+ slash: 3.0.0
+ dev: true
+ engines:
+ node: '>=8.6.0'
+ hasBin: true
+ requiresBuild: true
+ resolution:
+ integrity: sha512-FJkPoHHB+6s4a+jwPqBudBDvYZsoQW5/HBuMSehC8qDiCe50kpcxeqFoDSlow+9I6wg47YxBoT3WxaURlrDIIQ==
+ /iconv-lite/0.4.24:
+ dependencies:
+ safer-buffer: 2.1.2
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ /icss-utils/4.1.1:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
+ /identity-obj-proxy/3.0.0:
+ dependencies:
+ harmony-reflect: 1.6.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=
+ /ieee754/1.1.13:
+ resolution:
+ integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
+ /iferr/0.1.5:
+ dev: true
+ resolution:
+ integrity: sha1-xg7taebY/bazEEofy8ocGS3FtQE=
+ /ignore/3.3.10:
+ dev: true
+ resolution:
+ integrity: sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==
+ /ignore/4.0.6:
+ dev: true
+ engines:
+ node: '>= 4'
+ resolution:
+ integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+ /ignore/5.1.4:
+ dev: true
+ engines:
+ node: '>= 4'
+ resolution:
+ integrity: sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
+ /immediate/3.0.6:
+ dev: true
+ resolution:
+ integrity: sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
+ /immer/1.10.0:
+ dev: true
+ resolution:
+ integrity: sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
+ /import-cwd/2.1.0:
+ dependencies:
+ import-from: 2.1.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=
+ /import-fresh/2.0.0:
+ dependencies:
+ caller-path: 2.0.0
+ resolve-from: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-2BNVwVYS04bGH53dOSLUMEgipUY=
+ /import-fresh/3.2.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==
+ /import-from/2.1.0:
+ dependencies:
+ resolve-from: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-M1238qev/VOqpHHUuAId7ja387E=
+ /import-lazy/2.1.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=
+ /import-local/2.0.0:
+ dependencies:
+ pkg-dir: 3.0.0
+ resolve-cwd: 2.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ hasBin: true
+ resolution:
+ integrity: sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
+ /import-local/3.0.2:
+ dependencies:
+ pkg-dir: 4.2.0
+ resolve-cwd: 3.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ hasBin: true
+ resolution:
+ integrity: sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==
+ /imurmurhash/0.1.4:
+ dev: true
+ engines:
+ node: '>=0.8.19'
+ resolution:
+ integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=
+ /indent-string/4.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
+ /indexes-of/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
+ /indexof/0.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=
+ /infer-owner/1.0.4:
+ dev: true
+ resolution:
+ integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
+ /inflight/1.0.6:
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+ dev: true
+ resolution:
+ integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+ /inherits/2.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
+ /inherits/2.0.3:
+ resolution:
+ integrity: sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+ /inherits/2.0.4:
+ resolution:
+ integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+ /ini/1.3.5:
+ dev: true
+ resolution:
+ integrity: sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+ /inquirer/6.5.2:
+ dependencies:
+ ansi-escapes: 3.2.0
+ chalk: 2.4.2
+ cli-cursor: 2.1.0
+ cli-width: 2.2.0
+ external-editor: 3.1.0
+ figures: 2.0.0
+ lodash: 4.17.15
+ mute-stream: 0.0.7
+ run-async: 2.4.0
+ rxjs: 6.5.5
+ string-width: 2.1.1
+ strip-ansi: 5.2.0
+ through: 2.3.8
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==
+ /inquirer/7.0.4:
+ dependencies:
+ ansi-escapes: 4.3.1
+ chalk: 2.4.2
+ cli-cursor: 3.1.0
+ cli-width: 2.2.0
+ external-editor: 3.1.0
+ figures: 3.2.0
+ lodash: 4.17.15
+ mute-stream: 0.0.8
+ run-async: 2.4.0
+ rxjs: 6.5.5
+ string-width: 4.2.0
+ strip-ansi: 5.2.0
+ through: 2.3.8
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==
+ /inquirer/7.1.0:
+ dependencies:
+ ansi-escapes: 4.3.1
+ chalk: 3.0.0
+ cli-cursor: 3.1.0
+ cli-width: 2.2.0
+ external-editor: 3.1.0
+ figures: 3.2.0
+ lodash: 4.17.15
+ mute-stream: 0.0.8
+ run-async: 2.4.0
+ rxjs: 6.5.5
+ string-width: 4.2.0
+ strip-ansi: 6.0.0
+ through: 2.3.8
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==
+ /int64-buffer/0.1.10:
+ dev: true
+ resolution:
+ integrity: sha1-J3siiofZWtd30HwTgyAiQGpHNCM=
+ /internal-ip/4.3.0:
+ dependencies:
+ default-gateway: 4.2.0
+ ipaddr.js: 1.9.1
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==
+ /internal-slot/1.0.2:
+ dependencies:
+ es-abstract: 1.17.5
+ has: 1.0.3
+ side-channel: 1.0.2
+ dev: true
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==
+ /interpret/1.2.0:
+ dev: true
+ engines:
+ node: '>= 0.10'
+ resolution:
+ integrity: sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
+ /into-stream/3.1.0:
+ dependencies:
+ from2: 2.3.0
+ p-is-promise: 1.1.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=
+ /invariant/2.2.4:
+ dependencies:
+ loose-envify: 1.4.0
+ dev: true
+ resolution:
+ integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+ /invert-kv/2.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
+ /ip-regex/2.1.0:
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
+ /ip/1.1.5:
+ dev: true
+ resolution:
+ integrity: sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
+ /ipaddr.js/1.9.1:
+ engines:
+ node: '>= 0.10'
+ resolution:
+ integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+ /is-absolute-url/2.1.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=
+ /is-absolute-url/3.0.3:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==
+ /is-accessor-descriptor/0.1.6:
+ dependencies:
+ kind-of: 3.2.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+ /is-accessor-descriptor/1.0.0:
+ dependencies:
+ kind-of: 6.0.3
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+ /is-alphabetical/1.0.4:
+ dev: false
+ resolution:
+ integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==
+ /is-alphanumerical/1.0.4:
+ dependencies:
+ is-alphabetical: 1.0.4
+ is-decimal: 1.0.4
+ dev: false
+ resolution:
+ integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==
+ /is-arguments/1.0.4:
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
+ /is-arrayish/0.2.1:
+ resolution:
+ integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+ /is-arrayish/0.3.2:
+ dev: true
+ resolution:
+ integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+ /is-binary-path/1.0.1:
+ dependencies:
+ binary-extensions: 1.13.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=
+ /is-binary-path/2.1.0:
+ dependencies:
+ binary-extensions: 2.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ /is-buffer/1.1.6:
+ resolution:
+ integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+ /is-buffer/2.0.4:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==
+ /is-builtin-module/1.0.0:
+ dependencies:
+ builtin-modules: 1.1.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-VAVy0096wxGfj3bDDLwbHgN6/74=
+ /is-callable/1.1.5:
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
+ /is-ci/1.2.1:
+ dependencies:
+ ci-info: 1.6.0
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==
+ /is-ci/2.0.0:
+ dependencies:
+ ci-info: 2.0.0
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
+ /is-cidr/3.1.0:
+ dependencies:
+ cidr-regex: 2.0.10
+ dev: false
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-3kxTForpuj8O4iHn0ocsn1jxRm5VYm60GDghK6HXmpn4IyZOoRy9/GmdjFA2yEMqw91TB1/K3bFTuI7FlFNR1g==
+ /is-color-stop/1.1.0:
+ dependencies:
+ css-color-names: 0.0.4
+ hex-color-regex: 1.1.0
+ hsl-regex: 1.0.0
+ hsla-regex: 1.0.0
+ rgb-regex: 1.0.1
+ rgba-regex: 1.0.0
+ dev: true
+ resolution:
+ integrity: sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=
+ /is-data-descriptor/0.1.4:
+ dependencies:
+ kind-of: 3.2.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+ /is-data-descriptor/1.0.0:
+ dependencies:
+ kind-of: 6.0.3
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+ /is-date-object/1.0.2:
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
+ /is-decimal/1.0.4:
+ dev: false
+ resolution:
+ integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==
+ /is-descriptor/0.1.6:
+ dependencies:
+ is-accessor-descriptor: 0.1.6
+ is-data-descriptor: 0.1.4
+ kind-of: 5.1.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ /is-descriptor/1.0.2:
+ dependencies:
+ is-accessor-descriptor: 1.0.0
+ is-data-descriptor: 1.0.0
+ kind-of: 6.0.3
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ /is-directory/0.3.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
+ /is-docker/1.1.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-8EN01O7lMQ6ajhE78UlUEeRhdqE=
+ /is-docker/2.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==
+ /is-extendable/0.1.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+ /is-extendable/1.0.1:
+ dependencies:
+ is-plain-object: 2.0.4
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+ /is-extglob/2.1.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+ /is-fullwidth-code-point/1.0.0:
+ dependencies:
+ number-is-nan: 1.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
+ /is-fullwidth-code-point/2.0.0:
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+ /is-fullwidth-code-point/3.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+ /is-generator-fn/2.1.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
+ /is-glob/3.1.0:
+ dependencies:
+ is-extglob: 2.1.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
+ /is-glob/4.0.1:
+ dependencies:
+ is-extglob: 2.1.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+ /is-hexadecimal/1.0.4:
+ dev: false
+ resolution:
+ integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
+ /is-installed-globally/0.1.0:
+ dependencies:
+ global-dirs: 0.1.1
+ is-path-inside: 1.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=
+ /is-natural-number/4.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=
+ /is-npm/1.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-8vtjpl5JBbQGyGBydloaTceTufQ=
+ /is-npm/3.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-wsigDr1Kkschp2opC4G3yA6r9EgVA6NjRpWzIi9axXqeIaAATPRJc4uLujXe3Nd9uO8KoDyA4MD6aZSeXTADhA==
+ /is-number/3.0.0:
+ dependencies:
+ kind-of: 3.2.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+ /is-number/7.0.0:
+ dev: true
+ engines:
+ node: '>=0.12.0'
+ resolution:
+ integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+ /is-obj/1.0.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
+ /is-obj/2.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
+ /is-object/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-iVJojF7C/9awPsyF52ngKQMINHA=
+ /is-path-cwd/2.2.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==
+ /is-path-in-cwd/2.1.0:
+ dependencies:
+ is-path-inside: 2.1.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==
+ /is-path-inside/1.0.1:
+ dependencies:
+ path-is-inside: 1.0.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-jvW33lBDej/cprToZe96pVy0gDY=
+ /is-path-inside/2.1.0:
+ dependencies:
+ path-is-inside: 1.0.2
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==
+ /is-plain-obj/1.1.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
+ /is-plain-object/2.0.4:
+ dependencies:
+ isobject: 3.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ /is-promise/2.1.0:
+ dev: true
+ resolution:
+ integrity: sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
+ /is-redirect/1.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=
+ /is-regex/1.0.5:
+ dependencies:
+ has: 1.0.3
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
+ /is-regexp/1.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-/S2INUXEa6xaYz57mgnof6LLUGk=
+ /is-relative-path/1.0.2:
+ dev: true
+ resolution:
+ integrity: sha1-CRtGoNZ8HtD+hfH4z93gBrslHUY=
+ /is-resolvable/1.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
+ /is-retina/1.0.3:
+ dev: false
+ resolution:
+ integrity: sha1-10AbKGvqKuN/Ykd1iN5QTQuGR+M=
+ /is-retry-allowed/1.2.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==
+ /is-root/2.1.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==
+ /is-stream/1.1.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+ /is-stream/2.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
+ /is-string/1.0.5:
+ dev: true
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
+ /is-svg/3.0.0:
+ dependencies:
+ html-comment-regex: 1.1.2
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==
+ /is-symbol/1.0.3:
+ dependencies:
+ has-symbols: 1.0.1
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
+ /is-typedarray/1.0.0:
+ resolution:
+ integrity: sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+ /is-url/1.2.4:
+ dev: true
+ resolution:
+ integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
+ /is-utf8/0.2.1:
+ dev: false
+ resolution:
+ integrity: sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
+ /is-windows/1.0.2:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+ /is-wsl/1.1.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+ /is-wsl/2.1.1:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==
+ /is-yarn-global/0.3.0:
+ dev: true
+ resolution:
+ integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
+ /isarray/0.0.1:
+ dev: false
+ resolution:
+ integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
+ /isarray/1.0.0:
+ resolution:
+ integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+ /isarray/2.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=
+ /isexe/2.0.0:
+ resolution:
+ integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+ /iso8601-duration/1.2.0:
+ dev: true
+ resolution:
+ integrity: sha512-ErTBd++b17E8nmWII1K1uZtBgD1E8RjyvwmxlCjPHNqHMD7gmcMHOw0E8Ro/6+QT4PhHRSnnMo7bxa1vFPkwhg==
+ /isobject/2.1.0:
+ dependencies:
+ isarray: 1.0.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+ /isobject/3.0.1:
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+ /isomorphic-fetch/2.2.1:
+ dependencies:
+ node-fetch: 1.7.3
+ whatwg-fetch: 3.0.0
+ dev: true
+ resolution:
+ integrity: sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
+ /isomorphic-ws/4.0.1_ws@7.2.3:
+ dependencies:
+ ws: 7.2.3
+ dev: true
+ peerDependencies:
+ ws: '*'
+ resolution:
+ integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
+ /isstream/0.1.2:
+ resolution:
+ integrity: sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+ /istanbul-lib-coverage/2.0.5:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==
+ /istanbul-lib-coverage/3.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==
+ /istanbul-lib-instrument/3.3.0:
+ dependencies:
+ '@babel/generator': 7.9.5
+ '@babel/parser': 7.9.4
+ '@babel/template': 7.8.6
+ '@babel/traverse': 7.9.5
+ '@babel/types': 7.9.5
+ istanbul-lib-coverage: 2.0.5
+ semver: 6.3.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==
+ /istanbul-lib-instrument/4.0.1:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@babel/parser': 7.9.4
+ '@babel/template': 7.8.6
+ '@babel/traverse': 7.9.5
+ '@istanbuljs/schema': 0.1.2
+ istanbul-lib-coverage: 3.0.0
+ semver: 6.3.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==
+ /istanbul-lib-report/2.0.8:
+ dependencies:
+ istanbul-lib-coverage: 2.0.5
+ make-dir: 2.1.0
+ supports-color: 6.1.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==
+ /istanbul-lib-report/3.0.0:
+ dependencies:
+ istanbul-lib-coverage: 3.0.0
+ make-dir: 3.0.2
+ supports-color: 7.1.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==
+ /istanbul-lib-source-maps/3.0.6:
+ dependencies:
+ debug: 4.1.1
+ istanbul-lib-coverage: 2.0.5
+ make-dir: 2.1.0
+ rimraf: 2.7.1
+ source-map: 0.6.1
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==
+ /istanbul-lib-source-maps/4.0.0:
+ dependencies:
+ debug: 4.1.1
+ istanbul-lib-coverage: 3.0.0
+ source-map: 0.6.1
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==
+ /istanbul-reports/2.2.7:
+ dependencies:
+ html-escaper: 2.0.2
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==
+ /istanbul-reports/3.0.2:
+ dependencies:
+ html-escaper: 2.0.2
+ istanbul-lib-report: 3.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==
+ /isurl/1.0.0:
+ dependencies:
+ has-to-string-tag-x: 1.4.1
+ is-object: 1.0.1
+ dev: true
+ engines:
+ node: '>= 4'
+ resolution:
+ integrity: sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==
+ /jest-changed-files/24.9.0:
+ dependencies:
+ '@jest/types': 24.9.0
+ execa: 1.0.0
+ throat: 4.1.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==
+ /jest-changed-files/25.3.0:
+ dependencies:
+ '@jest/types': 25.3.0
+ execa: 3.4.0
+ throat: 5.0.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-eqd5hyLbUjIVvLlJ3vQ/MoPxsxfESVXG9gvU19XXjKzxr+dXmZIqCXiY0OiYaibwlHZBJl2Vebkc0ADEMzCXew==
+ /jest-cli/24.9.0:
+ dependencies:
+ '@jest/core': 24.9.0
+ '@jest/test-result': 24.9.0
+ '@jest/types': 24.9.0
+ chalk: 2.4.2
+ exit: 0.1.2
+ import-local: 2.0.0
+ is-ci: 2.0.0
+ jest-config: 24.9.0
+ jest-util: 24.9.0
+ jest-validate: 24.9.0
+ prompts: 2.3.2
+ realpath-native: 1.1.0
+ yargs: 13.3.2
+ dev: true
+ engines:
+ node: '>= 6'
+ hasBin: true
+ resolution:
+ integrity: sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==
+ /jest-cli/25.3.0:
+ dependencies:
+ '@jest/core': 25.3.0
+ '@jest/test-result': 25.3.0
+ '@jest/types': 25.3.0
+ chalk: 3.0.0
+ exit: 0.1.2
+ import-local: 3.0.2
+ is-ci: 2.0.0
+ jest-config: 25.3.0
+ jest-util: 25.3.0
+ jest-validate: 25.3.0
+ prompts: 2.3.2
+ realpath-native: 2.0.0
+ yargs: 15.3.1
+ dev: true
+ engines:
+ node: '>= 8.3'
+ hasBin: true
+ resolution:
+ integrity: sha512-XpNQPlW1tzpP7RGG8dxpkRegYDuLjzSiENu92+CYM87nEbmEPb3b4+yo8xcsHOnj0AG7DUt9b3uG8LuHI3MDzw==
+ /jest-config/24.9.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@jest/test-sequencer': 24.9.0
+ '@jest/types': 24.9.0
+ babel-jest: 24.9.0_@babel+core@7.9.0
+ chalk: 2.4.2
+ glob: 7.1.6
+ jest-environment-jsdom: 24.9.0
+ jest-environment-node: 24.9.0
+ jest-get-type: 24.9.0
+ jest-jasmine2: 24.9.0
+ jest-regex-util: 24.9.0
+ jest-resolve: 24.9.0_jest-resolve@24.9.0
+ jest-util: 24.9.0
+ jest-validate: 24.9.0
+ micromatch: 3.1.10
+ pretty-format: 24.9.0
+ realpath-native: 1.1.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==
+ /jest-config/25.3.0:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@jest/test-sequencer': 25.3.0
+ '@jest/types': 25.3.0
+ babel-jest: 25.3.0_@babel+core@7.9.0
+ chalk: 3.0.0
+ deepmerge: 4.2.2
+ glob: 7.1.6
+ jest-environment-jsdom: 25.3.0
+ jest-environment-node: 25.3.0
+ jest-get-type: 25.2.6
+ jest-jasmine2: 25.3.0
+ jest-regex-util: 25.2.6
+ jest-resolve: 25.3.0_jest-resolve@25.3.0
+ jest-util: 25.3.0
+ jest-validate: 25.3.0
+ micromatch: 4.0.2
+ pretty-format: 25.3.0
+ realpath-native: 2.0.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-CmF1JnNWFmoCSPC4tnU52wnVBpuxHjilA40qH/03IHxIevkjUInSMwaDeE6ACfxMPTLidBGBCO3EbxvzPbo8wA==
+ /jest-diff/24.9.0:
+ dependencies:
+ chalk: 2.4.2
+ diff-sequences: 24.9.0
+ jest-get-type: 24.9.0
+ pretty-format: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==
+ /jest-diff/25.3.0:
+ dependencies:
+ chalk: 3.0.0
+ diff-sequences: 25.2.6
+ jest-get-type: 25.2.6
+ pretty-format: 25.3.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-vyvs6RPoVdiwARwY4kqFWd4PirPLm2dmmkNzKqo38uZOzJvLee87yzDjIZLmY1SjM3XR5DwsUH+cdQ12vgqi1w==
+ /jest-docblock/24.9.0:
+ dependencies:
+ detect-newline: 2.1.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==
+ /jest-docblock/25.3.0:
+ dependencies:
+ detect-newline: 3.1.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==
+ /jest-each/24.9.0:
+ dependencies:
+ '@jest/types': 24.9.0
+ chalk: 2.4.2
+ jest-get-type: 24.9.0
+ jest-util: 24.9.0
+ pretty-format: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==
+ /jest-each/25.3.0:
+ dependencies:
+ '@jest/types': 25.3.0
+ chalk: 3.0.0
+ jest-get-type: 25.2.6
+ jest-util: 25.3.0
+ pretty-format: 25.3.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-aBfS4VOf/Qs95yUlX6d6WBv0szvOcTkTTyCIaLuQGj4bSHsT+Wd9dDngVHrCe5uytxpN8VM+NAloI6nbPjXfXw==
+ /jest-environment-jsdom-fourteen/1.0.1:
+ dependencies:
+ '@jest/environment': 24.9.0
+ '@jest/fake-timers': 24.9.0
+ '@jest/types': 24.9.0
+ jest-mock: 24.9.0
+ jest-util: 24.9.0
+ jsdom: 14.1.0
+ dev: true
+ resolution:
+ integrity: sha512-DojMX1sY+at5Ep+O9yME34CdidZnO3/zfPh8UW+918C5fIZET5vCjfkegixmsi7AtdYfkr4bPlIzmWnlvQkP7Q==
+ /jest-environment-jsdom/24.9.0:
+ dependencies:
+ '@jest/environment': 24.9.0
+ '@jest/fake-timers': 24.9.0
+ '@jest/types': 24.9.0
+ jest-mock: 24.9.0
+ jest-util: 24.9.0
+ jsdom: 11.12.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==
+ /jest-environment-jsdom/25.3.0:
+ dependencies:
+ '@jest/environment': 25.3.0
+ '@jest/fake-timers': 25.3.0
+ '@jest/types': 25.3.0
+ jest-mock: 25.3.0
+ jest-util: 25.3.0
+ jsdom: 15.2.1
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-jdE4bQN+k2QEZ9sWOxsqDJvMzbdFSCN/4tw8X0TQaCqyzKz58PyEf41oIr4WO7ERdp7WaJGBSUKF7imR3UW1lg==
+ /jest-environment-node/24.9.0:
+ dependencies:
+ '@jest/environment': 24.9.0
+ '@jest/fake-timers': 24.9.0
+ '@jest/types': 24.9.0
+ jest-mock: 24.9.0
+ jest-util: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==
+ /jest-environment-node/25.3.0:
+ dependencies:
+ '@jest/environment': 25.3.0
+ '@jest/fake-timers': 25.3.0
+ '@jest/types': 25.3.0
+ jest-mock: 25.3.0
+ jest-util: 25.3.0
+ semver: 6.3.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-XO09S29Nx1NU7TiMPHMoDIkxoGBuKSTbE+sHp0gXbeLDXhIdhysUI25kOqFFSD9AuDgvPvxWCXrvNqiFsOH33g==
+ /jest-get-type/24.9.0:
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==
+ /jest-get-type/25.2.6:
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==
+ /jest-haste-map/24.9.0:
+ dependencies:
+ '@jest/types': 24.9.0
+ anymatch: 2.0.0
+ fb-watchman: 2.0.1
+ graceful-fs: 4.2.3
+ invariant: 2.2.4
+ jest-serializer: 24.9.0
+ jest-util: 24.9.0
+ jest-worker: 24.9.0
+ micromatch: 3.1.10
+ sane: 4.1.0
+ walker: 1.0.7
+ dev: true
+ engines:
+ node: '>= 6'
+ optionalDependencies:
+ fsevents: 1.2.12
+ resolution:
+ integrity: sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==
+ /jest-haste-map/25.3.0:
+ dependencies:
+ '@jest/types': 25.3.0
+ anymatch: 3.1.1
+ fb-watchman: 2.0.1
+ graceful-fs: 4.2.3
+ jest-serializer: 25.2.6
+ jest-util: 25.3.0
+ jest-worker: 25.2.6
+ micromatch: 4.0.2
+ sane: 4.1.0
+ walker: 1.0.7
+ which: 2.0.2
+ dev: true
+ engines:
+ node: '>= 8.3'
+ optionalDependencies:
+ fsevents: 2.1.2
+ resolution:
+ integrity: sha512-LjXaRa+F8wwtSxo9G+hHD/Cp63PPQzvaBL9XCVoJD2rrcJO0Zr2+YYzAFWWYJ5GlPUkoaJFJtOuk0sL6MJY80A==
+ /jest-jasmine2/24.9.0:
+ dependencies:
+ '@babel/traverse': 7.9.5
+ '@jest/environment': 24.9.0
+ '@jest/test-result': 24.9.0
+ '@jest/types': 24.9.0
+ chalk: 2.4.2
+ co: 4.6.0
+ expect: 24.9.0
+ is-generator-fn: 2.1.0
+ jest-each: 24.9.0
+ jest-matcher-utils: 24.9.0
+ jest-message-util: 24.9.0
+ jest-runtime: 24.9.0
+ jest-snapshot: 24.9.0
+ jest-util: 24.9.0
+ pretty-format: 24.9.0
+ throat: 4.1.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==
+ /jest-jasmine2/25.3.0:
+ dependencies:
+ '@babel/traverse': 7.9.5
+ '@jest/environment': 25.3.0
+ '@jest/source-map': 25.2.6
+ '@jest/test-result': 25.3.0
+ '@jest/types': 25.3.0
+ chalk: 3.0.0
+ co: 4.6.0
+ expect: 25.3.0
+ is-generator-fn: 2.1.0
+ jest-each: 25.3.0
+ jest-matcher-utils: 25.3.0
+ jest-message-util: 25.3.0
+ jest-runtime: 25.3.0
+ jest-snapshot: 25.3.0
+ jest-util: 25.3.0
+ pretty-format: 25.3.0
+ throat: 5.0.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-NCYOGE6+HNzYFSui52SefgpsnIzvxjn6KAgqw66BdRp37xpMD/4kujDHLNW5bS5i53os5TcMn6jYrzQRO8VPrQ==
+ /jest-junit/10.0.0:
+ dependencies:
+ jest-validate: 24.9.0
+ mkdirp: 0.5.5
+ strip-ansi: 5.2.0
+ uuid: 3.4.0
+ xml: 1.0.1
+ dev: true
+ engines:
+ node: '>=8.0.0'
+ resolution:
+ integrity: sha512-dbOVRyxHprdSpwSAR9/YshLwmnwf+RSl5hf0kCGlhAcEeZY9aRqo4oNmaT0tLC16Zy9D0zekDjWkjHGjXlglaQ==
+ /jest-leak-detector/24.9.0:
+ dependencies:
+ jest-get-type: 24.9.0
+ pretty-format: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==
+ /jest-leak-detector/25.3.0:
+ dependencies:
+ jest-get-type: 25.2.6
+ pretty-format: 25.3.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-jk7k24dMIfk8LUSQQGN8PyOy9+J0NAfHZWiDmUDYVMctY8FLJQ1eQ8+PjMoN8PgwhLIggUqgYJnyRFvUz3jLRw==
+ /jest-matcher-utils/24.9.0:
+ dependencies:
+ chalk: 2.4.2
+ jest-diff: 24.9.0
+ jest-get-type: 24.9.0
+ pretty-format: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==
+ /jest-matcher-utils/25.3.0:
+ dependencies:
+ chalk: 3.0.0
+ jest-diff: 25.3.0
+ jest-get-type: 25.2.6
+ pretty-format: 25.3.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-ZBUJ2fchNIZt+fyzkuCFBb8SKaU//Rln45augfUtbHaGyVxCO++ANARdBK9oPGXU3hEDgyy7UHnOP/qNOJXFUg==
+ /jest-message-util/24.9.0:
+ dependencies:
+ '@babel/code-frame': 7.8.3
+ '@jest/test-result': 24.9.0
+ '@jest/types': 24.9.0
+ '@types/stack-utils': 1.0.1
+ chalk: 2.4.2
+ micromatch: 3.1.10
+ slash: 2.0.0
+ stack-utils: 1.0.2
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==
+ /jest-message-util/25.3.0:
+ dependencies:
+ '@babel/code-frame': 7.8.3
+ '@jest/types': 25.3.0
+ '@types/stack-utils': 1.0.1
+ chalk: 3.0.0
+ micromatch: 4.0.2
+ slash: 3.0.0
+ stack-utils: 1.0.2
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-5QNy9Id4WxJbRITEbA1T1kem9bk7y2fD0updZMSTNHtbEDnYOGLDPAuFBhFgVmOZpv0n6OMdVkK+WhyXEPCcOw==
+ /jest-mock/24.9.0:
+ dependencies:
+ '@jest/types': 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==
+ /jest-mock/25.3.0:
+ dependencies:
+ '@jest/types': 25.3.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-yRn6GbuqB4j3aYu+Z1ezwRiZfp0o9om5uOcBovVtkcRLeBCNP5mT0ysdenUsxAHnQUgGwPOE1wwhtQYe6NKirQ==
+ /jest-pnp-resolver/1.2.1_jest-resolve@24.9.0:
+ dependencies:
+ jest-resolve: 24.9.0_jest-resolve@24.9.0
+ dev: true
+ engines:
+ node: '>=6'
+ peerDependencies:
+ jest-resolve: '*'
+ peerDependenciesMeta:
+ jest-resolve:
+ optional: true
+ resolution:
+ integrity: sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==
+ /jest-pnp-resolver/1.2.1_jest-resolve@25.3.0:
+ dependencies:
+ jest-resolve: 25.3.0_jest-resolve@25.3.0
+ dev: true
+ engines:
+ node: '>=6'
+ peerDependencies:
+ jest-resolve: '*'
+ peerDependenciesMeta:
+ jest-resolve:
+ optional: true
+ resolution:
+ integrity: sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==
+ /jest-regex-util/24.9.0:
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==
+ /jest-regex-util/25.2.6:
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==
+ /jest-resolve-dependencies/24.9.0:
+ dependencies:
+ '@jest/types': 24.9.0
+ jest-regex-util: 24.9.0
+ jest-snapshot: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==
+ /jest-resolve-dependencies/25.3.0:
+ dependencies:
+ '@jest/types': 25.3.0
+ jest-regex-util: 25.2.6
+ jest-snapshot: 25.3.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-bDUlLYmHW+f7J7KgcY2lkq8EMRqKonRl0XoD4Wp5SJkgAxKJnsaIOlrrVNTfXYf+YOu3VCjm/Ac2hPF2nfsCIA==
+ /jest-resolve/24.9.0_jest-resolve@24.9.0:
+ dependencies:
+ '@jest/types': 24.9.0
+ browser-resolve: 1.11.3
+ chalk: 2.4.2
+ jest-pnp-resolver: 1.2.1_jest-resolve@24.9.0
+ realpath-native: 1.1.0
+ dev: true
+ engines:
+ node: '>= 6'
+ peerDependencies:
+ jest-resolve: '*'
+ resolution:
+ integrity: sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==
+ /jest-resolve/25.3.0_jest-resolve@25.3.0:
+ dependencies:
+ '@jest/types': 25.3.0
+ browser-resolve: 1.11.3
+ chalk: 3.0.0
+ jest-pnp-resolver: 1.2.1_jest-resolve@25.3.0
+ realpath-native: 2.0.0
+ resolve: 1.15.1
+ dev: true
+ engines:
+ node: '>= 8.3'
+ peerDependencies:
+ jest-resolve: '*'
+ resolution:
+ integrity: sha512-IHoQAAybulsJ+ZgWis+ekYKDAoFkVH5Nx/znpb41zRtpxj4fr2WNV9iDqavdSm8GIpMlsfZxbC/fV9DhW0q9VQ==
+ /jest-runner/24.9.0:
+ dependencies:
+ '@jest/console': 24.9.0
+ '@jest/environment': 24.9.0
+ '@jest/test-result': 24.9.0
+ '@jest/types': 24.9.0
+ chalk: 2.4.2
+ exit: 0.1.2
+ graceful-fs: 4.2.3
+ jest-config: 24.9.0
+ jest-docblock: 24.9.0
+ jest-haste-map: 24.9.0
+ jest-jasmine2: 24.9.0
+ jest-leak-detector: 24.9.0
+ jest-message-util: 24.9.0
+ jest-resolve: 24.9.0_jest-resolve@24.9.0
+ jest-runtime: 24.9.0
+ jest-util: 24.9.0
+ jest-worker: 24.9.0
+ source-map-support: 0.5.16
+ throat: 4.1.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==
+ /jest-runner/25.3.0:
+ dependencies:
+ '@jest/console': 25.3.0
+ '@jest/environment': 25.3.0
+ '@jest/test-result': 25.3.0
+ '@jest/types': 25.3.0
+ chalk: 3.0.0
+ exit: 0.1.2
+ graceful-fs: 4.2.3
+ jest-config: 25.3.0
+ jest-docblock: 25.3.0
+ jest-haste-map: 25.3.0
+ jest-jasmine2: 25.3.0
+ jest-leak-detector: 25.3.0
+ jest-message-util: 25.3.0
+ jest-resolve: 25.3.0_jest-resolve@25.3.0
+ jest-runtime: 25.3.0
+ jest-util: 25.3.0
+ jest-worker: 25.2.6
+ source-map-support: 0.5.16
+ throat: 5.0.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-csDqSC9qGHYWDrzrElzEgFbteztFeZJmKhSgY5jlCIcN0+PhActzRNku0DA1Xa1HxGOb0/AfbP1EGJlP4fGPtA==
+ /jest-runtime/24.9.0:
+ dependencies:
+ '@jest/console': 24.9.0
+ '@jest/environment': 24.9.0
+ '@jest/source-map': 24.9.0
+ '@jest/transform': 24.9.0
+ '@jest/types': 24.9.0
+ '@types/yargs': 13.0.8
+ chalk: 2.4.2
+ exit: 0.1.2
+ glob: 7.1.6
+ graceful-fs: 4.2.3
+ jest-config: 24.9.0
+ jest-haste-map: 24.9.0
+ jest-message-util: 24.9.0
+ jest-mock: 24.9.0
+ jest-regex-util: 24.9.0
+ jest-resolve: 24.9.0_jest-resolve@24.9.0
+ jest-snapshot: 24.9.0
+ jest-util: 24.9.0
+ jest-validate: 24.9.0
+ realpath-native: 1.1.0
+ slash: 2.0.0
+ strip-bom: 3.0.0
+ yargs: 13.3.2
+ dev: true
+ engines:
+ node: '>= 6'
+ hasBin: true
+ resolution:
+ integrity: sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==
+ /jest-runtime/25.3.0:
+ dependencies:
+ '@jest/console': 25.3.0
+ '@jest/environment': 25.3.0
+ '@jest/source-map': 25.2.6
+ '@jest/test-result': 25.3.0
+ '@jest/transform': 25.3.0
+ '@jest/types': 25.3.0
+ '@types/yargs': 15.0.4
+ chalk: 3.0.0
+ collect-v8-coverage: 1.0.1
+ exit: 0.1.2
+ glob: 7.1.6
+ graceful-fs: 4.2.3
+ jest-config: 25.3.0
+ jest-haste-map: 25.3.0
+ jest-message-util: 25.3.0
+ jest-mock: 25.3.0
+ jest-regex-util: 25.2.6
+ jest-resolve: 25.3.0_jest-resolve@25.3.0
+ jest-snapshot: 25.3.0
+ jest-util: 25.3.0
+ jest-validate: 25.3.0
+ realpath-native: 2.0.0
+ slash: 3.0.0
+ strip-bom: 4.0.0
+ yargs: 15.3.1
+ dev: true
+ engines:
+ node: '>= 8.3'
+ hasBin: true
+ resolution:
+ integrity: sha512-gn5KYB1wxXRM3nfw8fVpthFu60vxQUCr+ShGq41+ZBFF3DRHZRKj3HDWVAVB4iTNBj2y04QeAo5cZ/boYaPg0w==
+ /jest-serializer/24.9.0:
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==
+ /jest-serializer/25.2.6:
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-RMVCfZsezQS2Ww4kB5HJTMaMJ0asmC0BHlnobQC6yEtxiFKIxohFA4QSXSabKwSggaNkqxn6Z2VwdFCjhUWuiQ==
+ /jest-snapshot/24.9.0:
+ dependencies:
+ '@babel/types': 7.9.5
+ '@jest/types': 24.9.0
+ chalk: 2.4.2
+ expect: 24.9.0
+ jest-diff: 24.9.0
+ jest-get-type: 24.9.0
+ jest-matcher-utils: 24.9.0
+ jest-message-util: 24.9.0
+ jest-resolve: 24.9.0_jest-resolve@24.9.0
+ mkdirp: 0.5.5
+ natural-compare: 1.4.0
+ pretty-format: 24.9.0
+ semver: 6.3.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==
+ /jest-snapshot/25.3.0:
+ dependencies:
+ '@babel/types': 7.9.5
+ '@jest/types': 25.3.0
+ '@types/prettier': 1.19.1
+ chalk: 3.0.0
+ expect: 25.3.0
+ jest-diff: 25.3.0
+ jest-get-type: 25.2.6
+ jest-matcher-utils: 25.3.0
+ jest-message-util: 25.3.0
+ jest-resolve: 25.3.0_jest-resolve@25.3.0
+ make-dir: 3.0.2
+ natural-compare: 1.4.0
+ pretty-format: 25.3.0
+ semver: 6.3.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-GGpR6Oro2htJPKh5RX4PR1xwo5jCEjtvSPLW1IS7N85y+2bWKbiknHpJJRKSdGXghElb5hWaeQASJI4IiRayGg==
+ /jest-util/24.9.0:
+ dependencies:
+ '@jest/console': 24.9.0
+ '@jest/fake-timers': 24.9.0
+ '@jest/source-map': 24.9.0
+ '@jest/test-result': 24.9.0
+ '@jest/types': 24.9.0
+ callsites: 3.1.0
+ chalk: 2.4.2
+ graceful-fs: 4.2.3
+ is-ci: 2.0.0
+ mkdirp: 0.5.5
+ slash: 2.0.0
+ source-map: 0.6.1
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==
+ /jest-util/25.3.0:
+ dependencies:
+ '@jest/types': 25.3.0
+ chalk: 3.0.0
+ is-ci: 2.0.0
+ make-dir: 3.0.2
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-dc625P/KS/CpWTJJJxKc4bA3A6c+PJGBAqS8JTJqx4HqPoKNqXg/Ec8biL2Z1TabwK7E7Ilf0/ukSEXM1VwzNA==
+ /jest-validate/24.9.0:
+ dependencies:
+ '@jest/types': 24.9.0
+ camelcase: 5.3.1
+ chalk: 2.4.2
+ jest-get-type: 24.9.0
+ leven: 3.1.0
+ pretty-format: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==
+ /jest-validate/25.3.0:
+ dependencies:
+ '@jest/types': 25.3.0
+ camelcase: 5.3.1
+ chalk: 3.0.0
+ jest-get-type: 25.2.6
+ leven: 3.1.0
+ pretty-format: 25.3.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-3WuXgIZ4HXUvW6gk9twFFkT9j6zUorKnF2oEY8VEsHb7x5LGvVlN3WUsbqazVKuyXwvikO2zFJ/YTySMsMje2w==
+ /jest-watch-typeahead/0.4.2:
+ dependencies:
+ ansi-escapes: 4.3.1
+ chalk: 2.4.2
+ jest-regex-util: 24.9.0
+ jest-watcher: 24.9.0
+ slash: 3.0.0
+ string-length: 3.1.0
+ strip-ansi: 5.2.0
+ dev: true
+ resolution:
+ integrity: sha512-f7VpLebTdaXs81rg/oj4Vg/ObZy2QtGzAmGLNsqUS5G5KtSN68tFcIsbvNODfNyQxU78g7D8x77o3bgfBTR+2Q==
+ /jest-watcher/24.9.0:
+ dependencies:
+ '@jest/test-result': 24.9.0
+ '@jest/types': 24.9.0
+ '@types/yargs': 13.0.8
+ ansi-escapes: 3.2.0
+ chalk: 2.4.2
+ jest-util: 24.9.0
+ string-length: 2.0.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==
+ /jest-watcher/25.3.0:
+ dependencies:
+ '@jest/test-result': 25.3.0
+ '@jest/types': 25.3.0
+ ansi-escapes: 4.3.1
+ chalk: 3.0.0
+ jest-util: 25.3.0
+ string-length: 3.1.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-dtFkfidFCS9Ucv8azOg2hkiY3sgJEHeTLtGFHS+jfBEE7eRtrO6+2r1BokyDkaG2FOD7485r/SgpC1MFAENfeA==
+ /jest-worker/24.9.0:
+ dependencies:
+ merge-stream: 2.0.0
+ supports-color: 6.1.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==
+ /jest-worker/25.2.6:
+ dependencies:
+ merge-stream: 2.0.0
+ supports-color: 7.1.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-FJn9XDUSxcOR4cwDzRfL1z56rUofNTFs539FGASpd50RHdb6EVkhxQqktodW2mI49l+W3H+tFJDotCHUQF6dmA==
+ /jest/24.9.0:
+ dependencies:
+ import-local: 2.0.0
+ jest-cli: 24.9.0
+ dev: true
+ engines:
+ node: '>= 6'
+ hasBin: true
+ resolution:
+ integrity: sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==
+ /jest/25.3.0:
+ dependencies:
+ '@jest/core': 25.3.0
+ import-local: 3.0.2
+ jest-cli: 25.3.0
+ dev: true
+ engines:
+ node: '>= 8.3'
+ hasBin: true
+ resolution:
+ integrity: sha512-iKd5ShQSHzFT5IL/6h5RZJhApgqXSoPxhp5HEi94v6OAw9QkF8T7X+liEU2eEHJ1eMFYTHmeWLrpBWulsDpaUg==
+ /jmespath/0.15.0:
+ engines:
+ node: '>= 0.6.0'
+ resolution:
+ integrity: sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
+ /jquery/3.4.1:
+ dev: false
+ resolution:
+ integrity: sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==
+ /js-string-escape/1.0.1:
+ dev: true
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=
+ /js-tokens/3.0.2:
+ dev: true
+ resolution:
+ integrity: sha1-mGbfOVECEw449/mWvOtlRDIJwls=
+ /js-tokens/4.0.0:
+ resolution:
+ integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+ /js-yaml-loader/1.2.2:
+ dependencies:
+ js-yaml: 3.13.1
+ loader-utils: 1.4.0
+ un-eval: 1.2.0
+ dev: true
+ resolution:
+ integrity: sha512-H+NeuNrG6uOs/WMjna2SjkaCw13rMWiT/D7l9+9x5n8aq88BDsh2sRmdfxckWPIHtViYHWRG6XiCKYvS1dfyLg==
+ /js-yaml/3.13.1:
+ dependencies:
+ argparse: 1.0.10
+ esprima: 4.0.1
+ hasBin: true
+ resolution:
+ integrity: sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
+ /jsbn/0.1.1:
+ resolution:
+ integrity: sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+ /jsdom/11.12.0:
+ dependencies:
+ abab: 2.0.3
+ acorn: 5.7.4
+ acorn-globals: 4.3.4
+ array-equal: 1.0.0
+ cssom: 0.3.8
+ cssstyle: 1.4.0
+ data-urls: 1.1.0
+ domexception: 1.0.1
+ escodegen: 1.14.1
+ html-encoding-sniffer: 1.0.2
+ left-pad: 1.3.0
+ nwsapi: 2.2.0
+ parse5: 4.0.0
+ pn: 1.1.0
+ request: 2.88.2
+ request-promise-native: 1.0.8_request@2.88.2
+ sax: 1.2.4
+ symbol-tree: 3.2.4
+ tough-cookie: 2.5.0
+ w3c-hr-time: 1.0.2
+ webidl-conversions: 4.0.2
+ whatwg-encoding: 1.0.5
+ whatwg-mimetype: 2.3.0
+ whatwg-url: 6.5.0
+ ws: 5.2.2
+ xml-name-validator: 3.0.0
+ dev: true
+ resolution:
+ integrity: sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==
+ /jsdom/14.1.0:
+ dependencies:
+ abab: 2.0.3
+ acorn: 6.4.1
+ acorn-globals: 4.3.4
+ array-equal: 1.0.0
+ cssom: 0.3.8
+ cssstyle: 1.4.0
+ data-urls: 1.1.0
+ domexception: 1.0.1
+ escodegen: 1.14.1
+ html-encoding-sniffer: 1.0.2
+ nwsapi: 2.2.0
+ parse5: 5.1.0
+ pn: 1.1.0
+ request: 2.88.2
+ request-promise-native: 1.0.8_request@2.88.2
+ saxes: 3.1.11
+ symbol-tree: 3.2.4
+ tough-cookie: 2.5.0
+ w3c-hr-time: 1.0.2
+ w3c-xmlserializer: 1.1.2
+ webidl-conversions: 4.0.2
+ whatwg-encoding: 1.0.5
+ whatwg-mimetype: 2.3.0
+ whatwg-url: 7.1.0
+ ws: 6.2.1
+ xml-name-validator: 3.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-O901mfJSuTdwU2w3Sn+74T+RnDVP+FuV5fH8tcPWyqrseRAb0s5xOtPgCFiPOtLcyK7CLIJwPyD83ZqQWvA5ng==
+ /jsdom/15.2.1:
+ dependencies:
+ abab: 2.0.3
+ acorn: 7.1.1
+ acorn-globals: 4.3.4
+ array-equal: 1.0.0
+ cssom: 0.4.4
+ cssstyle: 2.2.0
+ data-urls: 1.1.0
+ domexception: 1.0.1
+ escodegen: 1.14.1
+ html-encoding-sniffer: 1.0.2
+ nwsapi: 2.2.0
+ parse5: 5.1.0
+ pn: 1.1.0
+ request: 2.88.2
+ request-promise-native: 1.0.8_request@2.88.2
+ saxes: 3.1.11
+ symbol-tree: 3.2.4
+ tough-cookie: 3.0.1
+ w3c-hr-time: 1.0.2
+ w3c-xmlserializer: 1.1.2
+ webidl-conversions: 4.0.2
+ whatwg-encoding: 1.0.5
+ whatwg-mimetype: 2.3.0
+ whatwg-url: 7.1.0
+ ws: 7.2.3
+ xml-name-validator: 3.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ peerDependencies:
+ canvas: ^2.5.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+ resolution:
+ integrity: sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==
+ /jsesc/0.5.0:
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
+ /jsesc/2.5.2:
+ dev: true
+ engines:
+ node: '>=4'
+ hasBin: true
+ resolution:
+ integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+ /json-buffer/3.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
+ /json-cycle/1.3.0:
+ dev: true
+ engines:
+ node: '>= 4'
+ resolution:
+ integrity: sha512-FD/SedD78LCdSvJaOUQAXseT8oQBb5z6IVYaQaCrVUlu9zOAr1BDdKyVYQaSD/GDsAMrXpKcOyBD4LIl8nfjHw==
+ /json-parse-better-errors/1.0.2:
+ resolution:
+ integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+ /json-refs/2.1.7:
+ dependencies:
+ commander: 2.20.3
+ graphlib: 2.1.8
+ js-yaml: 3.13.1
+ native-promise-only: 0.8.1
+ path-loader: 1.0.10
+ slash: 1.0.0
+ uri-js: 3.0.2
+ dev: true
+ engines:
+ node: '>=0.8'
+ hasBin: true
+ resolution:
+ integrity: sha1-uesB/in16j6Sh48VrqEK04taz4k=
+ /json-schema-traverse/0.4.1:
+ resolution:
+ integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+ /json-schema/0.2.3:
+ resolution:
+ integrity: sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+ /json-stable-stringify-without-jsonify/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
+ /json-stable-stringify/1.0.1:
+ dependencies:
+ jsonify: 0.0.0
+ dev: true
+ resolution:
+ integrity: sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=
+ /json-stringify-safe/5.0.1:
+ resolution:
+ integrity: sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+ /json3/3.3.3:
+ dev: true
+ resolution:
+ integrity: sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==
+ /json5/1.0.1:
+ dependencies:
+ minimist: 1.2.5
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+ /json5/2.1.3:
+ dependencies:
+ minimist: 1.2.5
+ dev: true
+ engines:
+ node: '>=6'
+ hasBin: true
+ resolution:
+ integrity: sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
+ /jsonata/1.8.2:
+ dev: true
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-ma5F/Bs47dZfJfDZ0Dt37eIbzVBVKZIDqsZSqdCCAPNHxKn+s3+CfMA6ahVVlf8Y1hyIjXkVLFU7yv4XxRfihA==
+ /jsonfile/2.4.0:
+ dev: true
+ optionalDependencies:
+ graceful-fs: 4.2.3
+ resolution:
+ integrity: sha1-NzaitCi4e72gzIO1P6PWM6NcKug=
+ /jsonfile/4.0.0:
+ optionalDependencies:
+ graceful-fs: 4.2.3
+ resolution:
+ integrity: sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
+ /jsonify/0.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
+ /jsonpath-plus/1.1.0:
+ dev: true
+ engines:
+ node: '>=6.0'
+ resolution:
+ integrity: sha512-ydqTBOuLcFCUr9e7AxJlKCFgxzEQ03HjnIim0hJSdk2NxD8MOsaMOrRgP6XWEm5q3VuDY5+cRT1DM9vLlGo/qA==
+ /jsonwebtoken/8.5.1:
+ dependencies:
+ jws: 3.2.2
+ lodash.includes: 4.3.0
+ lodash.isboolean: 3.0.3
+ lodash.isinteger: 4.0.4
+ lodash.isnumber: 3.0.3
+ lodash.isplainobject: 4.0.6
+ lodash.isstring: 4.0.1
+ lodash.once: 4.1.1
+ ms: 2.1.2
+ semver: 5.7.1
+ engines:
+ node: '>=4'
+ npm: '>=1.4.28'
+ resolution:
+ integrity: sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
+ /jsprim/1.4.1:
+ dependencies:
+ assert-plus: 1.0.0
+ extsprintf: 1.3.0
+ json-schema: 0.2.3
+ verror: 1.10.0
+ engines:
+ '0': node >=0.6.0
+ resolution:
+ integrity: sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+ /jsx-ast-utils/2.2.3:
+ dependencies:
+ array-includes: 3.1.1
+ object.assign: 4.1.0
+ dev: true
+ engines:
+ node: '>=4.0'
+ resolution:
+ integrity: sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==
+ /jszip/3.3.0:
+ dependencies:
+ lie: 3.3.0
+ pako: 1.0.11
+ readable-stream: 2.3.7
+ set-immediate-shim: 1.0.1
+ dev: true
+ resolution:
+ integrity: sha512-EJ9k766htB1ZWnsV5ZMDkKLgA+201r/ouFF8R2OigVjVdcm2rurcBrrdXaeqBJbqnUVMko512PYmlncBKE1Huw==
+ /jwa/1.4.1:
+ dependencies:
+ buffer-equal-constant-time: 1.0.1
+ ecdsa-sig-formatter: 1.0.11
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
+ /jwk-to-pem/2.0.3:
+ dependencies:
+ asn1.js: 5.3.0
+ elliptic: 6.5.2
+ safe-buffer: 5.2.0
+ dev: false
+ resolution:
+ integrity: sha512-T1MA0L3DVB2mOIZytZyNTdcAAOJscLLCi25dgzQtkHjTcuwpRW3BFnjj0eEpMORfJyZtFZ5wy++Ys6wsMolPsA==
+ /jws/3.2.2:
+ dependencies:
+ jwa: 1.4.1
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
+ /jwt-decode/2.2.0:
+ resolution:
+ integrity: sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=
+ /keyboard-key/1.1.0:
+ dev: false
+ resolution:
+ integrity: sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ==
+ /keyv/3.0.0:
+ dependencies:
+ json-buffer: 3.0.0
+ dev: true
+ resolution:
+ integrity: sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==
+ /keyv/3.1.0:
+ dependencies:
+ json-buffer: 3.0.0
+ dev: true
+ resolution:
+ integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==
+ /killable/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==
+ /kind-of/2.0.1:
+ dependencies:
+ is-buffer: 1.1.6
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=
+ /kind-of/3.2.2:
+ dependencies:
+ is-buffer: 1.1.6
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+ /kind-of/4.0.0:
+ dependencies:
+ is-buffer: 1.1.6
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+ /kind-of/5.1.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+ /kind-of/6.0.3:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+ /klaw/1.3.1:
+ dev: true
+ optionalDependencies:
+ graceful-fs: 4.2.3
+ resolution:
+ integrity: sha1-QIhDO0azsbolnXh4XY6W9zugJDk=
+ /kleur/3.0.3:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
+ /kuler/1.0.1:
+ dependencies:
+ colornames: 1.1.1
+ dev: true
+ resolution:
+ integrity: sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==
+ /last-call-webpack-plugin/3.0.0:
+ dependencies:
+ lodash: 4.17.15
+ webpack-sources: 1.4.3
+ dev: true
+ resolution:
+ integrity: sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==
+ /latest-version/3.1.0:
+ dependencies:
+ package-json: 4.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=
+ /latest-version/5.1.0:
+ dependencies:
+ package-json: 6.5.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==
+ /lazy-cache/0.2.7:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=
+ /lazy-cache/1.0.4:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-odePw6UEdMuAhF07O24dpJpEbo4=
+ /lazystream/1.0.0:
+ dependencies:
+ readable-stream: 2.3.7
+ dev: true
+ engines:
+ node: '>= 0.6.3'
+ resolution:
+ integrity: sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=
+ /lcid/2.0.0:
+ dependencies:
+ invert-kv: 2.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==
+ /left-pad/1.3.0:
+ deprecated: use String.prototype.padStart()
+ dev: true
+ resolution:
+ integrity: sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
+ /leven/3.1.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+ /levenary/1.1.1:
+ dependencies:
+ leven: 3.1.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==
+ /levn/0.3.0:
+ dependencies:
+ prelude-ls: 1.1.2
+ type-check: 0.3.2
+ dev: true
+ engines:
+ node: '>= 0.8.0'
+ resolution:
+ integrity: sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+ /lie/3.3.0:
+ dependencies:
+ immediate: 3.0.6
+ dev: true
+ resolution:
+ integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
+ /lines-and-columns/1.1.6:
+ resolution:
+ integrity: sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+ /load-json-file/2.0.0:
+ dependencies:
+ graceful-fs: 4.2.3
+ parse-json: 2.2.0
+ pify: 2.3.0
+ strip-bom: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=
+ /load-json-file/4.0.0:
+ dependencies:
+ graceful-fs: 4.2.3
+ parse-json: 4.0.0
+ pify: 3.0.0
+ strip-bom: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
+ /loader-fs-cache/1.0.3:
+ dependencies:
+ find-cache-dir: 0.1.1
+ mkdirp: 0.5.5
+ dev: true
+ resolution:
+ integrity: sha512-ldcgZpjNJj71n+2Mf6yetz+c9bM4xpKtNds4LbqXzU/PTdeAX0g3ytnU1AJMEcTk2Lex4Smpe3Q/eCTsvUBxbA==
+ /loader-runner/2.4.0:
+ dev: true
+ engines:
+ node: '>=4.3.0 <5.0.0 || >=5.10'
+ resolution:
+ integrity: sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
+ /loader-utils/1.2.3:
+ dependencies:
+ big.js: 5.2.2
+ emojis-list: 2.1.0
+ json5: 1.0.1
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ resolution:
+ integrity: sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
+ /loader-utils/1.4.0:
+ dependencies:
+ big.js: 5.2.2
+ emojis-list: 3.0.0
+ json5: 1.0.1
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ resolution:
+ integrity: sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
+ /locate-path/2.0.0:
+ dependencies:
+ p-locate: 2.0.0
+ path-exists: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
+ /locate-path/3.0.0:
+ dependencies:
+ p-locate: 3.0.0
+ path-exists: 3.0.0
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+ /locate-path/5.0.0:
+ dependencies:
+ p-locate: 4.1.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ /lodash._reinterpolate/3.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
+ /lodash.includes/4.3.0:
+ resolution:
+ integrity: sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
+ /lodash.isboolean/3.0.3:
+ resolution:
+ integrity: sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
+ /lodash.isinteger/4.0.4:
+ resolution:
+ integrity: sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
+ /lodash.isnumber/3.0.3:
+ resolution:
+ integrity: sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
+ /lodash.isplainobject/4.0.6:
+ resolution:
+ integrity: sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
+ /lodash.isstring/4.0.1:
+ resolution:
+ integrity: sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
+ /lodash.memoize/4.1.2:
+ dev: true
+ resolution:
+ integrity: sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+ /lodash.once/4.1.1:
+ resolution:
+ integrity: sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
+ /lodash.sortby/4.7.0:
+ dev: true
+ resolution:
+ integrity: sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
+ /lodash.template/4.5.0:
+ dependencies:
+ lodash._reinterpolate: 3.0.0
+ lodash.templatesettings: 4.2.0
+ dev: true
+ resolution:
+ integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
+ /lodash.templatesettings/4.2.0:
+ dependencies:
+ lodash._reinterpolate: 3.0.0
+ dev: true
+ resolution:
+ integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
+ /lodash.unescape/4.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=
+ /lodash.uniq/4.5.0:
+ dev: true
+ resolution:
+ integrity: sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+ /lodash/4.17.15:
+ resolution:
+ integrity: sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+ /log/6.0.0:
+ dependencies:
+ d: 1.0.1
+ duration: 0.2.2
+ es5-ext: 0.10.53
+ event-emitter: 0.3.5
+ sprintf-kit: 2.0.0
+ type: 1.2.0
+ dev: true
+ resolution:
+ integrity: sha512-sxChESNYJ/EcQv8C7xpmxhtTOngoXuMEqGDAkhXBEmt3MAzM3SM/TmIBOqnMEVdrOv1+VgZoYbo6U2GemQiU4g==
+ /logform/2.1.2:
+ dependencies:
+ colors: 1.4.0
+ fast-safe-stringify: 2.0.7
+ fecha: 2.3.3
+ ms: 2.1.2
+ triple-beam: 1.3.0
+ dev: true
+ resolution:
+ integrity: sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==
+ /loglevel/1.6.7:
+ dev: true
+ engines:
+ node: '>= 0.6.0'
+ resolution:
+ integrity: sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==
+ /lolex/5.1.2:
+ dependencies:
+ '@sinonjs/commons': 1.7.2
+ dev: true
+ resolution:
+ integrity: sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==
+ /loose-envify/1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
+ hasBin: true
+ resolution:
+ integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ /lower-case/2.0.1:
+ dependencies:
+ tslib: 1.11.1
+ dev: true
+ resolution:
+ integrity: sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==
+ /lowercase-keys/1.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=
+ /lowercase-keys/1.0.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
+ /lowercase-keys/2.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
+ /lowlight/1.11.0:
+ dependencies:
+ fault: 1.0.4
+ highlight.js: 9.13.1
+ dev: false
+ resolution:
+ integrity: sha512-xrGGN6XLL7MbTMdPD6NfWPwY43SNkjf/d0mecSx/CW36fUZTjRHEq0/Cdug3TWKtRXLWi7iMl1eP0olYxj/a4A==
+ /lru-cache/4.1.5:
+ dependencies:
+ pseudomap: 1.0.2
+ yallist: 2.1.2
+ dev: true
+ resolution:
+ integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
+ /lru-cache/5.1.1:
+ dependencies:
+ yallist: 3.1.1
+ dev: true
+ resolution:
+ integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ /lru-queue/0.1.0:
+ dependencies:
+ es5-ext: 0.10.53
+ dev: true
+ resolution:
+ integrity: sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=
+ /lsmod/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-mgD3bco26yP6BTUK/htYXUKZ5ks=
+ /luxon/1.23.0:
+ dev: true
+ resolution:
+ integrity: sha512-+6a/bXsCWrrR8vfbL41iM92es12zwV2Rum/KPkT+ubOZnnU3Sqbqok/FmD1xsWlWN2Y9Hu0fU/vNgU24ns7bpA==
+ /make-dir/1.3.0:
+ dependencies:
+ pify: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==
+ /make-dir/2.1.0:
+ dependencies:
+ pify: 4.0.1
+ semver: 5.7.1
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
+ /make-dir/3.0.2:
+ dependencies:
+ semver: 6.3.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==
+ /make-error/1.3.6:
+ dev: true
+ resolution:
+ integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+ /makeerror/1.0.11:
+ dependencies:
+ tmpl: 1.0.4
+ dev: true
+ resolution:
+ integrity: sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=
+ /mamacro/0.0.3:
+ dev: true
+ resolution:
+ integrity: sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==
+ /map-age-cleaner/0.1.3:
+ dependencies:
+ p-defer: 1.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
+ /map-cache/0.2.2:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+ /map-visit/1.0.0:
+ dependencies:
+ object-visit: 1.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+ /md5.js/1.3.5:
+ dependencies:
+ hash-base: 3.0.4
+ inherits: 2.0.4
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==
+ /md5/2.2.1:
+ dependencies:
+ charenc: 0.0.2
+ crypt: 0.0.2
+ is-buffer: 1.1.6
+ dev: false
+ resolution:
+ integrity: sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=
+ /mdn-data/2.0.4:
+ dev: true
+ resolution:
+ integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
+ /mdn-data/2.0.6:
+ dev: true
+ resolution:
+ integrity: sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
+ /media-typer/0.3.0:
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+ /mem/4.3.0:
+ dependencies:
+ map-age-cleaner: 0.1.3
+ mimic-fn: 2.1.0
+ p-is-promise: 2.1.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==
+ /memoize-one/5.1.1:
+ dev: false
+ resolution:
+ integrity: sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
+ /memoizee/0.4.14:
+ dependencies:
+ d: 1.0.1
+ es5-ext: 0.10.53
+ es6-weak-map: 2.0.3
+ event-emitter: 0.3.5
+ is-promise: 2.1.0
+ lru-queue: 0.1.0
+ next-tick: 1.1.0
+ timers-ext: 0.1.7
+ dev: true
+ resolution:
+ integrity: sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==
+ /memory-fs/0.4.1:
+ dependencies:
+ errno: 0.1.7
+ readable-stream: 2.3.7
+ dev: true
+ resolution:
+ integrity: sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
+ /memory-fs/0.5.0:
+ dependencies:
+ errno: 0.1.7
+ readable-stream: 2.3.7
+ dev: true
+ engines:
+ node: '>=4.3.0 <5.0.0 || >=5.10'
+ resolution:
+ integrity: sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==
+ /merge-deep/3.0.2:
+ dependencies:
+ arr-union: 3.1.0
+ clone-deep: 0.2.4
+ kind-of: 3.2.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-T7qC8kg4Zoti1cFd8Cr0M+qaZfOwjlPDEdZIIPPB2JZctjaPM4fX+i7HOId69tAti2fvO6X5ldfYUONDODsrkA==
+ /merge-descriptors/1.0.1:
+ resolution:
+ integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
+ /merge-stream/2.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+ /merge2/1.3.0:
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==
+ /methods/1.1.2:
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
+ /microevent.ts/0.1.1:
+ dev: true
+ resolution:
+ integrity: sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==
+ /micromatch/3.1.10:
+ dependencies:
+ arr-diff: 4.0.0
+ array-unique: 0.3.2
+ braces: 2.3.2
+ define-property: 2.0.2
+ extend-shallow: 3.0.2
+ extglob: 2.0.4
+ fragment-cache: 0.2.1
+ kind-of: 6.0.3
+ nanomatch: 1.2.13
+ object.pick: 1.3.0
+ regex-not: 1.0.2
+ snapdragon: 0.8.2
+ to-regex: 3.0.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+ /micromatch/4.0.2:
+ dependencies:
+ braces: 3.0.2
+ picomatch: 2.2.2
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+ /miller-rabin/4.0.1:
+ dependencies:
+ bn.js: 4.11.8
+ brorand: 1.1.0
+ hasBin: true
+ resolution:
+ integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
+ /mime-db/1.43.0:
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
+ /mime-types/2.1.26:
+ dependencies:
+ mime-db: 1.43.0
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==
+ /mime/1.6.0:
+ engines:
+ node: '>=4'
+ hasBin: true
+ resolution:
+ integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+ /mime/2.4.4:
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==
+ /mimic-fn/1.2.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
+ /mimic-fn/2.1.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+ /mimic-response/1.0.1:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
+ /mini-create-react-context/0.3.2_prop-types@15.7.2+react@16.13.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ gud: 1.0.0
+ prop-types: 15.7.2
+ react: 16.13.1
+ tiny-warning: 1.0.3
+ dev: false
+ peerDependencies:
+ prop-types: ^15.0.0
+ react: ^0.14.0 || ^15.0.0 || ^16.0.0
+ resolution:
+ integrity: sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==
+ /mini-css-extract-plugin/0.9.0_webpack@4.42.0:
+ dependencies:
+ loader-utils: 1.4.0
+ normalize-url: 1.9.1
+ schema-utils: 1.0.0
+ webpack: 4.42.0_webpack@4.42.0
+ webpack-sources: 1.4.3
+ dev: true
+ engines:
+ node: '>= 6.9.0'
+ peerDependencies:
+ webpack: ^4.4.0
+ resolution:
+ integrity: sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==
+ /minimalistic-assert/1.0.1:
+ resolution:
+ integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+ /minimalistic-crypto-utils/1.0.1:
+ resolution:
+ integrity: sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
+ /minimatch/3.0.4:
+ dependencies:
+ brace-expansion: 1.1.11
+ dev: true
+ resolution:
+ integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+ /minimist/1.2.5:
+ dev: true
+ resolution:
+ integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+ /minipass-collect/1.0.2:
+ dependencies:
+ minipass: 3.1.1
+ dev: true
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==
+ /minipass-flush/1.0.5:
+ dependencies:
+ minipass: 3.1.1
+ dev: true
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==
+ /minipass-pipeline/1.2.2:
+ dependencies:
+ minipass: 3.1.1
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==
+ /minipass/3.1.1:
+ dependencies:
+ yallist: 4.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==
+ /mississippi/3.0.0:
+ dependencies:
+ concat-stream: 1.6.2
+ duplexify: 3.7.1
+ end-of-stream: 1.4.4
+ flush-write-stream: 1.1.1
+ from2: 2.3.0
+ parallel-transform: 1.2.0
+ pump: 3.0.0
+ pumpify: 1.5.1
+ stream-each: 1.2.3
+ through2: 2.0.5
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ resolution:
+ integrity: sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==
+ /mixin-deep/1.3.2:
+ dependencies:
+ for-in: 1.0.2
+ is-extendable: 1.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
+ /mixin-object/2.0.1:
+ dependencies:
+ for-in: 0.1.8
+ is-extendable: 0.1.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=
+ /mkdirp/0.5.5:
+ dependencies:
+ minimist: 1.2.5
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
+ /mobx-react-form/2.0.8_mobx@5.15.4:
+ dependencies:
+ lodash: 4.17.15
+ mobx: 5.15.4
+ dev: false
+ engines:
+ node: '>=8.0.0'
+ peerDependencies:
+ mobx: ^2.5.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
+ resolution:
+ integrity: sha512-Z/JsXkN7B5xjG1tolHKytJiKmtLSdqkFKMco5AVagL8cQ0yJmE+iRZ212JKGHfkEKZrRWn7EDnX2STawIQFqxg==
+ /mobx-react-lite/2.0.5_mobx@5.15.4+react@16.13.1:
+ dependencies:
+ mobx: 5.15.4
+ react: 16.13.1
+ dev: false
+ peerDependencies:
+ mobx: ^4.0.0 || ^5.0.0
+ react: ^16.8.0
+ resolution:
+ integrity: sha512-7ifvIAHqxGDgVidRiSNIKLenZaspfhSDz9nkyWiyyZlqHbVTnxqNcB1jnQHEE9Kycl75Z//dN3IoQNeqWWsZ4g==
+ /mobx-react/6.2.2_mobx@5.15.4+react@16.13.1:
+ dependencies:
+ mobx: 5.15.4
+ mobx-react-lite: 2.0.5_mobx@5.15.4+react@16.13.1
+ react: 16.13.1
+ dev: false
+ peerDependencies:
+ mobx: ^5.15.4 || ^4.15.4
+ react: ^16.8.0 || 16.9.0-alpha.0
+ resolution:
+ integrity: sha512-Us6V4ng/iKIRJ8pWxdbdysC6bnS53ZKLKlVGBqzHx6J+gYPYbOotWvhHZnzh/W5mhpYXxlXif4kL2cxoWJOplQ==
+ /mobx-state-tree/3.15.0_mobx@5.15.4:
+ dependencies:
+ mobx: 5.15.4
+ dev: false
+ peerDependencies:
+ mobx: '>=4.8.0 <5.0.0 || >=5.8.0 <6.0.0'
+ resolution:
+ integrity: sha512-65vvHPBWlz1gmZggbMAdg9ZSEVMGtyLj0drPDTYo/yMv8NVel52mNgBUKxDYWsNU9XMX4GM71wABhx7fD8s0Uw==
+ /mobx/5.15.4:
+ dev: false
+ resolution:
+ integrity: sha512-xRFJxSU2Im3nrGCdjSuOTFmxVDGeqOHL+TyADCGbT0k4HHqGmx5u2yaHNryvoORpI4DfbzjJ5jPmuv+d7sioFw==
+ /module-definition/3.3.0:
+ dependencies:
+ ast-module-types: 2.6.0
+ node-source-walk: 4.2.0
+ dev: true
+ engines:
+ node: '>=6.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-HTplA9xwDzH67XJFC1YvZMUElWJD28DV0dUq7lhTs+JKJamUOWA/CcYWSlhW5amJO66uWtY7XdltT+LfX0wIVg==
+ /module-lookup-amd/6.2.0:
+ dependencies:
+ commander: 2.20.3
+ debug: 4.1.1
+ file-exists-dazinatorfork: 1.0.2
+ find: 0.3.0
+ requirejs: 2.3.6
+ requirejs-config-file: 3.1.2
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-uxHCj5Pw9psZiC1znjU2qPsubt6haCSsN9m7xmIdoTciEgfxUkE1vhtDvjHPuOXEZrVJhjKgkmkP+w73rRuelQ==
+ /moment/2.24.0:
+ resolution:
+ integrity: sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
+ /move-concurrently/1.0.1:
+ dependencies:
+ aproba: 1.2.0
+ copy-concurrently: 1.0.5
+ fs-write-stream-atomic: 1.0.10
+ mkdirp: 0.5.5
+ rimraf: 2.7.1
+ run-queue: 1.0.3
+ dev: true
+ resolution:
+ integrity: sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=
+ /mri/1.1.5:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-d2RKzMD4JNyHMbnbWnznPaa8vbdlq/4pNZ3IgdaGrVbBhebBsGUUE/6qorTMYNS6TwuH3ilfOlD2bf4Igh8CKg==
+ /ms/0.7.1:
+ dev: true
+ resolution:
+ integrity: sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=
+ /ms/2.0.0:
+ resolution:
+ integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+ /ms/2.1.1:
+ resolution:
+ integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+ /ms/2.1.2:
+ resolution:
+ integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+ /msgpack-lite/0.1.26:
+ dependencies:
+ event-lite: 0.1.2
+ ieee754: 1.1.13
+ int64-buffer: 0.1.10
+ isarray: 1.0.0
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha1-3TxQsm8FnyXn7e42REGDWOKprYk=
+ /multicast-dns-service-types/1.1.0:
+ dev: true
+ resolution:
+ integrity: sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=
+ /multicast-dns/6.2.3:
+ dependencies:
+ dns-packet: 1.3.1
+ thunky: 1.1.0
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==
+ /multimatch/3.0.0:
+ dependencies:
+ array-differ: 2.1.0
+ array-union: 1.0.2
+ arrify: 1.0.1
+ minimatch: 3.0.4
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-22foS/gqQfANZ3o+W7ST2x25ueHDVNWl/b9OlGcLpy/iKxjCpvcNCM51YCenUi7Mt/jAjjqv8JwZRs8YP5sRjA==
+ /mute-stream/0.0.7:
+ dev: true
+ resolution:
+ integrity: sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
+ /mute-stream/0.0.8:
+ dev: true
+ resolution:
+ integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
+ /nan/2.14.0:
+ dev: true
+ optional: true
+ resolution:
+ integrity: sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
+ /nanoid/2.1.11:
+ resolution:
+ integrity: sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
+ /nanomatch/1.2.13:
+ dependencies:
+ arr-diff: 4.0.0
+ array-unique: 0.3.2
+ define-property: 2.0.2
+ extend-shallow: 3.0.2
+ fragment-cache: 0.2.1
+ is-windows: 1.0.2
+ kind-of: 6.0.3
+ object.pick: 1.3.0
+ regex-not: 1.0.2
+ snapdragon: 0.8.2
+ to-regex: 3.0.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+ /native-promise-only/0.8.1:
+ dev: true
+ resolution:
+ integrity: sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=
+ /natural-compare/1.4.0:
+ dev: true
+ resolution:
+ integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+ /ncjsm/4.0.1:
+ dependencies:
+ builtin-modules: 3.1.0
+ deferred: 0.7.11
+ es5-ext: 0.10.53
+ es6-set: 0.1.5
+ find-requires: 1.0.0
+ fs2: 0.3.7
+ type: 2.0.0
+ dev: true
+ resolution:
+ integrity: sha512-gxh5Sgait8HyclaulfhgetHQGyhFm00ZQqISIfqtwFVnyWJ20rk+55SUamo9n3KhM6Vk63gemKPxIDYiSV/xZw==
+ /negotiator/0.6.2:
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
+ /neo-async/2.6.1:
+ dev: true
+ resolution:
+ integrity: sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
+ /next-tick/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-yobR/ogoFpsBICCOPchCS524NCw=
+ /next-tick/1.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
+ /nice-try/1.0.5:
+ resolution:
+ integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+ /no-case/3.0.3:
+ dependencies:
+ lower-case: 2.0.1
+ tslib: 1.11.1
+ dev: true
+ resolution:
+ integrity: sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==
+ /node-cache/4.2.1:
+ dependencies:
+ clone: 2.1.2
+ lodash: 4.17.15
+ dev: false
+ engines:
+ node: '>= 0.4.6'
+ resolution:
+ integrity: sha512-BOb67bWg2dTyax5kdef5WfU3X8xu4wPg+zHzkvls0Q/QpYycIFRLEEIdAx9Wma43DxG6Qzn4illdZoYseKWa4A==
+ /node-dir/0.1.17:
+ dependencies:
+ minimatch: 3.0.4
+ dev: true
+ engines:
+ node: '>= 0.10.5'
+ resolution:
+ integrity: sha1-X1Zl2TNRM1yqvvjxxVRRbPXx5OU=
+ /node-fetch/1.7.3:
+ dependencies:
+ encoding: 0.1.12
+ is-stream: 1.1.0
+ dev: true
+ resolution:
+ integrity: sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
+ /node-fetch/2.6.0:
+ engines:
+ node: 4.x || >=6.0.0
+ resolution:
+ integrity: sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
+ /node-forge/0.9.0:
+ dev: true
+ engines:
+ node: '>= 4.5.0'
+ resolution:
+ integrity: sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
+ /node-int64/0.4.0:
+ dev: true
+ resolution:
+ integrity: sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
+ /node-libs-browser/2.2.1:
+ dependencies:
+ assert: 1.5.0
+ browserify-zlib: 0.2.0
+ buffer: 4.9.2
+ console-browserify: 1.2.0
+ constants-browserify: 1.0.0
+ crypto-browserify: 3.12.0
+ domain-browser: 1.2.0
+ events: 3.1.0
+ https-browserify: 1.0.0
+ os-browserify: 0.3.0
+ path-browserify: 0.0.1
+ process: 0.11.10
+ punycode: 1.4.1
+ querystring-es3: 0.2.1
+ readable-stream: 2.3.7
+ stream-browserify: 2.0.2
+ stream-http: 2.8.3
+ string_decoder: 1.3.0
+ timers-browserify: 2.0.11
+ tty-browserify: 0.0.0
+ url: 0.11.0
+ util: 0.11.1
+ vm-browserify: 1.1.2
+ dev: true
+ resolution:
+ integrity: sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==
+ /node-modules-regexp/1.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=
+ /node-notifier/5.4.3:
+ dependencies:
+ growly: 1.3.0
+ is-wsl: 1.1.0
+ semver: 5.7.1
+ shellwords: 0.1.1
+ which: 1.3.1
+ dev: true
+ resolution:
+ integrity: sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==
+ /node-notifier/6.0.0:
+ dependencies:
+ growly: 1.3.0
+ is-wsl: 2.1.1
+ semver: 6.3.0
+ shellwords: 0.1.1
+ which: 1.3.1
+ dev: true
+ optional: true
+ resolution:
+ integrity: sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==
+ /node-releases/1.1.53:
+ dev: true
+ resolution:
+ integrity: sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ==
+ /node-source-walk/4.2.0:
+ dependencies:
+ '@babel/parser': 7.9.4
+ dev: true
+ engines:
+ node: '>=6.0'
+ resolution:
+ integrity: sha512-hPs/QMe6zS94f5+jG3kk9E7TNm4P2SulrKiLWMzKszBfNZvL/V6wseHlTd7IvfW0NZWqPtK3+9yYNr+3USGteA==
+ /nofilter/1.0.3:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-FlUlqwRK6reQCaFLAhMcF+6VkVG2caYjKQY3YsRDTl4/SEch595Qb3oLjJRDr8dkHAAOVj2pOx3VknfnSgkE5g==
+ /normalize-package-data/2.5.0:
+ dependencies:
+ hosted-git-info: 2.8.8
+ resolve: 1.15.1
+ semver: 5.7.1
+ validate-npm-package-license: 3.0.4
+ dev: true
+ resolution:
+ integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
+ /normalize-path/2.1.1:
+ dependencies:
+ remove-trailing-separator: 1.1.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+ /normalize-path/3.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+ /normalize-range/0.1.2:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
+ /normalize-url/1.9.1:
+ dependencies:
+ object-assign: 4.1.1
+ prepend-http: 1.0.4
+ query-string: 4.3.4
+ sort-keys: 1.1.2
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=
+ /normalize-url/2.0.1:
+ dependencies:
+ prepend-http: 2.0.0
+ query-string: 5.1.1
+ sort-keys: 2.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==
+ /normalize-url/3.3.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
+ /normalize-url/4.5.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
+ /npm-conf/1.1.3:
+ dependencies:
+ config-chain: 1.1.12
+ pify: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==
+ /npm-run-path/2.0.2:
+ dependencies:
+ path-key: 2.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
+ /npm-run-path/4.0.1:
+ dependencies:
+ path-key: 3.1.1
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+ /nth-check/1.0.2:
+ dependencies:
+ boolbase: 1.0.0
+ dev: true
+ resolution:
+ integrity: sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+ /num2fraction/1.2.2:
+ dev: true
+ resolution:
+ integrity: sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
+ /number-is-nan/1.0.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+ /numeral/2.0.6:
+ dev: false
+ resolution:
+ integrity: sha1-StCAk21EPCVhrtnyGX7//iX05QY=
+ /nwsapi/2.2.0:
+ dev: true
+ resolution:
+ integrity: sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
+ /oauth-sign/0.9.0:
+ resolution:
+ integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+ /object-assign/4.1.1:
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+ /object-component/0.0.3:
+ dev: true
+ resolution:
+ integrity: sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=
+ /object-copy/0.1.0:
+ dependencies:
+ copy-descriptor: 0.1.1
+ define-property: 0.2.5
+ kind-of: 3.2.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+ /object-hash/2.0.3:
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
+ /object-inspect/1.7.0:
+ resolution:
+ integrity: sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
+ /object-is/1.0.2:
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==
+ /object-keys/1.1.1:
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+ /object-path/0.11.4:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=
+ /object-visit/1.0.1:
+ dependencies:
+ isobject: 3.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+ /object.assign/4.1.0:
+ dependencies:
+ define-properties: 1.1.3
+ function-bind: 1.1.1
+ has-symbols: 1.0.1
+ object-keys: 1.1.1
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+ /object.entries/1.1.1:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ function-bind: 1.1.1
+ has: 1.0.3
+ dev: true
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==
+ /object.fromentries/2.0.2:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ function-bind: 1.1.1
+ has: 1.0.3
+ dev: true
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==
+ /object.getownpropertydescriptors/2.1.0:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ dev: true
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
+ /object.pick/1.3.0:
+ dependencies:
+ isobject: 3.0.1
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+ /object.values/1.1.1:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ function-bind: 1.1.1
+ has: 1.0.3
+ dev: true
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
+ /obuf/1.1.2:
+ dev: true
+ resolution:
+ integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
+ /on-finished/2.3.0:
+ dependencies:
+ ee-first: 1.1.1
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+ /on-headers/1.0.2:
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
+ /once/1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+ dev: true
+ resolution:
+ integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+ /one-time/0.0.4:
+ dev: true
+ resolution:
+ integrity: sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=
+ /onetime/2.0.1:
+ dependencies:
+ mimic-fn: 1.2.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
+ /onetime/5.1.0:
+ dependencies:
+ mimic-fn: 2.1.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==
+ /open/7.0.3:
+ dependencies:
+ is-docker: 2.0.0
+ is-wsl: 2.1.1
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-sP2ru2v0P290WFfv49Ap8MF6PkzGNnGlAwHweB4WR4mr5d2d0woiCluUeJ218w7/+PmoBy9JmYgD5A4mLcWOFA==
+ /opencollective-postinstall/2.0.2:
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==
+ /opn/5.5.0:
+ dependencies:
+ is-wsl: 1.1.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==
+ /optimize-css-assets-webpack-plugin/5.0.3_webpack@4.42.0:
+ dependencies:
+ cssnano: 4.1.10
+ last-call-webpack-plugin: 3.0.0
+ webpack: 4.42.0_webpack@4.42.0
+ dev: true
+ peerDependencies:
+ webpack: ^4.0.0
+ resolution:
+ integrity: sha512-q9fbvCRS6EYtUKKSwI87qm2IxlyJK5b4dygW1rKUBT6mMDhdG5e5bZT63v6tnJR9F9FB/H5a0HTmtw+laUBxKA==
+ /optionator/0.8.3:
+ dependencies:
+ deep-is: 0.1.3
+ fast-levenshtein: 2.0.6
+ levn: 0.3.0
+ prelude-ls: 1.1.2
+ type-check: 0.3.2
+ word-wrap: 1.2.3
+ dev: true
+ engines:
+ node: '>= 0.8.0'
+ resolution:
+ integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
+ /original/1.0.2:
+ dependencies:
+ url-parse: 1.4.7
+ dev: true
+ resolution:
+ integrity: sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==
+ /os-browserify/0.3.0:
+ dev: true
+ resolution:
+ integrity: sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
+ /os-locale/3.1.0:
+ dependencies:
+ execa: 1.0.0
+ lcid: 2.0.0
+ mem: 4.3.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
+ /os-tmpdir/1.0.2:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+ /p-cancelable/0.4.1:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==
+ /p-cancelable/1.1.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
+ /p-defer/1.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
+ /p-each-series/1.0.0:
+ dependencies:
+ p-reduce: 1.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=
+ /p-each-series/2.1.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==
+ /p-event/2.3.1:
+ dependencies:
+ p-timeout: 2.0.1
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==
+ /p-finally/1.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
+ /p-finally/2.0.1:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==
+ /p-is-promise/1.1.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=
+ /p-is-promise/2.1.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==
+ /p-limit/1.3.0:
+ dependencies:
+ p-try: 1.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
+ /p-limit/2.3.0:
+ dependencies:
+ p-try: 2.2.0
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ /p-locate/2.0.0:
+ dependencies:
+ p-limit: 1.3.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
+ /p-locate/3.0.0:
+ dependencies:
+ p-limit: 2.3.0
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+ /p-locate/4.1.0:
+ dependencies:
+ p-limit: 2.3.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ /p-map/2.1.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
+ /p-map/3.0.0:
+ dependencies:
+ aggregate-error: 3.0.1
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==
+ /p-reduce/1.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=
+ /p-retry/3.0.1:
+ dependencies:
+ retry: 0.12.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==
+ /p-timeout/2.0.1:
+ dependencies:
+ p-finally: 1.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==
+ /p-try/1.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
+ /p-try/2.2.0:
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+ /package-json/4.0.1:
+ dependencies:
+ got: 6.7.1
+ registry-auth-token: 3.4.0
+ registry-url: 3.1.0
+ semver: 5.7.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=
+ /package-json/6.5.0:
+ dependencies:
+ got: 9.6.0
+ registry-auth-token: 4.1.1
+ registry-url: 5.1.0
+ semver: 6.3.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==
+ /pako/1.0.11:
+ dev: true
+ resolution:
+ integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+ /parallel-transform/1.2.0:
+ dependencies:
+ cyclist: 1.0.1
+ inherits: 2.0.4
+ readable-stream: 2.3.7
+ dev: true
+ resolution:
+ integrity: sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==
+ /param-case/3.0.3:
+ dependencies:
+ dot-case: 3.0.3
+ tslib: 1.11.1
+ dev: true
+ resolution:
+ integrity: sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==
+ /parent-module/1.0.1:
+ dependencies:
+ callsites: 3.1.0
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+ /parse-asn1/5.1.5:
+ dependencies:
+ asn1.js: 4.10.1
+ browserify-aes: 1.2.0
+ create-hash: 1.2.0
+ evp_bytestokey: 1.0.3
+ pbkdf2: 3.0.17
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==
+ /parse-entities/1.2.2:
+ dependencies:
+ character-entities: 1.2.4
+ character-entities-legacy: 1.1.4
+ character-reference-invalid: 1.1.4
+ is-alphanumerical: 1.0.4
+ is-decimal: 1.0.4
+ is-hexadecimal: 1.0.4
+ dev: false
+ resolution:
+ integrity: sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==
+ /parse-json/2.2.0:
+ dependencies:
+ error-ex: 1.3.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
+ /parse-json/4.0.0:
+ dependencies:
+ error-ex: 1.3.2
+ json-parse-better-errors: 1.0.2
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+ /parse-json/5.0.0:
+ dependencies:
+ '@babel/code-frame': 7.8.3
+ error-ex: 1.3.2
+ json-parse-better-errors: 1.0.2
+ lines-and-columns: 1.1.6
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==
+ /parse-passwd/1.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
+ /parse5/4.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
+ /parse5/5.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
+ /parseqs/0.0.5:
+ dependencies:
+ better-assert: 1.0.2
+ dev: true
+ resolution:
+ integrity: sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=
+ /parseuri/0.0.5:
+ dependencies:
+ better-assert: 1.0.2
+ dev: true
+ resolution:
+ integrity: sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=
+ /parseurl/1.3.3:
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+ /pascal-case/3.1.1:
+ dependencies:
+ no-case: 3.0.3
+ tslib: 1.11.1
+ dev: true
+ resolution:
+ integrity: sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA==
+ /pascalcase/0.1.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+ /path-browserify/0.0.1:
+ dev: true
+ resolution:
+ integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==
+ /path-dirname/1.0.2:
+ dev: true
+ resolution:
+ integrity: sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
+ /path-exists/2.1.0:
+ dependencies:
+ pinkie-promise: 2.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=
+ /path-exists/3.0.0:
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+ /path-exists/4.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+ /path-extra/1.0.3:
+ dev: true
+ resolution:
+ integrity: sha1-fBEhiablDVlXkOetIDfkTkEMEWY=
+ /path-is-absolute/1.0.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+ /path-is-inside/1.0.2:
+ dev: true
+ resolution:
+ integrity: sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
+ /path-key/2.0.1:
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+ /path-key/3.1.1:
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+ /path-loader/1.0.10:
+ dependencies:
+ native-promise-only: 0.8.1
+ superagent: 3.8.3
+ dev: true
+ resolution:
+ integrity: sha512-CMP0v6S6z8PHeJ6NFVyVJm6WyJjIwFvyz2b0n2/4bKdS/0uZa/9sKUlYZzubrn3zuDRU0zIuEDX9DZYQ2ZI8TA==
+ /path-parse/1.0.6:
+ resolution:
+ integrity: sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+ /path-to-regexp/0.1.7:
+ resolution:
+ integrity: sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+ /path-to-regexp/1.8.0:
+ dependencies:
+ isarray: 0.0.1
+ dev: false
+ resolution:
+ integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
+ /path-type/2.0.0:
+ dependencies:
+ pify: 2.3.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=
+ /path-type/3.0.0:
+ dependencies:
+ pify: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
+ /path-type/4.0.0:
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+ /pbkdf2/3.0.17:
+ dependencies:
+ create-hash: 1.2.0
+ create-hmac: 1.1.7
+ ripemd160: 2.0.2
+ safe-buffer: 5.2.0
+ sha.js: 2.4.11
+ engines:
+ node: '>=0.12'
+ resolution:
+ integrity: sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==
+ /pend/1.2.0:
+ dev: true
+ resolution:
+ integrity: sha1-elfrVQpng/kRUzH89GY9XI4AelA=
+ /performance-now/2.1.0:
+ resolution:
+ integrity: sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+ /picomatch/2.2.2:
+ dev: true
+ engines:
+ node: '>=8.6'
+ resolution:
+ integrity: sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
+ /pify/2.3.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+ /pify/3.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+ /pify/4.0.1:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+ /pinkie-promise/2.0.1:
+ dependencies:
+ pinkie: 2.0.4
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-ITXW36ejWMBprJsXh3YogihFD/o=
+ /pinkie/2.0.4:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
+ /pirates/4.0.1:
+ dependencies:
+ node-modules-regexp: 1.0.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==
+ /pkg-dir/1.0.0:
+ dependencies:
+ find-up: 1.1.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-ektQio1bstYp1EcFb/TpyTFM89Q=
+ /pkg-dir/2.0.0:
+ dependencies:
+ find-up: 2.1.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
+ /pkg-dir/3.0.0:
+ dependencies:
+ find-up: 3.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==
+ /pkg-dir/4.2.0:
+ dependencies:
+ find-up: 4.1.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+ /pkg-up/2.0.0:
+ dependencies:
+ find-up: 2.1.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-yBmscoBZpGHKscOImivjxJoATX8=
+ /pkg-up/3.1.0:
+ dependencies:
+ find-up: 3.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==
+ /please-upgrade-node/3.2.0:
+ dependencies:
+ semver-compare: 1.0.0
+ dev: true
+ resolution:
+ integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==
+ /pn/1.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
+ /pnp-webpack-plugin/1.6.4:
+ dependencies:
+ ts-pnp: 1.1.6
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==
+ /popper.js/1.16.1:
+ deprecated: 'You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1'
+ dev: false
+ resolution:
+ integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
+ /portfinder/1.0.25:
+ dependencies:
+ async: 2.6.3
+ debug: 3.2.6
+ mkdirp: 0.5.5
+ dev: true
+ engines:
+ node: '>= 0.12.0'
+ resolution:
+ integrity: sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==
+ /posix-character-classes/0.1.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+ /postcss-attribute-case-insensitive/4.0.2:
+ dependencies:
+ postcss: 7.0.27
+ postcss-selector-parser: 6.0.2
+ dev: true
+ resolution:
+ integrity: sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA==
+ /postcss-browser-comments/3.0.0_browserslist@4.11.1:
+ dependencies:
+ browserslist: 4.11.1
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=8.0.0'
+ peerDependencies:
+ browserslist: ^4
+ resolution:
+ integrity: sha512-qfVjLfq7HFd2e0HW4s1dvU8X080OZdG46fFbIBFjW7US7YPDcWfRvdElvwMJr2LI6hMmD+7LnH2HcmXTs+uOig==
+ /postcss-calc/7.0.2:
+ dependencies:
+ postcss: 7.0.27
+ postcss-selector-parser: 6.0.2
+ postcss-value-parser: 4.0.3
+ dev: true
+ resolution:
+ integrity: sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ==
+ /postcss-color-functional-notation/2.0.1:
+ dependencies:
+ postcss: 7.0.27
+ postcss-values-parser: 2.0.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g==
+ /postcss-color-gray/5.0.0:
+ dependencies:
+ '@csstools/convert-colors': 1.4.0
+ postcss: 7.0.27
+ postcss-values-parser: 2.0.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw==
+ /postcss-color-hex-alpha/5.0.3:
+ dependencies:
+ postcss: 7.0.27
+ postcss-values-parser: 2.0.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw==
+ /postcss-color-mod-function/3.0.3:
+ dependencies:
+ '@csstools/convert-colors': 1.4.0
+ postcss: 7.0.27
+ postcss-values-parser: 2.0.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==
+ /postcss-color-rebeccapurple/4.0.1:
+ dependencies:
+ postcss: 7.0.27
+ postcss-values-parser: 2.0.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g==
+ /postcss-colormin/4.0.3:
+ dependencies:
+ browserslist: 4.11.1
+ color: 3.1.2
+ has: 1.0.3
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==
+ /postcss-convert-values/4.0.1:
+ dependencies:
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==
+ /postcss-custom-media/7.0.8:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg==
+ /postcss-custom-properties/8.0.11:
+ dependencies:
+ postcss: 7.0.27
+ postcss-values-parser: 2.0.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA==
+ /postcss-custom-selectors/5.1.2:
+ dependencies:
+ postcss: 7.0.27
+ postcss-selector-parser: 5.0.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w==
+ /postcss-dir-pseudo-class/5.0.0:
+ dependencies:
+ postcss: 7.0.27
+ postcss-selector-parser: 5.0.0
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ resolution:
+ integrity: sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw==
+ /postcss-discard-comments/4.0.2:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==
+ /postcss-discard-duplicates/4.0.2:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==
+ /postcss-discard-empty/4.0.1:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==
+ /postcss-discard-overridden/4.0.1:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==
+ /postcss-double-position-gradients/1.0.0:
+ dependencies:
+ postcss: 7.0.27
+ postcss-values-parser: 2.0.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA==
+ /postcss-env-function/2.0.2:
+ dependencies:
+ postcss: 7.0.27
+ postcss-values-parser: 2.0.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw==
+ /postcss-flexbugs-fixes/4.1.0:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ resolution:
+ integrity: sha512-jr1LHxQvStNNAHlgco6PzY308zvLklh7SJVYuWUwyUQncofaAlD2l+P/gxKHOdqWKe7xJSkVLFF/2Tp+JqMSZA==
+ /postcss-focus-visible/4.0.0:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g==
+ /postcss-focus-within/3.0.0:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w==
+ /postcss-font-variant/4.0.0:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ resolution:
+ integrity: sha512-M8BFYKOvCrI2aITzDad7kWuXXTm0YhGdP9Q8HanmN4EF1Hmcgs1KK5rSHylt/lUJe8yLxiSwWAHdScoEiIxztg==
+ /postcss-gap-properties/2.0.0:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg==
+ /postcss-image-set-function/3.0.1:
+ dependencies:
+ postcss: 7.0.27
+ postcss-values-parser: 2.0.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw==
+ /postcss-initial/3.0.2:
+ dependencies:
+ lodash.template: 4.5.0
+ postcss: 7.0.27
+ dev: true
+ resolution:
+ integrity: sha512-ugA2wKonC0xeNHgirR4D3VWHs2JcU08WAi1KFLVcnb7IN89phID6Qtg2RIctWbnvp1TM2BOmDtX8GGLCKdR8YA==
+ /postcss-lab-function/2.0.1:
+ dependencies:
+ '@csstools/convert-colors': 1.4.0
+ postcss: 7.0.27
+ postcss-values-parser: 2.0.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg==
+ /postcss-load-config/2.1.0:
+ dependencies:
+ cosmiconfig: 5.2.1
+ import-cwd: 2.1.0
+ dev: true
+ engines:
+ node: '>= 4'
+ resolution:
+ integrity: sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==
+ /postcss-loader/3.0.0:
+ dependencies:
+ loader-utils: 1.4.0
+ postcss: 7.0.27
+ postcss-load-config: 2.1.0
+ schema-utils: 1.0.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==
+ /postcss-logical/3.0.0:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA==
+ /postcss-media-minmax/4.0.0:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw==
+ /postcss-merge-longhand/4.0.11:
+ dependencies:
+ css-color-names: 0.0.4
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ stylehacks: 4.0.3
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==
+ /postcss-merge-rules/4.0.3:
+ dependencies:
+ browserslist: 4.11.1
+ caniuse-api: 3.0.0
+ cssnano-util-same-parent: 4.0.1
+ postcss: 7.0.27
+ postcss-selector-parser: 3.1.2
+ vendors: 1.0.4
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==
+ /postcss-minify-font-values/4.0.2:
+ dependencies:
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==
+ /postcss-minify-gradients/4.0.2:
+ dependencies:
+ cssnano-util-get-arguments: 4.0.0
+ is-color-stop: 1.1.0
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==
+ /postcss-minify-params/4.0.2:
+ dependencies:
+ alphanum-sort: 1.0.2
+ browserslist: 4.11.1
+ cssnano-util-get-arguments: 4.0.0
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ uniqs: 2.0.0
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==
+ /postcss-minify-selectors/4.0.2:
+ dependencies:
+ alphanum-sort: 1.0.2
+ has: 1.0.3
+ postcss: 7.0.27
+ postcss-selector-parser: 3.1.2
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==
+ /postcss-modules-extract-imports/2.0.0:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
+ /postcss-modules-local-by-default/3.0.2:
+ dependencies:
+ icss-utils: 4.1.1
+ postcss: 7.0.27
+ postcss-selector-parser: 6.0.2
+ postcss-value-parser: 4.0.3
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==
+ /postcss-modules-scope/2.2.0:
+ dependencies:
+ postcss: 7.0.27
+ postcss-selector-parser: 6.0.2
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==
+ /postcss-modules-values/3.0.0:
+ dependencies:
+ icss-utils: 4.1.1
+ postcss: 7.0.27
+ dev: true
+ resolution:
+ integrity: sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==
+ /postcss-nesting/7.0.1:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg==
+ /postcss-normalize-charset/4.0.1:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==
+ /postcss-normalize-display-values/4.0.2:
+ dependencies:
+ cssnano-util-get-match: 4.0.0
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==
+ /postcss-normalize-positions/4.0.2:
+ dependencies:
+ cssnano-util-get-arguments: 4.0.0
+ has: 1.0.3
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==
+ /postcss-normalize-repeat-style/4.0.2:
+ dependencies:
+ cssnano-util-get-arguments: 4.0.0
+ cssnano-util-get-match: 4.0.0
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==
+ /postcss-normalize-string/4.0.2:
+ dependencies:
+ has: 1.0.3
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==
+ /postcss-normalize-timing-functions/4.0.2:
+ dependencies:
+ cssnano-util-get-match: 4.0.0
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==
+ /postcss-normalize-unicode/4.0.1:
+ dependencies:
+ browserslist: 4.11.1
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==
+ /postcss-normalize-url/4.0.1:
+ dependencies:
+ is-absolute-url: 2.1.0
+ normalize-url: 3.3.0
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==
+ /postcss-normalize-whitespace/4.0.2:
+ dependencies:
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==
+ /postcss-normalize/8.0.1:
+ dependencies:
+ '@csstools/normalize.css': 10.1.0
+ browserslist: 4.11.1
+ postcss: 7.0.27
+ postcss-browser-comments: 3.0.0_browserslist@4.11.1
+ sanitize.css: 10.0.0
+ dev: true
+ engines:
+ node: '>=8.0.0'
+ resolution:
+ integrity: sha512-rt9JMS/m9FHIRroDDBGSMsyW1c0fkvOJPy62ggxSHUldJO7B195TqFMqIf+lY5ezpDcYOV4j86aUp3/XbxzCCQ==
+ /postcss-ordered-values/4.1.2:
+ dependencies:
+ cssnano-util-get-arguments: 4.0.0
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==
+ /postcss-overflow-shorthand/2.0.0:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g==
+ /postcss-page-break/2.0.0:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ resolution:
+ integrity: sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ==
+ /postcss-place/4.0.1:
+ dependencies:
+ postcss: 7.0.27
+ postcss-values-parser: 2.0.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg==
+ /postcss-preset-env/6.7.0:
+ dependencies:
+ autoprefixer: 9.7.6
+ browserslist: 4.11.1
+ caniuse-lite: 1.0.30001039
+ css-blank-pseudo: 0.1.4
+ css-has-pseudo: 0.10.0
+ css-prefers-color-scheme: 3.1.1
+ cssdb: 4.4.0
+ postcss: 7.0.27
+ postcss-attribute-case-insensitive: 4.0.2
+ postcss-color-functional-notation: 2.0.1
+ postcss-color-gray: 5.0.0
+ postcss-color-hex-alpha: 5.0.3
+ postcss-color-mod-function: 3.0.3
+ postcss-color-rebeccapurple: 4.0.1
+ postcss-custom-media: 7.0.8
+ postcss-custom-properties: 8.0.11
+ postcss-custom-selectors: 5.1.2
+ postcss-dir-pseudo-class: 5.0.0
+ postcss-double-position-gradients: 1.0.0
+ postcss-env-function: 2.0.2
+ postcss-focus-visible: 4.0.0
+ postcss-focus-within: 3.0.0
+ postcss-font-variant: 4.0.0
+ postcss-gap-properties: 2.0.0
+ postcss-image-set-function: 3.0.1
+ postcss-initial: 3.0.2
+ postcss-lab-function: 2.0.1
+ postcss-logical: 3.0.0
+ postcss-media-minmax: 4.0.0
+ postcss-nesting: 7.0.1
+ postcss-overflow-shorthand: 2.0.0
+ postcss-page-break: 2.0.0
+ postcss-place: 4.0.1
+ postcss-pseudo-class-any-link: 6.0.0
+ postcss-replace-overflow-wrap: 3.0.0
+ postcss-selector-matches: 4.0.0
+ postcss-selector-not: 4.0.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg==
+ /postcss-pseudo-class-any-link/6.0.0:
+ dependencies:
+ postcss: 7.0.27
+ postcss-selector-parser: 5.0.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew==
+ /postcss-reduce-initial/4.0.3:
+ dependencies:
+ browserslist: 4.11.1
+ caniuse-api: 3.0.0
+ has: 1.0.3
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==
+ /postcss-reduce-transforms/4.0.2:
+ dependencies:
+ cssnano-util-get-match: 4.0.0
+ has: 1.0.3
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==
+ /postcss-replace-overflow-wrap/3.0.0:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ resolution:
+ integrity: sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw==
+ /postcss-safe-parser/4.0.1:
+ dependencies:
+ postcss: 7.0.27
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-xZsFA3uX8MO3yAda03QrG3/Eg1LN3EPfjjf07vke/46HERLZyHrTsQ9E1r1w1W//fWEhtYNndo2hQplN2cVpCQ==
+ /postcss-selector-matches/4.0.0:
+ dependencies:
+ balanced-match: 1.0.0
+ postcss: 7.0.27
+ dev: true
+ resolution:
+ integrity: sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==
+ /postcss-selector-not/4.0.0:
+ dependencies:
+ balanced-match: 1.0.0
+ postcss: 7.0.27
+ dev: true
+ resolution:
+ integrity: sha512-W+bkBZRhqJaYN8XAnbbZPLWMvZD1wKTu0UxtFKdhtGjWYmxhkUneoeOhRJKdAE5V7ZTlnbHfCR+6bNwK9e1dTQ==
+ /postcss-selector-parser/3.1.2:
+ dependencies:
+ dot-prop: 5.2.0
+ indexes-of: 1.0.1
+ uniq: 1.0.1
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==
+ /postcss-selector-parser/5.0.0:
+ dependencies:
+ cssesc: 2.0.0
+ indexes-of: 1.0.1
+ uniq: 1.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==
+ /postcss-selector-parser/6.0.2:
+ dependencies:
+ cssesc: 3.0.0
+ indexes-of: 1.0.1
+ uniq: 1.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
+ /postcss-svgo/4.0.2:
+ dependencies:
+ is-svg: 3.0.0
+ postcss: 7.0.27
+ postcss-value-parser: 3.3.1
+ svgo: 1.3.2
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==
+ /postcss-unique-selectors/4.0.1:
+ dependencies:
+ alphanum-sort: 1.0.2
+ postcss: 7.0.27
+ uniqs: 2.0.0
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==
+ /postcss-value-parser/3.3.1:
+ dev: true
+ resolution:
+ integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
+ /postcss-value-parser/4.0.3:
+ dev: true
+ resolution:
+ integrity: sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==
+ /postcss-values-parser/1.5.0:
+ dependencies:
+ flatten: 1.0.3
+ indexes-of: 1.0.1
+ uniq: 1.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-3M3p+2gMp0AH3da530TlX8kiO1nxdTnc3C6vr8dMxRLIlh8UYkz0/wcwptSXjhtx2Fr0TySI7a+BHDQ8NL7LaQ==
+ /postcss-values-parser/2.0.1:
+ dependencies:
+ flatten: 1.0.3
+ indexes-of: 1.0.1
+ uniq: 1.0.1
+ dev: true
+ engines:
+ node: '>=6.14.4'
+ resolution:
+ integrity: sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==
+ /postcss/7.0.21:
+ dependencies:
+ chalk: 2.4.2
+ source-map: 0.6.1
+ supports-color: 6.1.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==
+ /postcss/7.0.27:
+ dependencies:
+ chalk: 2.4.2
+ source-map: 0.6.1
+ supports-color: 6.1.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==
+ /precinct/6.2.0:
+ dependencies:
+ commander: 2.20.3
+ debug: 4.1.1
+ detective-amd: 3.0.0
+ detective-cjs: 3.1.1
+ detective-es6: 2.1.0
+ detective-less: 1.0.2
+ detective-postcss: 3.0.1
+ detective-sass: 3.0.1
+ detective-scss: 2.0.1
+ detective-stylus: 1.0.0
+ detective-typescript: 5.7.0
+ module-definition: 3.3.0
+ node-source-walk: 4.2.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-BCAmnOxZzobF3H1/h/gq70pEyvX/BVLWCrzi8beFD22dqu5Z14qOghNUsI24Wg8oaTsGFcIjOGtFX5L9ttmjVg==
+ /prelude-ls/1.1.2:
+ dev: true
+ engines:
+ node: '>= 0.8.0'
+ resolution:
+ integrity: sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+ /prepend-http/1.0.4:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
+ /prepend-http/2.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
+ /prettier-linter-helpers/1.0.0:
+ dependencies:
+ fast-diff: 1.2.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
+ /prettier/1.19.1:
+ dev: true
+ engines:
+ node: '>=4'
+ hasBin: true
+ resolution:
+ integrity: sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
+ /pretty-bytes/5.3.0:
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==
+ /pretty-error/2.1.1:
+ dependencies:
+ renderkid: 2.0.3
+ utila: 0.4.0
+ dev: true
+ resolution:
+ integrity: sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=
+ /pretty-format/24.9.0:
+ dependencies:
+ '@jest/types': 24.9.0
+ ansi-regex: 4.1.0
+ ansi-styles: 3.2.1
+ react-is: 16.13.1
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==
+ /pretty-format/25.3.0:
+ dependencies:
+ '@jest/types': 25.3.0
+ ansi-regex: 5.0.0
+ ansi-styles: 4.2.1
+ react-is: 16.13.1
+ dev: true
+ engines:
+ node: '>= 8.3'
+ resolution:
+ integrity: sha512-wToHwF8bkQknIcFkBqNfKu4+UZqnrLn/Vr+wwKQwwvPzkBfDDKp/qIabFqdgtoi5PEnM8LFByVsOrHoa3SpTVA==
+ /pretty-quick/1.11.1_prettier@1.19.1:
+ dependencies:
+ chalk: 2.4.2
+ execa: 0.8.0
+ find-up: 2.1.0
+ ignore: 3.3.10
+ mri: 1.1.5
+ multimatch: 3.0.0
+ prettier: 1.19.1
+ dev: true
+ hasBin: true
+ peerDependencies:
+ prettier: '>=1.8.0'
+ resolution:
+ integrity: sha512-kSXCkcETfak7EQXz6WOkCeCqpbC4GIzrN/vaneTGMP/fAtD8NerA9bPhCUqHAks1geo7biZNl5uEMPceeneLuA==
+ /prettyoutput/1.2.0:
+ dependencies:
+ colors: 1.3.3
+ commander: 2.19.0
+ lodash: 4.17.15
+ dev: true
+ engines:
+ node: '>=4'
+ hasBin: true
+ resolution:
+ integrity: sha512-G2gJwLzLcYS+2m6bTAe+CcDpwak9YpcvpScI0tE4WYb2O3lEZD/YywkMNpGqsSx5wttGvh2UXaKROTKKCyM2dw==
+ /prismjs/1.17.1:
+ dev: false
+ optionalDependencies:
+ clipboard: 2.0.6
+ resolution:
+ integrity: sha512-PrEDJAFdUGbOP6xK/UsfkC5ghJsPJviKgnQOoxaDbBjwc8op68Quupwt1DeAFoG8GImPhiKXAvvsH7wDSLsu1Q==
+ /prismjs/1.20.0:
+ dev: false
+ optionalDependencies:
+ clipboard: 2.0.6
+ resolution:
+ integrity: sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ==
+ /private/0.1.8:
+ dev: true
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
+ /process-nextick-args/2.0.1:
+ dev: true
+ resolution:
+ integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+ /process/0.11.10:
+ dev: true
+ engines:
+ node: '>= 0.6.0'
+ resolution:
+ integrity: sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
+ /progress/2.0.3:
+ dev: true
+ engines:
+ node: '>=0.4.0'
+ resolution:
+ integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+ /promise-inflight/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+ /promise-polyfill/8.1.3:
+ dev: false
+ resolution:
+ integrity: sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g==
+ /promise-queue/2.2.5:
+ dev: true
+ engines:
+ node: '>= 0.8.0'
+ resolution:
+ integrity: sha1-L29ffA9tCBCelnZZx5uIqe1ek7Q=
+ /promise/8.1.0:
+ dependencies:
+ asap: 2.0.6
+ dev: true
+ resolution:
+ integrity: sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==
+ /prompts/2.3.2:
+ dependencies:
+ kleur: 3.0.3
+ sisteransi: 1.0.5
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==
+ /prop-types/15.7.2:
+ dependencies:
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ react-is: 16.13.1
+ resolution:
+ integrity: sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
+ /property-information/5.4.0:
+ dependencies:
+ xtend: 4.0.2
+ dev: false
+ resolution:
+ integrity: sha512-nmMWAm/3vKFGmmOWOcdLjgq/Hlxa+hsuR/px1Lp/UGEyc5A22A6l78Shc2C0E71sPmAqglni+HrS7L7VJ7AUCA==
+ /proto-list/1.2.4:
+ dev: true
+ resolution:
+ integrity: sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
+ /proxy-addr/2.0.6:
+ dependencies:
+ forwarded: 0.1.2
+ ipaddr.js: 1.9.1
+ engines:
+ node: '>= 0.10'
+ resolution:
+ integrity: sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
+ /prr/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-0/wRS6BplaRexok/SEzrHXj19HY=
+ /pseudomap/1.0.2:
+ dev: true
+ resolution:
+ integrity: sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
+ /psl/1.8.0:
+ resolution:
+ integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
+ /public-encrypt/4.0.3:
+ dependencies:
+ bn.js: 4.11.8
+ browserify-rsa: 4.0.1
+ create-hash: 1.2.0
+ parse-asn1: 5.1.5
+ randombytes: 2.1.0
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==
+ /pump/2.0.1:
+ dependencies:
+ end-of-stream: 1.4.4
+ once: 1.4.0
+ dev: true
+ resolution:
+ integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
+ /pump/3.0.0:
+ dependencies:
+ end-of-stream: 1.4.4
+ once: 1.4.0
+ dev: true
+ resolution:
+ integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+ /pumpify/1.5.1:
+ dependencies:
+ duplexify: 3.7.1
+ inherits: 2.0.4
+ pump: 2.0.1
+ dev: true
+ resolution:
+ integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==
+ /punycode/1.3.2:
+ resolution:
+ integrity: sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
+ /punycode/1.4.1:
+ dev: true
+ resolution:
+ integrity: sha1-wNWmOycYgArY4esPpSachN1BhF4=
+ /punycode/2.1.1:
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+ /q/1.5.1:
+ dev: true
+ engines:
+ node: '>=0.6.0'
+ teleport: '>=0.2.0'
+ resolution:
+ integrity: sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
+ /qs/6.5.2:
+ engines:
+ node: '>=0.6'
+ resolution:
+ integrity: sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+ /qs/6.7.0:
+ engines:
+ node: '>=0.6'
+ resolution:
+ integrity: sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
+ /qs/6.9.3:
+ dev: true
+ engines:
+ node: '>=0.6'
+ resolution:
+ integrity: sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==
+ /query-string/4.3.4:
+ dependencies:
+ object-assign: 4.1.1
+ strict-uri-encode: 1.1.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-u7aTucqRXCMlFbIosaArYJBD2+s=
+ /query-string/5.1.1:
+ dependencies:
+ decode-uri-component: 0.2.0
+ object-assign: 4.1.1
+ strict-uri-encode: 1.1.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==
+ /querystring-es3/0.2.1:
+ dev: true
+ engines:
+ node: '>=0.4.x'
+ resolution:
+ integrity: sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
+ /querystring/0.2.0:
+ engines:
+ node: '>=0.4.x'
+ resolution:
+ integrity: sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
+ /querystringify/2.1.1:
+ dev: true
+ resolution:
+ integrity: sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
+ /raf-schd/4.0.2:
+ dev: false
+ resolution:
+ integrity: sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
+ /raf/3.4.1:
+ dependencies:
+ performance-now: 2.1.0
+ dev: true
+ resolution:
+ integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
+ /ramda/0.25.0:
+ dev: true
+ resolution:
+ integrity: sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ==
+ /ramda/0.26.1:
+ dev: true
+ resolution:
+ integrity: sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==
+ /randombytes/2.1.0:
+ dependencies:
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+ /randomfill/1.0.4:
+ dependencies:
+ randombytes: 2.1.0
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==
+ /range-parser/1.2.1:
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+ /raven/1.2.1:
+ dependencies:
+ cookie: 0.3.1
+ json-stringify-safe: 5.0.1
+ lsmod: 1.0.0
+ stack-trace: 0.0.9
+ uuid: 3.0.0
+ dev: true
+ engines:
+ node: '>= 0.8.0'
+ hasBin: true
+ resolution:
+ integrity: sha1-lJwTTbAooZC3u/j3kKrlQbfAIL0=
+ /raw-body/2.4.0:
+ dependencies:
+ bytes: 3.1.0
+ http-errors: 1.7.2
+ iconv-lite: 0.4.24
+ unpipe: 1.0.0
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
+ /rc/1.2.8:
+ dependencies:
+ deep-extend: 0.6.0
+ ini: 1.3.5
+ minimist: 1.2.5
+ strip-json-comments: 2.0.1
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+ /react-app-polyfill/1.0.6:
+ dependencies:
+ core-js: 3.6.4
+ object-assign: 4.1.1
+ promise: 8.1.0
+ raf: 3.4.1
+ regenerator-runtime: 0.13.5
+ whatwg-fetch: 3.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-OfBnObtnGgLGfweORmdZbyEz+3dgVePQBb3zipiaDsMHV1NpWm0rDFYIVXFV/AK+x4VIIfWHhrdMIeoTLyRr2g==
+ /react-avatar/3.9.2_prop-types@15.7.2+react@16.13.1:
+ dependencies:
+ core-js: 3.6.4
+ is-retina: 1.0.3
+ md5: 2.2.1
+ prop-types: 15.7.2
+ react: 16.13.1
+ dev: false
+ peerDependencies:
+ prop-types: ^15.0.0 || ^16.0.0
+ react: ^15.0.0 || ^16.0.0
+ resolution:
+ integrity: sha512-ow20ap4guO/3OVgo50gu3GJTGzjFiswuVVEJja1zFpw7H9cj/DeqAELVfEb5zgsi81Cq3progilPlypxtpPZiQ==
+ /react-beautiful-dnd/11.0.5_react-dom@16.13.1+react@16.13.1:
+ dependencies:
+ '@babel/runtime-corejs2': 7.9.2
+ css-box-model: 1.2.0
+ memoize-one: 5.1.1
+ raf-schd: 4.0.2
+ react: 16.13.1
+ react-redux: 7.2.0_49f644e2f7de4182503f8b93abece808
+ redux: 4.0.5
+ tiny-invariant: 1.1.0
+ use-memo-one: 1.1.1_react@16.13.1
+ dev: false
+ peerDependencies:
+ react: ^16.8.5
+ react-dom: '*'
+ resolution:
+ integrity: sha512-7llby9U+jIfkINcyxPHVWU0HFYzqxMemUYgGHsFsbx4fZo1n/pW6sYKYzhxGxR3Ap5HxqswcQkKUZX4uEUWhlw==
+ /react-chartjs-2/2.9.0_993042a9951b76ec634148f49ee6c836:
+ dependencies:
+ chart.js: 2.9.3
+ lodash: 4.17.15
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ dev: false
+ peerDependencies:
+ chart.js: ^2.3
+ react: ^0.14.0 || ^15.0.0 || ^16.0.0-beta || ^16.0.0
+ react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0-beta || ^16.0.0
+ resolution:
+ integrity: sha512-IYwqUUnQRAJ9SNA978vxulHJTcUFTJk2LDVfbAyk0TnJFZZG7+6U/2flsE4MCw6WCbBjTTypy8T82Ch7XrPtRw==
+ /react-copy-to-clipboard/5.0.2_react@16.13.1:
+ dependencies:
+ copy-to-clipboard: 3.3.1
+ prop-types: 15.7.2
+ react: 16.13.1
+ dev: false
+ peerDependencies:
+ react: ^15.3.0 || ^16.0.0
+ resolution:
+ integrity: sha512-/2t5mLMMPuN5GmdXo6TebFa8IoFxZ+KTDDqYhcDm0PhkgEzSxVvIX26G20s1EB02A4h2UZgwtfymZ3lGJm0OLg==
+ /react-dev-utils/10.2.1:
+ dependencies:
+ '@babel/code-frame': 7.8.3
+ address: 1.1.2
+ browserslist: 4.10.0
+ chalk: 2.4.2
+ cross-spawn: 7.0.1
+ detect-port-alt: 1.1.6
+ escape-string-regexp: 2.0.0
+ filesize: 6.0.1
+ find-up: 4.1.0
+ fork-ts-checker-webpack-plugin: 3.1.1
+ global-modules: 2.0.0
+ globby: 8.0.2
+ gzip-size: 5.1.1
+ immer: 1.10.0
+ inquirer: 7.0.4
+ is-root: 2.1.0
+ loader-utils: 1.2.3
+ open: 7.0.3
+ pkg-up: 3.1.0
+ react-error-overlay: 6.0.7
+ recursive-readdir: 2.2.2
+ shell-quote: 1.7.2
+ strip-ansi: 6.0.0
+ text-table: 0.2.0
+ dev: true
+ engines:
+ node: '>=8.10'
+ resolution:
+ integrity: sha512-XxTbgJnYZmxuPtY3y/UV0D8/65NKkmaia4rXzViknVnZeVlklSh8u6TnaEYPfAi/Gh1TP4mEOXHI6jQOPbeakQ==
+ /react-dom/16.13.1_react@16.13.1:
+ dependencies:
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ prop-types: 15.7.2
+ react: 16.13.1
+ scheduler: 0.19.1
+ dev: false
+ peerDependencies:
+ react: ^16.13.1
+ resolution:
+ integrity: sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
+ /react-dotdotdot/1.3.1_eb0d650be231ffd0ace4a30b38162117:
+ dependencies:
+ object.pick: 1.3.0
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ dev: false
+ peerDependencies:
+ prop-types: '*'
+ react: '*'
+ react-dom: '*'
+ resolution:
+ integrity: sha512-ImqoKTD4ZdyfF/h7jdPCZur01QlZxx3A9/gZSf9mbvseNZwVTvd+dPwi/hg1UTtP+30luy2d5j0KG+XEfdBPLQ==
+ /react-dropzone/10.2.2_react@16.13.1:
+ dependencies:
+ attr-accept: 2.1.0
+ file-selector: 0.1.12
+ prop-types: 15.7.2
+ react: 16.13.1
+ dev: false
+ engines:
+ node: '>= 8'
+ peerDependencies:
+ react: '>= 16.8'
+ resolution:
+ integrity: sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA==
+ /react-easy-swipe/0.0.18:
+ dependencies:
+ prop-types: 15.7.2
+ dev: false
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-IddCZANbT0qVbGFEihfWOkZb/rFpeA3VV87SNOOqPzmSZ93G0nDSyHD28zuGhYJilwEP33MqYv/dwo+zaZha3Q==
+ /react-error-overlay/6.0.7:
+ dev: true
+ resolution:
+ integrity: sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
+ /react-idle-timer/4.2.12_eb0d650be231ffd0ace4a30b38162117:
+ dependencies:
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ dev: false
+ peerDependencies:
+ prop-types: ^15.x.x
+ react: ^16.x.x
+ react-dom: ^16.x.x
+ resolution:
+ integrity: sha512-YD/2Oe4PU5uRv/TH6zTxykKMHpRHWHPEWCUohda81o/jzsrlgyUrklfy46fd8WjgYhlNkJKsiX/GXJAQQC1hcQ==
+ /react-input-autosize/2.2.2_react@16.13.1:
+ dependencies:
+ prop-types: 15.7.2
+ react: 16.13.1
+ dev: false
+ peerDependencies:
+ react: ^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0
+ resolution:
+ integrity: sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==
+ /react-is/16.13.1:
+ resolution:
+ integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+ /react-popper/1.3.7_react@16.13.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ create-react-context: 0.3.0_prop-types@15.7.2+react@16.13.1
+ deep-equal: 1.1.1
+ popper.js: 1.16.1
+ prop-types: 15.7.2
+ react: 16.13.1
+ typed-styles: 0.0.7
+ warning: 4.0.3
+ dev: false
+ peerDependencies:
+ react: 0.14.x || ^15.0.0 || ^16.0.0
+ resolution:
+ integrity: sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==
+ /react-redux/7.2.0_49f644e2f7de4182503f8b93abece808:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ hoist-non-react-statics: 3.3.2
+ loose-envify: 1.4.0
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ react-is: 16.13.1
+ redux: 4.0.5
+ dev: false
+ peerDependencies:
+ react: ^16.8.3
+ react-dom: '*'
+ react-native: '*'
+ redux: ^2.0.0 || ^3.0.0 || ^4.0.0-0
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ resolution:
+ integrity: sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==
+ /react-responsive-carousel/3.1.57:
+ dependencies:
+ classnames: 2.2.6
+ prop-types: 15.7.2
+ react-easy-swipe: 0.0.18
+ dev: false
+ resolution:
+ integrity: sha512-26NR93dsNUUVUjhcdB0AeCqXwk4Q/9mamp1zE1mCMgfK2XWA4SGRfHdfvngc5DKPUhBgF+m3sc+TDhplCutZDw==
+ /react-router-dom/5.1.2_react@16.13.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ history: 4.10.1
+ loose-envify: 1.4.0
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-router: 5.1.2_react@16.13.1
+ tiny-invariant: 1.1.0
+ tiny-warning: 1.0.3
+ dev: false
+ peerDependencies:
+ react: '>=15'
+ resolution:
+ integrity: sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==
+ /react-router/5.1.2_react@16.13.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ history: 4.10.1
+ hoist-non-react-statics: 3.3.2
+ loose-envify: 1.4.0
+ mini-create-react-context: 0.3.2_prop-types@15.7.2+react@16.13.1
+ path-to-regexp: 1.8.0
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-is: 16.13.1
+ tiny-invariant: 1.1.0
+ tiny-warning: 1.0.3
+ dev: false
+ peerDependencies:
+ react: '>=15'
+ resolution:
+ integrity: sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==
+ /react-scripts/3.4.1:
+ dependencies:
+ '@babel/core': 7.9.0
+ '@svgr/webpack': 4.3.3
+ '@typescript-eslint/eslint-plugin': 2.27.0_9e31f0f459c1656d0a7ef30429cc70f8
+ '@typescript-eslint/parser': 2.27.0_eslint@6.8.0
+ babel-eslint: 10.1.0_eslint@6.8.0
+ babel-jest: 24.9.0_@babel+core@7.9.0
+ babel-loader: 8.1.0_@babel+core@7.9.0+webpack@4.42.0
+ babel-plugin-named-asset-import: 0.3.6_@babel+core@7.9.0
+ babel-preset-react-app: 9.1.2
+ camelcase: 5.3.1
+ case-sensitive-paths-webpack-plugin: 2.3.0
+ css-loader: 3.4.2_webpack@4.42.0
+ dotenv: 8.2.0
+ dotenv-expand: 5.1.0
+ eslint: 6.8.0
+ eslint-config-react-app: 5.2.1_c14ecc97ba42c4e073f7e6502a3f179f
+ eslint-loader: 3.0.3_eslint@6.8.0+webpack@4.42.0
+ eslint-plugin-flowtype: 4.6.0_eslint@6.8.0
+ eslint-plugin-import: 2.20.1_eslint@6.8.0
+ eslint-plugin-jsx-a11y: 6.2.3_eslint@6.8.0
+ eslint-plugin-react: 7.19.0_eslint@6.8.0
+ eslint-plugin-react-hooks: 1.7.0_eslint@6.8.0
+ file-loader: 4.3.0_webpack@4.42.0
+ fs-extra: 8.1.0
+ html-webpack-plugin: 4.0.0-beta.11_webpack@4.42.0
+ identity-obj-proxy: 3.0.0
+ jest: 24.9.0
+ jest-environment-jsdom-fourteen: 1.0.1
+ jest-resolve: 24.9.0_jest-resolve@24.9.0
+ jest-watch-typeahead: 0.4.2
+ mini-css-extract-plugin: 0.9.0_webpack@4.42.0
+ optimize-css-assets-webpack-plugin: 5.0.3_webpack@4.42.0
+ pnp-webpack-plugin: 1.6.4
+ postcss-flexbugs-fixes: 4.1.0
+ postcss-loader: 3.0.0
+ postcss-normalize: 8.0.1
+ postcss-preset-env: 6.7.0
+ postcss-safe-parser: 4.0.1
+ react-app-polyfill: 1.0.6
+ react-dev-utils: 10.2.1
+ resolve: 1.15.0
+ resolve-url-loader: 3.1.1
+ sass-loader: 8.0.2_webpack@4.42.0
+ semver: 6.3.0
+ style-loader: 0.23.1
+ terser-webpack-plugin: 2.3.5_webpack@4.42.0
+ ts-pnp: 1.1.6
+ url-loader: 2.3.0_file-loader@4.3.0+webpack@4.42.0
+ webpack: 4.42.0_webpack@4.42.0
+ webpack-dev-server: 3.10.3_webpack@4.42.0
+ webpack-manifest-plugin: 2.2.0_webpack@4.42.0
+ workbox-webpack-plugin: 4.3.1_webpack@4.42.0
+ dev: true
+ engines:
+ node: '>=8.10'
+ hasBin: true
+ optionalDependencies:
+ fsevents: 2.1.2
+ peerDependencies:
+ typescript: ^3.2.1
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ resolution:
+ integrity: sha512-JpTdi/0Sfd31mZA6Ukx+lq5j1JoKItX7qqEK4OiACjVQletM1P38g49d9/D0yTxp9FrSF+xpJFStkGgKEIRjlQ==
+ /react-select/3.1.0_react-dom@16.13.1+react@16.13.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ '@emotion/cache': 10.0.29
+ '@emotion/core': 10.0.28_react@16.13.1
+ '@emotion/css': 10.0.27
+ memoize-one: 5.1.1
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ react-input-autosize: 2.2.2_react@16.13.1
+ react-transition-group: 4.3.0_react-dom@16.13.1+react@16.13.1
+ dev: false
+ peerDependencies:
+ react: ^16.8.0
+ react-dom: ^16.8.0
+ resolution:
+ integrity: sha512-wBFVblBH1iuCBprtpyGtd1dGMadsG36W5/t2Aj8OE6WbByDg5jIFyT7X5gT+l0qmT5TqWhxX+VsKJvCEl2uL9g==
+ /react-sparklines/1.7.0_react-dom@16.13.1+react@16.13.1:
+ dependencies:
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ dev: false
+ peerDependencies:
+ react: '*'
+ react-dom: '*'
+ resolution:
+ integrity: sha512-bJFt9K4c5Z0k44G8KtxIhbG+iyxrKjBZhdW6afP+R7EnIq+iKjbWbEFISrf3WKNFsda+C46XAfnX0StS5fbDcg==
+ /react-syntax-highlighter/11.0.2_react@16.13.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ highlight.js: 9.13.1
+ lowlight: 1.11.0
+ prismjs: 1.20.0
+ react: 16.13.1
+ refractor: 2.10.1
+ dev: false
+ peerDependencies:
+ react: '>= 0.14.0'
+ resolution:
+ integrity: sha512-kqmpM2OH5OodInbEADKARwccwSQWBfZi0970l5Jhp4h39q9Q65C4frNcnd6uHE5pR00W8pOWj9HDRntj2G4Rww==
+ /react-table/6.11.5_eb0d650be231ffd0ace4a30b38162117:
+ dependencies:
+ '@types/react-table': 6.8.7
+ classnames: 2.2.6
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ react-is: 16.13.1
+ dev: false
+ peerDependencies:
+ prop-types: ^15.7.0
+ react: ^16.x.x
+ react-dom: ^16.x.x
+ resolution:
+ integrity: sha512-LM+AS9v//7Y7lAlgTWW/cW6Sn5VOb3EsSkKQfQTzOW8FngB1FUskLLNEVkAYsTX9LjOWR3QlGjykJqCE6eXT/g==
+ /react-timeago/4.4.0_react@16.13.1:
+ dependencies:
+ react: 16.13.1
+ dev: false
+ peerDependencies:
+ react: ^15.0.0 || ^16.0.0
+ resolution:
+ integrity: sha512-Zj8RchTqZEH27LAANemzMR2RpotbP2aMd+UIajfYMZ9KW4dMcViUVKzC7YmqfiqlFfz8B0bjDw2xUBjmcxDngA==
+ /react-transition-group/4.3.0_react-dom@16.13.1+react@16.13.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ dom-helpers: 5.1.4
+ loose-envify: 1.4.0
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ dev: false
+ peerDependencies:
+ react: '>=16.6.0'
+ react-dom: '>=16.6.0'
+ resolution:
+ integrity: sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw==
+ /react/16.13.1:
+ dependencies:
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ prop-types: 15.7.2
+ dev: false
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
+ /read-pkg-up/2.0.0:
+ dependencies:
+ find-up: 2.1.0
+ read-pkg: 2.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=
+ /read-pkg-up/4.0.0:
+ dependencies:
+ find-up: 3.0.0
+ read-pkg: 3.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==
+ /read-pkg/2.0.0:
+ dependencies:
+ load-json-file: 2.0.0
+ normalize-package-data: 2.5.0
+ path-type: 2.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=
+ /read-pkg/3.0.0:
+ dependencies:
+ load-json-file: 4.0.0
+ normalize-package-data: 2.5.0
+ path-type: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
+ /read-pkg/5.2.0:
+ dependencies:
+ '@types/normalize-package-data': 2.4.0
+ normalize-package-data: 2.5.0
+ parse-json: 5.0.0
+ type-fest: 0.6.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
+ /readable-stream/2.3.7:
+ dependencies:
+ core-util-is: 1.0.2
+ inherits: 2.0.4
+ isarray: 1.0.0
+ process-nextick-args: 2.0.1
+ safe-buffer: 5.1.2
+ string_decoder: 1.1.1
+ util-deprecate: 1.0.2
+ dev: true
+ resolution:
+ integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
+ /readable-stream/3.6.0:
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.3.0
+ util-deprecate: 1.0.2
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+ /readdirp/2.2.1:
+ dependencies:
+ graceful-fs: 4.2.3
+ micromatch: 3.1.10
+ readable-stream: 2.3.7
+ dev: true
+ engines:
+ node: '>=0.10'
+ resolution:
+ integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==
+ /readdirp/3.3.0:
+ dependencies:
+ picomatch: 2.2.2
+ dev: true
+ engines:
+ node: '>=8.10.0'
+ resolution:
+ integrity: sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==
+ /realpath-native/1.1.0:
+ dependencies:
+ util.promisify: 1.0.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==
+ /realpath-native/2.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==
+ /recursive-readdir/2.2.2:
+ dependencies:
+ minimatch: 3.0.4
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==
+ /redux/4.0.5:
+ dependencies:
+ loose-envify: 1.4.0
+ symbol-observable: 1.2.0
+ dev: false
+ resolution:
+ integrity: sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
+ /refractor/2.10.1:
+ dependencies:
+ hastscript: 5.1.2
+ parse-entities: 1.2.2
+ prismjs: 1.17.1
+ dev: false
+ resolution:
+ integrity: sha512-Xh9o7hQiQlDbxo5/XkOX6H+x/q8rmlmZKr97Ie1Q8ZM32IRRd3B/UxuA/yXDW79DBSXGWxm2yRTbcTVmAciJRw==
+ /regenerate-unicode-properties/8.2.0:
+ dependencies:
+ regenerate: 1.4.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==
+ /regenerate/1.4.0:
+ dev: true
+ resolution:
+ integrity: sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
+ /regenerator-runtime/0.11.1:
+ dev: true
+ resolution:
+ integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
+ /regenerator-runtime/0.13.5:
+ resolution:
+ integrity: sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
+ /regenerator-transform/0.14.4:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ private: 0.1.8
+ dev: true
+ resolution:
+ integrity: sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw==
+ /regex-not/1.0.2:
+ dependencies:
+ extend-shallow: 3.0.2
+ safe-regex: 1.1.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+ /regex-parser/2.2.10:
+ dev: true
+ resolution:
+ integrity: sha512-8t6074A68gHfU8Neftl0Le6KTDwfGAj7IyjPIMSfikI2wJUTHDMaIq42bUsfVnj8mhx0R+45rdUXHGpN164avA==
+ /regexp.prototype.flags/1.3.0:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
+ /regexpp/2.0.1:
+ dev: true
+ engines:
+ node: '>=6.5.0'
+ resolution:
+ integrity: sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
+ /regexpp/3.1.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==
+ /regexpu-core/4.7.0:
+ dependencies:
+ regenerate: 1.4.0
+ regenerate-unicode-properties: 8.2.0
+ regjsgen: 0.5.1
+ regjsparser: 0.6.4
+ unicode-match-property-ecmascript: 1.0.4
+ unicode-match-property-value-ecmascript: 1.2.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==
+ /registry-auth-token/3.4.0:
+ dependencies:
+ rc: 1.2.8
+ safe-buffer: 5.2.0
+ dev: true
+ resolution:
+ integrity: sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==
+ /registry-auth-token/4.1.1:
+ dependencies:
+ rc: 1.2.8
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA==
+ /registry-url/3.1.0:
+ dependencies:
+ rc: 1.2.8
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-PU74cPc93h138M+aOBQyRE4XSUI=
+ /registry-url/5.1.0:
+ dependencies:
+ rc: 1.2.8
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==
+ /regjsgen/0.5.1:
+ dev: true
+ resolution:
+ integrity: sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==
+ /regjsparser/0.6.4:
+ dependencies:
+ jsesc: 0.5.0
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==
+ /relateurl/0.2.7:
+ dev: true
+ engines:
+ node: '>= 0.10'
+ resolution:
+ integrity: sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
+ /remove-trailing-separator/1.1.0:
+ dev: true
+ resolution:
+ integrity: sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+ /renderkid/2.0.3:
+ dependencies:
+ css-select: 1.2.0
+ dom-converter: 0.2.0
+ htmlparser2: 3.10.1
+ strip-ansi: 3.0.1
+ utila: 0.4.0
+ dev: true
+ resolution:
+ integrity: sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA==
+ /repeat-element/1.1.3:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
+ /repeat-string/1.6.1:
+ dev: true
+ engines:
+ node: '>=0.10'
+ resolution:
+ integrity: sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+ /replaceall/0.1.6:
+ dev: true
+ engines:
+ node: '>= 0.8.x'
+ resolution:
+ integrity: sha1-gdgax663LX9cSUKt8ml6MiBojY4=
+ /request-promise-core/1.1.3_request@2.88.2:
+ dependencies:
+ lodash: 4.17.15
+ request: 2.88.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ peerDependencies:
+ request: ^2.34
+ resolution:
+ integrity: sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==
+ /request-promise-native/1.0.8_request@2.88.2:
+ dependencies:
+ request: 2.88.2
+ request-promise-core: 1.1.3_request@2.88.2
+ stealthy-require: 1.1.1
+ tough-cookie: 2.5.0
+ dev: true
+ engines:
+ node: '>=0.12.0'
+ peerDependencies:
+ request: ^2.34
+ resolution:
+ integrity: sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==
+ /request/2.88.2:
+ dependencies:
+ aws-sign2: 0.7.0
+ aws4: 1.9.1
+ caseless: 0.12.0
+ combined-stream: 1.0.8
+ extend: 3.0.2
+ forever-agent: 0.6.1
+ form-data: 2.3.3
+ har-validator: 5.1.3
+ http-signature: 1.2.0
+ is-typedarray: 1.0.0
+ isstream: 0.1.2
+ json-stringify-safe: 5.0.1
+ mime-types: 2.1.26
+ oauth-sign: 0.9.0
+ performance-now: 2.1.0
+ qs: 6.5.2
+ safe-buffer: 5.2.0
+ tough-cookie: 2.5.0
+ tunnel-agent: 0.6.0
+ uuid: 3.4.0
+ deprecated: 'request has been deprecated, see https://github.com/request/request/issues/3142'
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
+ /require-directory/2.1.1:
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+ /require-main-filename/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
+ /require-main-filename/2.0.0:
+ resolution:
+ integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+ /require-resolve/0.0.2:
+ dependencies:
+ x-path: 0.0.2
+ dev: true
+ resolution:
+ integrity: sha1-urQQqxruLz9Vt5MXRR3TQodk5vM=
+ /requirejs-config-file/3.1.2:
+ dependencies:
+ esprima: 4.0.1
+ make-dir: 2.1.0
+ stringify-object: 3.3.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-sdLWywcDuNz7EIOhenSbRfT4YF84nItDv90coN2htbokjmU2QeyQuSBZILQUKNksepl8UPVU+hgYySFaDxbJPQ==
+ /requirejs/2.3.6:
+ dev: true
+ engines:
+ node: '>=0.4.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==
+ /requires-port/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+ /resolve-cwd/2.0.0:
+ dependencies:
+ resolve-from: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=
+ /resolve-cwd/3.0.0:
+ dependencies:
+ resolve-from: 5.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+ /resolve-dependency-path/2.0.0:
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-DIgu+0Dv+6v2XwRaNWnumKu7GPufBBOr5I1gRPJHkvghrfCGOooJODFvgFimX/KRxk9j0whD2MnKHzM1jYvk9w==
+ /resolve-dir/1.0.1:
+ dependencies:
+ expand-tilde: 2.0.2
+ global-modules: 1.0.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=
+ /resolve-from/3.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-six699nWiBvItuZTM17rywoYh0g=
+ /resolve-from/4.0.0:
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+ /resolve-from/5.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+ /resolve-pathname/3.0.0:
+ dev: false
+ resolution:
+ integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
+ /resolve-url-loader/3.1.1:
+ dependencies:
+ adjust-sourcemap-loader: 2.0.0
+ camelcase: 5.3.1
+ compose-function: 3.0.3
+ convert-source-map: 1.7.0
+ es6-iterator: 2.0.3
+ loader-utils: 1.2.3
+ postcss: 7.0.21
+ rework: 1.0.1
+ rework-visit: 1.0.0
+ source-map: 0.6.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-K1N5xUjj7v0l2j/3Sgs5b8CjrrgtC70SmdCuZiJ8tSyb5J+uk3FoeZ4b7yTnH6j7ngI+Bc5bldHJIa8hYdu2gQ==
+ /resolve-url/0.2.1:
+ deprecated: 'https://github.com/lydell/resolve-url#deprecated'
+ dev: true
+ resolution:
+ integrity: sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+ /resolve/1.1.7:
+ dev: true
+ resolution:
+ integrity: sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
+ /resolve/1.15.0:
+ dependencies:
+ path-parse: 1.0.6
+ dev: true
+ resolution:
+ integrity: sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw==
+ /resolve/1.15.1:
+ dependencies:
+ path-parse: 1.0.6
+ resolution:
+ integrity: sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==
+ /responselike/1.0.2:
+ dependencies:
+ lowercase-keys: 1.0.0
+ dev: true
+ resolution:
+ integrity: sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=
+ /restore-cursor/2.0.0:
+ dependencies:
+ onetime: 2.0.1
+ signal-exit: 3.0.3
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
+ /restore-cursor/3.1.0:
+ dependencies:
+ onetime: 5.1.0
+ signal-exit: 3.0.3
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
+ /ret/0.1.15:
+ dev: true
+ engines:
+ node: '>=0.12'
+ resolution:
+ integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+ /retry/0.12.0:
+ dev: true
+ engines:
+ node: '>= 4'
+ resolution:
+ integrity: sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
+ /reusify/1.0.4:
+ dev: true
+ engines:
+ iojs: '>=1.0.0'
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+ /rework-visit/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-mUWygD8hni96ygCtuLyfZA+ELJo=
+ /rework/1.0.1:
+ dependencies:
+ convert-source-map: 0.3.5
+ css: 2.2.4
+ dev: true
+ resolution:
+ integrity: sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=
+ /rgb-regex/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-wODWiC3w4jviVKR16O3UGRX+rrE=
+ /rgba-regex/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
+ /rimraf/2.2.8:
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=
+ /rimraf/2.6.3:
+ dependencies:
+ glob: 7.1.6
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
+ /rimraf/2.7.1:
+ dependencies:
+ glob: 7.1.6
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+ /rimraf/3.0.2:
+ dependencies:
+ glob: 7.1.6
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+ /ripemd160/2.0.2:
+ dependencies:
+ hash-base: 3.0.4
+ inherits: 2.0.4
+ resolution:
+ integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==
+ /rsvp/4.8.5:
+ dev: true
+ engines:
+ node: 6.* || >= 7.*
+ resolution:
+ integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
+ /run-async/2.4.0:
+ dependencies:
+ is-promise: 2.1.0
+ dev: true
+ engines:
+ node: '>=0.12.0'
+ resolution:
+ integrity: sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==
+ /run-node/1.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ hasBin: true
+ resolution:
+ integrity: sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==
+ /run-parallel/1.1.9:
+ dev: true
+ resolution:
+ integrity: sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==
+ /run-queue/1.0.3:
+ dependencies:
+ aproba: 1.2.0
+ dev: true
+ resolution:
+ integrity: sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=
+ /rxjs/6.5.5:
+ dependencies:
+ tslib: 1.11.1
+ dev: true
+ engines:
+ npm: '>=2.0.0'
+ resolution:
+ integrity: sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==
+ /safe-buffer/5.1.2:
+ resolution:
+ integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+ /safe-buffer/5.2.0:
+ resolution:
+ integrity: sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+ /safe-regex/1.1.0:
+ dependencies:
+ ret: 0.1.15
+ dev: true
+ resolution:
+ integrity: sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+ /safer-buffer/2.1.2:
+ resolution:
+ integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+ /sane/4.1.0:
+ dependencies:
+ '@cnakazawa/watch': 1.0.4
+ anymatch: 2.0.0
+ capture-exit: 2.0.0
+ exec-sh: 0.3.4
+ execa: 1.0.0
+ fb-watchman: 2.0.1
+ micromatch: 3.1.10
+ minimist: 1.2.5
+ walker: 1.0.7
+ dev: true
+ engines:
+ node: 6.* || 8.* || >= 10.*
+ hasBin: true
+ resolution:
+ integrity: sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==
+ /sanitize.css/10.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-vTxrZz4dX5W86M6oVWVdOVe72ZiPs41Oi7Z6Km4W5Turyz28mrXSJhhEBZoRtzJWIv3833WKVwLSDWWkEfupMg==
+ /sass-loader/8.0.2_webpack@4.42.0:
+ dependencies:
+ clone-deep: 4.0.1
+ loader-utils: 1.4.0
+ neo-async: 2.6.1
+ schema-utils: 2.6.5
+ semver: 6.3.0
+ webpack: 4.42.0_webpack@4.42.0
+ dev: true
+ engines:
+ node: '>= 8.9.0'
+ peerDependencies:
+ fibers: '>= 3.1.0'
+ node-sass: ^4.0.0
+ sass: ^1.3.0
+ webpack: ^4.36.0 || ^5.0.0
+ peerDependenciesMeta:
+ fibers:
+ optional: true
+ node-sass:
+ optional: true
+ sass:
+ optional: true
+ resolution:
+ integrity: sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==
+ /sass-lookup/3.0.0:
+ dependencies:
+ commander: 2.20.3
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-TTsus8CfFRn1N44bvdEai1no6PqdmDiQUiqW5DlpmtT+tYnIt1tXtDIph5KA1efC+LmioJXSnCtUVpcK9gaKIg==
+ /sax/1.2.1:
+ resolution:
+ integrity: sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
+ /sax/1.2.4:
+ dev: true
+ resolution:
+ integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+ /saxes/3.1.11:
+ dependencies:
+ xmlchars: 2.2.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==
+ /scheduler/0.19.1:
+ dependencies:
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ dev: false
+ resolution:
+ integrity: sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
+ /schema-utils/1.0.0:
+ dependencies:
+ ajv: 6.12.0
+ ajv-errors: 1.0.1_ajv@6.12.0
+ ajv-keywords: 3.4.1_ajv@6.12.0
+ dev: true
+ engines:
+ node: '>= 4'
+ resolution:
+ integrity: sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==
+ /schema-utils/2.6.5:
+ dependencies:
+ ajv: 6.12.0
+ ajv-keywords: 3.4.1_ajv@6.12.0
+ dev: true
+ engines:
+ node: '>= 8.9.0'
+ resolution:
+ integrity: sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==
+ /seek-bzip/1.0.5:
+ dependencies:
+ commander: 2.8.1
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=
+ /select-hose/2.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
+ /select/1.1.2:
+ dev: false
+ optional: true
+ resolution:
+ integrity: sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
+ /selfsigned/1.10.7:
+ dependencies:
+ node-forge: 0.9.0
+ dev: true
+ resolution:
+ integrity: sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==
+ /semantic-ui-react/0.88.2_react-dom@16.13.1+react@16.13.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ '@semantic-ui-react/event-stack': 3.1.1_react-dom@16.13.1+react@16.13.1
+ '@stardust-ui/react-component-event-listener': 0.38.0_react-dom@16.13.1+react@16.13.1
+ '@stardust-ui/react-component-ref': 0.38.0_react-dom@16.13.1+react@16.13.1
+ classnames: 2.2.6
+ keyboard-key: 1.1.0
+ lodash: 4.17.15
+ prop-types: 15.7.2
+ react: 16.13.1
+ react-dom: 16.13.1_react@16.13.1
+ react-is: 16.13.1
+ react-popper: 1.3.7_react@16.13.1
+ shallowequal: 1.1.0
+ dev: false
+ peerDependencies:
+ react: ^16.8.0
+ react-dom: ^16.8.0
+ resolution:
+ integrity: sha512-+02kN2z8PuA/cMdvDUsHhbJmBzxxgOXVHMFr9XK7zGb0wkW9A6OPQMFokWz7ozlVtKjN6r7zsb+Qvjk/qq1OWw==
+ /semver-compare/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
+ /semver-diff/2.1.0:
+ dependencies:
+ semver: 5.7.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=
+ /semver-regex/1.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-kqSWkGX5xwxpR1PVUkj8aPj2Usk=
+ /semver/5.5.0:
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==
+ /semver/5.7.1:
+ hasBin: true
+ resolution:
+ integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+ /semver/6.3.0:
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+ /semver/7.0.0:
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
+ /send/0.17.1:
+ dependencies:
+ debug: 2.6.9
+ depd: 1.1.2
+ destroy: 1.0.4
+ encodeurl: 1.0.2
+ escape-html: 1.0.3
+ etag: 1.8.1
+ fresh: 0.5.2
+ http-errors: 1.7.3
+ mime: 1.6.0
+ ms: 2.1.1
+ on-finished: 2.3.0
+ range-parser: 1.2.1
+ statuses: 1.5.0
+ engines:
+ node: '>= 0.8.0'
+ resolution:
+ integrity: sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
+ /serialize-javascript/2.1.2:
+ dev: true
+ resolution:
+ integrity: sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
+ /serve-index/1.9.1:
+ dependencies:
+ accepts: 1.3.7
+ batch: 0.6.1
+ debug: 2.6.9
+ escape-html: 1.0.3
+ http-errors: 1.6.3
+ mime-types: 2.1.26
+ parseurl: 1.3.3
+ dev: true
+ engines:
+ node: '>= 0.8.0'
+ resolution:
+ integrity: sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=
+ /serve-static/1.14.1:
+ dependencies:
+ encodeurl: 1.0.2
+ escape-html: 1.0.3
+ parseurl: 1.3.3
+ send: 0.17.1
+ engines:
+ node: '>= 0.8.0'
+ resolution:
+ integrity: sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
+ /serverless-deployment-bucket/1.1.1:
+ dependencies:
+ chalk: 2.4.2
+ dev: true
+ resolution:
+ integrity: sha512-oeafNyErJ2ZQWr+chQRzz7r/iognwozRA6k6ECMnXtxFJL4BbYQJfq7+VyoI77atc9a0GXzPoc27aM+sQE1NRQ==
+ /serverless-http/2.3.2:
+ dev: false
+ engines:
+ node: '>=8.0'
+ optionalDependencies:
+ '@types/aws-lambda': 8.10.48
+ resolution:
+ integrity: sha512-tUUpj2USho2s+X+7js0KQZ2PWqdDNXKkiz9rdYqu3CG/3hsvMve9IBm/R6gILgijGgOfkCKSjsrPnPG08Y7M6g==
+ /serverless-offline/5.12.1_serverless@1.67.3:
+ dependencies:
+ '@hapi/boom': 7.4.11
+ '@hapi/h2o2': 8.3.2
+ '@hapi/hapi': 18.4.1
+ cuid: 2.1.8
+ hapi-plugin-websocket: 2.3.0_@hapi+hapi@18.4.1
+ js-string-escape: 1.0.1
+ jsonpath-plus: 1.1.0
+ jsonwebtoken: 8.5.1
+ luxon: 1.23.0
+ object.fromentries: 2.0.2
+ semver: 6.3.0
+ serverless: 1.67.3
+ trim-newlines: 3.0.0
+ update-notifier: 3.0.1
+ velocityjs: 1.1.5
+ dev: true
+ engines:
+ node: '>=8.10.0'
+ peerDependencies:
+ serverless: '>= 1.48.1'
+ resolution:
+ integrity: sha512-OXgfXWZM8RxXie1NXNvjQk7TpM3KI/lyJd4pmakcL7XNZADCd1ph5yOvVdDlJAZgmrkaq2tzSG8ZaKDE66JTmg==
+ /serverless-plugin-scripts/1.0.2:
+ dev: true
+ resolution:
+ integrity: sha1-IYCMPP0KGoTkjAZgsPbzcLVmVIY=
+ /serverless-s3-sync/1.12.0:
+ dependencies:
+ '@auth0/s3': 1.0.0
+ bluebird: 3.7.2
+ chalk: 2.4.2
+ mime: 2.4.4
+ minimatch: 3.0.4
+ dev: true
+ resolution:
+ integrity: sha512-bJYdS/J3FleL/GJXM0SnoTTNUAAyhYAh4JD4jbLEdZ83zJ4aEhPelWHOCnpVWDRf350cIS1ctyhL6P1L2c7qKA==
+ /serverless-tencent-tools/1.0.14:
+ dependencies:
+ '@tencent-sdk/capi': 0.2.15
+ dijkstrajs: 1.0.1
+ dot-qs: 0.2.0
+ duplexify: 4.1.1
+ end-of-stream: 1.4.4
+ https-proxy-agent: 5.0.0
+ socket.io-client: 2.3.0
+ socket.io-stream: 0.9.1
+ winston: 3.2.1
+ dev: true
+ resolution:
+ integrity: sha512-25jyMEBS9zuewtJw01N2V5CISTPeCCbNgJMveXpRCfUWASt0pYuVqaWBKZt3C7AbHVn75NZQg0XQsy0y9EXjyw==
+ /serverless-webpack/5.3.1_webpack@4.42.1:
+ dependencies:
+ archiver: 2.1.1
+ bluebird: 3.7.2
+ fs-extra: 4.0.3
+ glob: 7.1.6
+ is-builtin-module: 1.0.0
+ lodash: 4.17.15
+ semver: 5.7.1
+ ts-node: 3.3.0
+ webpack: 4.42.1_webpack@4.42.1
+ dev: true
+ peerDependencies:
+ webpack: '>= 3.0.0 < 6'
+ resolution:
+ integrity: sha512-lo5C7xpPRgY79gNbdbI7+y4f7WTzaYGxOczbiKaLuKpUIfO2VlA7HPOObC5s1n6LqviGZSBtHWqIqiUTCXJL0A==
+ /serverless/1.67.3:
+ dependencies:
+ '@serverless/cli': 1.4.0
+ '@serverless/components': 2.29.0
+ '@serverless/enterprise-plugin': 3.6.6
+ archiver: 1.3.0
+ async: 1.5.2
+ aws-sdk: 2.656.0
+ bluebird: 3.7.2
+ boxen: 3.2.0
+ cachedir: 2.3.0
+ chalk: 2.4.2
+ child-process-ext: 2.1.1
+ ci-info: 1.6.0
+ d: 1.0.1
+ dayjs: 1.8.23
+ decompress: 4.2.1
+ download: 7.1.0
+ essentials: 1.1.1
+ fast-levenshtein: 2.0.6
+ filesize: 3.6.1
+ fs-extra: 0.30.0
+ get-stdin: 5.0.1
+ globby: 6.1.0
+ graceful-fs: 4.2.3
+ https-proxy-agent: 4.0.0
+ inquirer: 6.5.2
+ is-docker: 1.1.0
+ is-wsl: 2.1.1
+ js-yaml: 3.13.1
+ json-cycle: 1.3.0
+ json-refs: 2.1.7
+ jszip: 3.3.0
+ jwt-decode: 2.2.0
+ lodash: 4.17.15
+ memoizee: 0.4.14
+ mkdirp: 0.5.5
+ nanomatch: 1.2.13
+ ncjsm: 4.0.1
+ node-fetch: 1.7.3
+ object-hash: 2.0.3
+ p-limit: 2.3.0
+ promise-queue: 2.2.5
+ raven: 1.2.1
+ rc: 1.2.8
+ replaceall: 0.1.6
+ semver: 5.7.1
+ semver-regex: 1.0.0
+ stream-promise: 3.2.0
+ tabtab: 3.0.2
+ untildify: 3.0.3
+ update-notifier: 2.5.0
+ uuid: 2.0.3
+ write-file-atomic: 2.4.3
+ yaml-ast-parser: 0.0.43
+ yargs-parser: 16.1.0
+ dev: true
+ engines:
+ node: '>=6.0'
+ hasBin: true
+ requiresBuild: true
+ resolution:
+ integrity: sha512-GELorbWZI0iLroPAwuHBDF7xlTAlSfhkcSjsb0CBdBgKz8EU8eqhalzl8dU+C+hOM5j/LJK/ATwaIxJXndqwCw==
+ /set-blocking/2.0.0:
+ resolution:
+ integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+ /set-immediate-shim/1.0.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=
+ /set-value/2.0.1:
+ dependencies:
+ extend-shallow: 2.0.1
+ is-extendable: 0.1.1
+ is-plain-object: 2.0.4
+ split-string: 3.1.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
+ /setimmediate/1.0.5:
+ dev: true
+ resolution:
+ integrity: sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+ /setprototypeof/1.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
+ /setprototypeof/1.1.1:
+ resolution:
+ integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
+ /sha.js/2.4.11:
+ dependencies:
+ inherits: 2.0.4
+ safe-buffer: 5.2.0
+ hasBin: true
+ resolution:
+ integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
+ /shallow-clone/0.1.2:
+ dependencies:
+ is-extendable: 0.1.1
+ kind-of: 2.0.1
+ lazy-cache: 0.2.7
+ mixin-object: 2.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-WQnodLp3EG1zrEFM/sH/yofZcGA=
+ /shallow-clone/3.0.1:
+ dependencies:
+ kind-of: 6.0.3
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+ /shallowequal/1.1.0:
+ dev: false
+ resolution:
+ integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
+ /shebang-command/1.2.0:
+ dependencies:
+ shebang-regex: 1.0.0
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+ /shebang-command/2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ /shebang-regex/1.0.0:
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+ /shebang-regex/3.0.0:
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+ /shell-quote/1.7.2:
+ dev: true
+ resolution:
+ integrity: sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
+ /shellwords/0.1.1:
+ dev: true
+ resolution:
+ integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
+ /shortid/2.2.15:
+ dependencies:
+ nanoid: 2.1.11
+ resolution:
+ integrity: sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw==
+ /showdown/1.9.1:
+ dependencies:
+ yargs: 14.2.3
+ dev: false
+ hasBin: true
+ resolution:
+ integrity: sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA==
+ /side-channel/1.0.2:
+ dependencies:
+ es-abstract: 1.17.5
+ object-inspect: 1.7.0
+ dev: true
+ resolution:
+ integrity: sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==
+ /signal-exit/3.0.3:
+ dev: true
+ resolution:
+ integrity: sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
+ /simple-git/1.132.0:
+ dependencies:
+ debug: 4.1.1
+ dev: true
+ resolution:
+ integrity: sha512-xauHm1YqCTom1sC9eOjfq3/9RKiUA9iPnxBbrY2DdL8l4ADMu0jjM5l5lphQP5YWNqAL2aXC/OeuQ76vHtW5fg==
+ /simple-swizzle/0.2.2:
+ dependencies:
+ is-arrayish: 0.3.2
+ dev: true
+ resolution:
+ integrity: sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
+ /sisteransi/1.0.5:
+ dev: true
+ resolution:
+ integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
+ /slash/1.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=
+ /slash/2.0.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
+ /slash/3.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+ /slice-ansi/2.1.0:
+ dependencies:
+ ansi-styles: 3.2.1
+ astral-regex: 1.0.0
+ is-fullwidth-code-point: 2.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
+ /slugify/1.4.0:
+ dev: false
+ engines:
+ node: '>=8.0.0'
+ resolution:
+ integrity: sha512-FtLNsMGBSRB/0JOE2A0fxlqjI6fJsgHGS13iTuVT28kViI4JjUiNqp/vyis0ZXYcMnpR3fzGNkv+6vRlI2GwdQ==
+ /snapdragon-node/2.1.1:
+ dependencies:
+ define-property: 1.0.0
+ isobject: 3.0.1
+ snapdragon-util: 3.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+ /snapdragon-util/3.0.1:
+ dependencies:
+ kind-of: 3.2.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+ /snapdragon/0.8.2:
+ dependencies:
+ base: 0.11.2
+ debug: 2.6.9
+ define-property: 0.2.5
+ extend-shallow: 2.0.1
+ map-cache: 0.2.2
+ source-map: 0.5.7
+ source-map-resolve: 0.5.3
+ use: 3.1.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
+ /socket.io-client/2.3.0:
+ dependencies:
+ backo2: 1.0.2
+ base64-arraybuffer: 0.1.5
+ component-bind: 1.0.0
+ component-emitter: 1.2.1
+ debug: 4.1.1
+ engine.io-client: 3.4.0
+ has-binary2: 1.0.3
+ has-cors: 1.1.0
+ indexof: 0.0.1
+ object-component: 0.0.3
+ parseqs: 0.0.5
+ parseuri: 0.0.5
+ socket.io-parser: 3.3.0
+ to-array: 0.1.4
+ dev: true
+ resolution:
+ integrity: sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==
+ /socket.io-parser/3.3.0:
+ dependencies:
+ component-emitter: 1.2.1
+ debug: 3.1.0
+ isarray: 2.0.1
+ dev: true
+ resolution:
+ integrity: sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==
+ /socket.io-stream/0.9.1:
+ dependencies:
+ component-bind: 1.0.0
+ debug: 2.2.0
+ dev: true
+ resolution:
+ integrity: sha1-QhJYMWKIuDrGk7DUPv0J1tQ6upc=
+ /sockjs-client/1.4.0:
+ dependencies:
+ debug: 3.2.6
+ eventsource: 1.0.7
+ faye-websocket: 0.11.3
+ inherits: 2.0.4
+ json3: 3.3.3
+ url-parse: 1.4.7
+ dev: true
+ resolution:
+ integrity: sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==
+ /sockjs/0.3.19:
+ dependencies:
+ faye-websocket: 0.10.0
+ uuid: 3.4.0
+ dev: true
+ resolution:
+ integrity: sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==
+ /sort-keys-length/1.0.1:
+ dependencies:
+ sort-keys: 1.1.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=
+ /sort-keys/1.1.2:
+ dependencies:
+ is-plain-obj: 1.1.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-RBttTTRnmPG05J6JIK37oOVD+a0=
+ /sort-keys/2.0.0:
+ dependencies:
+ is-plain-obj: 1.1.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=
+ /source-list-map/2.0.1:
+ dev: true
+ resolution:
+ integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
+ /source-map-resolve/0.5.3:
+ dependencies:
+ atob: 2.1.2
+ decode-uri-component: 0.2.0
+ resolve-url: 0.2.1
+ source-map-url: 0.4.0
+ urix: 0.1.0
+ dev: true
+ resolution:
+ integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
+ /source-map-support/0.4.18:
+ dependencies:
+ source-map: 0.5.7
+ dev: true
+ resolution:
+ integrity: sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==
+ /source-map-support/0.5.16:
+ dependencies:
+ buffer-from: 1.1.1
+ source-map: 0.6.1
+ dev: true
+ resolution:
+ integrity: sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
+ /source-map-url/0.4.0:
+ dev: true
+ resolution:
+ integrity: sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+ /source-map/0.5.7:
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+ /source-map/0.6.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+ /source-map/0.7.3:
+ dev: true
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+ /space-separated-tokens/1.1.5:
+ dev: false
+ resolution:
+ integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==
+ /spdx-correct/3.1.0:
+ dependencies:
+ spdx-expression-parse: 3.0.0
+ spdx-license-ids: 3.0.5
+ dev: true
+ resolution:
+ integrity: sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==
+ /spdx-exceptions/2.2.0:
+ dev: true
+ resolution:
+ integrity: sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==
+ /spdx-expression-parse/3.0.0:
+ dependencies:
+ spdx-exceptions: 2.2.0
+ spdx-license-ids: 3.0.5
+ dev: true
+ resolution:
+ integrity: sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==
+ /spdx-license-ids/3.0.5:
+ dev: true
+ resolution:
+ integrity: sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
+ /spdy-transport/3.0.0:
+ dependencies:
+ debug: 4.1.1
+ detect-node: 2.0.4
+ hpack.js: 2.1.6
+ obuf: 1.1.2
+ readable-stream: 3.6.0
+ wbuf: 1.7.3
+ dev: true
+ resolution:
+ integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==
+ /spdy/4.0.2:
+ dependencies:
+ debug: 4.1.1
+ handle-thing: 2.0.1
+ http-deceiver: 1.2.7
+ select-hose: 2.0.0
+ spdy-transport: 3.0.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==
+ /split-string/3.1.0:
+ dependencies:
+ extend-shallow: 3.0.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+ /split2/3.1.1:
+ dependencies:
+ readable-stream: 3.6.0
+ dev: true
+ resolution:
+ integrity: sha512-emNzr1s7ruq4N+1993yht631/JH+jaj0NYBosuKmLcq+JkGQ9MmTw1RB1fGaTCzUuseRIClrlSLHRNYGwWQ58Q==
+ /sprintf-js/1.0.3:
+ resolution:
+ integrity: sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+ /sprintf-kit/2.0.0:
+ dependencies:
+ es5-ext: 0.10.53
+ dev: true
+ resolution:
+ integrity: sha512-/0d2YTn8ZFVpIPAU230S9ZLF8WDkSSRWvh/UOLM7zzvkCchum1TtouRgyV8OfgOaYilSGU4lSSqzwBXJVlAwUw==
+ /sshpk/1.16.1:
+ dependencies:
+ asn1: 0.2.4
+ assert-plus: 1.0.0
+ bcrypt-pbkdf: 1.0.2
+ dashdash: 1.14.1
+ ecc-jsbn: 0.1.2
+ getpass: 0.1.7
+ jsbn: 0.1.1
+ safer-buffer: 2.1.2
+ tweetnacl: 0.14.5
+ engines:
+ node: '>=0.10.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
+ /ssri/6.0.1:
+ dependencies:
+ figgy-pudding: 3.5.2
+ dev: true
+ resolution:
+ integrity: sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
+ /ssri/7.1.0:
+ dependencies:
+ figgy-pudding: 3.5.2
+ minipass: 3.1.1
+ dev: true
+ engines:
+ node: '>= 8'
+ resolution:
+ integrity: sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==
+ /stable/0.1.8:
+ dev: true
+ resolution:
+ integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+ /stack-trace/0.0.10:
+ dev: true
+ resolution:
+ integrity: sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
+ /stack-trace/0.0.9:
+ dev: true
+ resolution:
+ integrity: sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=
+ /stack-utils/1.0.2:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
+ /static-extend/0.1.2:
+ dependencies:
+ define-property: 0.2.5
+ object-copy: 0.1.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+ /statuses/1.5.0:
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+ /stealthy-require/1.1.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+ /stream-browserify/2.0.2:
+ dependencies:
+ inherits: 2.0.4
+ readable-stream: 2.3.7
+ dev: true
+ resolution:
+ integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==
+ /stream-each/1.2.3:
+ dependencies:
+ end-of-stream: 1.4.4
+ stream-shift: 1.0.1
+ dev: true
+ resolution:
+ integrity: sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==
+ /stream-http/2.8.3:
+ dependencies:
+ builtin-status-codes: 3.0.0
+ inherits: 2.0.4
+ readable-stream: 2.3.7
+ to-arraybuffer: 1.0.1
+ xtend: 4.0.2
+ dev: true
+ resolution:
+ integrity: sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==
+ /stream-promise/3.2.0:
+ dependencies:
+ 2-thenable: 1.0.0
+ es5-ext: 0.10.53
+ is-stream: 1.1.0
+ dev: true
+ resolution:
+ integrity: sha512-P+7muTGs2C8yRcgJw/PPt61q7O517tDHiwYEzMWo1GSBCcZedUMT/clz7vUNsSxFphIlJ6QUL4GexQKlfJoVtA==
+ /stream-shift/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
+ /streamsink/1.2.0:
+ dev: true
+ resolution:
+ integrity: sha1-76/unx4i01ke1949yqlcP1559zw=
+ /strict-uri-encode/1.1.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
+ /string-length/2.0.0:
+ dependencies:
+ astral-regex: 1.0.0
+ strip-ansi: 4.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=
+ /string-length/3.1.0:
+ dependencies:
+ astral-regex: 1.0.0
+ strip-ansi: 5.2.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==
+ /string-width/1.0.2:
+ dependencies:
+ code-point-at: 1.1.0
+ is-fullwidth-code-point: 1.0.0
+ strip-ansi: 3.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
+ /string-width/2.1.1:
+ dependencies:
+ is-fullwidth-code-point: 2.0.0
+ strip-ansi: 4.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+ /string-width/3.1.0:
+ dependencies:
+ emoji-regex: 7.0.3
+ is-fullwidth-code-point: 2.0.0
+ strip-ansi: 5.2.0
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
+ /string-width/4.2.0:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
+ /string.prototype.matchall/4.0.2:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ has-symbols: 1.0.1
+ internal-slot: 1.0.2
+ regexp.prototype.flags: 1.3.0
+ side-channel: 1.0.2
+ dev: true
+ resolution:
+ integrity: sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==
+ /string.prototype.trimend/1.0.0:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ resolution:
+ integrity: sha512-EEJnGqa/xNfIg05SxiPSqRS7S9qwDhYts1TSLR1BQfYUfPe1stofgGKvwERK9+9yf+PpfBMlpBaCHucXGPQfUA==
+ /string.prototype.trimleft/2.1.2:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ string.prototype.trimstart: 1.0.0
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==
+ /string.prototype.trimright/2.1.2:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ string.prototype.trimend: 1.0.0
+ engines:
+ node: '>= 0.4'
+ resolution:
+ integrity: sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==
+ /string.prototype.trimstart/1.0.0:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ resolution:
+ integrity: sha512-iCP8g01NFYiiBOnwG1Xc3WZLyoo+RuBymwIlWncShXDDJYWN6DbnM3odslBJdgCdRlq94B5s63NWAZlcn2CS4w==
+ /string_decoder/1.1.1:
+ dependencies:
+ safe-buffer: 5.1.2
+ dev: true
+ resolution:
+ integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ /string_decoder/1.3.0:
+ dependencies:
+ safe-buffer: 5.2.0
+ dev: true
+ resolution:
+ integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ /stringify-object/3.3.0:
+ dependencies:
+ get-own-enumerable-property-symbols: 3.0.2
+ is-obj: 1.0.1
+ is-regexp: 1.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==
+ /strip-ansi/3.0.1:
+ dependencies:
+ ansi-regex: 2.1.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+ /strip-ansi/4.0.0:
+ dependencies:
+ ansi-regex: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+ /strip-ansi/5.2.0:
+ dependencies:
+ ansi-regex: 4.1.0
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
+ /strip-ansi/6.0.0:
+ dependencies:
+ ansi-regex: 5.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
+ /strip-bom/2.0.0:
+ dependencies:
+ is-utf8: 0.2.1
+ dev: false
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=
+ /strip-bom/3.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+ /strip-bom/4.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
+ /strip-comments/1.0.2:
+ dependencies:
+ babel-extract-comments: 1.0.0
+ babel-plugin-transform-object-rest-spread: 6.26.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-kL97alc47hoyIQSV165tTt9rG5dn4w1dNnBhOQ3bOU1Nc1hel09jnXANaHJ7vzHLd4Ju8kseDGzlev96pghLFw==
+ /strip-dirs/2.1.0:
+ dependencies:
+ is-natural-number: 4.0.1
+ dev: true
+ resolution:
+ integrity: sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==
+ /strip-eof/1.0.0:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
+ /strip-final-newline/2.0.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+ /strip-json-comments/2.0.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-PFMZQukIwml8DsNEhYwobHygpgo=
+ /strip-json-comments/3.1.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==
+ /strip-outer/1.0.1:
+ dependencies:
+ escape-string-regexp: 1.0.5
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==
+ /style-loader/0.23.1:
+ dependencies:
+ loader-utils: 1.4.0
+ schema-utils: 1.0.0
+ dev: true
+ engines:
+ node: '>= 0.12.0'
+ resolution:
+ integrity: sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg==
+ /stylehacks/4.0.3:
+ dependencies:
+ browserslist: 4.11.1
+ postcss: 7.0.27
+ postcss-selector-parser: 3.1.2
+ dev: true
+ engines:
+ node: '>=6.9.0'
+ resolution:
+ integrity: sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==
+ /stylus-lookup/3.0.2:
+ dependencies:
+ commander: 2.20.3
+ debug: 4.1.1
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-oEQGHSjg/AMaWlKe7gqsnYzan8DLcGIHe0dUaFkucZZ14z4zjENRlQMCHT4FNsiWnJf17YN9OvrCfCoi7VvOyg==
+ /superagent/3.8.3:
+ dependencies:
+ component-emitter: 1.3.0
+ cookiejar: 2.1.2
+ debug: 3.2.6
+ extend: 3.0.2
+ form-data: 2.5.1
+ formidable: 1.2.2
+ methods: 1.1.2
+ mime: 1.6.0
+ qs: 6.9.3
+ readable-stream: 2.3.7
+ dev: true
+ engines:
+ node: '>= 4.0'
+ resolution:
+ integrity: sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==
+ /supports-color/2.0.0:
+ dev: true
+ engines:
+ node: '>=0.8.0'
+ resolution:
+ integrity: sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
+ /supports-color/5.5.0:
+ dependencies:
+ has-flag: 3.0.0
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ /supports-color/6.1.0:
+ dependencies:
+ has-flag: 3.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
+ /supports-color/7.1.0:
+ dependencies:
+ has-flag: 4.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
+ /supports-hyperlinks/2.1.0:
+ dependencies:
+ has-flag: 4.0.0
+ supports-color: 7.1.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==
+ /svg-parser/2.0.4:
+ dev: true
+ resolution:
+ integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
+ /svgo/1.3.2:
+ dependencies:
+ chalk: 2.4.2
+ coa: 2.0.2
+ css-select: 2.1.0
+ css-select-base-adapter: 0.1.1
+ css-tree: 1.0.0-alpha.37
+ csso: 4.0.3
+ js-yaml: 3.13.1
+ mkdirp: 0.5.5
+ object.values: 1.1.1
+ sax: 1.2.4
+ stable: 0.1.8
+ unquote: 1.1.1
+ util.promisify: 1.0.1
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==
+ /symbol-observable/1.2.0:
+ dev: false
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
+ /symbol-tree/3.2.4:
+ dev: true
+ resolution:
+ integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+ /table/5.4.6:
+ dependencies:
+ ajv: 6.12.0
+ lodash: 4.17.15
+ slice-ansi: 2.1.0
+ string-width: 3.1.0
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ resolution:
+ integrity: sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
+ /tabtab/3.0.2:
+ dependencies:
+ debug: 4.1.1
+ es6-promisify: 6.1.0
+ inquirer: 6.5.2
+ minimist: 1.2.5
+ mkdirp: 0.5.5
+ untildify: 3.0.3
+ dev: true
+ resolution:
+ integrity: sha512-jANKmUe0sIQc/zTALTBy186PoM/k6aPrh3A7p6AaAfF6WPSbTx1JYeGIGH162btpH+mmVEXln+UxwViZHO2Jhg==
+ /tapable/1.1.3:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
+ /tar-stream/1.6.2:
+ dependencies:
+ bl: 1.2.2
+ buffer-alloc: 1.2.0
+ end-of-stream: 1.4.4
+ fs-constants: 1.0.0
+ readable-stream: 2.3.7
+ to-buffer: 1.1.1
+ xtend: 4.0.2
+ dev: true
+ engines:
+ node: '>= 0.8.0'
+ resolution:
+ integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
+ /term-size/1.2.0:
+ dependencies:
+ execa: 0.7.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=
+ /terminal-link/2.1.1:
+ dependencies:
+ ansi-escapes: 4.3.1
+ supports-hyperlinks: 2.1.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==
+ /terser-webpack-plugin/1.4.3_webpack@4.41.2:
+ dependencies:
+ cacache: 12.0.4
+ find-cache-dir: 2.1.0
+ is-wsl: 1.1.0
+ schema-utils: 1.0.0
+ serialize-javascript: 2.1.2
+ source-map: 0.6.1
+ terser: 4.6.11
+ webpack: 4.41.2_webpack@4.41.2
+ webpack-sources: 1.4.3
+ worker-farm: 1.7.0
+ dev: true
+ engines:
+ node: '>= 6.9.0'
+ peerDependencies:
+ webpack: ^4.0.0
+ resolution:
+ integrity: sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==
+ /terser-webpack-plugin/1.4.3_webpack@4.42.0:
+ dependencies:
+ cacache: 12.0.4
+ find-cache-dir: 2.1.0
+ is-wsl: 1.1.0
+ schema-utils: 1.0.0
+ serialize-javascript: 2.1.2
+ source-map: 0.6.1
+ terser: 4.6.11
+ webpack: 4.42.0_webpack@4.42.0
+ webpack-sources: 1.4.3
+ worker-farm: 1.7.0
+ dev: true
+ engines:
+ node: '>= 6.9.0'
+ peerDependencies:
+ webpack: ^4.0.0
+ resolution:
+ integrity: sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==
+ /terser-webpack-plugin/1.4.3_webpack@4.42.1:
+ dependencies:
+ cacache: 12.0.4
+ find-cache-dir: 2.1.0
+ is-wsl: 1.1.0
+ schema-utils: 1.0.0
+ serialize-javascript: 2.1.2
+ source-map: 0.6.1
+ terser: 4.6.11
+ webpack: 4.42.1_webpack@4.42.1
+ webpack-sources: 1.4.3
+ worker-farm: 1.7.0
+ dev: true
+ engines:
+ node: '>= 6.9.0'
+ peerDependencies:
+ webpack: ^4.0.0
+ resolution:
+ integrity: sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==
+ /terser-webpack-plugin/2.3.5_webpack@4.42.0:
+ dependencies:
+ cacache: 13.0.1
+ find-cache-dir: 3.3.1
+ jest-worker: 25.2.6
+ p-limit: 2.3.0
+ schema-utils: 2.6.5
+ serialize-javascript: 2.1.2
+ source-map: 0.6.1
+ terser: 4.6.11
+ webpack: 4.42.0_webpack@4.42.0
+ webpack-sources: 1.4.3
+ dev: true
+ engines:
+ node: '>= 8.9.0'
+ peerDependencies:
+ webpack: ^4.0.0 || ^5.0.0
+ resolution:
+ integrity: sha512-WlWksUoq+E4+JlJ+h+U+QUzXpcsMSSNXkDy9lBVkSqDn1w23Gg29L/ary9GeJVYCGiNJJX7LnVc4bwL1N3/g1w==
+ /terser/4.6.11:
+ dependencies:
+ commander: 2.20.3
+ source-map: 0.6.1
+ source-map-support: 0.5.16
+ dev: true
+ engines:
+ node: '>=6.0.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-76Ynm7OXUG5xhOpblhytE7X58oeNSmC8xnNhjWVo8CksHit0U0kO4hfNbPrrYwowLWFgM2n9L176VNx2QaHmtA==
+ /test-exclude/5.2.3:
+ dependencies:
+ glob: 7.1.6
+ minimatch: 3.0.4
+ read-pkg-up: 4.0.0
+ require-main-filename: 2.0.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==
+ /test-exclude/6.0.0:
+ dependencies:
+ '@istanbuljs/schema': 0.1.2
+ glob: 7.1.6
+ minimatch: 3.0.4
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
+ /text-hex/1.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
+ /text-table/0.2.0:
+ dev: true
+ resolution:
+ integrity: sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
+ /throat/4.1.0:
+ dev: true
+ resolution:
+ integrity: sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
+ /throat/5.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
+ /through/2.3.8:
+ dev: true
+ resolution:
+ integrity: sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+ /through2/2.0.5:
+ dependencies:
+ readable-stream: 2.3.7
+ xtend: 4.0.2
+ dev: true
+ resolution:
+ integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
+ /thunky/1.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
+ /timed-out/4.0.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=
+ /timers-browserify/2.0.11:
+ dependencies:
+ setimmediate: 1.0.5
+ dev: true
+ engines:
+ node: '>=0.6.0'
+ resolution:
+ integrity: sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==
+ /timers-ext/0.1.7:
+ dependencies:
+ es5-ext: 0.10.53
+ next-tick: 1.1.0
+ dev: true
+ resolution:
+ integrity: sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==
+ /timsort/0.3.0:
+ dev: true
+ resolution:
+ integrity: sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+ /tiny-emitter/2.1.0:
+ dev: false
+ optional: true
+ resolution:
+ integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
+ /tiny-invariant/1.1.0:
+ dev: false
+ resolution:
+ integrity: sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
+ /tiny-warning/1.0.3:
+ dev: false
+ resolution:
+ integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+ /tmp/0.0.33:
+ dependencies:
+ os-tmpdir: 1.0.2
+ dev: true
+ engines:
+ node: '>=0.6.0'
+ resolution:
+ integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
+ /tmpl/1.0.4:
+ dev: true
+ resolution:
+ integrity: sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
+ /to-array/0.1.4:
+ dev: true
+ resolution:
+ integrity: sha1-F+bBH3PdTz10zaek/zI46a2b+JA=
+ /to-arraybuffer/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
+ /to-buffer/1.1.1:
+ dev: true
+ resolution:
+ integrity: sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==
+ /to-fast-properties/2.0.0:
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+ /to-object-path/0.3.0:
+ dependencies:
+ kind-of: 3.2.2
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+ /to-readable-stream/1.0.0:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==
+ /to-regex-range/2.1.1:
+ dependencies:
+ is-number: 3.0.0
+ repeat-string: 1.6.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+ /to-regex-range/5.0.1:
+ dependencies:
+ is-number: 7.0.0
+ dev: true
+ engines:
+ node: '>=8.0'
+ resolution:
+ integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ /to-regex/3.0.2:
+ dependencies:
+ define-property: 2.0.2
+ extend-shallow: 3.0.2
+ regex-not: 1.0.2
+ safe-regex: 1.1.0
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+ /toastr/2.1.4:
+ dependencies:
+ jquery: 3.4.1
+ dev: false
+ resolution:
+ integrity: sha1-i0O+ZPudDEFIcURvLbjoyk6V8YE=
+ /toggle-selection/1.0.6:
+ dev: false
+ resolution:
+ integrity: sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
+ /toidentifier/1.0.0:
+ engines:
+ node: '>=0.6'
+ resolution:
+ integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+ /toposort/2.0.2:
+ dev: false
+ resolution:
+ integrity: sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
+ /tough-cookie/2.5.0:
+ dependencies:
+ psl: 1.8.0
+ punycode: 2.1.1
+ engines:
+ node: '>=0.8'
+ resolution:
+ integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+ /tough-cookie/3.0.1:
+ dependencies:
+ ip-regex: 2.1.0
+ psl: 1.8.0
+ punycode: 2.1.1
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
+ /tr46/1.0.1:
+ dependencies:
+ punycode: 2.1.1
+ dev: true
+ resolution:
+ integrity: sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
+ /traverse-chain/0.1.0:
+ dev: true
+ resolution:
+ integrity: sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=
+ /traverse/0.6.6:
+ dev: true
+ resolution:
+ integrity: sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=
+ /trim-newlines/3.0.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==
+ /trim-repeated/1.0.0:
+ dependencies:
+ escape-string-regexp: 1.0.5
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-42RqLqTokTEr9+rObPsFOAvAHCE=
+ /triple-beam/1.3.0:
+ dev: true
+ resolution:
+ integrity: sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
+ /ts-node/3.3.0:
+ dependencies:
+ arrify: 1.0.1
+ chalk: 2.4.2
+ diff: 3.5.0
+ make-error: 1.3.6
+ minimist: 1.2.5
+ mkdirp: 0.5.5
+ source-map-support: 0.4.18
+ tsconfig: 6.0.0
+ v8flags: 3.1.3
+ yn: 2.0.0
+ dev: true
+ engines:
+ node: '>=4.2.0'
+ hasBin: true
+ resolution:
+ integrity: sha1-wTxqMCTjC+EYDdUwOPwgkonUv2k=
+ /ts-pnp/1.1.6:
+ dev: true
+ engines:
+ node: '>=6'
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ resolution:
+ integrity: sha512-CrG5GqAAzMT7144Cl+UIFP7mz/iIhiy+xQ6GGcnjTezhALT02uPMRw7tgDSESgB5MsfKt55+GPWw4ir1kVtMIQ==
+ /tsconfig/6.0.0:
+ dependencies:
+ strip-bom: 3.0.0
+ strip-json-comments: 2.0.1
+ dev: true
+ resolution:
+ integrity: sha1-aw6DdgA9evGGT434+J3QBZ/80DI=
+ /tslib/1.11.1:
+ resolution:
+ integrity: sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
+ /tsutils/3.17.1:
+ dependencies:
+ tslib: 1.11.1
+ dev: true
+ engines:
+ node: '>= 6'
+ peerDependencies:
+ typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
+ resolution:
+ integrity: sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==
+ /tsutils/3.17.1_typescript@3.8.3:
+ dependencies:
+ tslib: 1.11.1
+ typescript: 3.8.3
+ dev: true
+ engines:
+ node: '>= 6'
+ peerDependencies:
+ typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
+ resolution:
+ integrity: sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==
+ /tty-browserify/0.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=
+ /tunnel-agent/0.6.0:
+ dependencies:
+ safe-buffer: 5.2.0
+ resolution:
+ integrity: sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+ /tweetnacl/0.14.5:
+ resolution:
+ integrity: sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+ /type-check/0.3.2:
+ dependencies:
+ prelude-ls: 1.1.2
+ dev: true
+ engines:
+ node: '>= 0.8.0'
+ resolution:
+ integrity: sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+ /type-detect/4.0.8:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+ /type-fest/0.11.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==
+ /type-fest/0.3.1:
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==
+ /type-fest/0.6.0:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
+ /type-fest/0.8.1:
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+ /type-is/1.6.18:
+ dependencies:
+ media-typer: 0.3.0
+ mime-types: 2.1.26
+ engines:
+ node: '>= 0.6'
+ resolution:
+ integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+ /type/1.2.0:
+ dev: true
+ resolution:
+ integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
+ /type/2.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==
+ /typed-styles/0.0.7:
+ dev: false
+ resolution:
+ integrity: sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==
+ /typedarray-to-buffer/3.1.5:
+ dependencies:
+ is-typedarray: 1.0.0
+ dev: true
+ resolution:
+ integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
+ /typedarray/0.0.6:
+ dev: true
+ resolution:
+ integrity: sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+ /typeface-lato/0.0.75:
+ dev: false
+ resolution:
+ integrity: sha512-iA5uJD4PSTyIE4BDiSOexQeXkDkiJuX4Hu3wh3saJ06EB2TvJayab1Lbbmqq2je/LQv7KCQZHZmC0k4hedd8sw==
+ /typescript/3.8.3:
+ dev: true
+ engines:
+ node: '>=4.2.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
+ /un-eval/1.2.0:
+ dev: true
+ resolution:
+ integrity: sha512-Wlj/pum6dQtGTPD/lclDtoVPkSfpjPfy1dwnnKw/sZP5DpBH9fLhBgQfsqNhe5/gS1D+vkZUuB771NRMUPA5CA==
+ /unbzip2-stream/1.4.0:
+ dependencies:
+ buffer: 5.5.0
+ through: 2.3.8
+ dev: true
+ resolution:
+ integrity: sha512-kVx7CDAsdBSWVf404Mw7oI9i09w5/mTT/Ruk+RWa64PLYKvsAucLLFHvQtnvjeADM4ZizxrvG5SHnF4Te4T2Cg==
+ /underscore/1.10.2:
+ dev: false
+ resolution:
+ integrity: sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==
+ /unfetch/4.1.0:
+ dev: false
+ resolution:
+ integrity: sha512-crP/n3eAPUJxZXM9T80/yv0YhkTEx2K1D3h7D1AJM6fzsWZrxdyRuLN0JH/dkZh1LNH8LxCnBzoPFCPbb2iGpg==
+ /unicode-canonical-property-names-ecmascript/1.0.4:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
+ /unicode-match-property-ecmascript/1.0.4:
+ dependencies:
+ unicode-canonical-property-names-ecmascript: 1.0.4
+ unicode-property-aliases-ecmascript: 1.1.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==
+ /unicode-match-property-value-ecmascript/1.2.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==
+ /unicode-property-aliases-ecmascript/1.1.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
+ /union-value/1.0.1:
+ dependencies:
+ arr-union: 3.1.0
+ get-value: 2.0.6
+ is-extendable: 0.1.1
+ set-value: 2.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
+ /uniq/1.0.1:
+ dev: true
+ resolution:
+ integrity: sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
+ /uniqs/2.0.0:
+ dev: true
+ resolution:
+ integrity: sha1-/+3ks2slKQaW5uFl1KWe25mOawI=
+ /unique-filename/1.1.1:
+ dependencies:
+ unique-slug: 2.0.2
+ dev: true
+ resolution:
+ integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
+ /unique-slug/2.0.2:
+ dependencies:
+ imurmurhash: 0.1.4
+ dev: true
+ resolution:
+ integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
+ /unique-string/1.0.0:
+ dependencies:
+ crypto-random-string: 1.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=
+ /universalify/0.1.2:
+ engines:
+ node: '>= 4.0.0'
+ resolution:
+ integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+ /unpipe/1.0.0:
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+ /unquote/1.1.1:
+ dev: true
+ resolution:
+ integrity: sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=
+ /unset-value/1.0.0:
+ dependencies:
+ has-value: 0.3.1
+ isobject: 3.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+ /untildify/3.0.3:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==
+ /unzip-response/2.0.1:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=
+ /upath/1.2.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
+ /update-notifier/2.5.0:
+ dependencies:
+ boxen: 1.3.0
+ chalk: 2.4.2
+ configstore: 3.1.2
+ import-lazy: 2.1.0
+ is-ci: 1.2.1
+ is-installed-globally: 0.1.0
+ is-npm: 1.0.0
+ latest-version: 3.1.0
+ semver-diff: 2.1.0
+ xdg-basedir: 3.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==
+ /update-notifier/3.0.1:
+ dependencies:
+ boxen: 3.2.0
+ chalk: 2.4.2
+ configstore: 4.0.0
+ has-yarn: 2.1.0
+ import-lazy: 2.1.0
+ is-ci: 2.0.0
+ is-installed-globally: 0.1.0
+ is-npm: 3.0.0
+ is-yarn-global: 0.3.0
+ latest-version: 5.1.0
+ semver-diff: 2.1.0
+ xdg-basedir: 3.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-grrmrB6Zb8DUiyDIaeRTBCkgISYUgETNe7NglEbVsrLWXeESnlCSP50WfRSj/GmzMPl6Uchj24S/p80nP/ZQrQ==
+ /uri-js/3.0.2:
+ dependencies:
+ punycode: 2.1.1
+ dev: true
+ resolution:
+ integrity: sha1-+QuFhQf4HepNz7s8TD2/orVX+qo=
+ /uri-js/4.2.2:
+ dependencies:
+ punycode: 2.1.1
+ resolution:
+ integrity: sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+ /urijs/1.19.2:
+ dev: true
+ resolution:
+ integrity: sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w==
+ /urix/0.1.0:
+ deprecated: 'Please see https://github.com/lydell/urix#deprecated'
+ dev: true
+ resolution:
+ integrity: sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+ /url-loader/2.3.0_file-loader@4.3.0+webpack@4.42.0:
+ dependencies:
+ file-loader: 4.3.0_webpack@4.42.0
+ loader-utils: 1.4.0
+ mime: 2.4.4
+ schema-utils: 2.6.5
+ webpack: 4.42.0_webpack@4.42.0
+ dev: true
+ engines:
+ node: '>= 8.9.0'
+ peerDependencies:
+ file-loader: '*'
+ webpack: ^4.0.0
+ peerDependenciesMeta:
+ file-loader:
+ optional: true
+ resolution:
+ integrity: sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog==
+ /url-parse-lax/1.0.0:
+ dependencies:
+ prepend-http: 1.0.4
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=
+ /url-parse-lax/3.0.0:
+ dependencies:
+ prepend-http: 2.0.0
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=
+ /url-parse/1.4.7:
+ dependencies:
+ querystringify: 2.1.1
+ requires-port: 1.0.0
+ dev: true
+ resolution:
+ integrity: sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
+ /url-to-options/1.0.1:
+ dev: true
+ engines:
+ node: '>= 4'
+ resolution:
+ integrity: sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=
+ /url/0.10.3:
+ dependencies:
+ punycode: 1.3.2
+ querystring: 0.2.0
+ resolution:
+ integrity: sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=
+ /url/0.11.0:
+ dependencies:
+ punycode: 1.3.2
+ querystring: 0.2.0
+ dev: true
+ resolution:
+ integrity: sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
+ /urlencode/1.1.0:
+ dependencies:
+ iconv-lite: 0.4.24
+ dev: true
+ resolution:
+ integrity: sha1-HyuibwE8hfATP3o61v8nMK33y7c=
+ /use-memo-one/1.1.1_react@16.13.1:
+ dependencies:
+ react: 16.13.1
+ dev: false
+ peerDependencies:
+ react: ^16.8.0
+ resolution:
+ integrity: sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==
+ /use/3.1.1:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+ /utf8/3.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==
+ /util-deprecate/1.0.2:
+ dev: true
+ resolution:
+ integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+ /util.promisify/1.0.0:
+ dependencies:
+ define-properties: 1.1.3
+ object.getownpropertydescriptors: 2.1.0
+ dev: true
+ resolution:
+ integrity: sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
+ /util.promisify/1.0.1:
+ dependencies:
+ define-properties: 1.1.3
+ es-abstract: 1.17.5
+ has-symbols: 1.0.1
+ object.getownpropertydescriptors: 2.1.0
+ dev: true
+ resolution:
+ integrity: sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==
+ /util/0.10.3:
+ dependencies:
+ inherits: 2.0.1
+ dev: true
+ resolution:
+ integrity: sha1-evsa/lCAUkZInj23/g7TeTNqwPk=
+ /util/0.11.1:
+ dependencies:
+ inherits: 2.0.3
+ dev: true
+ resolution:
+ integrity: sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==
+ /utila/0.4.0:
+ dev: true
+ resolution:
+ integrity: sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
+ /utils-merge/1.0.1:
+ engines:
+ node: '>= 0.4.0'
+ resolution:
+ integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
+ /uuid/2.0.3:
+ dev: true
+ resolution:
+ integrity: sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=
+ /uuid/3.0.0:
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha1-Zyj8BFnEUNeWqZwxg3VpvfZy1yg=
+ /uuid/3.3.2:
+ hasBin: true
+ resolution:
+ integrity: sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
+ /uuid/3.4.0:
+ hasBin: true
+ resolution:
+ integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+ /v8-compile-cache/2.0.3:
+ dev: true
+ resolution:
+ integrity: sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==
+ /v8-compile-cache/2.1.0:
+ dev: true
+ resolution:
+ integrity: sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==
+ /v8-to-istanbul/4.1.3:
+ dependencies:
+ '@types/istanbul-lib-coverage': 2.0.1
+ convert-source-map: 1.7.0
+ source-map: 0.7.3
+ dev: true
+ engines:
+ node: 8.x.x || >=10.10.0
+ resolution:
+ integrity: sha512-sAjOC+Kki6aJVbUOXJbcR0MnbfjvBzwKZazEJymA2IX49uoOdEdk+4fBq5cXgYgiyKtAyrrJNtBZdOeDIF+Fng==
+ /v8flags/3.1.3:
+ dependencies:
+ homedir-polyfill: 1.0.3
+ dev: true
+ engines:
+ node: '>= 0.10'
+ resolution:
+ integrity: sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==
+ /validate-npm-package-license/3.0.4:
+ dependencies:
+ spdx-correct: 3.1.0
+ spdx-expression-parse: 3.0.0
+ dev: true
+ resolution:
+ integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
+ /validatorjs/3.18.1:
+ dependencies:
+ date-fns: 2.1.0
+ dev: false
+ resolution:
+ integrity: sha512-ZyHd0lJKNft3nUe+tYtTui2B5GwdKexWB55qNljccJouW/eo06YhYpCYjPlN/F5n/o0eS1uvb1Janh6eRl+TBQ==
+ /value-equal/1.0.1:
+ dev: false
+ resolution:
+ integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
+ /vary/1.1.2:
+ engines:
+ node: '>= 0.8'
+ resolution:
+ integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
+ /velocityjs/1.1.5:
+ dev: true
+ engines:
+ node: '>=0.8.0'
+ hasBin: true
+ resolution:
+ integrity: sha512-U4ANK4MRYSczVZjOp9FkAQoPO9geKSy3CWrBShPxMoWyqDox8SW8AZYiKtlCrV21ucONUtlU0iF3+KKK9AGoyA==
+ /vendors/1.0.4:
+ dev: true
+ resolution:
+ integrity: sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
+ /verror/1.10.0:
+ dependencies:
+ assert-plus: 1.0.0
+ core-util-is: 1.0.2
+ extsprintf: 1.3.0
+ engines:
+ '0': node >=0.6.0
+ resolution:
+ integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+ /vm-browserify/1.1.2:
+ dev: true
+ resolution:
+ integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
+ /w3c-hr-time/1.0.2:
+ dependencies:
+ browser-process-hrtime: 1.0.0
+ dev: true
+ resolution:
+ integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
+ /w3c-xmlserializer/1.1.2:
+ dependencies:
+ domexception: 1.0.1
+ webidl-conversions: 4.0.2
+ xml-name-validator: 3.0.0
+ dev: true
+ resolution:
+ integrity: sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==
+ /walkdir/0.0.11:
+ dev: true
+ engines:
+ node: '>=0.6.0'
+ resolution:
+ integrity: sha1-oW0CXrkxvQO1LzCMrtD0D86+lTI=
+ /walker/1.0.7:
+ dependencies:
+ makeerror: 1.0.11
+ dev: true
+ resolution:
+ integrity: sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=
+ /warning/4.0.3:
+ dependencies:
+ loose-envify: 1.4.0
+ dev: false
+ resolution:
+ integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
+ /watchpack/1.6.1:
+ dependencies:
+ chokidar: 2.1.8
+ graceful-fs: 4.2.3
+ neo-async: 2.6.1
+ dev: true
+ resolution:
+ integrity: sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA==
+ /wbuf/1.7.3:
+ dependencies:
+ minimalistic-assert: 1.0.1
+ dev: true
+ resolution:
+ integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==
+ /webidl-conversions/4.0.2:
+ dev: true
+ resolution:
+ integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+ /webpack-cli/3.3.11_webpack@4.42.1:
+ dependencies:
+ chalk: 2.4.2
+ cross-spawn: 6.0.5
+ enhanced-resolve: 4.1.0
+ findup-sync: 3.0.0
+ global-modules: 2.0.0
+ import-local: 2.0.0
+ interpret: 1.2.0
+ loader-utils: 1.2.3
+ supports-color: 6.1.0
+ v8-compile-cache: 2.0.3
+ webpack: 4.42.1_webpack@4.42.1
+ yargs: 13.2.4
+ dev: true
+ engines:
+ node: '>=6.11.5'
+ hasBin: true
+ peerDependencies:
+ webpack: 4.x.x
+ resolution:
+ integrity: sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g==
+ /webpack-dev-middleware/3.7.2_webpack@4.42.0:
+ dependencies:
+ memory-fs: 0.4.1
+ mime: 2.4.4
+ mkdirp: 0.5.5
+ range-parser: 1.2.1
+ webpack: 4.42.0_webpack@4.42.0
+ webpack-log: 2.0.0
+ dev: true
+ engines:
+ node: '>= 6'
+ peerDependencies:
+ webpack: ^4.0.0
+ resolution:
+ integrity: sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==
+ /webpack-dev-server/3.10.3_webpack@4.42.0:
+ dependencies:
+ ansi-html: 0.0.7
+ bonjour: 3.5.0
+ chokidar: 2.1.8
+ compression: 1.7.4
+ connect-history-api-fallback: 1.6.0
+ debug: 4.1.1
+ del: 4.1.1
+ express: 4.17.1
+ html-entities: 1.2.1
+ http-proxy-middleware: 0.19.1
+ import-local: 2.0.0
+ internal-ip: 4.3.0
+ ip: 1.1.5
+ is-absolute-url: 3.0.3
+ killable: 1.0.1
+ loglevel: 1.6.7
+ opn: 5.5.0
+ p-retry: 3.0.1
+ portfinder: 1.0.25
+ schema-utils: 1.0.0
+ selfsigned: 1.10.7
+ semver: 6.3.0
+ serve-index: 1.9.1
+ sockjs: 0.3.19
+ sockjs-client: 1.4.0
+ spdy: 4.0.2
+ strip-ansi: 3.0.1
+ supports-color: 6.1.0
+ url: 0.11.0
+ webpack: 4.42.0_webpack@4.42.0
+ webpack-dev-middleware: 3.7.2_webpack@4.42.0
+ webpack-log: 2.0.0
+ ws: 6.2.1
+ yargs: 12.0.5
+ dev: true
+ engines:
+ node: '>= 6.11.5'
+ hasBin: true
+ peerDependencies:
+ webpack: ^4.0.0 || ^5.0.0
+ webpack-cli: '*'
+ peerDependenciesMeta:
+ webpack-cli:
+ optional: true
+ resolution:
+ integrity: sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ==
+ /webpack-log/2.0.0:
+ dependencies:
+ ansi-colors: 3.2.4
+ uuid: 3.4.0
+ dev: true
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==
+ /webpack-manifest-plugin/2.2.0_webpack@4.42.0:
+ dependencies:
+ fs-extra: 7.0.1
+ lodash: 4.17.15
+ object.entries: 1.1.1
+ tapable: 1.1.3
+ webpack: 4.42.0_webpack@4.42.0
+ dev: true
+ engines:
+ node: '>=6.11.5'
+ peerDependencies:
+ webpack: 2 || 3 || 4
+ resolution:
+ integrity: sha512-9S6YyKKKh/Oz/eryM1RyLVDVmy3NSPV0JXMRhZ18fJsq+AwGxUY34X54VNwkzYcEmEkDwNxuEOboCZEebJXBAQ==
+ /webpack-node-externals/1.7.2:
+ dev: true
+ resolution:
+ integrity: sha512-ajerHZ+BJKeCLviLUUmnyd5B4RavLF76uv3cs6KNuO8W+HuQaEs0y0L7o40NQxdPy5w0pcv8Ew7yPUAQG0UdCg==
+ /webpack-sources/1.4.3:
+ dependencies:
+ source-list-map: 2.0.1
+ source-map: 0.6.1
+ dev: true
+ resolution:
+ integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
+ /webpack/4.41.2_webpack@4.41.2:
+ dependencies:
+ '@webassemblyjs/ast': 1.8.5
+ '@webassemblyjs/helper-module-context': 1.8.5
+ '@webassemblyjs/wasm-edit': 1.8.5
+ '@webassemblyjs/wasm-parser': 1.8.5
+ acorn: 6.4.1
+ ajv: 6.12.0
+ ajv-keywords: 3.4.1_ajv@6.12.0
+ chrome-trace-event: 1.0.2
+ enhanced-resolve: 4.1.1
+ eslint-scope: 4.0.3
+ json-parse-better-errors: 1.0.2
+ loader-runner: 2.4.0
+ loader-utils: 1.4.0
+ memory-fs: 0.4.1
+ micromatch: 3.1.10
+ mkdirp: 0.5.5
+ neo-async: 2.6.1
+ node-libs-browser: 2.2.1
+ schema-utils: 1.0.0
+ tapable: 1.1.3
+ terser-webpack-plugin: 1.4.3_webpack@4.41.2
+ watchpack: 1.6.1
+ webpack-sources: 1.4.3
+ dev: true
+ engines:
+ node: '>=6.11.5'
+ hasBin: true
+ peerDependencies:
+ webpack: '*'
+ resolution:
+ integrity: sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A==
+ /webpack/4.42.0_webpack@4.42.0:
+ dependencies:
+ '@webassemblyjs/ast': 1.8.5
+ '@webassemblyjs/helper-module-context': 1.8.5
+ '@webassemblyjs/wasm-edit': 1.8.5
+ '@webassemblyjs/wasm-parser': 1.8.5
+ acorn: 6.4.1
+ ajv: 6.12.0
+ ajv-keywords: 3.4.1_ajv@6.12.0
+ chrome-trace-event: 1.0.2
+ enhanced-resolve: 4.1.1
+ eslint-scope: 4.0.3
+ json-parse-better-errors: 1.0.2
+ loader-runner: 2.4.0
+ loader-utils: 1.4.0
+ memory-fs: 0.4.1
+ micromatch: 3.1.10
+ mkdirp: 0.5.5
+ neo-async: 2.6.1
+ node-libs-browser: 2.2.1
+ schema-utils: 1.0.0
+ tapable: 1.1.3
+ terser-webpack-plugin: 1.4.3_webpack@4.42.0
+ watchpack: 1.6.1
+ webpack-sources: 1.4.3
+ dev: true
+ engines:
+ node: '>=6.11.5'
+ hasBin: true
+ peerDependencies:
+ webpack: '*'
+ resolution:
+ integrity: sha512-EzJRHvwQyBiYrYqhyjW9AqM90dE4+s1/XtCfn7uWg6cS72zH+2VPFAlsnW0+W0cDi0XRjNKUMoJtpSi50+Ph6w==
+ /webpack/4.42.1_webpack@4.42.1:
+ dependencies:
+ '@webassemblyjs/ast': 1.9.0
+ '@webassemblyjs/helper-module-context': 1.9.0
+ '@webassemblyjs/wasm-edit': 1.9.0
+ '@webassemblyjs/wasm-parser': 1.9.0
+ acorn: 6.4.1
+ ajv: 6.12.0
+ ajv-keywords: 3.4.1_ajv@6.12.0
+ chrome-trace-event: 1.0.2
+ enhanced-resolve: 4.1.1
+ eslint-scope: 4.0.3
+ json-parse-better-errors: 1.0.2
+ loader-runner: 2.4.0
+ loader-utils: 1.4.0
+ memory-fs: 0.4.1
+ micromatch: 3.1.10
+ mkdirp: 0.5.5
+ neo-async: 2.6.1
+ node-libs-browser: 2.2.1
+ schema-utils: 1.0.0
+ tapable: 1.1.3
+ terser-webpack-plugin: 1.4.3_webpack@4.42.1
+ watchpack: 1.6.1
+ webpack-sources: 1.4.3
+ dev: true
+ engines:
+ node: '>=6.11.5'
+ hasBin: true
+ peerDependencies:
+ webpack: '*'
+ resolution:
+ integrity: sha512-SGfYMigqEfdGchGhFFJ9KyRpQKnipvEvjc1TwrXEPCM6H5Wywu10ka8o3KGrMzSMxMQKt8aCHUFh5DaQ9UmyRg==
+ /websocket-driver/0.7.3:
+ dependencies:
+ http-parser-js: 0.4.10
+ safe-buffer: 5.2.0
+ websocket-extensions: 0.1.3
+ dev: true
+ engines:
+ node: '>=0.8.0'
+ resolution:
+ integrity: sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==
+ /websocket-extensions/0.1.3:
+ dev: true
+ engines:
+ node: '>=0.8.0'
+ resolution:
+ integrity: sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
+ /websocket-framed/1.2.2:
+ dependencies:
+ encodr: 1.2.2
+ eventemitter3: 4.0.0
+ dev: true
+ engines:
+ node: '>=8.0.0'
+ resolution:
+ integrity: sha512-7EeuDADPk6SLmpBiSnxg7P/ZxFKb7WOjpA+pvcsnKLwTGzJO1aob+gxUXETW93cozUIOUPGS6+rgaClAjig1qQ==
+ /whatwg-encoding/1.0.5:
+ dependencies:
+ iconv-lite: 0.4.24
+ dev: true
+ resolution:
+ integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
+ /whatwg-fetch/3.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
+ /whatwg-mimetype/2.3.0:
+ dev: true
+ resolution:
+ integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+ /whatwg-url/6.5.0:
+ dependencies:
+ lodash.sortby: 4.7.0
+ tr46: 1.0.1
+ webidl-conversions: 4.0.2
+ dev: true
+ resolution:
+ integrity: sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
+ /whatwg-url/7.1.0:
+ dependencies:
+ lodash.sortby: 4.7.0
+ tr46: 1.0.1
+ webidl-conversions: 4.0.2
+ dev: true
+ resolution:
+ integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
+ /which-module/2.0.0:
+ resolution:
+ integrity: sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+ /which/1.3.1:
+ dependencies:
+ isexe: 2.0.0
+ hasBin: true
+ resolution:
+ integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ /which/2.0.2:
+ dependencies:
+ isexe: 2.0.0
+ engines:
+ node: '>= 8'
+ hasBin: true
+ resolution:
+ integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ /widest-line/2.0.1:
+ dependencies:
+ string-width: 2.1.1
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==
+ /winston-transport/4.3.0:
+ dependencies:
+ readable-stream: 2.3.7
+ triple-beam: 1.3.0
+ dev: true
+ engines:
+ node: '>= 6.4.0'
+ resolution:
+ integrity: sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==
+ /winston/3.2.1:
+ dependencies:
+ async: 2.6.3
+ diagnostics: 1.1.1
+ is-stream: 1.1.0
+ logform: 2.1.2
+ one-time: 0.0.4
+ readable-stream: 3.6.0
+ stack-trace: 0.0.10
+ triple-beam: 1.3.0
+ winston-transport: 4.3.0
+ dev: true
+ engines:
+ node: '>= 6.4.0'
+ resolution:
+ integrity: sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==
+ /word-wrap/1.2.3:
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
+ /workbox-background-sync/4.3.1:
+ dependencies:
+ workbox-core: 4.3.1
+ dev: true
+ resolution:
+ integrity: sha512-1uFkvU8JXi7L7fCHVBEEnc3asPpiAL33kO495UMcD5+arew9IbKW2rV5lpzhoWcm/qhGB89YfO4PmB/0hQwPRg==
+ /workbox-broadcast-update/4.3.1:
+ dependencies:
+ workbox-core: 4.3.1
+ dev: true
+ resolution:
+ integrity: sha512-MTSfgzIljpKLTBPROo4IpKjESD86pPFlZwlvVG32Kb70hW+aob4Jxpblud8EhNb1/L5m43DUM4q7C+W6eQMMbA==
+ /workbox-build/4.3.1:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ '@hapi/joi': 15.1.1
+ common-tags: 1.8.0
+ fs-extra: 4.0.3
+ glob: 7.1.6
+ lodash.template: 4.5.0
+ pretty-bytes: 5.3.0
+ stringify-object: 3.3.0
+ strip-comments: 1.0.2
+ workbox-background-sync: 4.3.1
+ workbox-broadcast-update: 4.3.1
+ workbox-cacheable-response: 4.3.1
+ workbox-core: 4.3.1
+ workbox-expiration: 4.3.1
+ workbox-google-analytics: 4.3.1
+ workbox-navigation-preload: 4.3.1
+ workbox-precaching: 4.3.1
+ workbox-range-requests: 4.3.1
+ workbox-routing: 4.3.1
+ workbox-strategies: 4.3.1
+ workbox-streams: 4.3.1
+ workbox-sw: 4.3.1
+ workbox-window: 4.3.1
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ resolution:
+ integrity: sha512-UHdwrN3FrDvicM3AqJS/J07X0KXj67R8Cg0waq1MKEOqzo89ap6zh6LmaLnRAjpB+bDIz+7OlPye9iii9KBnxw==
+ /workbox-cacheable-response/4.3.1:
+ dependencies:
+ workbox-core: 4.3.1
+ dev: true
+ resolution:
+ integrity: sha512-Rp5qlzm6z8IOvnQNkCdO9qrDgDpoPNguovs0H8C+wswLuPgSzSp9p2afb5maUt9R1uTIwOXrVQMmPfPypv+npw==
+ /workbox-core/4.3.1:
+ dev: true
+ resolution:
+ integrity: sha512-I3C9jlLmMKPxAC1t0ExCq+QoAMd0vAAHULEgRZ7kieCdUd919n53WC0AfvokHNwqRhGn+tIIj7vcb5duCjs2Kg==
+ /workbox-expiration/4.3.1:
+ dependencies:
+ workbox-core: 4.3.1
+ dev: true
+ resolution:
+ integrity: sha512-vsJLhgQsQouv9m0rpbXubT5jw0jMQdjpkum0uT+d9tTwhXcEZks7qLfQ9dGSaufTD2eimxbUOJfWLbNQpIDMPw==
+ /workbox-google-analytics/4.3.1:
+ dependencies:
+ workbox-background-sync: 4.3.1
+ workbox-core: 4.3.1
+ workbox-routing: 4.3.1
+ workbox-strategies: 4.3.1
+ dev: true
+ resolution:
+ integrity: sha512-xzCjAoKuOb55CBSwQrbyWBKqp35yg1vw9ohIlU2wTy06ZrYfJ8rKochb1MSGlnoBfXGWss3UPzxR5QL5guIFdg==
+ /workbox-navigation-preload/4.3.1:
+ dependencies:
+ workbox-core: 4.3.1
+ dev: true
+ resolution:
+ integrity: sha512-K076n3oFHYp16/C+F8CwrRqD25GitA6Rkd6+qAmLmMv1QHPI2jfDwYqrytOfKfYq42bYtW8Pr21ejZX7GvALOw==
+ /workbox-precaching/4.3.1:
+ dependencies:
+ workbox-core: 4.3.1
+ dev: true
+ resolution:
+ integrity: sha512-piSg/2csPoIi/vPpp48t1q5JLYjMkmg5gsXBQkh/QYapCdVwwmKlU9mHdmy52KsDGIjVaqEUMFvEzn2LRaigqQ==
+ /workbox-range-requests/4.3.1:
+ dependencies:
+ workbox-core: 4.3.1
+ dev: true
+ resolution:
+ integrity: sha512-S+HhL9+iTFypJZ/yQSl/x2Bf5pWnbXdd3j57xnb0V60FW1LVn9LRZkPtneODklzYuFZv7qK6riZ5BNyc0R0jZA==
+ /workbox-routing/4.3.1:
+ dependencies:
+ workbox-core: 4.3.1
+ dev: true
+ resolution:
+ integrity: sha512-FkbtrODA4Imsi0p7TW9u9MXuQ5P4pVs1sWHK4dJMMChVROsbEltuE79fBoIk/BCztvOJ7yUpErMKa4z3uQLX+g==
+ /workbox-strategies/4.3.1:
+ dependencies:
+ workbox-core: 4.3.1
+ dev: true
+ resolution:
+ integrity: sha512-F/+E57BmVG8dX6dCCopBlkDvvhg/zj6VDs0PigYwSN23L8hseSRwljrceU2WzTvk/+BSYICsWmRq5qHS2UYzhw==
+ /workbox-streams/4.3.1:
+ dependencies:
+ workbox-core: 4.3.1
+ dev: true
+ resolution:
+ integrity: sha512-4Kisis1f/y0ihf4l3u/+ndMkJkIT4/6UOacU3A4BwZSAC9pQ9vSvJpIi/WFGQRH/uPXvuVjF5c2RfIPQFSS2uA==
+ /workbox-sw/4.3.1:
+ dev: true
+ resolution:
+ integrity: sha512-0jXdusCL2uC5gM3yYFT6QMBzKfBr2XTk0g5TPAV4y8IZDyVNDyj1a8uSXy3/XrvkVTmQvLN4O5k3JawGReXr9w==
+ /workbox-webpack-plugin/4.3.1_webpack@4.42.0:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ json-stable-stringify: 1.0.1
+ webpack: 4.42.0_webpack@4.42.0
+ workbox-build: 4.3.1
+ dev: true
+ engines:
+ node: '>=4.0.0'
+ peerDependencies:
+ webpack: ^2.0.0 || ^3.0.0 || ^4.0.0
+ resolution:
+ integrity: sha512-gJ9jd8Mb8wHLbRz9ZvGN57IAmknOipD3W4XNE/Lk/4lqs5Htw4WOQgakQy/o/4CoXQlMCYldaqUg+EJ35l9MEQ==
+ /workbox-window/4.3.1:
+ dependencies:
+ workbox-core: 4.3.1
+ dev: true
+ resolution:
+ integrity: sha512-C5gWKh6I58w3GeSc0wp2Ne+rqVw8qwcmZnQGpjiek8A2wpbxSJb1FdCoQVO+jDJs35bFgo/WETgl1fqgsxN0Hg==
+ /worker-farm/1.7.0:
+ dependencies:
+ errno: 0.1.7
+ dev: true
+ resolution:
+ integrity: sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==
+ /worker-rpc/0.1.1:
+ dependencies:
+ microevent.ts: 0.1.1
+ dev: true
+ resolution:
+ integrity: sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==
+ /wrap-ansi/2.1.0:
+ dependencies:
+ string-width: 1.0.2
+ strip-ansi: 3.0.1
+ dev: true
+ engines:
+ node: '>=0.10.0'
+ resolution:
+ integrity: sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
+ /wrap-ansi/5.1.0:
+ dependencies:
+ ansi-styles: 3.2.1
+ string-width: 3.1.0
+ strip-ansi: 5.2.0
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
+ /wrap-ansi/6.2.0:
+ dependencies:
+ ansi-styles: 4.2.1
+ string-width: 4.2.0
+ strip-ansi: 6.0.0
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+ /wrappy/1.0.2:
+ dev: true
+ resolution:
+ integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+ /write-file-atomic/2.4.1:
+ dependencies:
+ graceful-fs: 4.2.3
+ imurmurhash: 0.1.4
+ signal-exit: 3.0.3
+ dev: true
+ resolution:
+ integrity: sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==
+ /write-file-atomic/2.4.3:
+ dependencies:
+ graceful-fs: 4.2.3
+ imurmurhash: 0.1.4
+ signal-exit: 3.0.3
+ dev: true
+ resolution:
+ integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==
+ /write-file-atomic/3.0.3:
+ dependencies:
+ imurmurhash: 0.1.4
+ is-typedarray: 1.0.0
+ signal-exit: 3.0.3
+ typedarray-to-buffer: 3.1.5
+ dev: true
+ resolution:
+ integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
+ /write/1.0.3:
+ dependencies:
+ mkdirp: 0.5.5
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
+ /ws/5.2.2:
+ dependencies:
+ async-limiter: 1.0.1
+ dev: true
+ resolution:
+ integrity: sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==
+ /ws/6.1.4:
+ dependencies:
+ async-limiter: 1.0.1
+ dev: true
+ resolution:
+ integrity: sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==
+ /ws/6.2.1:
+ dependencies:
+ async-limiter: 1.0.1
+ dev: true
+ resolution:
+ integrity: sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
+ /ws/7.2.1:
+ dev: true
+ engines:
+ node: '>=8.3.0'
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: ^5.0.2
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+ resolution:
+ integrity: sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==
+ /ws/7.2.3:
+ dev: true
+ engines:
+ node: '>=8.3.0'
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: ^5.0.2
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+ resolution:
+ integrity: sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==
+ /x-path/0.0.2:
+ dependencies:
+ path-extra: 1.0.3
+ dev: true
+ resolution:
+ integrity: sha1-KU0Ha7l6dwbMBwu7Km/YxU32exI=
+ /xdg-basedir/3.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=
+ /xml-name-validator/3.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+ /xml/1.0.1:
+ resolution:
+ integrity: sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=
+ /xml2js/0.4.19:
+ dependencies:
+ sax: 1.2.1
+ xmlbuilder: 9.0.7
+ resolution:
+ integrity: sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
+ /xmlbuilder/9.0.7:
+ engines:
+ node: '>=4.0'
+ resolution:
+ integrity: sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
+ /xmlchars/2.2.0:
+ dev: true
+ resolution:
+ integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+ /xmlhttprequest-ssl/1.5.5:
+ dev: true
+ engines:
+ node: '>=0.4.0'
+ resolution:
+ integrity: sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=
+ /xregexp/4.3.0:
+ dependencies:
+ '@babel/runtime-corejs3': 7.9.2
+ dev: true
+ resolution:
+ integrity: sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==
+ /xtend/4.0.2:
+ engines:
+ node: '>=0.4'
+ resolution:
+ integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+ /y18n/4.0.0:
+ resolution:
+ integrity: sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+ /yallist/2.1.2:
+ dev: true
+ resolution:
+ integrity: sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
+ /yallist/3.1.1:
+ dev: true
+ resolution:
+ integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+ /yallist/4.0.0:
+ dev: true
+ resolution:
+ integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+ /yaml-ast-parser/0.0.43:
+ dev: true
+ resolution:
+ integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==
+ /yaml/1.8.3:
+ dependencies:
+ '@babel/runtime': 7.9.2
+ engines:
+ node: '>= 6'
+ resolution:
+ integrity: sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw==
+ /yamljs/0.3.0:
+ dependencies:
+ argparse: 1.0.10
+ glob: 7.1.6
+ dev: true
+ hasBin: true
+ resolution:
+ integrity: sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==
+ /yargs-parser/11.1.1:
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+ dev: true
+ resolution:
+ integrity: sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==
+ /yargs-parser/13.1.2:
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+ dev: true
+ resolution:
+ integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
+ /yargs-parser/15.0.1:
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+ dev: false
+ resolution:
+ integrity: sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==
+ /yargs-parser/16.1.0:
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+ dev: true
+ resolution:
+ integrity: sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==
+ /yargs-parser/18.1.2:
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+ dev: true
+ engines:
+ node: '>=6'
+ resolution:
+ integrity: sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==
+ /yargs/12.0.5:
+ dependencies:
+ cliui: 4.1.0
+ decamelize: 1.2.0
+ find-up: 3.0.0
+ get-caller-file: 1.0.3
+ os-locale: 3.1.0
+ require-directory: 2.1.1
+ require-main-filename: 1.0.1
+ set-blocking: 2.0.0
+ string-width: 2.1.1
+ which-module: 2.0.0
+ y18n: 4.0.0
+ yargs-parser: 11.1.1
+ dev: true
+ resolution:
+ integrity: sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
+ /yargs/13.2.4:
+ dependencies:
+ cliui: 5.0.0
+ find-up: 3.0.0
+ get-caller-file: 2.0.5
+ os-locale: 3.1.0
+ require-directory: 2.1.1
+ require-main-filename: 2.0.0
+ set-blocking: 2.0.0
+ string-width: 3.1.0
+ which-module: 2.0.0
+ y18n: 4.0.0
+ yargs-parser: 13.1.2
+ dev: true
+ resolution:
+ integrity: sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==
+ /yargs/13.3.2:
+ dependencies:
+ cliui: 5.0.0
+ find-up: 3.0.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ require-main-filename: 2.0.0
+ set-blocking: 2.0.0
+ string-width: 3.1.0
+ which-module: 2.0.0
+ y18n: 4.0.0
+ yargs-parser: 13.1.2
+ dev: true
+ resolution:
+ integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
+ /yargs/14.2.3:
+ dependencies:
+ cliui: 5.0.0
+ decamelize: 1.2.0
+ find-up: 3.0.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ require-main-filename: 2.0.0
+ set-blocking: 2.0.0
+ string-width: 3.1.0
+ which-module: 2.0.0
+ y18n: 4.0.0
+ yargs-parser: 15.0.1
+ dev: false
+ resolution:
+ integrity: sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==
+ /yargs/15.3.1:
+ dependencies:
+ cliui: 6.0.0
+ decamelize: 1.2.0
+ find-up: 4.1.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ require-main-filename: 2.0.0
+ set-blocking: 2.0.0
+ string-width: 4.2.0
+ which-module: 2.0.0
+ y18n: 4.0.0
+ yargs-parser: 18.1.2
+ dev: true
+ engines:
+ node: '>=8'
+ resolution:
+ integrity: sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==
+ /yauzl/2.10.0:
+ dependencies:
+ buffer-crc32: 0.2.13
+ fd-slicer: 1.1.0
+ dev: true
+ resolution:
+ integrity: sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
+ /yeast/0.1.2:
+ dev: true
+ resolution:
+ integrity: sha1-AI4G2AlDIMNy28L47XagymyKxBk=
+ /yn/2.0.0:
+ dev: true
+ engines:
+ node: '>=4'
+ resolution:
+ integrity: sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=
+ /zip-stream/1.2.0:
+ dependencies:
+ archiver-utils: 1.3.0
+ compress-commons: 1.2.2
+ lodash: 4.17.15
+ readable-stream: 2.3.7
+ dev: true
+ engines:
+ node: '>= 0.10.0'
+ resolution:
+ integrity: sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000000..afa5784c3c
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,6 @@
+packages:
+ - addons/*/packages/*
+ - main/packages/*
+ - main/solution/*
+ - main/integration-tests
+ - main/cicd/*
diff --git a/scripts/build-all-packages.sh b/scripts/build-all-packages.sh
new file mode 100755
index 0000000000..f3ad35d076
--- /dev/null
+++ b/scripts/build-all-packages.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+pnpm run build --recursive --if-present
\ No newline at end of file
diff --git a/scripts/environment-delete.sh b/scripts/environment-delete.sh
new file mode 100755
index 0000000000..5b9a48337f
--- /dev/null
+++ b/scripts/environment-delete.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+set -e
+
+cd "$(dirname "${BASH_SOURCE[0]}")"
+# shellcheck disable=SC1091
+[[ $UTIL_SOURCED != yes && -f ./util.sh ]] && source ./util.sh
+
+# Ensure settings file exists
+ensure_setttings_file "$@"
+
+# Install
+install_dependencies "$@"
+
+# Delete
+printf "\nWARNING: THIS COMMAND WILL DELETE YOUR ENVIRONMENT AND LEAD TO DATA LOSS.\n"
+printf "\nAre you sure you want to proceed? Press ENTER to ABORT OR Type the environment name to confirm removal (%s): " "$STAGE"
+read -r confirmation
+
+if [[ "$STAGE" != "$confirmation" ]]
+then
+ printf "\nConfirmation mismatch. Exiting ...\n"
+ exit
+fi
+
+function componentRemove {
+ COMPONENT_DIR=$1
+ COMPONENT_NAME=$2
+
+ printf "\Removing component: %s ...\n\n" "$COMPONENT_NAME"
+ cd "$SOLUTION_DIR/$COMPONENT_DIR"
+ $EXEC sls remove -s "$STAGE"
+}
+
+componentRemove "ui" "UI"
+componentRemove "post-deployment" "Post-Deployment"
+componentRemove "edge-lambda" "Edge-Lambda"
+componentRemove "backend" "Backend"
+componentRemove "infrastructure" "Infrastructure"
+
+printf "\n----- ENVIRONMENT DELETED SUCCESSFULLY 🎉 -----\n\n\n"
\ No newline at end of file
diff --git a/scripts/environment-deploy.sh b/scripts/environment-deploy.sh
new file mode 100755
index 0000000000..4e316c7de3
--- /dev/null
+++ b/scripts/environment-deploy.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+set -e
+
+pushd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null
+# shellcheck disable=SC1091
+[[ $UTIL_SOURCED != yes && -f ./util.sh ]] && source ./util.sh
+popd > /dev/null
+
+# Ensure settings file exists
+ensure_setttings_file "$@"
+
+# Install
+install_dependencies "$@"
+
+function disableStats {
+ COMPONENT_DIR=$1
+ pushd "$SOLUTION_DIR/$COMPONENT_DIR" > /dev/null
+ # Disable serverless stats (only strictly needs to be done one time)
+ $EXEC sls slstats --disable -s "$STAGE"
+ popd > /dev/null
+}
+
+function componentDeploy {
+ COMPONENT_DIR=$1
+ COMPONENT_NAME=$2
+
+ pushd "$SOLUTION_DIR/$COMPONENT_DIR" > /dev/null
+ printf "\nDeploying component: %s ...\n\n" "$COMPONENT_NAME"
+ $EXEC sls deploy -s "$STAGE"
+ printf "\nDeployed component: %s successfully \n\n" "$COMPONENT_NAME"
+ popd > /dev/null
+}
+
+disableStats "infrastructure"
+componentDeploy "infrastructure" "Infrastructure"
+componentDeploy "backend" "Backend"
+componentDeploy "edge-lambda" "Edge-Lambda"
+componentDeploy "post-deployment" "Post-Deployment"
+
+# We now need to invoke the post deployment lambda (we can do this locally)
+#$EXEC sls invoke local -f postDeployment -s $STAGE
+printf "\nInvoking post-deployment steps\n\n"
+pushd "$SOLUTION_DIR/post-deployment" > /dev/null
+$EXEC sls invoke -f postDeployment -l -s "$STAGE"
+popd > /dev/null
+
+# Deploy UI
+pushd "$SOLUTION_DIR/ui" > /dev/null
+
+# first we package locally (to populate .env.local only)
+printf "\nPackaging website UI\n\n"
+$EXEC sls package-ui --local=true -s "$STAGE"
+# then we package for deployment
+# (to populate .env.production and create a build via "npm build")
+$EXEC sls package-ui -s "$STAGE"
+
+printf "\nDeploying website UI\n\n"
+# Deploy it to S3, invalidate CloudFront cache
+$EXEC sls deploy-ui --invalidate-cache=true -s "$STAGE"
+printf "\nDeployed website UI successfully\n\n"
+popd > /dev/null
+
+printf "\n----- ENVIRONMENT DEPLOYED SUCCESSFULLY 🎉 -----\n\n"
+pushd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null
+
+# shellcheck disable=SC1091
+source ./get-info.sh "$@"
+
+popd > /dev/null
diff --git a/scripts/get-cicd-pipeline-artifacts-bucket-info.sh b/scripts/get-cicd-pipeline-artifacts-bucket-info.sh
new file mode 100755
index 0000000000..fa03b0b15d
--- /dev/null
+++ b/scripts/get-cicd-pipeline-artifacts-bucket-info.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+set -e
+
+pushd "$(dirname ${BASH_SOURCE[0]})"
+[[ $UTIL_SOURCED != yes && -f ./util.sh ]] && source ./util.sh
+popd
+
+# Ensure settings file exists
+ensure_setttings_file "$@"
+
+# Install
+install_dependencies "$@"
+
+function get_cicd_pipeline_artifacts_bucket_info() {
+ pushd $SOLUTION_ROOT_DIR/cicd/cicd-pipeline
+ local pipeline_stack_name=$($EXEC sls info -s $STAGE | grep 'stack:' --ignore-case | sed 's/ //g' | cut -d':' -f2)
+ popd
+
+ echo "pipeline_stack_name=${pipeline_stack_name}"
+
+ local solution_name="$(cat $CONFIG_DIR/settings/$STAGE.yml | grep 'solutionName:' --ignore-case | sed 's/ //g' | cut -d':' -f2)"
+ local aws_region="$(cat $CONFIG_DIR/settings/$STAGE.yml | grep 'awsRegion:' --ignore-case | sed 's/ //g' | cut -d':' -f2)"
+ local aws_profile="$(cat $CONFIG_DIR/settings/$STAGE.yml | grep 'awsProfile:' --ignore-case | sed 's/ //g' | cut -d':' -f2)"
+
+ if [ $aws_profile ]; then
+ artifacts_s3_bucket_arn="$(aws cloudformation describe-stacks --stack-name $pipeline_stack_name --output text --region $aws_region --profile $aws_profile --query 'Stacks[0].Outputs[?OutputKey==`AppArtifactBucketArn`].OutputValue')"
+ artifacts_kms_key_arn="$(aws cloudformation describe-stacks --stack-name $pipeline_stack_name --output text --region $aws_region --profile $aws_profile --query 'Stacks[0].Outputs[?OutputKey==`ArtifactBucketKeyArn`].OutputValue')"
+ else
+ artifacts_s3_bucket_arn="$(aws cloudformation describe-stacks --stack-name $pipeline_stack_name --output text --region $aws_region --query 'Stacks[0].Outputs[?OutputKey==`AppArtifactBucketArn`].OutputValue')"
+ artifacts_kms_key_arn="$(aws cloudformation describe-stacks --stack-name $pipeline_stack_name --output text --region $aws_region --query 'Stacks[0].Outputs[?OutputKey==`ArtifactBucketKeyArn`].OutputValue')"
+ fi
+
+ printf "\n\n\n-------------------------------------------------------------------------"
+ printf "\nCI/CD Pipeline Artifacts Bucket Info:"
+ printf "\n-------------------------------------------------------------------------"
+ printf "\n\nArtifacts Bucket Arn (AppArtifactBucketArn) : ${artifacts_s3_bucket_arn}"
+ printf "\nArtifacts KMS Key Arn (ArtifactBucketKeyArn) : ${artifacts_kms_key_arn}"
+ printf "\n\n-------------------------------------------------------------------------\n\n"
+}
+
+get_cicd_pipeline_artifacts_bucket_info
\ No newline at end of file
diff --git a/scripts/get-info.sh b/scripts/get-info.sh
new file mode 100755
index 0000000000..a6c52b49b7
--- /dev/null
+++ b/scripts/get-info.sh
@@ -0,0 +1,92 @@
+#!/bin/bash
+set -e
+set -o pipefail
+
+# This sets STAGE to $1 if present and not null, otherwise it sets stage to
+# $STAGE from the environment if present, else it defaults to $USER
+STAGE="${1:-${STAGE:-$USER}}"
+
+pushd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null
+# shellcheck disable=SC1091
+[[ $UTIL_SOURCED != yes && -f ./util.sh ]] && source ./util.sh
+popd > /dev/null
+
+# Ensure settings file exists
+ensure_setttings_file "$@"
+
+# Setup the execution command
+init_package_manager
+
+##
+# Sets the following environment variables containing information about the deployed environment and displays a
+# human friendly summary message containing info about the environment
+#
+# WEBSITE_DOMAIN_NAME
+# WEBSITE_ENDPOINT
+# API_ENDPOINT
+#
+##
+function get_info() {
+ pushd "$SOLUTION_DIR/infrastructure" > /dev/null
+ local stack_name_infrastructure
+ stack_name_infrastructure=$($EXEC sls info -s "$STAGE" | grep 'stack:' --ignore-case | sed 's/ //g' | cut -d':' -f2)
+ popd > /dev/null
+
+ pushd "$SOLUTION_DIR/backend" > /dev/null
+ local stack_name_backend
+ stack_name_backend=$($EXEC sls info -s "$STAGE" | grep 'stack:' --ignore-case | sed 's/ //g' | cut -d':' -f2)
+ popd > /dev/null
+
+ local solution_name aws_region aws_profile
+
+ # Note that we disable exit on non-zero for this section as the awsProfile
+ # will not be present in the CI/CD pipeline YAML and that fact, combined
+ # with set -o pipefail, will cause this script to exit with a non-zero rc.
+ set +e
+ solution_name="$(grep '^solutionName:' --ignore-case < "$CONFIG_DIR/settings/$STAGE.yml" | sed 's/ //g' | cut -d':' -f2)"
+ aws_region="$(grep '^awsRegion:' --ignore-case < "$CONFIG_DIR/settings/$STAGE.yml" | sed 's/ //g' | cut -d':' -f2)"
+ aws_profile="$(grep '^awsProfile:' < "$CONFIG_DIR/settings/$STAGE.yml" | sed 's/ //g' | cut -d':' -f2)"
+ set -e
+
+ local root_psswd_cmd=''
+ local website_domain_name=''
+
+ if [ "$aws_profile" ]; then
+ root_psswd_cmd="aws ssm get-parameters --names /$STAGE/$solution_name/user/root/password --output text --region $aws_region --profile $aws_profile --with-decryption --query Parameters[0].Value"
+ # shellcheck disable=SC2016
+ website_domain_name="$(aws cloudformation describe-stacks --stack-name "$stack_name_infrastructure" --output text --region "$aws_region" --profile "$aws_profile" --query 'Stacks[0].Outputs[?OutputKey==`CloudFrontEndpoint`].OutputValue')"
+ # shellcheck disable=SC2016
+ api_endpoint="$(aws cloudformation describe-stacks --stack-name "$stack_name_backend" --output text --region "$aws_region" --profile "$aws_profile" --query 'Stacks[0].Outputs[?OutputKey==`ServiceEndpoint`].OutputValue')"
+ else
+ root_psswd_cmd="aws ssm get-parameters --names /$STAGE/$solution_name/user/root/password --output text --region $aws_region --with-decryption --query Parameters[0].Value"
+ # shellcheck disable=SC2016
+ website_domain_name="$(aws cloudformation describe-stacks --stack-name "$stack_name_infrastructure" --output text --region "$aws_region" --query 'Stacks[0].Outputs[?OutputKey==`CloudFrontEndpoint`].OutputValue')"
+ # shellcheck disable=SC2016
+ api_endpoint="$(aws cloudformation describe-stacks --stack-name "$stack_name_backend" --output text --region "$aws_region" --query 'Stacks[0].Outputs[?OutputKey==`ServiceEndpoint`].OutputValue')"
+ fi
+
+ export ENV_NAME="${STAGE}"
+ export WEBSITE_DOMAIN_NAME="${website_domain_name}"
+ export WEBSITE_ENDPOINT="https://${website_domain_name}"
+ export API_ENDPOINT="${api_endpoint}"
+
+ echo "-------------------------------------------------------------------------"
+ echo "Summary:"
+ echo "-------------------------------------------------------------------------"
+ echo "Env Name : ${ENV_NAME}"
+ echo "Solution : ${solution_name}"
+ echo "Website URL : ${WEBSITE_ENDPOINT}"
+ echo "API Endpoint : ${API_ENDPOINT}"
+
+ # only show profile and root password when running in an interactive terminal
+ if [ -t 1 ] ; then
+ [ -z "${aws_profile}" ] || echo "AWS Profile : ${aws_profile}"
+ root_passwd="$(${root_psswd_cmd})"
+ echo "Root Password : ${root_passwd}"
+ else
+ echo "Root Password : execute ${root_psswd_cmd}"
+ fi
+ echo "-------------------------------------------------------------------------"
+}
+
+get_info
diff --git a/scripts/get-relying-party-info.sh b/scripts/get-relying-party-info.sh
new file mode 100755
index 0000000000..32a2aab7fa
--- /dev/null
+++ b/scripts/get-relying-party-info.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+set -e
+set -o pipefail
+
+pushd "$(dirname ${BASH_SOURCE[0]})"
+[[ $UTIL_SOURCED != yes && -f ./util.sh ]] && source ./util.sh
+popd
+
+# Ensure settings file exists
+ensure_setttings_file "$@"
+
+# Setup the execution command
+init_package_manager
+
+##
+# Displays human friendly summary message containing information required to configure Relying Party Trust in ADFS
+##
+function get_rp_info() {
+ local solution_name="$(cat $CONFIG_DIR/settings/$STAGE.yml | grep 'solutionName:' --ignore-case | sed 's/ //g' | cut -d':' -f2)"
+ local aws_region="$(cat $CONFIG_DIR/settings/$STAGE.yml | grep 'awsRegion:' --ignore-case | sed 's/ //g' | cut -d':' -f2)"
+ local userpool_name="${STAGE}-${solution_name}-userPool"
+ local aws_profile="$(cat $CONFIG_DIR/settings/$STAGE.yml | grep -w 'awsProfile:' --ignore-case | sed 's/ //g' | cut -d':' -f2)"
+ local aws_profile_cli_param=""
+ if [ $aws_profile ]; then
+ aws_profile_cli_param="--profile $aws_profile"
+ fi
+
+ userpool_id=$(aws cognito-idp list-user-pools \
+ $aws_profile_cli_param \
+ --region $aws_region \
+ --max-results 60 \
+ --output text --query "UserPools[?Name=='${userpool_name}'].Id")
+
+ userpool_signing_cert=$(aws cognito-idp get-signing-certificate \
+ $aws_profile_cli_param \
+ --user-pool-id ${userpool_id} \
+ --output text)
+
+ domain_name_prefix=$(aws cognito-idp describe-user-pool \
+ $aws_profile_cli_param \
+ --user-pool-id ${userpool_id} \
+ --output text --query "UserPool.Domain")
+
+ printf "\n\n\n-------------------------------------------------------------------------\n"
+ echo "Summary:"
+ echo "-------------------------------------------------------------------------"
+ printf "\n\n"
+ echo "User Pool Id : ${userpool_id}"
+ echo "Relying Party Id (Cognito User Pool URN) : urn:amazon:cognito:sp:${userpool_id}"
+ echo "(Login) SAML Assersion Consumer Endpoint : https://${domain_name_prefix}.auth.${aws_region}.amazoncognito.com/saml2/idpresponse"
+ echo "(Logout) SAML Logout Endpoint : https://${domain_name_prefix}.auth.${aws_region}.amazoncognito.com/saml2/logout"
+ echo "User Pool Signing Cert : ${userpool_signing_cert}"
+ echo "Solution : ${solution_name}"
+ echo "Environment Name : ${STAGE}"
+ printf "\n\n-------------------------------------------------------------------------\n\n"
+}
+
+get_rp_info
diff --git a/scripts/install.sh b/scripts/install.sh
new file mode 100755
index 0000000000..162ef10809
--- /dev/null
+++ b/scripts/install.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+set -e
+echo 'About to install all dependencies, please hang in there tight'
+
+pushd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null
+# shellcheck disable=SC1091
+[[ $UTIL_SOURCED != yes && -f ./util.sh ]] && source ./util.sh
+popd > /dev/null
+
+# Install
+install_dependencies
\ No newline at end of file
diff --git a/scripts/run-integration-tests.sh b/scripts/run-integration-tests.sh
new file mode 100755
index 0000000000..5368e57918
--- /dev/null
+++ b/scripts/run-integration-tests.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -e
+
+printf "\n\nInitializing env variables required for running integration test against env %s\n" "$@"
+# shellcheck disable=SC1091
+source ./scripts/get-info.sh "$@"
+
+printf "\n\nExecuting integration tests against env %s\n" "$@"
+pnpm run intTest --recursive --if-present
\ No newline at end of file
diff --git a/scripts/run-static-code-analysis.sh b/scripts/run-static-code-analysis.sh
new file mode 100755
index 0000000000..67327293ba
--- /dev/null
+++ b/scripts/run-static-code-analysis.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+pnpm run lint --recursive --if-present
\ No newline at end of file
diff --git a/scripts/run-unit-tests.sh b/scripts/run-unit-tests.sh
new file mode 100755
index 0000000000..78034fc8d1
--- /dev/null
+++ b/scripts/run-unit-tests.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+pnpm run test --recursive --if-present
\ No newline at end of file
diff --git a/scripts/util.sh b/scripts/util.sh
new file mode 100755
index 0000000000..ac068975fe
--- /dev/null
+++ b/scripts/util.sh
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+set -e
+
+UTIL_SOURCED=yes
+export UTIL_SOURCED
+
+# https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself
+SOURCE="${BASH_SOURCE[0]}"
+while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
+ DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )"
+ SOURCE="$(readlink "$SOURCE")"
+ [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
+done
+DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )"
+
+pushd "${DIR}/.." > /dev/null
+export SOLUTION_ROOT_DIR="${PWD}"
+export SOLUTION_DIR="${SOLUTION_ROOT_DIR}/main/solution"
+export CONFIG_DIR="${SOLUTION_ROOT_DIR}/main/config"
+export INT_TEST_DIR="${SOLUTION_ROOT_DIR}/main/integration-tests"
+popd > /dev/null
+
+function init_package_manager() {
+ PACKAGE_MANAGER=pnpm
+ case "$PACKAGE_MANAGER" in
+ yarn)
+ EXEC="yarn run"
+ RUN_SCRIPT="yarn run"
+ INSTALL_RECURSIVE="yarn workspaces run install"
+ ;;
+ npm)
+ EXEC="npx"
+ RUN_SCRIPT="npm run"
+ INSTALL_RECURSIVE=
+ ;;
+ pnpm)
+ EXEC="pnpx"
+ RUN_SCRIPT="pnpm run"
+ export EXEC RUN_SCRIPT
+ INSTALL_RECURSIVE="pnpm recursive install"
+ ;;
+ *)
+ echo "error: Unknown package manager: '${PACKAGE_MANAGER}''" >&2
+ exit 1
+ ;;
+ esac
+}
+
+function install_dependencies() {
+ init_package_manager
+
+ # Install
+ pushd "$SOLUTION_DIR"
+ [[ -n "$INSTALL_RECURSIVE" ]] && $INSTALL_RECURSIVE
+ popd
+}
+
+function ensure_setttings_file() {
+ # Accept 1st argument. Default: username
+ STAGE=${1:-$USER}
+
+ ENVIRONMENT_CONFIG=$CONFIG_DIR/settings/$STAGE.yml
+
+ if ! test -f "$ENVIRONMENT_CONFIG"
+ then
+ printf "\nEnvironment configuration does not exist!"
+ printf "\nPlease either create the environment configuration by copying demo.yaml or make sure you typed the environment name correctly!\n\n"
+ printf "Exiting ...\n\n"
+ exit
+ fi
+}