├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── architecture.jpg ├── deployment ├── build-s3-dist.sh └── run-unit-tests.sh ├── solution-manifest.yaml └── source ├── appsync-lambda-resolver ├── index.ts ├── jest.config.js ├── lib │ └── utils.ts ├── package.json ├── test │ ├── index.test.ts │ └── setJestEnvironmentVariables.ts └── tsconfig.json ├── ava-issue-handler ├── index.ts ├── jest.config.js ├── package.json ├── test │ ├── index.test.ts │ └── setJestEnvironmentVariables.ts └── tsconfig.json ├── cdk-infrastructure ├── .gitignore ├── .npmignore ├── bin │ └── amazon-virtual-andon.ts ├── cdk.json ├── jest.config.js ├── lib │ ├── amazon-virtual-andon-stack.ts │ ├── appregistry │ │ └── application-resource.ts │ ├── back-end │ │ ├── appsync-api │ │ │ ├── appsync-api-construct.ts │ │ │ ├── resolver │ │ │ │ ├── Area.process.req.vtl │ │ │ │ ├── Area.site.req.vtl │ │ │ │ ├── Area.station.req.vtl │ │ │ │ ├── Device.station.req.vtl │ │ │ │ ├── Event.process.req.vtl │ │ │ │ ├── Mutation.create.req.vtl │ │ │ │ ├── Mutation.createIssue.req.vtl │ │ │ │ ├── Mutation.delete.req.vtl │ │ │ │ ├── Mutation.putPermission.req.vtl │ │ │ │ ├── Mutation.updateEvent.req.vtl │ │ │ │ ├── Mutation.updateIssue.req.vtl │ │ │ │ ├── Process.area.req.vtl │ │ │ │ ├── Process.event.req.vtl │ │ │ │ ├── Query.get.req.vtl │ │ │ │ ├── Query.issuesByDevice.req.vtl │ │ │ │ ├── Query.issuesBySiteAreaStatus.req.vtl │ │ │ │ ├── Query.listAreas.req.vtl │ │ │ │ ├── Query.listDevices.req.vtl │ │ │ │ ├── Query.listEvents.req.vtl │ │ │ │ ├── Query.listPermissions.req.vtl │ │ │ │ ├── Query.listProcesses.req.vtl │ │ │ │ ├── Query.listRootCauses.req.vtl │ │ │ │ ├── Query.listRootCausesByName.req.vtl │ │ │ │ ├── Query.listSites.req.vtl │ │ │ │ ├── Query.listSitesByName.req.vtl │ │ │ │ ├── Query.listStations.req.vtl │ │ │ │ ├── Response.prev.vtl │ │ │ │ ├── Response.vtl │ │ │ │ ├── Site.area.req.vtl │ │ │ │ ├── Station.area.req.vtl │ │ │ │ ├── Station.device.req.vtl │ │ │ │ └── Subscription.res.vtl │ │ │ └── schema.graphql │ │ ├── back-end-construct.ts │ │ ├── data-analysis │ │ │ └── data-analysis-construct.ts │ │ └── external-integrations │ │ │ └── external-integrations-construct.ts │ ├── common-resources │ │ ├── common-resources-construct.ts │ │ └── solution-helper │ │ │ └── solution-helper-construct.ts │ └── front-end │ │ └── front-end-construct.ts ├── package.json ├── test │ └── amazon-virtual-andon-stack.test.ts ├── tsconfig.json └── utils │ └── utils.ts ├── cognito-trigger ├── index.ts ├── jest.config.js ├── lib │ └── utils.ts ├── package.json ├── test │ ├── index.test.ts │ └── setJestEnvironmentVariables.ts └── tsconfig.json ├── console ├── package.json ├── public │ ├── index.html │ └── manifest.json ├── src │ ├── App.tsx │ ├── assets │ │ └── css │ │ │ └── style.scss │ ├── components │ │ ├── DataTable.tsx │ │ ├── EmptyCol.tsx │ │ ├── EmptyRow.tsx │ │ ├── Enums.tsx │ │ ├── Footer.tsx │ │ ├── Interfaces.tsx │ │ ├── NoMatch.tsx │ │ └── Routes.tsx │ ├── graphql │ │ ├── mutations.js │ │ ├── queries.js │ │ └── subscriptions.js │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── types │ │ └── aws-amplify-react.d.ts │ ├── util │ │ ├── CognitoController.tsx │ │ ├── CustomUtil.tsx │ │ ├── GraphQLCommon.tsx │ │ └── lang │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es.json │ │ │ ├── fr.json │ │ │ ├── ja.json │ │ │ ├── ko.json │ │ │ ├── th.json │ │ │ └── zh.json │ └── views │ │ ├── AddEditEvent.tsx │ │ ├── Area.tsx │ │ ├── Client.tsx │ │ ├── Device.tsx │ │ ├── Event.tsx │ │ ├── Home.tsx │ │ ├── IssuesReport.tsx │ │ ├── Main.tsx │ │ ├── Observer.tsx │ │ ├── Permission.tsx │ │ ├── PermissionSetting.tsx │ │ ├── Process.tsx │ │ ├── RootCause.tsx │ │ ├── Site.tsx │ │ ├── Station.tsx │ │ └── User.tsx └── tsconfig.json ├── external-integrations-handler ├── index.ts ├── jest.config.js ├── lib │ └── utils.ts ├── package.json ├── test │ ├── index.test.ts │ └── setJestEnvironmentVariables.ts └── tsconfig.json ├── glue-job-scripts ├── etl-cleanup.py └── etl-data-export.py ├── solution-helper ├── generate-solution-constants.ts ├── index.ts ├── jest.config.js ├── lib │ └── utils.ts ├── package.json ├── test │ ├── index.test.ts │ └── setJestEnvironmentVariables.ts └── tsconfig.json └── solution-utils ├── get-options.ts ├── jest.config.js ├── logger.ts ├── metrics.ts ├── package.json ├── test ├── get-options.test.ts ├── logger.test.ts ├── metrics.test.ts └── setJestEnvironmentVariables.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Please complete the following information about the solution:** 20 | - [ ] Version: [e.g. v1.0.0] 21 | - [ ] Region: [e.g. us-east-1] 22 | - [ ] Was the solution modified from the version published on this repository? 23 | - [ ] If the answer to the previous question was yes, are the changes available on GitHub? 24 | - [ ] Have you checked your [service quotas](https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) for the sevices this solution uses? 25 | - [ ] Were there any errors in the CloudWatch Logs? 26 | 27 | **Screenshots** 28 | If applicable, add screenshots to help explain your problem (please **DO NOT include sensitive information**). 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this solution 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the feature you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | **/dist 3 | **/open-source 4 | **/.zip 5 | **/tmp 6 | **/out-tsc 7 | **/global-s3-assets 8 | **/regional-s3-assets 9 | **/build 10 | 11 | # dependencies 12 | **/node_modules 13 | 14 | # e2e 15 | **/e2e/*.js 16 | **/e2e/*.map 17 | 18 | # misc 19 | **/npm-debug.log 20 | **/testem.log 21 | **/.vscode 22 | **.nyc_output 23 | **.pem 24 | **/console_backup 25 | **/coverage 26 | **/andon_config.js 27 | deployment/create-stack.sh 28 | deployment/update-packages.sh 29 | source/console/public/assets/andon_config.js 30 | 31 | # System Files 32 | **/.DS_Store 33 | source/test 34 | 35 | # viperlight 36 | **viperlight** 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [3.0.6] - 2024-02-05 8 | ### Changed 9 | - Pinned cdk-nag to version 2.27.17 10 | 11 | ## [3.0.5] - 2023-11-03 12 | ### Changed 13 | - Pinned crypto-js to version 4.2.0 14 | 15 | ## [3.0.4] - 2023-10-20 16 | ### Changed 17 | - Bumped version for @babel/traverse dependency 18 | 19 | ## [3.0.3] - 2023-10-04 20 | ### Changed 21 | - Fix React build issue 22 | - upgraded aws-cdk libraries 23 | 24 | ## [3.0.2] - 2023-08-28 25 | ### Changed 26 | - Upgraded to aws-cdk-lib/assertions library 27 | - refactored code to reduce cognitive complexity 28 | - Upgraded AWS constructs dependencies 29 | - add enforceSSL flag for S3 buckets 30 | 31 | ## [3.0.1] - 2023-04-21 32 | ### Changed 33 | - Upgraded to Node 18 34 | - Added App registry integration 35 | - Upgraded to CDK 2, React Scripts 5, Axios 1 36 | - Upgraded to use ES2022 37 | 38 | ## [3.0.0] - 2021-10-28 39 | ⚠ BREAKING CHANGES 40 | Version 3.0.0 does not support upgrading from previous versions. 41 | ### Added 42 | - Option to export the solution’s DynamoDB table data to Amazon S3 for in-depth data analysis 43 | - Ability to enter nested “sub events” after creating events 44 | - Edit option for closed issues for Root Cause and Comments 45 | - Option to subscribe multiple email addresses and/or phone numbers to Events for Amazon SNS notifications 46 | - Multi-language support: Thai 47 | 48 | ### Changed 49 | - Architecture is now maintained using the AWS [Cloud Development Kit](https://aws.amazon.com/cdk/) 50 | - Consolidated previous Metrics & History pages into the new Issue Reporting page 51 | - Capture the users who create, acknowledge, and close issues so they can be viewed in the Issue Reporting screen 52 | 53 | ## [2.2.0] - 2021-07-07 54 | ### Added 55 | - Added ability to upload images and associate them with events 56 | - Added ability to add additional messages to root causes when closing issues 57 | - Added ability for issues to be created in response to a JSON object being uploaded to an S3 bucket 58 | - Added an option to view issues for all areas within a site in the Observer view 59 | - Display relative time (i.e. created 5 minutes ago) when viewing an issue 60 | 61 | ### Changed 62 | - Changed URL structure to allow bookmarking 63 | 64 | ### Fixed 65 | - Fixed an issue when editing a user's permissions 66 | 67 | ## [2.1.2] - 2021-05-20 68 | ### Fixed 69 | - Fixed AppSync subscription issue on the Client and Observer pages 70 | 71 | ## [2.1.1] - 2021-03-31 72 | ### Fixed 73 | - Removed IoT rule from the CloudFormation template which blocked to deploy the stack 74 | - Node.JS packages version to mitigate the security vulnerabilities 75 | 76 | ## [2.1.0] - 2020-08-31 77 | ### Added 78 | - Multi-language support: German, English, Spanish, French, Japanese, Korean, and simplified Chinese 79 | 80 | ### Changed 81 | - Users will not see choosing root cause pop-up when there is no attached root cause to the event when they close issues. 82 | 83 | ### Fixed 84 | - Fix duplicated devices insertion at permission setting 85 | 86 | ## [2.0.0] - 2020-07-07 87 | ### Added 88 | - Breadcrumb on every UI page 89 | - Whole resources into the AWS CloudFormation template 90 | - Unit tests for Lambda functions 91 | - User management by administrator 92 | - Permission management for associate group users 93 | - Cache selection at Client page 94 | - Hierarchy data deletion 95 | - Amazon SNS topic deletion when an event is deleted. 96 | - Data validation on AppSync resolver 97 | - Root cause management by admin group users 98 | - Root cause submission by engineer group users 99 | 100 | ### Changed 101 | - Update bootstrap version from 3 to 4 102 | - TypeScript is the main programming language for UI. 103 | - Directory path for source code 104 | - Revise the custom resource 105 | - Rejected issues will have 0 resolution time. 106 | - Prevent creating same name data (e.g. same site name, same area name under the same site, and so on) 107 | 108 | ### Removed 109 | - The material dashboard template 110 | - AWS Amplify CLI 111 | 112 | ## [1.0.1] - 2020-01-20 113 | ### Changed 114 | - CodeBuild image to aws/codebuild/standard:3.0 115 | - Amplify CLI to v4.12.0 116 | - IAM permissions for AndonAmplifyPolicy 117 | 118 | ## [1.0.0] - 2019-11-04 119 | ### Added 120 | - Solution initial version 121 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-solutions/amazon-virtual-andon/issues), or [recently closed](https://github.com/aws-solutions/amazon-virtual-andon/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 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. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | 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'](https://github.com/aws-solutions/amazon-virtual-andon/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | 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. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-solutions/amazon-virtual-andon/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Amazon Virtual Andon 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | Licensed under the Apache License Version 2.0 (the "License"). You may not use this file except 5 | in compliance with the License. A copy of the License is located at http://www.apache.org/licenses/ 6 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 7 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the 8 | specific language governing permissions and limitations under the License. 9 | 10 | ********************** 11 | THIRD PARTY COMPONENTS 12 | ********************** 13 | This software includes third party software subject to the following copyrights: 14 | 15 | @aws-amplify/pubsub under the Apache License Version 2.0 16 | @aws-amplify/ui-react under the Apache License Version 2.0 17 | @aws-cdk/assert under the Apache License Version 2.0 18 | @aws-cdk/aws-appsync under the Apache License Version 2.0 19 | @aws-cdk/aws-cloudfront under the Apache License Version 2.0 20 | @aws-cdk/aws-dynamodb under the Apache License Version 2.0 21 | @aws-cdk/aws-events under the Apache License Version 2.0 22 | @aws-cdk/aws-glue under the Apache License Version 2.0 23 | @aws-cdk/aws-iam under the Apache License Version 2.0 24 | @aws-cdk/aws-iot under the Apache License Version 2.0 25 | @aws-cdk/aws-kms under the Apache License Version 2.0 26 | @aws-cdk/aws-lambda under the Apache License Version 2.0 27 | @aws-cdk/aws-s3 under the Apache License Version 2.0 28 | @aws-cdk/aws-sns under the Apache License Version 2.0 29 | @aws-cdk/core under the Apache License Version 2.0 30 | @aws-solutions-constructs/aws-cloudfront-s3 under the Apache License Version 2.0 31 | @aws-solutions-constructs/aws-iot-lambda under the Apache License Version 2.0 32 | @types/jest under the Massachusetts Institute of Technology (MIT) license 33 | @types/node under the Massachusetts Institute of Technology (MIT) license 34 | @types/react-csv under the Massachusetts Institute of Technology (MIT) license 35 | @types/react-dom under the Massachusetts Institute of Technology (MIT) license 36 | @types/react-notification-system under the Massachusetts Institute of Technology (MIT) license 37 | @types/react-router-bootstrap under the Massachusetts Institute of Technology (MIT) license 38 | @types/react-router-dom under the Massachusetts Institute of Technology (MIT) license 39 | @types/uuid under the Massachusetts Institute of Technology (MIT) license 40 | aws-amplify under the Apache License Version 2.0 41 | aws-appsync under the Apache License Version 2.0 42 | aws-cdk under the Apache License Version 2.0 43 | aws-sdk under the Apache License Version 2.0 44 | axios under the Massachusetts Institute of Technology (MIT) license 45 | bootstrap under the Massachusetts Institute of Technology (MIT) license 46 | buffer under the Massachusetts Institute of Technology (MIT) license 47 | es6-promise under the Massachusetts Institute of Technology (MIT) license 48 | graphql-tag under the Massachusetts Institute of Technology (MIT) license 49 | isomorphic-fetch under the Massachusetts Institute of Technology (MIT) license 50 | jest under the Massachusetts Institute of Technology (MIT) license 51 | moment under the Massachusetts Institute of Technology (MIT) license 52 | node-sass under the Massachusetts Institute of Technology (MIT) license 53 | react under the Massachusetts Institute of Technology (MIT) license 54 | react-bootstrap under the Massachusetts Institute of Technology (MIT) license 55 | react-cookie under the Massachusetts Institute of Technology (MIT) license 56 | react-csv under the Massachusetts Institute of Technology (MIT) license 57 | react-dom under the Massachusetts Institute of Technology (MIT) license 58 | react-icons under the Massachusetts Institute of Technology (MIT) license 59 | react-notification-system under the Massachusetts Institute of Technology (MIT) license 60 | react-router-bootstrap under the Apache License Version 2.0 61 | react-router-dom under the Massachusetts Institute of Technology (MIT) license 62 | react-scripts under the Massachusetts Institute of Technology (MIT) license 63 | ts-jest under the Massachusetts Institute of Technology (MIT) license 64 | ts-node under the Massachusetts Institute of Technology (MIT) license 65 | typescript under the Apache License Version 2.0 66 | uuid under the Massachusetts Institute of Technology (MIT) license -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon Virtual Andon 2 | Amazon Virtual Andon is a self-service, cloud based andon system that makes it easy for 3 | any business to deploy andon in their factory. It is based on the same technology used 4 | by the Amazon Fulfillment centers built on AWS. 5 | 6 | The events occurring on the factory floor are captured either using a web-interface or connecting the machines to AWS IoT core that publish the events to a topic. These events are then stored in DynamoDB. Using the IoT Rule Engine, the events are integrated with other AWS services such as SNS to send notifications about the events. 7 | 8 | The solution comes with 4 different user personas, Admin, Manager, Engineer and Associate. 9 | 10 | For more information and a detailed deployment guide visit the Amazon Virtual Andon solution at https://aws.amazon.com/solutions/implementations/amazon-virtual-andon/. 11 | 12 | ## Architecture Overview 13 | ![Architecture](architecture.jpg) 14 | Please refer to our [documentation](https://docs.aws.amazon.com/solutions/latest/amazon-virtual-andon/architecture-overview.html) for more details on the architecture. 15 | 16 | ## Running unit tests for customization 17 | * Clone the repository, then make the desired code changes 18 | * Next, run unit tests to make sure added customization passes the tests 19 | ```bash 20 | cd ./deployment 21 | chmod +x ./run-unit-tests.sh 22 | ./run-unit-tests.sh 23 | ``` 24 | 25 | ## Building distributable for customization 26 | * Configure the bucket name of your target Amazon S3 distribution bucket 27 | ``` 28 | export REGION=aws-region-code # the AWS region to launch the solution (e.g. us-east-1) 29 | export DIST_OUTPUT_BUCKET=bucket-name-prefix # prefix for the bucket where customized code will reside 30 | export SOLUTION_NAME=amazon-virtual-andon 31 | export VERSION=my-version # version number for the customized code 32 | ``` 33 | _Note:_ You would have to create an S3 bucket with the name `-`. We recommend using a randomized value for `bucket-name-prefix`. `aws_region` is where you are testing the customized solution. We also recommend that you ensure this bucket is not public. 34 | 35 | * Now build the distributable: 36 | ```bash 37 | chmod +x ./build-s3-dist.sh 38 | ./build-s3-dist.sh $DIST_OUTPUT_BUCKET $SOLUTION_NAME $VERSION 39 | ``` 40 | 41 | * Deploy the distributable to an Amazon S3 bucket in your account. _Note:_ you must have the AWS Command Line Interface installed. 42 | ```bash 43 | aws s3 cp ./regional-s3-assets/ s3://$DIST_OUTPUT_BUCKET-$REGION/$SOLUTION_NAME/$VERSION/ --recursive --acl bucket-owner-full-control --profile aws-cred-profile-name 44 | ``` 45 | 46 | * Get the link of the amazon-virtual-andon.template uploaded to your Amazon S3 bucket. 47 | * Deploy the Amazon Virtual Andon solution to your account by launching a new AWS CloudFormation stack using the link of the amazon-virtual-andon.template. 48 | 49 | ## Deploying using cloudformation command 50 | 51 | ```bash 52 | aws cloudformation create-stack \ 53 | --profile ${AWS_PROFILE:-default} \ 54 | --region ${REGION} \ 55 | --template-url https://${DIST_BUCKET_PREFIX}-${REGION}.s3.amazonaws.com/${SOLUTION_NAME}/${VERSION}/amazon-virtual-andon.template \ 56 | --stack-name AmazonVirtualAndon \ 57 | --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \ 58 | --parameters \ 59 | ParameterKey=AdministratorEmail,ParameterValue=(email) 60 | ``` 61 | 62 | ## Collection of operational metrics 63 | This solution collects anonymous operational metrics to help AWS improve the 64 | quality of features of the solution. For more information, including how to disable 65 | this capability, please see the [implementation guide](https://docs.aws.amazon.com/solutions/latest/amazon-virtual-andon/collection-of-operational-metrics.html). 66 | 67 | *** 68 | 69 | Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 70 | 71 | Licensed under the Apache License, Version 2.0 (the "License"); 72 | you may not use this file except in compliance with the License. 73 | You may obtain a copy of the License at 74 | 75 | http://www.apache.org/licenses/LICENSE-2.0 76 | 77 | Unless required by applicable law or agreed to in writing, software 78 | distributed under the License is distributed on an "AS IS" BASIS, 79 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 80 | See the License for the specific language governing permissions and 81 | limitations under the License. -------------------------------------------------------------------------------- /architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/amazon-virtual-andon/6071a02e4f72ec0e8712ffdda0e89352e7d0b484/architecture.jpg -------------------------------------------------------------------------------- /deployment/build-s3-dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This assumes all of the OS-level configuration has been completed and git repo has already been cloned 4 | # 5 | # This script should be run from the repo's deployment directory 6 | # cd deployment 7 | # ./build-s3-dist.sh source-bucket-base-name trademarked-solution-name version-code 8 | # 9 | # Paramenters: 10 | # - source-bucket-base-name: Name for the S3 bucket location where the template will source the Lambda 11 | # code from. The template will append '-[region_name]' to this bucket name. 12 | # For example: ./build-s3-dist.sh solutions my-solution v1.0.0 13 | # The template will then expect the source code to be located in the solutions-[region_name] bucket 14 | # 15 | # - trademarked-solution-name: name of the solution for consistency 16 | # 17 | # - version-code: version of the package 18 | 19 | # Check to see if input has been provided: 20 | if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then 21 | echo "Please provide the base source bucket name, trademark approved solution name and version where the lambda code will eventually reside." 22 | echo "For example: ./build-s3-dist.sh solutions trademarked-solution-name v1.0.0" 23 | exit 1 24 | fi 25 | 26 | # Exit immediately if a command exits with a non-zero status. 27 | set -e 28 | 29 | # Get reference for all important folders 30 | template_dir="$PWD" 31 | template_dist_dir="$template_dir/global-s3-assets" 32 | build_dist_dir="$template_dir/regional-s3-assets" 33 | source_dir="$template_dir/../source" 34 | cdk_source_dir="$source_dir/cdk-infrastructure" 35 | 36 | echo "------------------------------------------------------------------------------" 37 | echo "[Init] Clean old dist folders" 38 | echo "------------------------------------------------------------------------------" 39 | echo "rm -rf $template_dist_dir" 40 | rm -rf $template_dist_dir 41 | echo "mkdir -p $template_dist_dir" 42 | mkdir -p $template_dist_dir 43 | echo "rm -rf $build_dist_dir" 44 | rm -rf $build_dist_dir 45 | echo "mkdir -p $build_dist_dir" 46 | mkdir -p $build_dist_dir 47 | 48 | echo "------------------------------------------------------------------------------" 49 | echo "Synthesize the CDK project into a CloudFormation template" 50 | echo "------------------------------------------------------------------------------" 51 | export SOLUTION_BUCKET_NAME_PLACEHOLDER=$1 52 | export SOLUTION_NAME_PLACEHOLDER=$2 53 | export SOLUTION_VERSION_PLACEHOLDER=$3 54 | 55 | cd $cdk_source_dir 56 | npm run clean 57 | npm install 58 | node_modules/aws-cdk/bin/cdk synth --asset-metadata false --path-metadata false > $template_dist_dir/$2.template 59 | 60 | echo "------------------------------------------------------------------------------" 61 | echo "Glue jobs scripts" 62 | echo "------------------------------------------------------------------------------" 63 | mkdir -p $build_dist_dir/glue-job-scripts 64 | cp $source_dir/glue-job-scripts/*.py $build_dist_dir/glue-job-scripts/ 65 | 66 | declare -a lambda_packages=( 67 | "ava-issue-handler" 68 | "solution-helper" 69 | "appsync-lambda-resolver" 70 | "external-integrations-handler" 71 | "cognito-trigger" 72 | ) 73 | 74 | for lambda_package in "${lambda_packages[@]}" 75 | do 76 | echo "------------------------------------------------------------------------------" 77 | echo "Building Lambda package: $lambda_package" 78 | echo "------------------------------------------------------------------------------" 79 | cd $source_dir/$lambda_package 80 | npm run package 81 | # Check the result of the package step and exit if a failure is identified 82 | if [ $? -eq 0 ] 83 | then 84 | echo "Package for $lambda_package built successfully" 85 | else 86 | echo "******************************************************************************" 87 | echo "Lambda package build FAILED for $lambda_package" 88 | echo "******************************************************************************" 89 | exit 1 90 | fi 91 | mv dist/package.zip $build_dist_dir/$lambda_package.zip 92 | rm -rf dist 93 | done 94 | 95 | echo "------------------------------------------------------------------------------" 96 | echo "[Build] Amazon Virtual Andon Console" 97 | echo "------------------------------------------------------------------------------" 98 | cd $source_dir/console 99 | GENERATE_SOURCEMAP=false INLINE_RUNTIME_CHUNK=false npm run build 100 | mkdir $build_dist_dir/console 101 | cp -r ./build/* $build_dist_dir/console 102 | 103 | echo "------------------------------------------------------------------------------" 104 | echo "[Create] Console manifest" 105 | echo "------------------------------------------------------------------------------" 106 | cd $source_dir/console/build 107 | manifest=(`find * -type f ! -iname "andon_config.js" ! -iname ".DS_Store"`) 108 | manifest_json=$(IFS=,;printf "%s" "${manifest[*]}") 109 | echo "{\"files\":[\"$manifest_json\"]}" | sed 's/,/","/g' >> $build_dist_dir/console/site-manifest.json 110 | -------------------------------------------------------------------------------- /deployment/run-unit-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This assumes all of the OS-level configuration has been completed and git repo has already been cloned 4 | # 5 | # This script should be run from the repo's deployment directory 6 | # cd deployment 7 | # ./run-unit-tests.sh 8 | # 9 | 10 | [ "$DEBUG" == 'true' ] && set -x 11 | set -e 12 | 13 | prepare_jest_coverage_report() { 14 | local component_name=$1 15 | 16 | if [ ! -d "coverage" ]; then 17 | echo "ValidationError: Missing required directory coverage after running unit tests" 18 | exit 129 19 | fi 20 | 21 | # prepare coverage reports 22 | rm -fr coverage/lcov-report 23 | mkdir -p $coverage_reports_top_path/jest 24 | coverage_report_path=$coverage_reports_top_path/jest/$component_name 25 | rm -fr $coverage_report_path 26 | mv coverage $coverage_report_path 27 | } 28 | 29 | run_javascript_test() { 30 | local component_path=$1 31 | local component_name=$2 32 | 33 | echo "------------------------------------------------------------------------------" 34 | echo "[Test] Run javascript unit test with coverage for $component_name" 35 | echo "------------------------------------------------------------------------------" 36 | echo "cd $component_path" 37 | cd $component_path 38 | 39 | # run unit tests 40 | npm test 41 | 42 | # prepare coverage reports 43 | prepare_jest_coverage_report $component_name 44 | } 45 | 46 | # Get reference for all important folders 47 | template_dir="$PWD" 48 | source_dir="$template_dir/../source" 49 | coverage_reports_top_path=$source_dir/test/coverage-reports 50 | 51 | # Test the attached Lambda function 52 | declare -a lambda_packages=( 53 | "ava-issue-handler" 54 | "appsync-lambda-resolver" 55 | "solution-helper" 56 | "solution-utils" 57 | "cdk-infrastructure" 58 | "external-integrations-handler" 59 | "cognito-trigger" 60 | ) 61 | 62 | for lambda_package in "${lambda_packages[@]}" 63 | do 64 | run_javascript_test $source_dir/$lambda_package $lambda_package 65 | 66 | # Check the result of the test and exit if a failure is identified 67 | if [ $? -eq 0 ] 68 | then 69 | echo "Test for $lambda_package passed" 70 | else 71 | echo "******************************************************************************" 72 | echo "Lambda test FAILED for $lambda_package" 73 | echo "******************************************************************************" 74 | exit 1 75 | fi 76 | done 77 | -------------------------------------------------------------------------------- /solution-manifest.yaml: -------------------------------------------------------------------------------- 1 | id: SO0071 2 | name: Virtual Andon on AWS 3 | version: v3.0.6 4 | cloudformation_templates: 5 | - template: virtual-andon-on-aws.template 6 | build_environment: 7 | build_image: 'aws/codebuild/standard:7.0' 8 | -------------------------------------------------------------------------------- /source/appsync-lambda-resolver/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [''], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | }, 7 | coverageReporters: [ 8 | 'text', 9 | ['lcov', { 'projectRoot': '../' }] 10 | ], 11 | setupFiles: ['./test/setJestEnvironmentVariables.ts'], 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 13 | }; 14 | -------------------------------------------------------------------------------- /source/appsync-lambda-resolver/lib/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * Structure of a request coming from an AppSync resolver 6 | */ 7 | export interface IAppSyncResolverRequest { 8 | arguments: { 9 | previousSms: string; 10 | previousEmail: string; 11 | }; 12 | identity: any; 13 | request: any; 14 | prev?: { 15 | result: any 16 | }; 17 | info: { 18 | parentTypeName: string; 19 | fieldName: string; 20 | }; 21 | } 22 | 23 | /** 24 | * Structure of a subscription to the solution's SNS topic 25 | */ 26 | export interface IAvaTopicSubscription { 27 | protocol: SubscriptionProtocols; 28 | endpoint: string; 29 | subscriptionArn?: string; 30 | filterPolicy?: IAvaSnsFilterPolicy; 31 | } 32 | 33 | /** 34 | * Structure of the SNS Filter Policy that will be applied when creating 35 | * subscriptions to the solution's SNS topic 36 | */ 37 | export interface IAvaSnsFilterPolicy { 38 | eventId: string[]; 39 | } 40 | 41 | /** 42 | * Available SNS subscription protocols 43 | */ 44 | export enum SubscriptionProtocols { 45 | EMAIL = 'email', 46 | SMS = 'sms' 47 | } 48 | 49 | /** 50 | * The AppSync mutation fields that can be supported by this Lambda resolver 51 | */ 52 | export enum SupportedMutationFieldNames { 53 | CREATE_EVENT = 'createEvent', 54 | UPDATE_EVENT = 'updateEvent', 55 | DELETE_EVENT = 'deleteEvent' 56 | } 57 | 58 | /** 59 | * The AppSync query fields that can be supported by this Lambda resolver 60 | */ 61 | export enum SupportedQueryFieldNames { 62 | GET_PREV_DAY_ISSUES_STATS = 'getPrevDayIssuesStats' 63 | } 64 | 65 | /** 66 | * The AppSync parent types that can be supported by this Lambda resolver 67 | */ 68 | export enum SupportedParentTypeNames { 69 | MUTATION = 'Mutation', 70 | QUERY = 'Query' 71 | } 72 | 73 | /** 74 | * Structure for the output of the `getPrevDayIssuesStats` Query 75 | */ 76 | export interface IGetPrevDayIssuesStatsOutput { 77 | open: number; 78 | acknowledged: number; 79 | closed: number; 80 | lastThreeHours: number; 81 | } -------------------------------------------------------------------------------- /source/appsync-lambda-resolver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appsync-lambda-resolver", 3 | "version": "3.0.6", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions" 7 | }, 8 | "description": "Resolver for various AppSync functions", 9 | "main": "index.js", 10 | "dependencies": { 11 | "moment": "^2.29.4", 12 | "aws-sdk": "2.1354.0" 13 | }, 14 | "devDependencies": { 15 | "@types/jest": "^29.5.0", 16 | "@types/node": "^18.15.11", 17 | "@types/uuid": "^9.0.1", 18 | "jest": "^29.5.0", 19 | "ts-jest": "^29.1.0", 20 | "ts-node": "^10.9.1", 21 | "typescript": "^5.0.3" 22 | }, 23 | "scripts": { 24 | "clean": "rm -rf node_modules/ dist/ coverage/ package-lock.json", 25 | "prebuild": "npm run clean && npm install", 26 | "build": "tsc --build tsconfig.json", 27 | "package": "npm run prebuild && npm run build && npm prune --production && rsync -avrq ./node_modules ./dist && npm run include-solution-utils && npm run package:zip", 28 | "package:zip": "cd dist && zip -q -r9 ./package.zip * && cd ..", 29 | "pretest": "npm run clean && npm install", 30 | "test": "jest --coverage --silent", 31 | "include-solution-utils": "npm run solution-utils:prep && npm run solution-utils:package", 32 | "solution-utils:prep": "rm -rf dist/solution-utils && mkdir dist/solution-utils", 33 | "solution-utils:package": "cd ../solution-utils && npm run package && cd dist/ && rsync -avrq . ../../$npm_package_name/dist/solution-utils/ && cd ../../$npm_package_name" 34 | }, 35 | "engines": { 36 | "node": ">=18.0.0" 37 | }, 38 | "license": "Apache-2.0" 39 | } -------------------------------------------------------------------------------- /source/appsync-lambda-resolver/test/setJestEnvironmentVariables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env.LOGGING_LEVEL = 'VERBOSE'; 5 | process.env.AWS_LAMBDA_FUNCTION_NAME = 'appsync-lambda-resolver-fn-name'; 6 | process.env.DATA_HIERARCHY_TABLE_NAME = 'data-table'; 7 | process.env.ISSUES_TABLE_NAME = 'issues-table'; 8 | process.env.ISSUE_NOTIFICATION_TOPIC_ARN = 'arn:of:topic'; 9 | -------------------------------------------------------------------------------- /source/appsync-lambda-resolver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "types": [ 12 | "node", 13 | "@types/jest" 14 | ] 15 | }, 16 | "include": [ 17 | "**/*.ts" 18 | ], 19 | "exclude": [ 20 | "package", 21 | "dist", 22 | "**/*.map" 23 | ] 24 | } -------------------------------------------------------------------------------- /source/ava-issue-handler/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | require('isomorphic-fetch'); 5 | import { AWSAppSyncClient, AUTH_TYPE, AWSAppSyncClientOptions } from 'aws-appsync'; 6 | import gql from 'graphql-tag'; 7 | import { getOptions } from '../solution-utils/get-options'; 8 | import Logger, { LoggingLevel as LogLevel } from '../solution-utils/logger'; 9 | 10 | import SNS from 'aws-sdk/clients/sns'; 11 | const awsSdkOptions = getOptions(); 12 | const sns = new SNS(awsSdkOptions); 13 | 14 | const createIssue = `mutation CreateIssue($input: CreateIssueInput!) { 15 | createIssue(input: $input) { 16 | id 17 | eventId 18 | eventDescription 19 | type 20 | priority 21 | siteName 22 | processName 23 | areaName 24 | stationName 25 | deviceName 26 | created 27 | createdAt 28 | acknowledged 29 | closed 30 | resolutionTime 31 | acknowledgedTime 32 | status 33 | version 34 | issueSource 35 | additionalDetails 36 | } 37 | }`; 38 | 39 | const updateIssue = `mutation UpdateIssue($input: UpdateIssueInput!) { 40 | updateIssue(input: $input) { 41 | id 42 | eventId 43 | eventDescription 44 | type 45 | priority 46 | siteName 47 | processName 48 | areaName 49 | stationName 50 | deviceName 51 | created 52 | acknowledged 53 | closed 54 | resolutionTime 55 | acknowledgedTime 56 | status 57 | version 58 | comment 59 | } 60 | }`; 61 | 62 | let appsyncClient: AWSAppSyncClient; 63 | 64 | const { API_ENDPOINT, ISSUE_NOTIFICATION_TOPIC_ARN, LOGGING_LEVEL } = process.env; 65 | 66 | // Available in the lambda runtime by default 67 | const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_REGION, AWS_LAMBDA_FUNCTION_NAME } = process.env; 68 | 69 | const logger = new Logger(AWS_LAMBDA_FUNCTION_NAME, LOGGING_LEVEL); 70 | 71 | /** 72 | * Handler function. 73 | */ 74 | export async function handler(event: IHandlerInput) { 75 | logger.log(LogLevel.INFO, 'Received event', JSON.stringify(event, null, 2)); 76 | 77 | try { 78 | if (!appsyncClient) { 79 | const config: AWSAppSyncClientOptions = { 80 | region: AWS_REGION, 81 | auth: { 82 | type: AUTH_TYPE.AWS_IAM, 83 | credentials: { 84 | accessKeyId: AWS_ACCESS_KEY_ID, 85 | secretAccessKey: AWS_SECRET_ACCESS_KEY, 86 | sessionToken: AWS_SESSION_TOKEN 87 | } 88 | }, 89 | disableOffline: true, 90 | url: API_ENDPOINT 91 | }; 92 | appsyncClient = new AWSAppSyncClient(config); 93 | } 94 | 95 | let result: any; 96 | 97 | if (event.status === 'open') { 98 | // Mutate a new issue 99 | result = await doAppSyncMutation(createIssue, { input: { ...event, fullEventDescription: undefined } }); 100 | 101 | // Publish SNS message to the issue notification topic 102 | await sendMessageToSnsTopic(event); 103 | } else { 104 | // Mutate an updated issue 105 | result = await doAppSyncMutation(updateIssue, { input: event }); 106 | } 107 | 108 | return result.data; 109 | } catch (error) { 110 | logger.log(LogLevel.ERROR, error); 111 | return error; 112 | } 113 | } 114 | 115 | /** 116 | * Uses the AppSync client to perform the supplied mutation with the supplied variables 117 | * @param {string} mutationGql 118 | * @param {object} variables 119 | * @returns {object} AppSync response 120 | */ 121 | async function doAppSyncMutation(mutationGql: string, variables: any): Promise { 122 | return appsyncClient.mutate({ 123 | mutation: gql(mutationGql), 124 | variables 125 | }); 126 | } 127 | 128 | /** 129 | * Publishes a message on the solution's Issue Notification Topic to notify that an issue has been created for this device 130 | * @param {IHandlerInput} eventData Event data for this Lambda function that contains properties used to construct the SNS message 131 | */ 132 | async function sendMessageToSnsTopic(eventData: IHandlerInput): Promise { 133 | const snsParams: SNS.PublishInput = { 134 | MessageAttributes: { 135 | eventId: { 136 | DataType: 'String', 137 | StringValue: eventData.eventId 138 | } 139 | }, 140 | Message: getSnsMessageString(eventData), 141 | TopicArn: ISSUE_NOTIFICATION_TOPIC_ARN 142 | }; 143 | 144 | logger.log(LogLevel.VERBOSE, 'Publishing message', JSON.stringify(snsParams, null, 2)); 145 | const snsResponse = await sns.publish(snsParams).promise(); 146 | logger.log(LogLevel.INFO, `Message sent to the topic: ${snsResponse.MessageId}`); 147 | } 148 | 149 | /** 150 | * Returns a formatted string to be used as the SNS message body when publishing to the Issue Notification Topic 151 | * @param {IHandlerInput} eventData Event data for this Lambda function that contains properties used to construct the SNS message 152 | */ 153 | function getSnsMessageString(eventData: IHandlerInput): string { 154 | return [ 155 | 'The following Issue has been raised:', 156 | `Event: ${eventData.fullEventDescription || eventData.eventDescription}`, 157 | `Device: ${eventData.deviceName}`, 158 | '', 'Additional Details', '-----', 159 | `Site: ${eventData.siteName}`, 160 | `Area: ${eventData.areaName}`, 161 | `Process: ${eventData.processName}`, 162 | `Station: ${eventData.stationName}` 163 | ].join('\n') 164 | } 165 | 166 | interface IHandlerInput { 167 | id: string; 168 | eventId: string; 169 | eventDescription: string; 170 | fullEventDescription?: string; 171 | eventType?: string; 172 | priority: string; 173 | siteName: string; 174 | areaName: string; 175 | processName: string; 176 | stationName: string; 177 | deviceName: string; 178 | created: string; 179 | status: string; 180 | createdBy: string; 181 | issueSource: string; 182 | } -------------------------------------------------------------------------------- /source/ava-issue-handler/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | }, 7 | coverageReporters: [ 8 | 'text', 9 | ['lcov', { 'projectRoot': '../' }] 10 | ], 11 | setupFiles: ['./test/setJestEnvironmentVariables.ts'] 12 | }; 13 | -------------------------------------------------------------------------------- /source/ava-issue-handler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ava-issue-handler", 3 | "version": "3.0.6", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions" 7 | }, 8 | "description": "Amazon Virtual Andon issue handler", 9 | "main": "index.js", 10 | "dependencies": { 11 | "aws-appsync": "^4.1.9", 12 | "es6-promise": "^4.2.8", 13 | "graphql": "^15.3.0", 14 | "graphql-tag": "^2.11.0", 15 | "isomorphic-fetch": "^3.0.0", 16 | "aws-sdk": "2.1354.0" 17 | }, 18 | "devDependencies": { 19 | "@types/jest": "^29.5.0", 20 | "@types/node": "^18.15.11", 21 | "jest": "^29.5.0", 22 | "ts-jest": "^29.1.0", 23 | "ts-node": "^10.9.1", 24 | "typescript": "^5.0.3" 25 | }, 26 | "scripts": { 27 | "clean": "rm -rf node_modules/ dist/ coverage/ package-lock.json", 28 | "prebuild": "npm run clean && npm install", 29 | "build": "tsc --build tsconfig.json", 30 | "package": "npm run prebuild && npm run build && npm prune --production && rsync -avrq ./node_modules ./dist && npm run include-solution-utils && npm run package:zip", 31 | "package:zip": "cd dist && zip -q -r9 ./package.zip * && cd ..", 32 | "pretest": "npm run clean && npm install", 33 | "test": "jest --coverage --silent", 34 | "include-solution-utils": "npm run solution-utils:prep && npm run solution-utils:package", 35 | "solution-utils:prep": "rm -rf dist/solution-utils && mkdir dist/solution-utils", 36 | "solution-utils:package": "cd ../solution-utils && npm run package && cd dist/ && rsync -avrq . ../../$npm_package_name/dist/solution-utils/ && cd ../../$npm_package_name" 37 | }, 38 | "engines": { 39 | "node": ">=18.0.0" 40 | }, 41 | "license": "Apache-2.0" 42 | } -------------------------------------------------------------------------------- /source/ava-issue-handler/test/index.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Mock AppSync 5 | const mockMutation = jest.fn(); 6 | jest.mock('aws-appsync', () => { 7 | return { 8 | AUTH_TYPE: { AWS_IAM: 'AWS_IAM' }, 9 | AWSAppSyncClient: jest.fn(() => ({ mutate: mockMutation })) 10 | }; 11 | }); 12 | 13 | // Mock S3 14 | const mockS3GetObject = jest.fn(); 15 | jest.mock('aws-sdk/clients/s3', () => { 16 | return jest.fn(() => ({ 17 | getObject: mockS3GetObject 18 | })); 19 | }); 20 | 21 | // Mock SNS 22 | const mockSnsPublish = jest.fn(); 23 | jest.mock('aws-sdk/clients/sns', () => { 24 | return jest.fn(() => ({ 25 | publish: mockSnsPublish 26 | })); 27 | }); 28 | 29 | // Mock DynamoDB 30 | const mockDDBGet = jest.fn(); 31 | const mockDDBQuery = jest.fn(); 32 | const mockDocumentClient = jest.fn(() => ({ 33 | get: mockDDBGet, 34 | query: mockDDBQuery 35 | })); 36 | 37 | jest.mock('aws-sdk/clients/dynamodb', () => { 38 | return { 39 | DocumentClient: mockDocumentClient 40 | }; 41 | }); 42 | 43 | // Spy on the console messages 44 | const consoleLogSpy = jest.spyOn(console, 'log'); 45 | const consoleErrorSpy = jest.spyOn(console, 'error'); 46 | 47 | // Import handler 48 | const index = require('../index'); 49 | 50 | describe('IoT Topic Events', function () { 51 | const OLD_ENV = process.env; 52 | 53 | const openEvent = { 54 | "id": "0012345678", 55 | "eventId": "eventId", 56 | "eventDescription": "equipment issue", 57 | "type": "equipment issue", 58 | "priority": "low", 59 | "topicArn": "arn:of:topic", 60 | "siteName": "lhr14", 61 | "processName": "packaging", 62 | "areaName": "floor1", 63 | "stationName": "station1", 64 | "deviceName": "device1", 65 | "created": "2007-04-05T12:30-02:00", 66 | "acknowledged": "2007-04-05T12:30-02:00", 67 | "closed": "2007-04-05T12:30-02:00", 68 | "status": "open" 69 | }; 70 | 71 | const closedEvent = { 72 | "id": "0012345678", 73 | "eventId": "eventId", 74 | "eventDescription": "equipment issue", 75 | "type": "equipment issue", 76 | "priority": "low", 77 | "topicArn": "arn:of:topic", 78 | "siteName": "lhr14", 79 | "processName": "packaging", 80 | "areaName": "floor1", 81 | "stationName": "station1", 82 | "deviceName": "device1", 83 | "created": "2007-04-05T12:30-02:00", 84 | "acknowledged": "2007-04-05T12:30-02:00", 85 | "closed": "2007-04-05T12:30-02:00", 86 | "status": "closed" 87 | }; 88 | 89 | beforeEach(() => { 90 | process.env = { ...OLD_ENV }; 91 | jest.resetModules(); 92 | consoleLogSpy.mockClear(); 93 | consoleErrorSpy.mockClear(); 94 | mockMutation.mockReset(); 95 | mockSnsPublish.mockReset(); 96 | mockDDBGet.mockReset(); 97 | mockDDBQuery.mockReset(); 98 | mockS3GetObject.mockReset(); 99 | }); 100 | 101 | afterEach(() => { 102 | jest.clearAllMocks(); 103 | }); 104 | 105 | afterAll(() => { 106 | process.env = OLD_ENV; 107 | }); 108 | 109 | it('should pass to create an issue', async function () { 110 | expect.assertions(2); 111 | 112 | // Mock AppSync mutation 113 | mockMutation.mockImplementation(() => { 114 | return Promise.resolve({ data: 'Success' }); 115 | }); 116 | 117 | // Mock AWS SDK 118 | mockSnsPublish.mockImplementation(() => { 119 | return { 120 | promise() { 121 | // sns:Publish 122 | return Promise.resolve({ MessageId: 'Success' }); 123 | } 124 | }; 125 | }); 126 | 127 | const event = { ...openEvent }; 128 | const result = await index.handler(event); 129 | expect(result).toEqual('Success'); 130 | expect(mockSnsPublish).toHaveBeenCalledWith({ 131 | MessageAttributes: { 132 | eventId: { 133 | DataType: 'String', 134 | StringValue: 'eventId' 135 | } 136 | }, 137 | Message: [ 138 | 'The following Issue has been raised:', 139 | 'Event: equipment issue', 140 | 'Device: device1', 141 | '', 'Additional Details', '-----', 142 | 'Site: lhr14', 143 | 'Area: floor1', 144 | 'Process: packaging', 145 | 'Station: station1' 146 | ].join('\n'), 147 | TopicArn: 'arn:of:topic' 148 | }) 149 | }); 150 | 151 | it('should pass to update an issue', async function () { 152 | // Mock AppSync mutation 153 | mockMutation.mockImplementation(() => { 154 | return Promise.resolve({ data: 'Success' }); 155 | }); 156 | 157 | const event = { ...closedEvent }; 158 | const result = await index.handler(event); 159 | expect(result).toEqual('Success'); 160 | }); 161 | 162 | it('should fail when SNS publishing fails', async function () { 163 | // Mock AppSync mutation 164 | mockMutation.mockImplementation(() => { 165 | return Promise.resolve({ data: 'Success' }); 166 | }); 167 | 168 | // Mock AWS SDK 169 | mockSnsPublish.mockImplementation(() => { 170 | return { 171 | promise() { 172 | // sns:Publish 173 | return Promise.reject({ message: 'Publish failed.' }); 174 | } 175 | }; 176 | }); 177 | 178 | const event = { ...openEvent }; 179 | const result = await index.handler(event); 180 | expect(result).toEqual({ message: 'Publish failed.' }); 181 | }); 182 | 183 | it('should fail when GraphQL mutation fails', async function () { 184 | // Mock AppSync mutation 185 | mockMutation.mockImplementation(() => { 186 | return Promise.reject({ message: 'GraphQL mutation failed.' }); 187 | }); 188 | 189 | const event = { ...closedEvent }; 190 | const result = await index.handler(event); 191 | expect(result).toEqual({ message: 'GraphQL mutation failed.' }); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /source/ava-issue-handler/test/setJestEnvironmentVariables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env.AWS_REGION = 'mock-region-1'; 5 | process.env.API_ENDPOINT = 'https//graphql.com/graphql'; 6 | process.env.DATA_HIERARCHY_TABLE = 'data-hierarchy-table'; 7 | process.env.ISSUES_TABLE = 'issue-table-name'; 8 | process.env.ISSUE_NOTIFICATION_TOPIC_ARN = 'arn:of:topic'; 9 | -------------------------------------------------------------------------------- /source/ava-issue-handler/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "types": [ 12 | "node", 13 | "@types/jest" 14 | ] 15 | }, 16 | "include": [ 17 | "**/*.ts" 18 | ], 19 | "exclude": [ 20 | "package", 21 | "dist", 22 | "**/*.map" 23 | ] 24 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/bin/amazon-virtual-andon.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as cdk from "aws-cdk-lib"; 5 | import { AmazonVirtualAndonStack, AmazonVirtualAndonStackProps } from '../lib/amazon-virtual-andon-stack'; 6 | import { AwsSolutionsChecks } from "cdk-nag"; 7 | 8 | const app = new cdk.App(); 9 | cdk.Aspects.of(app).add(new AwsSolutionsChecks()); 10 | 11 | new AmazonVirtualAndonStack(app, 'AmazonVirtualAndonStack', getProps()); // NOSONAR: typescript:S1848 12 | 13 | function getProps(): AmazonVirtualAndonStackProps { 14 | const { 15 | SOLUTION_BUCKET_NAME_PLACEHOLDER, 16 | SOLUTION_NAME_PLACEHOLDER, 17 | SOLUTION_VERSION_PLACEHOLDER 18 | } = process.env; 19 | 20 | if (typeof SOLUTION_BUCKET_NAME_PLACEHOLDER !== 'string' || SOLUTION_BUCKET_NAME_PLACEHOLDER.trim() === '') { 21 | throw new Error('Missing required environment variable: SOLUTION_BUCKET_NAME_PLACEHOLDER'); 22 | } 23 | 24 | if (typeof SOLUTION_NAME_PLACEHOLDER !== 'string' || SOLUTION_NAME_PLACEHOLDER.trim() === '') { 25 | throw new Error('Missing required environment variable: SOLUTION_NAME_PLACEHOLDER'); 26 | } 27 | 28 | if (typeof SOLUTION_VERSION_PLACEHOLDER !== 'string' || SOLUTION_VERSION_PLACEHOLDER.trim() === '') { 29 | throw new Error('Missing required environment variable: SOLUTION_VERSION_PLACEHOLDER'); 30 | } 31 | 32 | const solutionId = 'SO0071'; 33 | const solutionDisplayName = 'Amazon Virtual Andon'; 34 | const solutionVersion = SOLUTION_VERSION_PLACEHOLDER; 35 | const solutionName = SOLUTION_NAME_PLACEHOLDER; 36 | const solutionAssetHostingBucketNamePrefix = SOLUTION_BUCKET_NAME_PLACEHOLDER; 37 | const description = `(${solutionId}) - ${solutionDisplayName}. Version ${solutionVersion}`; 38 | const synthesizer = new cdk.DefaultStackSynthesizer({generateBootstrapVersionRule: false}); 39 | 40 | return { 41 | description, 42 | solutionId, 43 | solutionName, 44 | solutionDisplayName, 45 | solutionVersion, 46 | solutionAssetHostingBucketNamePrefix, 47 | synthesizer 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/amazon-virtual-andon.ts", 3 | "context": { 4 | "aws-cdk:enableDiffNoFail": "true", 5 | "@aws-cdk/core:stackRelativeExports": "true", 6 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 7 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 8 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 9 | "@aws-cdk/core:newStyleStackSynthesis": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | }, 7 | coverageReporters: [ 8 | 'text', 9 | ['lcov', {'projectRoot': '../'}] 10 | ] 11 | }; -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/appregistry/application-resource.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Application, AttributeGroup } from "@aws-cdk/aws-servicecatalogappregistry-alpha"; 5 | import { Aws, CfnMapping, Fn, Stack, Tags } from "aws-cdk-lib"; 6 | import { AmazonVirtualAndonStackProps } from "../amazon-virtual-andon-stack"; 7 | 8 | declare module "aws-cdk-lib" { 9 | export interface CfnMapping { 10 | setDataMapValue(key: string, value: string): void; 11 | findInDataMap(key: string): string; 12 | } 13 | } 14 | 15 | CfnMapping.prototype.setDataMapValue = function (key: string, value: string): void { 16 | this.setValue("Data", key, value); 17 | }; 18 | 19 | CfnMapping.prototype.findInDataMap = function (key: string): string { 20 | return this.findInMap("Data", key); 21 | }; 22 | 23 | 24 | // Set an arbitrary value to use as a prefix for the DefaultApplicationAttributeGroup name 25 | // This may change in the future, and must not match the previous two prefixes 26 | const attributeGroupPrefix = "S01"; 27 | 28 | // Declare KVP object for type checked values 29 | const AppRegistryMetadata = { 30 | ID: "ID", 31 | Version: "Version", 32 | AppRegistryApplicationName: "AppRegistryApplicationName", 33 | SolutionName: "SolutionName", 34 | ApplicationType: "ApplicationType" 35 | }; 36 | 37 | export function applyAppRegistry(stack: Stack, solutionProps: AmazonVirtualAndonStackProps) { 38 | // Declare CFN Mappings 39 | const map = new CfnMapping(stack, "SolutionMapping", { lazy: true }); 40 | map.setDataMapValue(AppRegistryMetadata.ID, solutionProps.solutionId); 41 | map.setDataMapValue(AppRegistryMetadata.Version, solutionProps.solutionVersion); 42 | map.setDataMapValue(AppRegistryMetadata.AppRegistryApplicationName, solutionProps.solutionName); 43 | map.setDataMapValue(AppRegistryMetadata.SolutionName, solutionProps.solutionName); 44 | map.setDataMapValue(AppRegistryMetadata.ApplicationType, "AWS-Solutions"); 45 | 46 | const application = new Application(stack, "Application", { 47 | applicationName: Fn.join("-", [ 48 | map.findInDataMap(AppRegistryMetadata.AppRegistryApplicationName), 49 | Aws.REGION, 50 | Aws.ACCOUNT_ID, 51 | Aws.STACK_NAME 52 | ]), 53 | description: solutionProps.description ?? "Amazon Virtual Andon Description" 54 | }); 55 | application.associateApplicationWithStack(stack); 56 | 57 | Tags.of(application).add("Solutions:SolutionID", map.findInDataMap(AppRegistryMetadata.ID)); 58 | Tags.of(application).add("Solutions:SolutionName", map.findInDataMap(AppRegistryMetadata.SolutionName)); 59 | Tags.of(application).add("Solutions:SolutionVersion", map.findInDataMap(AppRegistryMetadata.Version)); 60 | Tags.of(application).add("Solutions:ApplicationType", map.findInDataMap(AppRegistryMetadata.ApplicationType)); 61 | 62 | const attributeGroup = new AttributeGroup(stack, "DefaultApplicationAttributeGroup", { 63 | // Use SolutionName as a unique prefix for the attribute group name 64 | attributeGroupName: Fn.join("-", [attributeGroupPrefix, Aws.REGION, Aws.STACK_NAME]), 65 | description: "Attribute group for solution information", 66 | attributes: { 67 | applicationType: map.findInDataMap(AppRegistryMetadata.ApplicationType), 68 | version: map.findInDataMap(AppRegistryMetadata.Version), 69 | solutionID: map.findInDataMap(AppRegistryMetadata.ID), 70 | solutionName: map.findInDataMap(AppRegistryMetadata.SolutionName) 71 | } 72 | }); 73 | attributeGroup.associateWith(application); 74 | } 75 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Area.process.req.vtl: -------------------------------------------------------------------------------- 1 | #set( $limit = $util.defaultIfNull($context.args.limit, 10) ) 2 | { 3 | "version": "2017-02-28", 4 | "operation": "Query", 5 | "query": { 6 | "expressionNames": { 7 | "#type": "type", 8 | "#parent": "parentId" 9 | }, 10 | "expressionValues": { 11 | ":type": $util.dynamodb.toDynamoDBJson("PROCESS"), 12 | ":parent": $util.dynamodb.toDynamoDBJson($ctx.source.id) 13 | }, 14 | "expression": "#type = :type and #parent = :parent" 15 | }, 16 | "scanIndexForward": #if( $context.args.sortDirection ) 17 | #if( $context.args.sortDirection == "ASC" ) 18 | true 19 | #else 20 | false 21 | #end 22 | #else 23 | true 24 | #end, 25 | "filter": #if( $context.args.filter ) 26 | $util.transform.toDynamoDBFilterExpression($ctx.args.filter) 27 | #else 28 | null 29 | #end, 30 | "limit": $limit, 31 | "nextToken": #if( $context.args.nextToken ) 32 | "$context.args.nextToken" 33 | #else 34 | null 35 | #end, 36 | "index": "ByTypeAndParent-index" 37 | } 38 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Area.site.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2017-02-28", 3 | "operation": "GetItem", 4 | "key": { 5 | "id": $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.areaSiteId, "___xamznone____")), 6 | "type": $util.dynamodb.toDynamoDBJson("SITE") 7 | } 8 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Area.station.req.vtl: -------------------------------------------------------------------------------- 1 | #set( $limit = $util.defaultIfNull($context.args.limit, 10) ) 2 | { 3 | "version": "2017-02-28", 4 | "operation": "Query", 5 | "query": { 6 | "expressionNames": { 7 | "#type": "type", 8 | "#parent": "parentId" 9 | }, 10 | "expressionValues": { 11 | ":type": $util.dynamodb.toDynamoDBJson("STATION"), 12 | ":parent": $util.dynamodb.toDynamoDBJson($ctx.source.id) 13 | }, 14 | "expression": "#type = :type and #parent = :parent" 15 | }, 16 | "scanIndexForward": #if( $context.args.sortDirection ) 17 | #if( $context.args.sortDirection == "ASC" ) 18 | true 19 | #else 20 | false 21 | #end 22 | #else 23 | true 24 | #end, 25 | "filter": #if( $context.args.filter ) 26 | $util.transform.toDynamoDBFilterExpression($ctx.args.filter) 27 | #else 28 | null 29 | #end, 30 | "limit": $limit, 31 | "nextToken": #if( $context.args.nextToken ) 32 | "$context.args.nextToken" 33 | #else 34 | null 35 | #end, 36 | "index": "ByTypeAndParent-index" 37 | } 38 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Device.station.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2017-02-28", 3 | "operation": "GetItem", 4 | "key": { 5 | "id": $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.deviceStationId, "___xamznone____")), 6 | "type": $util.dynamodb.toDynamoDBJson("STATION") 7 | } 8 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Event.process.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2017-02-28", 3 | "operation": "GetItem", 4 | "key": { 5 | "id": $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.eventProcessId, "___xamznone____")), 6 | "type": $util.dynamodb.toDynamoDBJson("PROCESS") 7 | } 8 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Mutation.create.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | ## Check duplication 18 | #set ($duplicated = $ctx.prev.result.items) 19 | #if ($duplicated.size() > 0) 20 | #if ($ctx.args.rootCause) 21 | $util.error("Same root cause already exists.", "DataDuplicatedError") 22 | #else 23 | $util.error("Same name already exists.", "DataDuplicatedError") 24 | #end 25 | #end 26 | 27 | ## Check validation 28 | #if ($ctx.args.sms) 29 | #if (!$util.matches("^((\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4})(,\s*((\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}))*$", $ctx.args.sms)) 30 | $util.error("SMS No. must be a comma-separated list of valid phone numbers.") 31 | #end 32 | #end 33 | #if ($ctx.args.email) 34 | #if (!$util.matches("^([_a-z0-9-]+(\.[_a-z0-9-]+)*(\+[a-z0-9-]+)?@\w+([\.-]?\w+)*(\.\w{2,3})+)(,\s*([_a-z0-9-]+(\.[_a-z0-9-]+)*(\+[a-z0-9-]+)?@\w+([\.-]?\w+)*(\.\w{2,3})+))*$", $ctx.args.email)) 35 | $util.error("E-Mail must be a comma-separated list of valid E-Mail addresses.") 36 | #end 37 | #end 38 | 39 | ## Set default values 40 | $util.qr($ctx.args.put("version", $util.defaultIfNull($ctx.args.version, 1))) 41 | $util.qr($ctx.args.put("createdAt", $util.defaultIfNull($ctx.args.createdAt, $util.time.nowISO8601()))) 42 | $util.qr($ctx.args.put("updatedAt", $util.defaultIfNull($ctx.args.updatedAt, $util.time.nowISO8601()))) 43 | #if ($ctx.args.type == "AREA") 44 | $util.qr($ctx.args.put("parentId", $ctx.args.areaSiteId)) 45 | #end 46 | #if ($ctx.args.type == "STATION") 47 | $util.qr($ctx.args.put("parentId", $ctx.args.stationAreaId)) 48 | #end 49 | #if ($ctx.args.type == "DEVICE") 50 | $util.qr($ctx.args.put("parentId", $ctx.args.deviceStationId)) 51 | #end 52 | #if ($ctx.args.type == "PROCESS") 53 | $util.qr($ctx.args.put("parentId", $ctx.args.processAreaId)) 54 | #end 55 | #if ($ctx.args.type == "EVENT") 56 | #if ( ! $ctx.args.parentId) 57 | ## If the parentId does not exist, this is a top-level event so use the process ID as the parentId 58 | $util.qr($ctx.args.put("parentId", $ctx.args.eventProcessId)) 59 | #end 60 | #end 61 | 62 | { 63 | "version": "2017-02-28", 64 | "operation": "PutItem", 65 | "key": { 66 | "id": $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.args.id, $util.autoId())), 67 | "type": $util.dynamodb.toDynamoDBJson($ctx.args.type), 68 | }, 69 | "attributeValues": $util.dynamodb.toMapValuesJson($context.args) 70 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Mutation.createIssue.req.vtl: -------------------------------------------------------------------------------- 1 | ## [Start] Determine request authentication mode ** 2 | #if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) ) 3 | #set( $authMode = "userPools" ) 4 | #end 5 | ## [End] Determine request authentication mode ** 6 | ## [Start] Check authMode and execute owner/group checks ** 7 | #if( $authMode == "userPools" ) 8 | ## [Start] Static Group Authorization Checks ** 9 | #set($isStaticGroupAuthorized = $util.defaultIfNull( 10 | $isStaticGroupAuthorized, false)) 11 | ## Authorization rule: { allow: groups, groups: ["AdminGroup"], groupClaim: "cognito:groups" } ** 12 | #set( $userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), []) ) 13 | #set( $allowedGroups = ["AdminGroup"] ) 14 | #foreach( $userGroup in $userGroups ) 15 | #if( $allowedGroups.contains($userGroup) ) 16 | #set( $isStaticGroupAuthorized = true ) 17 | #break 18 | #end 19 | #end 20 | ## Authorization rule: { allow: groups, groups: ["ManagerGroup"], groupClaim: "cognito:groups" } ** 21 | #set( $userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), []) ) 22 | #set( $allowedGroups = ["ManagerGroup"] ) 23 | #foreach( $userGroup in $userGroups ) 24 | #if( $allowedGroups.contains($userGroup) ) 25 | #set( $isStaticGroupAuthorized = true ) 26 | #break 27 | #end 28 | #end 29 | ## Authorization rule: { allow: groups, groups: ["AssociateGroup"], groupClaim: "cognito:groups" } ** 30 | #set( $userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), []) ) 31 | #set( $allowedGroups = ["AssociateGroup"] ) 32 | #foreach( $userGroup in $userGroups ) 33 | #if( $allowedGroups.contains($userGroup) ) 34 | #set( $isStaticGroupAuthorized = true ) 35 | #break 36 | #end 37 | #end 38 | ## Authorization rule: { allow: groups, groups: ["EngineerGroup"], groupClaim: "cognito:groups" } ** 39 | #set( $userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), []) ) 40 | #set( $allowedGroups = ["EngineerGroup"] ) 41 | #foreach( $userGroup in $userGroups ) 42 | #if( $allowedGroups.contains($userGroup) ) 43 | #set( $isStaticGroupAuthorized = true ) 44 | #break 45 | #end 46 | #end 47 | ## [End] Static Group Authorization Checks ** 48 | 49 | 50 | ## No Dynamic Group Authorization Rules ** 51 | 52 | 53 | ## No Owner Authorization Rules ** 54 | 55 | 56 | ## [Start] Throw if unauthorized ** 57 | #if( !($isStaticGroupAuthorized == true || $isDynamicGroupAuthorized == true || $isOwnerAuthorized == true) ) 58 | $util.unauthorized() 59 | #end 60 | ## [End] Throw if unauthorized ** 61 | #end 62 | ## [End] Check authMode and execute owner/group checks ** 63 | 64 | #if( $util.isNull($dynamodbNameOverrideMap) ) 65 | #set( $dynamodbNameOverrideMap = { 66 | "areaName#status#processName#stationName#deviceName#created": "areaNameStatusProcessNameStationNameDeviceNameCreated" 67 | } ) 68 | #else 69 | $util.qr($dynamodbNameOverrideMap.put("areaName#status#processName#stationName#deviceName#created", "areaNameStatusProcessNameStationNameDeviceNameCreated")) 70 | #end 71 | $util.qr($ctx.args.input.put("areaName#status#processName#stationName#deviceName#created","${ctx.args.input.areaName}#${ctx.args.input.status}#${ctx.args.input.processName}#${ctx.args.input.stationName}#${ctx.args.input.deviceName}#${ctx.args.input.created}")) 72 | #if( $util.isNull($dynamodbNameOverrideMap) ) 73 | #set( $dynamodbNameOverrideMap = { 74 | "areaName#status#processName#eventDescription#stationName#deviceName#created": "areaNameStatusProcessNameEventDescriptionStationNameDeviceNameCreated" 75 | } ) 76 | #else 77 | $util.qr($dynamodbNameOverrideMap.put("areaName#status#processName#eventDescription#stationName#deviceName#created", "areaNameStatusProcessNameEventDescriptionStationNameDeviceNameCreated")) 78 | #end 79 | $util.qr($ctx.args.input.put("areaName#status#processName#eventDescription#stationName#deviceName#created","${ctx.args.input.areaName}#${ctx.args.input.status}#${ctx.args.input.processName}#${ctx.args.input.eventDescription}#${ctx.args.input.stationName}#${ctx.args.input.deviceName}#${ctx.args.input.created}")) 80 | 81 | ## [Start] Set the primary @key. ** 82 | #set( $modelObjectKey = { 83 | "id": $util.dynamodb.toDynamoDB($ctx.args.input.id) 84 | } ) 85 | ## [End] Set the primary @key. ** 86 | 87 | ## [Start] Setting "version" to 1. ** 88 | $util.qr($ctx.args.input.put("version", 1)) 89 | ## [End] Setting "version" to 1. ** 90 | 91 | ## [Start] Prepare DynamoDB PutItem Request. ** 92 | $util.qr($context.args.input.put("createdDateUtc", $util.defaultIfNull($ctx.args.createdDateUtc, $util.time.nowFormatted("yyyy-MM-dd", "+00:00")))) 93 | $util.qr($context.args.input.put("createdAt", $util.defaultIfNull($ctx.args.input.createdAt, $util.time.nowISO8601()))) 94 | $util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601()))) 95 | $util.qr($context.args.input.put("deviceName#eventId", "${ctx.args.input.deviceName}#${ctx.args.input.eventId}")) 96 | #set( $condition = { 97 | "expression": "attribute_not_exists(#id)", 98 | "expressionNames": { 99 | "#id": "id" 100 | } 101 | } ) 102 | #if( $context.args.condition ) 103 | #set( $condition.expressionValues = {} ) 104 | #set( $conditionFilterExpressions = $util.parseJson($util.transform.toDynamoDBConditionExpression($context.args.condition)) ) 105 | $util.qr($condition.put("expression", "($condition.expression) AND $conditionFilterExpressions.expression")) 106 | $util.qr($condition.expressionNames.putAll($conditionFilterExpressions.expressionNames)) 107 | $util.qr($condition.expressionValues.putAll($conditionFilterExpressions.expressionValues)) 108 | #end 109 | #if( $condition.expressionValues && $condition.expressionValues.size() == 0 ) 110 | #set( $condition = { 111 | "expression": $condition.expression, 112 | "expressionNames": $condition.expressionNames 113 | } ) 114 | #end 115 | { 116 | "version": "2017-02-28", 117 | "operation": "PutItem", 118 | "key": #if( $modelObjectKey ) $util.toJson($modelObjectKey) #else { 119 | "id": $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.args.input.id, $util.autoId())) 120 | } #end, 121 | "attributeValues": $util.dynamodb.toMapValuesJson($context.args.input), 122 | "condition": $util.toJson($condition) 123 | } 124 | ## [End] Prepare DynamoDB PutItem Request. ** -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Mutation.delete.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | { 18 | "version": "2017-02-28", 19 | "operation": "DeleteItem", 20 | "key": { 21 | "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), 22 | "type": $util.dynamodb.toDynamoDBJson($ctx.args.type) 23 | } 24 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Mutation.putPermission.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | ## Set parentId 18 | $util.qr($context.args.input.put("parentId", "NONE")) 19 | 20 | ## Set updatedAt 21 | $util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601()))) 22 | 23 | { 24 | "version": "2017-02-28", 25 | "operation": "PutItem", 26 | "key": { 27 | "id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id), 28 | "type": $util.dynamodb.toDynamoDBJson("PERMISSION") 29 | }, 30 | "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input) 31 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Mutation.updateEvent.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | #set ($update = {}) 18 | 19 | ## Check validation 20 | #if ($ctx.args.sms and $ctx.args.sms != "") 21 | #if (!$util.matches("^((\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4})(,\s*((\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}))*$", $ctx.args.sms)) 22 | $util.error("SMS No. must be a comma-separated list of valid phone numbers.") 23 | #end 24 | #end 25 | #if ($ctx.args.email and $ctx.args.email != "") 26 | #if (!$util.matches("^([_a-z0-9-]+(\.[_a-z0-9-]+)*(\+[a-z0-9-]+)?@\w+([\.-]?\w+)*(\.\w{2,3})+)(,\s*([_a-z0-9-]+(\.[_a-z0-9-]+)*(\+[a-z0-9-]+)?@\w+([\.-]?\w+)*(\.\w{2,3})+))*$", $ctx.args.email)) 27 | $util.error("E-Mail must be a comma-separated list of valid E-Mail addresses.") 28 | #end 29 | #end 30 | 31 | ## Set updatedAt 32 | #set ($updatedAt = $util.defaultIfNull($ctx.args.updatedAt, $util.time.nowISO8601())) 33 | #set ($expression = "SET") 34 | #set ($expressionValues = {}) 35 | 36 | ## Set expression and expressionValues 37 | #if ($ctx.args.sms) 38 | #set ($expression = "$expression sms = :sms,") 39 | $util.qr($expressionValues.put(":sms", $util.dynamodb.toDynamoDB($ctx.args.sms))) 40 | #end 41 | #if ($ctx.args.email) 42 | #set ($expression = "$expression email = :email,") 43 | $util.qr($expressionValues.put(":email", $util.dynamodb.toDynamoDB($ctx.args.email))) 44 | #end 45 | #if ($ctx.args.rootCauses) 46 | #set ($expression = "$expression rootCauses = :rootCauses,") 47 | $util.qr($expressionValues.put(":rootCauses", $util.dynamodb.toDynamoDB($ctx.args.rootCauses))) 48 | #end 49 | #if ($ctx.args.eventImgKey) 50 | #set ($expression = "$expression eventImgKey = :eventImgKey,") 51 | $util.qr($expressionValues.put(":eventImgKey", $util.dynamodb.toDynamoDB($ctx.args.eventImgKey))) 52 | #end 53 | #if ($ctx.args.alias) 54 | #set ($expression = "$expression alias = :alias,") 55 | $util.qr($expressionValues.put(":alias", $util.dynamodb.toDynamoDB($ctx.args.alias))) 56 | #end 57 | 58 | $util.qr($expressionValues.put(":version", $util.dynamodb.toDynamoDB($util.defaultIfNull($ctx.args.version, 1)))) 59 | $util.qr($expressionValues.put(":updatedAt", $util.dynamodb.toDynamoDB($updatedAt))) 60 | 61 | { 62 | "version": "2017-02-28", 63 | "operation": "UpdateItem", 64 | "key": { 65 | "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), 66 | "type": $util.dynamodb.toDynamoDBJson("EVENT") 67 | }, 68 | "update": { 69 | "expression": "$expression version = :version, updatedAt = :updatedAt", 70 | "expressionValues": $util.toJson($expressionValues) 71 | } 72 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Process.area.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2017-02-28", 3 | "operation": "GetItem", 4 | "key": { 5 | "id": $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.processAreaId, "___xamznone____")), 6 | "type": $util.dynamodb.toDynamoDBJson("AREA") 7 | } 8 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Process.event.req.vtl: -------------------------------------------------------------------------------- 1 | #set( $limit = $util.defaultIfNull($context.args.limit, 10) ) 2 | { 3 | "version": "2017-02-28", 4 | "operation": "Query", 5 | "query": { 6 | "expressionNames": { 7 | "#type": "type", 8 | "#parent": "parentId" 9 | }, 10 | "expressionValues": { 11 | ":type": $util.dynamodb.toDynamoDBJson("EVENT"), 12 | ":parent": $util.dynamodb.toDynamoDBJson($ctx.source.id) 13 | }, 14 | "expression": "#type = :type and #parent = :parent" 15 | }, 16 | "scanIndexForward": #if( $context.args.sortDirection ) 17 | #if( $context.args.sortDirection == "ASC" ) 18 | true 19 | #else 20 | false 21 | #end 22 | #else 23 | true 24 | #end, 25 | "filter": #if( $context.args.filter ) 26 | $util.transform.toDynamoDBFilterExpression($ctx.args.filter) 27 | #else 28 | null 29 | #end, 30 | "limit": $limit, 31 | "nextToken": #if( $context.args.nextToken ) 32 | "$context.args.nextToken" 33 | #else 34 | null 35 | #end, 36 | "index": "ByTypeAndParent-index" 37 | } 38 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Query.get.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup","ManagerGroup","EngineerGroup","AssociateGroup"]) 5 | 6 | ## If the request is to get a site, an area, a station, or a process, only AdminGroup can access the data. 7 | ## The other case would be getting a user permission by every group users. 8 | #if ($ctx.args.id) 9 | ## For 'getEvent' and 'getPermission', do not restrict to only the AdminGroup 10 | #if ($ctx.info.fieldName != "getEvent" && $ctx.info.fieldName != "getPermission") 11 | #set ($allowedGroups = ["AdminGroup"]) 12 | #end 13 | #end 14 | 15 | #foreach ($userGroup in $userGroups) 16 | #if ($allowedGroups.contains($userGroup)) 17 | #set ($isAllowed = true) 18 | #break 19 | #end 20 | #end 21 | 22 | ## Throw authorized if the user is not authorized. 23 | #if ($isAllowed == false) 24 | $util.unauthorized() 25 | #end 26 | 27 | ## If it needs to get permission, and the user is only in AssociateGroup, get permission. 28 | #if ($ctx.stash.permissionCheck) 29 | #if ($userGroups.size() == 1 && $userGroups.contains("AssociateGroup")) 30 | { 31 | "version": "2017-02-28", 32 | "operation": "GetItem", 33 | "key": { 34 | "id": { "S": "$ctx.identity.sub" }, 35 | "type": $util.dynamodb.toDynamoDBJson("PERMISSION") 36 | } 37 | } 38 | #else 39 | #return({}) 40 | #end 41 | #else 42 | { 43 | "version": "2017-02-28", 44 | "operation": "GetItem", 45 | "key": { 46 | "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), 47 | "type": $util.dynamodb.toDynamoDBJson($ctx.args.type) 48 | } 49 | } 50 | #end -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Query.listAreas.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup","ManagerGroup","EngineerGroup","AssociateGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | { 18 | "version": "2017-02-28", 19 | "operation": "Query", 20 | "query": { 21 | "expressionNames": { 22 | "#type": "type", 23 | "#parent": "parentId" 24 | }, 25 | "expressionValues": { 26 | ":type": $util.dynamodb.toDynamoDBJson("AREA"), 27 | ":parent": $util.dynamodb.toDynamoDBJson($ctx.args.areaSiteId) 28 | }, 29 | "expression": "#type = :type and #parent = :parent" 30 | }, 31 | #if( $context.args.name ) 32 | "filter": { 33 | "expression" : "#name = :name", 34 | "expressionNames" : { 35 | "#name" : "name" 36 | }, 37 | "expressionValues" : { 38 | ":name" : { "S" : "$ctx.args.name" } 39 | } 40 | }, 41 | #end 42 | "index": "ByTypeAndParent-index", 43 | #if ($ctx.args.nextToken) 44 | "nextToken": "$ctx.args.nextToken", 45 | #end 46 | "limit": $util.defaultIfNull($ctx.args.limit, 50) 47 | } 48 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Query.listDevices.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup","ManagerGroup","EngineerGroup","AssociateGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | { 18 | "version": "2017-02-28", 19 | "operation": "Query", 20 | "query": { 21 | "expression": "#type = :type and #parent = :parent", 22 | "expressionNames": { 23 | "#type": "type", 24 | "#parent": "parentId" 25 | }, 26 | "expressionValues": { 27 | ":type": $util.dynamodb.toDynamoDBJson("DEVICE"), 28 | ":parent": $util.dynamodb.toDynamoDBJson($ctx.args.deviceStationId) 29 | }, 30 | }, 31 | #if( $context.args.name ) 32 | "filter": { 33 | "expression" : "#name = :name", 34 | "expressionNames" : { 35 | "#name" : "name" 36 | }, 37 | "expressionValues" : { 38 | ":name" : { "S" : "$ctx.args.name" } 39 | } 40 | }, 41 | #end 42 | "index": "ByTypeAndParent-index", 43 | #if( $ctx.args.nextToken ) 44 | "nextToken": "$ctx.args.nextToken", 45 | #end 46 | "limit": $util.defaultIfNull($ctx.args.limit, 50) 47 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Query.listEvents.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup","ManagerGroup","EngineerGroup","AssociateGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | { 18 | "version": "2017-02-28", 19 | "operation": "Query", 20 | "query": { 21 | "expressionNames": { 22 | "#type": "type", 23 | #if( $context.args.parentId ) "#parent": "parentId" #end 24 | }, 25 | "expressionValues": { 26 | ":type": $util.dynamodb.toDynamoDBJson("EVENT"), 27 | #if( $context.args.parentId ) ":parent": $util.dynamodb.toDynamoDBJson($ctx.args.parentId) #end 28 | }, 29 | "expression": "#type = :type #if( $context.args.parentId ) and #parent = :parent #end" 30 | }, 31 | #if( $context.args.name || $context.args.eventProcessId ) 32 | "filter": { 33 | #if( $context.args.name && $context.args.eventProcessId ) 34 | "expression" : "#name = :name and #process = :process", 35 | #elseif( $context.args.name ) 36 | "expression" : "#name = :name", 37 | #else 38 | "expression" : "#process = :process", 39 | #end 40 | "expressionNames" : { 41 | #if( $context.args.name ) "#name" : "name", #end 42 | #if( $context.args.eventProcessId ) "#process" : "eventProcessId", #end 43 | }, 44 | "expressionValues" : { 45 | #if( $context.args.name ) ":name" : { "S" : "$ctx.args.name" }, #end 46 | #if( $context.args.eventProcessId ) ":process" : { "S" : "$ctx.args.eventProcessId" }, #end 47 | } 48 | }, 49 | #end 50 | "index": "ByTypeAndParent-index", 51 | #if ($ctx.args.nextToken) 52 | "nextToken": "$ctx.args.nextToken", 53 | #end 54 | "limit": $util.defaultIfNull($ctx.args.limit, 20) 55 | } 56 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Query.listPermissions.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | { 18 | "version": "2017-02-28", 19 | "operation": "Query", 20 | "index": "ByTypeAndParent-index", 21 | "limit": $util.defaultIfNull($ctx.args.limit, 50), 22 | #if( $ctx.args.nextToken ) 23 | "nextToken": "$ctx.args.nextToken", 24 | #end 25 | "query": { 26 | "expression": "#type = :type and #parent = :parent", 27 | "expressionNames": { 28 | "#type": "type", 29 | "#parent": "parentId" 30 | }, 31 | "expressionValues": { 32 | ":type": $util.dynamodb.toDynamoDBJson("PERMISSION"), 33 | ":parent": $util.dynamodb.toDynamoDBJson("NONE") 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Query.listProcesses.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup","ManagerGroup","EngineerGroup","AssociateGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | { 18 | "version": "2017-02-28", 19 | "operation": "Query", 20 | "query": { 21 | "expressionNames": { 22 | "#type": "type", 23 | "#parent": "parentId" 24 | }, 25 | "expressionValues": { 26 | ":type": $util.dynamodb.toDynamoDBJson("PROCESS"), 27 | ":parent": $util.dynamodb.toDynamoDBJson($ctx.args.processAreaId) 28 | }, 29 | "expression": "#type = :type and #parent = :parent" 30 | }, 31 | #if( $context.args.name ) 32 | "filter": { 33 | "expression" : "#name = :name", 34 | "expressionNames" : { 35 | "#name" : "name" 36 | }, 37 | "expressionValues" : { 38 | ":name" : { "S" : "$ctx.args.name" } 39 | } 40 | }, 41 | #end 42 | "index": "ByTypeAndParent-index", 43 | #if ($ctx.args.nextToken) 44 | "nextToken": "$ctx.args.nextToken", 45 | #end 46 | "limit": $util.defaultIfNull($ctx.args.limit, 20) 47 | } 48 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Query.listRootCauses.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup","ManagerGroup","EngineerGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | { 18 | "version": "2017-02-28", 19 | "operation": "Query", 20 | "index": "ByTypeAndName-index", 21 | "limit": $util.defaultIfNull($ctx.args.limit, 50), 22 | #if( $ctx.args.nextToken ) 23 | "nextToken": "$ctx.args.nextToken", 24 | #end 25 | "query": { 26 | "expression": "#type = :type", 27 | "expressionNames": { 28 | "#type": "type" 29 | }, 30 | "expressionValues": { 31 | ":type": $util.dynamodb.toDynamoDBJson("ROOT_CAUSE") 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Query.listRootCausesByName.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | { 18 | "version": "2017-02-28", 19 | "operation": "Query", 20 | "index": "ByTypeAndName-index", 21 | "query": { 22 | "expression": "#type = :type and #name = :name", 23 | "expressionNames": { 24 | "#type": "type", 25 | "#name": "name" 26 | }, 27 | "expressionValues": { 28 | ":type": $util.dynamodb.toDynamoDBJson("ROOT_CAUSE"), 29 | ":name": $util.dynamodb.toDynamoDBJson($ctx.args.name) 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Query.listSites.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup","ManagerGroup","EngineerGroup","AssociateGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | { 18 | "version": "2017-02-28", 19 | "operation": "Query", 20 | "index": "ByTypeAndName-index", 21 | "limit": $util.defaultIfNull($ctx.args.limit, 50), 22 | #if( $ctx.args.nextToken ) 23 | "nextToken": "$ctx.args.nextToken", 24 | #end 25 | "query": { 26 | "expression": "#type = :type", 27 | "expressionNames": { 28 | "#type": "type" 29 | }, 30 | "expressionValues": { 31 | ":type": $util.dynamodb.toDynamoDBJson("SITE") 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Query.listSitesByName.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | { 18 | "version": "2017-02-28", 19 | "operation": "Query", 20 | "index": "ByTypeAndName-index", 21 | "limit": $util.defaultIfNull($ctx.args.limit, 50), 22 | #if( $ctx.args.nextToken ) 23 | "nextToken": "$ctx.args.nextToken", 24 | #end 25 | "query": { 26 | "expression": "#type = :type AND #name = :name", 27 | "expressionNames": { 28 | "#type": "type", 29 | "#name": "name" 30 | }, 31 | "expressionValues": { 32 | ":type": $util.dynamodb.toDynamoDBJson("SITE"), 33 | ":name": $util.dynamodb.toDynamoDBJson($ctx.args.name) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Query.listStations.req.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup","ManagerGroup","EngineerGroup","AssociateGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | { 18 | "version": "2017-02-28", 19 | "operation": "Query", 20 | "query": { 21 | "expressionNames": { 22 | "#type": "type", 23 | "#parent": "parentId" 24 | }, 25 | "expressionValues": { 26 | ":type": $util.dynamodb.toDynamoDBJson("STATION"), 27 | ":parent": $util.dynamodb.toDynamoDBJson($ctx.args.stationAreaId) 28 | }, 29 | "expression": "#type = :type and #parent = :parent" 30 | }, 31 | #if( $context.args.name ) 32 | "filter": { 33 | "expression" : "#name = :name", 34 | "expressionNames" : { 35 | "#name" : "name" 36 | }, 37 | "expressionValues" : { 38 | ":name" : { "S" : "$ctx.args.name" } 39 | } 40 | }, 41 | #end 42 | "index": "ByTypeAndParent-index", 43 | #if ($ctx.args.nextToken) 44 | "nextToken": "$ctx.args.nextToken", 45 | #end 46 | "limit": $util.defaultIfNull($ctx.args.limit, 20) 47 | } 48 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Response.prev.vtl: -------------------------------------------------------------------------------- 1 | ## Raise a GraphQL field error in case of a datasource invocation error 2 | #if ($ctx.error) 3 | $util.error($ctx.error.message, $ctx.error.type) 4 | #end 5 | 6 | ## Pass back the result from DynamoDB. 7 | $util.toJson($ctx.prev.result) -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Response.vtl: -------------------------------------------------------------------------------- 1 | ## Raise a GraphQL field error in case of a datasource invocation error 2 | #if ($ctx.error) 3 | $util.error($ctx.error.message, $ctx.error.type) 4 | #end 5 | ## Pass back the result from DynamoDB. ** 6 | 7 | ## If permission exists, remove unauthorized ones. 8 | #if ($ctx.stash.permissionCheck && !$ctx.prev.result.isEmpty()) 9 | #set ($currentResult = $ctx.result.items) 10 | #set ($permissions = $ctx.prev.result) 11 | #set ($result = {}) 12 | #set ($items = []) 13 | #set ($allowedValues = []) 14 | 15 | ## Check if this is for issue or others. 16 | #if ($ctx.stash.type == "issue") 17 | ## Check site name 18 | #foreach ($value in $currentResult) 19 | #foreach ($allowedValue in $permissions.sites) 20 | #if ($allowedValue.name == $value.siteName) 21 | $util.qr($items.add($value)) 22 | #end 23 | #end 24 | #end 25 | 26 | ## Check area name 27 | #set ($currentResult = $items) 28 | #set ($items = []) 29 | #foreach ($value in $currentResult) 30 | #foreach ($allowedValue in $permissions.areas) 31 | #if ($allowedValue.name == $value.areaName) 32 | $util.qr($items.add($value)) 33 | #end 34 | #end 35 | #end 36 | 37 | ## Check process name 38 | #set ($currentResult = $items) 39 | #set ($items = []) 40 | #foreach ($value in $currentResult) 41 | #foreach ($allowedValue in $permissions.processes) 42 | #if ($allowedValue.name == $value.processName) 43 | $util.qr($items.add($value)) 44 | #end 45 | #end 46 | #end 47 | 48 | ## Check station name 49 | #set ($currentResult = $items) 50 | #set ($items = []) 51 | #foreach ($value in $currentResult) 52 | #foreach ($allowedValue in $permissions.stations) 53 | #if ($allowedValue.name == $value.stationName) 54 | $util.qr($items.add($value)) 55 | #end 56 | #end 57 | #end 58 | 59 | ## Check device name 60 | #set ($currentResult = $items) 61 | #set ($items = []) 62 | #foreach ($value in $currentResult) 63 | #foreach ($allowedValue in $permissions.devices) 64 | #if ($allowedValue.name == $value.deviceName) 65 | $util.qr($items.add($value)) 66 | #end 67 | #end 68 | #end 69 | #else 70 | #if ($ctx.stash.type == "site") 71 | #set ($allowedValues = $permissions.sites) 72 | #elseif ($ctx.stash.type == "area") 73 | #set ($allowedValues = $permissions.areas) 74 | #elseif ($ctx.stash.type == "process") 75 | #set ($allowedValues = $permissions.processes) 76 | #elseif ($ctx.stash.type == "station") 77 | #set ($allowedValues = $permissions.stations) 78 | #elseif ($ctx.stash.type == "device") 79 | #set ($allowedValues = $permissions.devices) 80 | #elseif ($ctx.stash.type == "event") 81 | #set ($allowedValues = $permissions.processes) 82 | #end 83 | 84 | ## For event, it checks permission with process ID because there's no such permission for event. 85 | #foreach ($value in $currentResult) 86 | #foreach ($allowedValue in $allowedValues) 87 | #if ($ctx.stash.type == "event") 88 | #if ($allowedValue.id == $value.eventProcessId) 89 | $util.qr($items.add($value)) 90 | #end 91 | #else 92 | #if ($allowedValue.id == $value.id) 93 | $util.qr($items.add($value)) 94 | #end 95 | #end 96 | #end 97 | #end 98 | #end 99 | 100 | ## Check if nextToken is available. 101 | #if ($ctx.result.nextToken) 102 | $util.qr($result.put("nextToken", $ctx.result.nextToken)) 103 | #end 104 | 105 | $util.qr($result.put("items", $items)) 106 | $util.toJson($result) 107 | #else 108 | $util.toJson($ctx.result) 109 | #end -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Site.area.req.vtl: -------------------------------------------------------------------------------- 1 | #set( $limit = $util.defaultIfNull($context.args.limit, 10) ) 2 | #set( $query = { 3 | "expression": "#type = :type and #connectionAttribute = :connectionAttribute", 4 | "expressionNames": { 5 | "#type": "type", 6 | "#connectionAttribute": "parentId" 7 | }, 8 | "expressionValues": { 9 | ":type": { "S": "AREA" }, 10 | ":connectionAttribute": { 11 | "S": "$context.source.id" 12 | } 13 | } 14 | } ) 15 | { 16 | "version": "2017-02-28", 17 | "operation": "Query", 18 | "query": $util.toJson($query), 19 | "scanIndexForward": #if( $context.args.sortDirection ) 20 | #if( $context.args.sortDirection == "ASC" ) 21 | true 22 | #else 23 | false 24 | #end 25 | #else 26 | true 27 | #end, 28 | "filter": #if( $context.args.filter ) 29 | $util.transform.toDynamoDBFilterExpression($ctx.args.filter) 30 | #else 31 | null 32 | #end, 33 | "limit": $limit, 34 | "nextToken": #if( $context.args.nextToken ) 35 | "$context.args.nextToken" 36 | #else 37 | null 38 | #end, 39 | "index": "ByTypeAndParent-index" 40 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Station.area.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2017-02-28", 3 | "operation": "GetItem", 4 | "key": { 5 | "id": $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.source.stationAreaId, "___xamznone____")), 6 | "type": $util.dynamodb.toDynamoDBJson("AREA") 7 | } 8 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Station.device.req.vtl: -------------------------------------------------------------------------------- 1 | #set( $limit = $util.defaultIfNull($context.args.limit, 10) ) 2 | { 3 | "version": "2017-02-28", 4 | "operation": "Query", 5 | "query": { 6 | "expression": "#type = :type and #parent = :parent", 7 | "expressionNames": { 8 | "#type": "type", 9 | "#parent": "parentId" 10 | }, 11 | "expressionValues": { 12 | ":type": { "S": "DEVICE" }, 13 | ":parent": { "S": "$context.source.id" } 14 | } 15 | }, 16 | "scanIndexForward": #if( $context.args.sortDirection ) 17 | #if( $context.args.sortDirection == "ASC" ) 18 | true 19 | #else 20 | false 21 | #end 22 | #else 23 | true 24 | #end, 25 | "filter": #if( $context.args.filter ) 26 | $util.transform.toDynamoDBFilterExpression($ctx.args.filter) 27 | #else 28 | null 29 | #end, 30 | "limit": $limit, 31 | "nextToken": #if( $context.args.nextToken ) 32 | "$context.args.nextToken" 33 | #else 34 | null 35 | #end, 36 | "index": "ByTypeAndParent-index" 37 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/back-end/appsync-api/resolver/Subscription.res.vtl: -------------------------------------------------------------------------------- 1 | ## Check authorization 2 | #set ($isAllowed = false) 3 | #set ($userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), [])) 4 | #set ($allowedGroups = ["AdminGroup","ManagerGroup","EngineerGroup","AssociateGroup"]) 5 | #foreach ($userGroup in $userGroups) 6 | #if ($allowedGroups.contains($userGroup)) 7 | #set ($isAllowed = true) 8 | #break 9 | #end 10 | #end 11 | 12 | ## Throw authorized if the user is not authorized. 13 | #if ($isAllowed == false) 14 | $util.unauthorized() 15 | #end 16 | 17 | $util.toJson(null) -------------------------------------------------------------------------------- /source/cdk-infrastructure/lib/common-resources/common-resources-construct.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { SolutionHelper } from './solution-helper/solution-helper-construct'; 5 | import { Construct } from 'constructs'; 6 | import { NagSuppressions } from 'cdk-nag'; 7 | 8 | export interface CommonResourcesProps { 9 | readonly defaultLanguage: string; 10 | readonly sendAnonymousData: string; 11 | readonly solutionId: string; 12 | readonly solutionVersion: string; 13 | readonly solutionDisplayName: string; 14 | readonly sourceCodeBucketName: string; 15 | readonly sourceCodeKeyPrefix: string; 16 | readonly loggingLevel: string; 17 | } 18 | 19 | /** 20 | * Construct that creates Common Resources for the solution. A logging S3 bucket will be created along 21 | * with a SolutionHelper construct 22 | */ 23 | export class CommonResources extends Construct { 24 | public readonly solutionHelper: SolutionHelper; 25 | 26 | constructor(scope: Construct, id: string, props: CommonResourcesProps) { 27 | super(scope, id); 28 | 29 | this.solutionHelper = new SolutionHelper(this, 'SolutionHelper', { 30 | sendAnonymousData: props.sendAnonymousData, 31 | solutionId: props.solutionId, 32 | solutionVersion: props.solutionVersion, 33 | solutionDisplayName: props.solutionDisplayName, 34 | sourceCodeBucketName: props.sourceCodeBucketName, 35 | sourceCodeKeyPrefix: props.sourceCodeKeyPrefix, 36 | loggingLevel: props.loggingLevel 37 | }); 38 | 39 | 40 | NagSuppressions.addResourceSuppressions( 41 | this.solutionHelper, 42 | [ 43 | { 44 | id: "AwsSolutions-IAM5", 45 | appliesTo: ["Action::s3:GetObject*"], 46 | reason: "Cloudwatch logs policy needs access to all logs arns because it's creating log groups" 47 | } 48 | ], 49 | true 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-virtual-andon", 3 | "version": "3.0.6", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions" 7 | }, 8 | "description": "CDK Infrastructure that will be used by this solution", 9 | "license": "Apache-2.0", 10 | "bin": { 11 | "amazon-virtual-andon": "bin/amazon-virtual-andon.js" 12 | }, 13 | "engines": { 14 | "node": ">=18.0.0" 15 | }, 16 | "scripts": { 17 | "clean": "rm -rf node_modules/ cdk.out/ coverage/ package-lock.json", 18 | "clean-synth": "npm run clean && npm install && npm run cdk synth --asset-metadata false --path-metadata false --json false", 19 | "pretest": "npm run clean && npm install", 20 | "test": "jest --coverage", 21 | "cdk": "cdk" 22 | }, 23 | "devDependencies": { 24 | "@aws-cdk/aws-glue-alpha": "^2.99.1-alpha.0", 25 | "@aws-cdk/aws-servicecatalogappregistry-alpha": "^2.99.1-alpha.0", 26 | "@aws-solutions-constructs/aws-cloudfront-s3": "2.44.0", 27 | "@aws-solutions-constructs/aws-iot-lambda": "2.44.0", 28 | "@types/jest": "^29.5.0", 29 | "@types/node": "^18.15.11", 30 | "aws-cdk": "2.99.1", 31 | "aws-cdk-lib": "2.99.1", 32 | "constructs": "^10.1.283", 33 | "jest": "^29.5.0", 34 | "ts-jest": "^29.1.0", 35 | "ts-node": "^10.9.1", 36 | "typescript": "~5.0.3", 37 | "cdk-nag": "2.27.17" 38 | } 39 | } -------------------------------------------------------------------------------- /source/cdk-infrastructure/test/amazon-virtual-andon-stack.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { AttributeType, Table } from 'aws-cdk-lib/aws-dynamodb'; 6 | import * as cdk from 'aws-cdk-lib'; 7 | import { addCfnSuppressRules } from '../utils/utils'; 8 | import * as CdkInfrastructure from '../lib/amazon-virtual-andon-stack'; 9 | 10 | let stack: cdk.Stack; 11 | 12 | describe('CDK Infra Tests', () => { 13 | 14 | beforeAll(() => { 15 | const app = new cdk.App(); 16 | stack = new CdkInfrastructure.AmazonVirtualAndonStack(app, 'TestStack', { 17 | description: 'AVA Test Stack', 18 | solutionAssetHostingBucketNamePrefix: 'hosting-bucket', 19 | solutionDisplayName: 'AVA Test', 20 | solutionId: 'SOxyz', 21 | solutionName: 'ava-test', 22 | solutionVersion: 'v3.0.3', 23 | }); 24 | }); 25 | 26 | test('CDK Template Infra Resource Test', () => { 27 | Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::Application', 1); 28 | 29 | Template.fromStack(stack).resourceCountIs('AWS::Cognito::UserPoolClient', 1); 30 | 31 | Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 14); 32 | 33 | Template.fromStack(stack).resourceCountIs('AWS::IAM::Role', 16); 34 | 35 | Template.fromStack(stack).resourceCountIs('AWS::IoT::Policy', 1); 36 | 37 | Template.fromStack(stack).resourceCountIs('AWS::Glue::Table', 2); 38 | 39 | Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 6); 40 | 41 | Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 4); 42 | 43 | Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 2); 44 | 45 | Template.fromStack(stack).resourceCountIs('AWS::SNS::Topic', 1); 46 | 47 | Template.fromStack(stack).resourceCountIs('AWS::AppSync::GraphQLApi', 1); 48 | }); 49 | 50 | 51 | }); 52 | 53 | test('utils', () => { 54 | const stack = new cdk.Stack(); 55 | 56 | const testTable1 = new cdk.CfnResource(stack, 'TestTable1', { 57 | type: 'AWS::DynamoDB::Table' 58 | }); 59 | 60 | const testTable2 = new Table(stack, 'TestTable2', { 61 | partitionKey: { name: 'name', type: AttributeType.STRING } 62 | }); 63 | 64 | addCfnSuppressRules(testTable1, [{ id: 'abc', reason: 'mock reason' }]); 65 | addCfnSuppressRules(testTable1, [{ id: 'xyz', reason: 'mock reason' }]); 66 | addCfnSuppressRules(testTable2, [{ id: 'xyz', reason: 'mock reason' }]); 67 | 68 | expect.assertions(1); 69 | expect(Object.keys(Template.fromStack(stack).findResources('AWS::DynamoDB::Table')).length).toBeGreaterThan(0); 70 | }); 71 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /source/cdk-infrastructure/utils/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CfnResource, Resource, IAspect } from 'aws-cdk-lib'; 5 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 6 | import { CustomResourceActions, IAndonWebsiteConfig } from '../../solution-helper/lib/utils'; 7 | import { CfnFunction } from 'aws-cdk-lib/aws-lambda'; 8 | import { IConstruct } from 'constructs'; 9 | 10 | /** 11 | * The CFN NAG suppress rule interface 12 | * @interface CfnNagSuppressRule 13 | */ 14 | interface CfnNagSuppressRule { 15 | id: string; 16 | reason: string; 17 | } 18 | 19 | /** 20 | * Adds CFN NAG suppress rules to the CDK resource. 21 | * @param resource The CDK resource 22 | * @param rules The CFN NAG suppress rules 23 | */ 24 | export function addCfnSuppressRules(resource: Resource | CfnResource, rules: CfnNagSuppressRule[]) { 25 | if (resource instanceof Resource) { 26 | resource = resource.node.defaultChild as CfnResource; 27 | } 28 | 29 | if (resource.cfnOptions.metadata?.cfn_nag?.rules_to_suppress) { 30 | resource.cfnOptions.metadata.cfn_nag.rules_to_suppress.push(...rules); 31 | } else { 32 | resource.addMetadata('cfn_nag', { 33 | rules_to_suppress: rules 34 | }); 35 | } 36 | } 37 | 38 | export enum IotConstants { 39 | ISSUES_TOPIC = 'ava/issues', 40 | DEVICES_TOPIC = 'ava/devices', 41 | GROUPS_TOPIC = 'ava/groups' 42 | } 43 | 44 | export interface ISetupPutWebsiteConfigCustomResourceProps { 45 | readonly hostingBucket: Bucket; 46 | readonly customResourceAction: CustomResourceActions.PUT_WEBSITE_CONFIG; 47 | readonly andonWebsiteConfigFileName: 'andon_config'; 48 | readonly andonWebsiteConfig: IAndonWebsiteConfig; 49 | } 50 | 51 | 52 | /** 53 | * CDK Aspect to add common CFN Nag rule suppressions to Lambda functions 54 | */ 55 | export class LambdaFunctionAspect implements IAspect { 56 | visit(node: IConstruct): void { 57 | const resource = node as CfnResource; 58 | 59 | if (resource instanceof CfnFunction) { 60 | const rules = [ 61 | { id: 'W89', reason: 'VPC for Lambda is not needed. This serverless architecture does not deploy a VPC.' }, 62 | { id: 'W92', reason: 'ReservedConcurrentExecutions is not needed for this Lambda function.' } 63 | ]; 64 | 65 | addCfnSuppressRules(resource, rules); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /source/cognito-trigger/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Import packages 5 | import Logger, { LoggingLevel as LogLevel } from '../solution-utils/logger'; 6 | import { CognitoTriggerSource, CognitoUserStatus, IHandlerInput } from './lib/utils'; 7 | 8 | const { AWS_LAMBDA_FUNCTION_NAME, LOGGING_LEVEL } = process.env; 9 | 10 | const logger = new Logger(AWS_LAMBDA_FUNCTION_NAME, LOGGING_LEVEL); 11 | 12 | /** 13 | * Request handler. 14 | */ 15 | export async function handler(event: IHandlerInput): Promise { 16 | logger.log(LogLevel.INFO, 'Received event', JSON.stringify(event, null, 2)); 17 | 18 | if (event.triggerSource === CognitoTriggerSource.POST_CONFIRM && event.request.userAttributes['cognito:user_status'] === CognitoUserStatus.EXTERNAL_PROVIDER) { 19 | // Add your implementation here to handle users that came from an external provider. For example, add them to 20 | // default groups or set permissions in Amazon Virtual Andon 21 | logger.log(LogLevel.INFO, 'Handling federated user'); 22 | } 23 | 24 | return event; 25 | } 26 | -------------------------------------------------------------------------------- /source/cognito-trigger/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [''], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | }, 7 | coverageReporters: [ 8 | 'text', 9 | ['lcov', { 'projectRoot': '../' }] 10 | ], 11 | setupFiles: ['./test/setJestEnvironmentVariables.ts'], 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 13 | }; 14 | -------------------------------------------------------------------------------- /source/cognito-trigger/lib/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * Structure of Cognito input to this function 6 | */ 7 | export interface IHandlerInput { 8 | userPoolId: string; 9 | userName: string; 10 | triggerSource: string; 11 | request: { 12 | userAttributes: { 13 | sub: string; 14 | email_verified: string; 15 | 'cognito:user_status': string; 16 | identities: string; 17 | } 18 | } 19 | } 20 | 21 | export enum CognitoTriggerSource { 22 | POST_CONFIRM = 'PostConfirmation_ConfirmSignUp' 23 | } 24 | 25 | export enum CognitoUserStatus { 26 | EXTERNAL_PROVIDER = 'EXTERNAL_PROVIDER' 27 | } -------------------------------------------------------------------------------- /source/cognito-trigger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cognito-trigger", 3 | "version": "3.0.6", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions" 7 | }, 8 | "description": "Triggered when a new user is confirmed in the user pool to allow for custom actions to be taken", 9 | "main": "index.js", 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "@types/jest": "^29.5.0", 13 | "@types/node": "^18.15.11", 14 | "jest": "^29.5.0", 15 | "ts-jest": "^29.1.0", 16 | "ts-node": "^10.9.1", 17 | "typescript": "^5.0.3" 18 | }, 19 | "engines": { 20 | "node": ">=18.0.0" 21 | }, 22 | "scripts": { 23 | "clean": "rm -rf node_modules/ dist/ coverage/ package-lock.json", 24 | "prebuild": "npm run clean && npm install", 25 | "build": "tsc --build tsconfig.json", 26 | "package": "npm run prebuild && npm run build && npm prune --production && rsync -avrq ./node_modules ./dist && npm run include-solution-utils && npm run package:zip", 27 | "package:zip": "cd dist && zip -q -r9 ./package.zip * && cd ..", 28 | "pretest": "npm run clean && npm install", 29 | "test": "jest --coverage --silent", 30 | "include-solution-utils": "npm run solution-utils:prep && npm run solution-utils:package", 31 | "solution-utils:prep": "rm -rf dist/solution-utils && mkdir dist/solution-utils", 32 | "solution-utils:package": "cd ../solution-utils && npm run package && cd dist/ && rsync -avrq . ../../$npm_package_name/dist/solution-utils/ && cd ../../$npm_package_name" 33 | }, 34 | "license": "Apache-2.0" 35 | } -------------------------------------------------------------------------------- /source/cognito-trigger/test/index.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CognitoTriggerSource, CognitoUserStatus, IHandlerInput } from '../lib/utils'; 5 | 6 | // Mock context 7 | const context = { 8 | logStreamName: 'log-stream' 9 | }; 10 | 11 | // Spy on the console messages 12 | const consoleInfoSpy = jest.spyOn(console, 'info'); 13 | const consoleLogSpy = jest.spyOn(console, 'log'); 14 | const consoleErrorSpy = jest.spyOn(console, 'error'); 15 | 16 | describe('cognito-trigger', function () { 17 | it('should log the event and return it for non federated users', async function () { 18 | expect.assertions(2); 19 | 20 | // Import handler 21 | const index = require('../index'); 22 | 23 | const event: IHandlerInput = { 24 | userPoolId: 'user-pool-id', 25 | userName: 'user-name', 26 | triggerSource: 'trigger-src', 27 | request: { 28 | userAttributes: { 29 | "cognito:user_status": 'CONFIRMED', 30 | email_verified: 'false', 31 | identities: '', 32 | sub: 'user-uuid' 33 | } 34 | } 35 | }; 36 | 37 | const result = await index.handler(event, context); 38 | expect(result).toEqual(event); 39 | expect(consoleInfoSpy).toHaveBeenCalledWith('[fn-name]', 'Received event', JSON.stringify(event, null, 2)); 40 | }); 41 | 42 | it('should log the event and return it for federated users', async function () { 43 | expect.assertions(3); 44 | 45 | // Import handler 46 | const index = require('../index'); 47 | 48 | const event: IHandlerInput = { 49 | userPoolId: 'user-pool-id', 50 | userName: 'user-name', 51 | triggerSource: CognitoTriggerSource.POST_CONFIRM, 52 | request: { 53 | userAttributes: { 54 | "cognito:user_status": CognitoUserStatus.EXTERNAL_PROVIDER, 55 | email_verified: 'false', 56 | identities: '', 57 | sub: 'user-uuid' 58 | } 59 | } 60 | }; 61 | 62 | const result = await index.handler(event, context); 63 | expect(result).toEqual(event); 64 | expect(consoleInfoSpy).toHaveBeenCalledWith('[fn-name]', 'Received event', JSON.stringify(event, null, 2)); 65 | expect(consoleInfoSpy).toHaveBeenCalledWith('[fn-name]', 'Handling federated user'); 66 | }); 67 | }); -------------------------------------------------------------------------------- /source/cognito-trigger/test/setJestEnvironmentVariables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env.LOGGING_LEVEL = 'VERBOSE'; 5 | process.env.AWS_LAMBDA_FUNCTION_NAME = 'fn-name'; 6 | -------------------------------------------------------------------------------- /source/cognito-trigger/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "types": [ 12 | "node", 13 | "@types/jest" 14 | ] 15 | }, 16 | "include": [ 17 | "**/*.ts" 18 | ], 19 | "exclude": [ 20 | "package", 21 | "dist", 22 | "**/*.map" 23 | ] 24 | } -------------------------------------------------------------------------------- /source/console/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-virtual-andon", 3 | "license": "Apache-2.0", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions" 7 | }, 8 | "description": "Amazon Virtual Andon", 9 | "version": "3.0.6", 10 | "private": true, 11 | "dependencies": { 12 | "@aws-amplify/pubsub": "^4.1.10", 13 | "@aws-amplify/ui-react": "^1.2.26", 14 | "@types/react-csv": "~1.1.1", 15 | "@types/react-dom": "~17.0.2", 16 | "@types/react-notification-system": "~0.2.40", 17 | "@types/react-router-bootstrap": "~0.24.5", 18 | "@types/uuid": "~8.3.0", 19 | "aws-amplify": "^4.3.0", 20 | "aws-sdk": "^2.996.0", 21 | "axios": "~1.3.5", 22 | "bootstrap": "~4.6.0", 23 | "buffer": "^6.0.3", 24 | "moment": "^2.29.1", 25 | "react": "~16.14.0", 26 | "react-bootstrap": "^1.6.1", 27 | "react-cookie": "~4.0.3", 28 | "react-csv": "~2.0.3", 29 | "react-dom": "~16.14.0", 30 | "react-icons": "~4.2.0", 31 | "react-notification-system": "~0.4.0", 32 | "react-router-bootstrap": "~0.25.0", 33 | "react-router-dom": "~5.3.4", 34 | "sass": "^1.60.0", 35 | "typescript": "~4.3.0", 36 | "uuid": "~8.3.2" 37 | }, 38 | "devDependencies": { 39 | "@types/react": "^17.0.65", 40 | "@types/react-router-dom": "^5.3.3", 41 | "react-scripts": "^5.0.1" 42 | }, 43 | "overrides": { 44 | "fast-xml-parser": "4.3.2", 45 | "nth-check": "2.1.1", 46 | "crypto-js": "4.2.0" 47 | }, 48 | "scripts": { 49 | "start": "react-scripts start", 50 | "build:init": "rm -rf package-lock.json node_modules/ build/", 51 | "build": "npm run build:init && npm install && react-scripts build", 52 | "test": "CI=true react-scripts test --coverage --passWithNoTests", 53 | "eject": "react-scripts eject" 54 | }, 55 | "engines": { 56 | "node": ">=18.0.0" 57 | }, 58 | "eslintConfig": { 59 | "extends": "react-app" 60 | }, 61 | "browserslist": { 62 | "production": [ 63 | ">0.2%", 64 | "not dead", 65 | "not op_mini all" 66 | ], 67 | "development": [ 68 | "last 1 chrome version", 69 | "last 1 firefox version", 70 | "last 1 safari version" 71 | ] 72 | }, 73 | "jest": { 74 | "collectCoverageFrom": [ 75 | "src/**/*.{ts,tsx}", 76 | "!src/react-app-env.d.ts", 77 | "!src/types/aws-amplify-react.d.ts", 78 | "!src/index.tsx" 79 | ] 80 | } 81 | } -------------------------------------------------------------------------------- /source/console/public/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | Amazon Virtual Andon 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /source/console/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "AVA", 3 | "name": "Amazon Virtual Andon", 4 | "icons": [], 5 | "start_url": ".", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff" 9 | } -------------------------------------------------------------------------------- /source/console/src/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Import React, Amplify, and AWS SDK packages 5 | import React from 'react'; 6 | import NotificationSystem from 'react-notification-system'; 7 | import Amplify from 'aws-amplify'; 8 | import { withAuthenticator } from '@aws-amplify/ui-react'; 9 | import { AuthState } from "@aws-amplify/ui-components"; 10 | import { Logger } from '@aws-amplify/core'; 11 | import Auth from '@aws-amplify/auth'; 12 | import PubSub from '@aws-amplify/pubsub'; 13 | import { AWSIoTProvider } from '@aws-amplify/pubsub/lib/Providers'; 14 | import AWS from 'aws-sdk'; 15 | 16 | import Footer from './components/Footer'; 17 | import { LOGGING_LEVEL, handleSubscriptionError } from './util/CustomUtil'; 18 | import { adminRoutes, managerRoutes, engineerRoutes, associateRoutes } from './components/Routes'; 19 | import { IRoute } from './components/Interfaces'; 20 | 21 | // Import views 22 | import Main from './views/Main'; 23 | 24 | /** 25 | * Properties Interface 26 | * @interface IPros 27 | */ 28 | interface IProps { 29 | authState?: any; 30 | } 31 | 32 | /** 33 | * State Interface 34 | * @interface IState 35 | */ 36 | interface IState { 37 | routes: IRoute[]; 38 | } 39 | 40 | /** 41 | * Types of subscriptions that will be maintained by the main App class 42 | */ 43 | export enum AppSubscriptionTypes { 44 | GROUP 45 | } 46 | 47 | // Logging 48 | const LOGGER = new Logger('App', LOGGING_LEVEL); 49 | 50 | // Declare Amazon Virtual Andon console configuration 51 | declare let andon_config: any; 52 | Amplify.addPluggable(new AWSIoTProvider({ 53 | aws_pubsub_region: andon_config.aws_project_region, 54 | aws_pubsub_endpoint: andon_config.aws_iot_endpoint + '/mqtt' 55 | })); 56 | PubSub.configure(andon_config); 57 | Amplify.configure(andon_config); 58 | Amplify.configure({ 59 | Storage: { 60 | bucket: andon_config.website_bucket, 61 | region: andon_config.aws_project_region 62 | } 63 | }); 64 | 65 | /** 66 | * The default application 67 | * @class App 68 | */ 69 | class App extends React.Component { 70 | // User group change subscription 71 | private groupSubscription: any; 72 | // Notification 73 | private notificationSystem = React.createRef(); 74 | 75 | constructor(props: Readonly) { 76 | super(props); 77 | 78 | this.state = { 79 | routes: [] 80 | }; 81 | 82 | this.groupSubscription = null; 83 | 84 | this.handleNotification = this.handleNotification.bind(this); 85 | this.handleAuthStateChange = this.handleAuthStateChange.bind(this); 86 | this.configureSubscription = this.configureSubscription.bind(this); 87 | } 88 | 89 | /** 90 | * React componentDidMount function 91 | */ 92 | async componentDidMount() { 93 | await this.handleAuthStateChange(AuthState.SignedIn) 94 | } 95 | 96 | /** 97 | * React componentWillUnmount function 98 | */ 99 | componentWillUnmount() { 100 | if (this.groupSubscription) this.groupSubscription.unsubscribe(); 101 | } 102 | 103 | /** 104 | * Handle notification. 105 | * @param {string} message - Notification message 106 | * @param {string} level - Notification level 107 | * @param {number} autoDismiss - Notification auto dismiss second 108 | */ 109 | handleNotification(message: string, level: string, autoDismiss: number) { 110 | const notification = this.notificationSystem.current; 111 | 112 | notification.addNotification({ 113 | message: (
{message}
), 114 | level, 115 | position: 'tr', 116 | autoDismiss 117 | }); 118 | } 119 | 120 | /** 121 | * Handle auth state change. 122 | * @param {string} state - Amplify auth state 123 | */ 124 | async handleAuthStateChange(state: string) { 125 | if (state === 'signedin') { 126 | const user = await Auth.currentAuthenticatedUser(); 127 | const groups = user.signInUserSession.idToken.payload['cognito:groups']; 128 | 129 | if (!groups) { 130 | this.setState({ routes: [] }); 131 | } else if (groups.includes('AdminGroup')) { 132 | this.setState({ routes: adminRoutes }); 133 | } else if (groups.includes('ManagerGroup')) { 134 | this.setState({ routes: managerRoutes }); 135 | } else if (groups.includes('EngineerGroup')) { 136 | this.setState({ routes: engineerRoutes }); 137 | } else if (groups.includes('AssociateGroup')) { 138 | this.setState({ routes: associateRoutes }); 139 | } else { 140 | this.setState({ routes: [] }); 141 | } 142 | 143 | // IoT policy is necessary to connect, publish, subscribe, and receive message. 144 | const credentials = await Auth.currentCredentials(); 145 | AWS.config.update({ 146 | region: andon_config.aws_project_region, 147 | credentials: Auth.essentialCredentials(credentials) 148 | }); 149 | 150 | const identityId = credentials.identityId; 151 | const params = { 152 | policyName: andon_config.aws_iot_policy_name, 153 | principal: identityId 154 | }; 155 | 156 | try { 157 | await new AWS.Iot().attachPrincipalPolicy(params).promise(); 158 | await this.configureSubscription(AppSubscriptionTypes.GROUP); 159 | } catch (error) { 160 | LOGGER.error('Error occurred while attaching principal policy', error); 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * Configures the subscription for the supplied `subscriptionType` 167 | * @param subscriptionType The type of subscription to configure 168 | * @param delayMS (Optional) This value will be used to set a delay for reestablishing the subscription if the socket connection is lost 169 | */ 170 | async configureSubscription(subscriptionType: AppSubscriptionTypes, delayMS: number = 10): Promise { 171 | try { 172 | if (subscriptionType === AppSubscriptionTypes.GROUP) { 173 | const user = await Auth.currentAuthenticatedUser(); 174 | const userId = user.signInUserSession.idToken.payload.sub; 175 | 176 | if (this.groupSubscription) { this.groupSubscription.unsubscribe(); } 177 | 178 | // Subscribe user group change for the user 179 | this.groupSubscription = PubSub.subscribe(`ava/groups/${userId}`).subscribe({ 180 | next: (data: any) => { 181 | // If user's group is changed, sign out the user. 182 | this.handleNotification(data.value, 'warning', 0); 183 | Auth.signOut(); 184 | this.groupSubscription.unsubscribe(); 185 | }, 186 | error: async (e: any) => { 187 | await handleSubscriptionError(e, subscriptionType, this.configureSubscription, delayMS); 188 | } 189 | }); 190 | } 191 | } catch (err) { 192 | console.error('Unable to configure subscription', err); 193 | } 194 | } 195 | 196 | /** 197 | * Render this page. 198 | */ 199 | render() { 200 | return ( 201 |
202 | 203 |
204 |
205 |
206 | ); 207 | } 208 | } 209 | 210 | export default withAuthenticator(App); -------------------------------------------------------------------------------- /source/console/src/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | $color-red: #ff0000; 2 | $color-white: #FFFFFF; 3 | $color-btn-primary: #FF9900; 4 | $color-btn-link: #007EB9; 5 | $color-event-inactive: #e9e9e9; 6 | $color-event-open: #ff6961; 7 | $color-event-acknowledged: #fdfd96; 8 | $color-issue-open: #ef5350; 9 | $color-sub-event-info: #6ac3e3; 10 | 11 | html * { 12 | font-family: "Amazon Ember", "Helvetica Neue", Arial, Helvetica, sans-serif !important; 13 | } 14 | 15 | .container { 16 | max-width: 95% !important; 17 | padding: 15px 0 0 0 ; 18 | margin-right: auto; 19 | margin-left: auto; 20 | } 21 | 22 | @media (min-width: 576px) { 23 | .modal-dialog { 24 | max-width: 80% !important; 25 | } 26 | } 27 | 28 | .main-wrapper { 29 | padding: 0 0 100px; 30 | position: relative; 31 | } 32 | 33 | .text-align-center { 34 | text-align: center; 35 | } 36 | 37 | body { 38 | -webkit-font-smoothing: antialiased; 39 | } 40 | 41 | .table > thead > tr > th { 42 | font-size: 14px; 43 | vertical-align: middle; 44 | } 45 | 46 | .table > tbody > tr > td { 47 | font-size: 14px; 48 | vertical-align: middle; 49 | } 50 | 51 | .table > tfoot > tr > th { 52 | font-size: 14px; 53 | vertical-align: middle; 54 | } 55 | 56 | .table-align-top { 57 | vertical-align: top !important; 58 | } 59 | 60 | .fixed-th-150 { 61 | width: 150px; 62 | } 63 | 64 | .fixed-th-50 { 65 | width: 50px; 66 | } 67 | 68 | .fixed-th-20 { 69 | width: 20px; 70 | } 71 | 72 | .view { 73 | position: relative; 74 | } 75 | 76 | .notification { 77 | position: absolute; 78 | top: 0; 79 | right: 50px; 80 | z-index: 10; 81 | } 82 | 83 | .custom-card { 84 | margin-bottom: 20px; 85 | } 86 | 87 | .custom-card-big { 88 | padding: 10px 10px 10px 10px; 89 | } 90 | 91 | .custom-card-event { 92 | text-align: center; 93 | height: 100%; 94 | min-height: 32.5vh; 95 | background-color: $color-event-inactive !important; 96 | cursor: pointer; 97 | } 98 | 99 | .custom-card-sub-event { 100 | text-align: center; 101 | height: 100%; 102 | min-height: 165px !important; 103 | max-height: 165px !important; 104 | min-width: 165px !important; 105 | max-width: 165px !important; 106 | background-color: $color-event-inactive !important; 107 | cursor: pointer; 108 | } 109 | 110 | .custom-card-event-col { 111 | margin-top: 5px; 112 | margin-bottom: 5px; 113 | } 114 | 115 | .custom-card-issue { 116 | margin-top: 5px; 117 | margin-bottom: 5px; 118 | } 119 | 120 | .custom-card-issue > .card-header { 121 | background: $color-issue-open; 122 | color: $color-white; 123 | } 124 | 125 | .event-open { 126 | background-color: $color-event-open !important; 127 | } 128 | 129 | .event-acknowledged { 130 | background-color: $color-event-acknowledged !important; 131 | } 132 | 133 | .event-closed-rejected { 134 | background-color: $color-event-inactive !important; 135 | } 136 | 137 | .sub-event-info { 138 | background-color: $color-sub-event-info !important; 139 | } 140 | 141 | .sub-event-back-icon { 142 | font-size: xx-large; 143 | } 144 | 145 | .sub-event-name-container { 146 | float: left; 147 | margin-right: 1rem; 148 | } 149 | 150 | .new-sub-event-name-container { 151 | margin-right: 1rem; 152 | } 153 | 154 | .sub-event-button-container > button { 155 | margin-right: 1rem; 156 | } 157 | 158 | .sub-event-back { 159 | background-color: $color-event-inactive !important; 160 | font-size: 2rem; 161 | cursor: pointer; 162 | } 163 | 164 | .btn-primary { 165 | background-color: $color-btn-primary !important; 166 | border-color: $color-btn-primary !important; 167 | } 168 | 169 | .btn-link { 170 | color: $color-btn-link !important; 171 | } 172 | 173 | .cookie-banner { 174 | position: fixed !important; 175 | width: 100%; 176 | bottom: 57px; 177 | z-index: 99999 !important; 178 | text-align: center; 179 | display: inline-block !important; 180 | font-size: 14px; 181 | } 182 | 183 | .cookie-banner-font { 184 | color: $color-white !important; 185 | } 186 | 187 | .cookie-banner-font > a { 188 | color: $color-white !important; 189 | text-decoration: underline; 190 | } 191 | 192 | ul { 193 | padding-inline-start: 20px; 194 | margin-bottom: 0 !important; 195 | } 196 | 197 | .required-field { 198 | color: $color-red; 199 | } 200 | 201 | .form-group-no-margin { 202 | margin-bottom: 0 !important; 203 | } 204 | 205 | .amplify-s3-image { 206 | --height : 100px; 207 | --width: 100px; 208 | overflow: hidden; 209 | } 210 | 211 | .amplify-s3-image.event-image { 212 | margin-right: 5px; 213 | } 214 | 215 | .amplify-s3-image.event-image.selected { 216 | height: 100px; 217 | border: 5px solid $color-btn-primary; 218 | display: inline-block; 219 | } 220 | 221 | .event-image-thumbnail-container { 222 | position: relative; 223 | width: 40px; 224 | height: 40px; 225 | } 226 | 227 | .event-image-thumbnail-container > .amplify-s3-image { 228 | --height : 40px; 229 | --width: 40px; 230 | } 231 | 232 | .event-image-thumbnail-container div { 233 | position: relative; 234 | height: 100%; 235 | width: 100%; 236 | } 237 | 238 | .event-image-thumbnail { 239 | max-width: 100%; 240 | max-height: 100%; 241 | position: absolute; 242 | top: 50%; 243 | transform: translateY(-50%); 244 | } 245 | 246 | .client-event-img-container { 247 | width: 100%; 248 | flex-grow: 1; 249 | display: flex; 250 | } 251 | 252 | .client-event-img-container > .amplify-s3-image { 253 | --height: auto; 254 | --width: 75%; 255 | } 256 | 257 | .client-event-card-body { 258 | display: flex; 259 | flex-direction: column; 260 | } 261 | 262 | .client-event-img-container > div { 263 | max-height: 100%; 264 | flex-grow: 1; 265 | display: flex; 266 | align-items: flex-end; 267 | justify-content: center; 268 | } 269 | 270 | .client-event-img-container img { 271 | margin: auto; 272 | max-width: 75%; 273 | } 274 | 275 | .div-upload-new-image-button { 276 | position: relative; 277 | margin-top: 5px; 278 | } 279 | 280 | .div-upload-new-image-button input { 281 | width: 100%; 282 | height: 100%; 283 | display: inline-block; 284 | position: absolute; 285 | left: 0px; 286 | top: 0px; 287 | opacity: 0; 288 | cursor: pointer; 289 | } 290 | 291 | .automated-issues-heading .automated-issues-heading-label { 292 | font-size: x-large; 293 | margin-right: 1rem; 294 | } 295 | 296 | .anomaly-modal-name-col { 297 | width: 20rem; 298 | } 299 | 300 | .sub-event-card-col { 301 | width: 10rem !important; 302 | } 303 | 304 | .sub-event-modal-form-group { 305 | padding: 0 !important; 306 | } 307 | 308 | .sub-event-to-be-deleted { 309 | text-decoration: line-through; 310 | } 311 | 312 | .sub-event-list-indent-0 { 313 | margin-left: 0; 314 | } 315 | 316 | .sub-event-list-indent-1 { 317 | margin-left: 3rem; 318 | } 319 | 320 | .sub-event-image-select-container { 321 | margin-top: 1rem; 322 | } 323 | 324 | .sub-event-image-button-container { 325 | margin-top: 1rem; 326 | } 327 | 328 | .sub-event-image-button-container > button { 329 | margin-right: 1rem; 330 | } 331 | 332 | .sub-event-image-thumbnail-container { 333 | float: left; 334 | } 335 | 336 | .sub-event-image-thumbnail-container > .amplify-s3-image { 337 | --height : 40px; 338 | --width: 40px; 339 | } 340 | -------------------------------------------------------------------------------- /source/console/src/components/EmptyCol.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Import React and Amplify packages 5 | import React from 'react'; 6 | 7 | /** 8 | * The class returns an empty col. 9 | * @class EmptyCol 10 | */ 11 | class EmptyCol extends React.Component { 12 | /** 13 | * Render the page 14 | */ 15 | render() { 16 | return ( 17 |   18 | ); 19 | } 20 | } 21 | 22 | export default EmptyCol; -------------------------------------------------------------------------------- /source/console/src/components/EmptyRow.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Import React and Amplify packages 5 | import React from 'react'; 6 | 7 | // Import React Bootstrap components 8 | import Row from 'react-bootstrap/Row'; 9 | import Col from 'react-bootstrap/Col'; 10 | 11 | // Import EmptyCol 12 | import EmptyCol from './EmptyCol'; 13 | 14 | /** 15 | * The class returns an empty row. 16 | * @class EmptyRow 17 | */ 18 | class EmptyRow extends React.Component { 19 | /** 20 | * Render the page 21 | */ 22 | render() { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | } 32 | 33 | export default EmptyRow; -------------------------------------------------------------------------------- /source/console/src/components/Enums.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * Enumerate for the modal type. 6 | * @enum ModalType 7 | */ 8 | export enum ModalType { 9 | None, 10 | Add, 11 | Delete, 12 | Edit, 13 | Upload 14 | } 15 | 16 | /** 17 | * Enumerate for the event priority. 18 | * @enum EventPriority 19 | */ 20 | export enum EventPriority { 21 | Low = 'low', 22 | Medium = 'medium', 23 | High = 'high', 24 | Critical = 'critical' 25 | } 26 | 27 | /** 28 | * Enumerate for the sort by keyword. 29 | * @enum SortBy 30 | */ 31 | export enum SortBy { 32 | Asc = 'asc', 33 | Desc = 'desc' 34 | } 35 | 36 | /** 37 | * Enumerate for the user group. 38 | * @enum UserGroups 39 | */ 40 | export enum UserGroups { 41 | AdminGroup = 'AdminGroup', 42 | ManagerGroup = 'ManagerGroup', 43 | EngineerGroup = 'EngineerGroup', 44 | AssociateGroup = 'AssociateGroup' 45 | } 46 | 47 | /** 48 | * Various types of permissions a user can have 49 | * @enum PermissionTypes 50 | */ 51 | export enum AVAPermissionTypes { 52 | Site = 'Site', 53 | Area = 'Area', 54 | Process = 'Process', 55 | Station = 'Station', 56 | Device = 'Device' 57 | } -------------------------------------------------------------------------------- /source/console/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Import React and Amplify packages 5 | import React from 'react'; 6 | import { Cookies } from 'react-cookie'; 7 | import { I18n } from 'aws-amplify'; 8 | 9 | // Import React Bootstrap components 10 | import Navbar from 'react-bootstrap/Navbar'; 11 | import Nav from 'react-bootstrap/Nav'; 12 | import Form from 'react-bootstrap/Form'; 13 | import Button from 'react-bootstrap/Button'; 14 | import {Buffer} from 'buffer'; 15 | 16 | // Import custom setting 17 | import EmptyCol from './EmptyCol'; 18 | 19 | /** 20 | * Properties Interface 21 | * @interface IProps 22 | */ 23 | interface IProps { } 24 | 25 | /** 26 | * State Interface 27 | * @interface IState 28 | */ 29 | interface IState { 30 | showCookieBanner: boolean; 31 | cookieLanguageCode: string; 32 | } 33 | 34 | /** 35 | * Footer class returns the default footer HTML tags including copyright and AWS Solutions link. 36 | * @class Footer 37 | */ 38 | class Footer extends React.Component { 39 | private cookies: Cookies; 40 | 41 | constructor(props: Readonly) { 42 | super(props); 43 | 44 | this.state = { 45 | showCookieBanner: false, 46 | cookieLanguageCode: '' 47 | }; 48 | 49 | this.cookies = new Cookies(); 50 | this.handleLanguageChange = this.handleLanguageChange.bind(this); 51 | this.handleCookieBanner = this.handleCookieBanner.bind(this); 52 | } 53 | 54 | /** 55 | * ComponentDidMount function. 56 | */ 57 | componentDidMount() { 58 | const cookie = this.cookies.get('ui_cookie'); 59 | if (!cookie) { 60 | this.setState({ showCookieBanner: true }); 61 | } 62 | 63 | const localeCode = new Cookies().get('ui_locale'); 64 | let cookieLanguageCode = localeCode; 65 | 66 | if (localeCode === 'ja') { 67 | cookieLanguageCode = 'jp'; 68 | } else if (localeCode === 'zh') { 69 | cookieLanguageCode = 'cn'; 70 | } 71 | 72 | this.setState({ cookieLanguageCode }); 73 | } 74 | 75 | /** 76 | * Handle the language change. Each changes last for 20 years. 77 | * @param {any} event - Language change event 78 | */ 79 | handleLanguageChange(event: any) { 80 | let cookieExpires = new Date(); 81 | cookieExpires.setFullYear(cookieExpires.getFullYear() + 20); 82 | this.cookies.set('ui_locale', event.target.value, { expires: cookieExpires, path: '/', secure: true }); 83 | window.location.reload(); 84 | } 85 | 86 | /** 87 | * Handle cookie banner. 88 | */ 89 | handleCookieBanner() { 90 | const cookie = Buffer.from(new Date().toISOString()).toString('base64'); 91 | let cookieExpires = new Date(); 92 | 93 | cookieExpires.setMonth(cookieExpires.getMonth() + 1); 94 | this.cookies.set('ui_cookie', cookie, { expires: cookieExpires, path: '/', secure: true }); 95 | 96 | this.setState({ showCookieBanner: false }); 97 | } 98 | 99 | /** 100 | * Render the footer page. 101 | */ 102 | render() { 103 | return ( 104 |
105 | { 106 | this.state.showCookieBanner && 107 | 108 | 109 | {I18n.get('info.cookie')} 110 | 111 | 112 | 113 | 114 | } 115 | 116 | 117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
129 | 132 |
133 |
134 |
135 | ); 136 | } 137 | } 138 | 139 | export default Footer; -------------------------------------------------------------------------------- /source/console/src/components/Interfaces.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Import React packages 5 | import React from 'react'; 6 | import { IconType } from 'react-icons/lib/cjs'; 7 | 8 | /** 9 | * Page route interface 10 | * @interface IRoute 11 | */ 12 | export interface IRoute { 13 | path: string; 14 | nameCode?: string; 15 | description: string; 16 | component: typeof React.Component; 17 | icon?: IconType; 18 | visible: boolean; 19 | search?: string; 20 | } 21 | 22 | /** 23 | * General GraphQL query data interface 24 | * @interface IGeneralQueryData 25 | */ 26 | export interface IGeneralQueryData { 27 | parentId?: string; 28 | id?: string; 29 | type: string; 30 | name: string; 31 | description: string; 32 | version?: number; 33 | visible?: boolean; 34 | alias?: string; 35 | } 36 | 37 | /** 38 | * Event interface 39 | * @interface IEvent 40 | */ 41 | export interface IEvent extends IGeneralQueryData { 42 | eventProcessId?: string; 43 | priority: string; 44 | sms?: string; 45 | email?: string; 46 | rootCauses?: string[]; 47 | isActive?: boolean; 48 | isClosedRejected?: boolean; 49 | isOpen?: boolean; 50 | isAcknowledged?: boolean; 51 | activeIssueId?: string; 52 | updateIssueVersion?: number; 53 | createIssueTime?: string; 54 | createIssueTimeUtc?: string; 55 | eventImgKey?: string; 56 | eventType?: string; 57 | issueAdditionalDetails?: string; 58 | } 59 | 60 | /** 61 | * Event update interface 62 | * @interface IEventUpdate 63 | */ 64 | export interface IEventUpdate { 65 | id: string; 66 | sms?: string; 67 | email?: string; 68 | previousSms?: string; 69 | previousEmail?: string; 70 | rootCauses: string[]; 71 | eventImgKey?: string; 72 | alias?: string; 73 | } 74 | 75 | /** 76 | * Issue interface 77 | * @interface IIssue 78 | */ 79 | export interface IIssue { 80 | id: string; 81 | eventId: string; 82 | eventDescription: string; 83 | type: string; 84 | priority: string; 85 | siteName: string; 86 | processName: string; 87 | areaName: string; 88 | stationName: string; 89 | deviceName: string; 90 | created: string; 91 | createdAt: string; 92 | acknowledged: string; 93 | closed: string; 94 | resolutionTime: number; 95 | acknowledgedTime: number; 96 | status: string; 97 | version: number; 98 | visible?: boolean; 99 | expectedVersion?: number; 100 | rootCause?: string | null; 101 | comment?: string | null; 102 | createdBy: string; 103 | closedBy?: string; 104 | acknowledgedBy?: string; 105 | rejectedBy?: string; 106 | openFor?: number | string | null; 107 | additionalDetails?: string; 108 | } 109 | 110 | /** 111 | * UpdateIssueResponse Interface 112 | * @interface IUpdateIssueResponse 113 | */ 114 | export interface IUpdateIssueResponse { 115 | data: { 116 | updateIssue?: { 117 | id: string; 118 | eventId: string; 119 | eventDescription: string; 120 | type: string; 121 | priority: string; 122 | siteName: string; 123 | processName: string; 124 | areaName: string; 125 | stationName: string; 126 | deviceName: string; 127 | created: string; 128 | createdAt: string; 129 | acknowledged: string; 130 | closed: string; 131 | resolutionTime: string; 132 | acknowledgedTime: string; 133 | status: string; 134 | version: string; 135 | rootCause: string; 136 | comment: string; 137 | } 138 | }, 139 | errors?: any[]; 140 | } 141 | 142 | /** 143 | * Top issue interface 144 | * @interface ITopIssue 145 | */ 146 | export interface ITopIssue { 147 | processName: string; 148 | eventDescription: string; 149 | count: number; 150 | totalResolutionSeconds: number; 151 | averageResolutionTime?: number; 152 | } 153 | 154 | /** 155 | * Selected data interface 156 | * @interface ISelectedData 157 | */ 158 | export interface ISelectedData { 159 | id?: string; 160 | name: string; 161 | parentId?: string; 162 | } 163 | 164 | /** 165 | * User interface 166 | * @interface IUser 167 | */ 168 | export interface IUser { 169 | username: string; 170 | groups: string[]; 171 | status: string; 172 | userId?: string; 173 | visible?: boolean; 174 | } 175 | 176 | /** 177 | * CSV User interface 178 | * @interface ICSVUser 179 | */ 180 | export interface ICSVUser { 181 | username: string; 182 | groups: string; 183 | } 184 | 185 | /** 186 | * File upload result interface 187 | * @interface IUploadResult 188 | */ 189 | export interface IUploadResult { 190 | name: string; 191 | result: string; 192 | } 193 | 194 | /** 195 | * Permission interface 196 | * @interface IPermission 197 | */ 198 | export interface IPermission { 199 | id: string; 200 | username: string; 201 | sites: ISelectedData[]; 202 | areas: ISelectedData[]; 203 | processes: ISelectedData[]; 204 | stations: ISelectedData[]; 205 | devices: ISelectedData[]; 206 | version: number; 207 | visible?: boolean; 208 | } 209 | 210 | /** 211 | * Root cause interface 212 | * @interface IRootCause 213 | */ 214 | export interface IRootCause { 215 | id: string; 216 | name: string; 217 | visible?: boolean; 218 | deleted?: boolean; 219 | } 220 | 221 | /** 222 | * Represents all aspects of a site 223 | * @interface ISiteData 224 | */ 225 | export interface ISiteData { 226 | siteName: string; 227 | areas: ISelectedData[]; 228 | processes: ISelectedData[]; 229 | stations: ISelectedData[]; 230 | devices: ISelectedData[]; 231 | } -------------------------------------------------------------------------------- /source/console/src/components/NoMatch.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Import React and Amplify packages 5 | import React from 'react'; 6 | import { I18n } from 'aws-amplify'; 7 | 8 | // Import React Bootstrap components 9 | import Container from 'react-bootstrap/Container'; 10 | import Row from 'react-bootstrap/Row'; 11 | import Col from 'react-bootstrap/Col'; 12 | import Jumbotron from 'react-bootstrap/Jumbotron'; 13 | 14 | /** 15 | * The class returns an error when path matches nothing. 16 | * @class NoMatch 17 | */ 18 | class NoMatch extends React.Component { 19 | /** 20 | * Render the page 21 | */ 22 | render() { 23 | return ( 24 |
25 | 26 | 27 | 28 | 29 |

{ I18n.get('text.not.found') }: {window.location.pathname}

30 |
31 | 32 |
33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | export default NoMatch; -------------------------------------------------------------------------------- /source/console/src/components/Routes.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Interfaces 5 | import { IRoute } from './Interfaces'; 6 | 7 | // Icons 8 | import { GoLocation, GoPerson, GoEye, GoGraph, GoOrganization, GoLock, GoGear } from 'react-icons/go'; 9 | 10 | // Views 11 | import Site from '../views/Site'; 12 | import Area from '../views/Area'; 13 | import Station from '../views/Station'; 14 | import Process from '../views/Process'; 15 | import Device from '../views/Device'; 16 | import Event from '../views/Event'; 17 | import Client from '../views/Client'; 18 | import Observer from '../views/Observer'; 19 | import IssuesReport from '../views/IssuesReport'; 20 | import User from '../views/User'; 21 | import Permission from '../views/Permission'; 22 | import PermissionSetting from '../views/PermissionSetting'; 23 | import RootCause from '../views/RootCause'; 24 | import AddEditEvent from '../views/AddEditEvent'; 25 | 26 | /** 27 | * Admin group routes 28 | */ 29 | export const adminRoutes: IRoute[] = [ 30 | { 31 | path: '/sites', 32 | nameCode: 'text.sites', 33 | description: 'Sites', 34 | component: Site, 35 | icon: GoLocation, 36 | visible: true 37 | }, 38 | { 39 | path: '/sites/:siteId', 40 | description: 'Site - Areas', 41 | component: Area, 42 | visible: false 43 | }, 44 | { 45 | path: '/areas/:areaId/stations', 46 | description: 'Site - Area - Stations', 47 | component: Station, 48 | visible: false 49 | }, 50 | { 51 | path: '/stations/:stationId', 52 | description: 'Site - Area - Station - Devices', 53 | component: Device, 54 | visible: false 55 | }, 56 | { 57 | path: '/areas/:areaId/processes', 58 | description: 'Site - Area - Processes', 59 | component: Process, 60 | visible: false 61 | }, 62 | { 63 | path: '/processes/:processId', 64 | description: 'Site - Area - Process - Events', 65 | component: Event, 66 | visible: false 67 | }, 68 | { 69 | path: '/client', 70 | nameCode: 'menu.client', 71 | description: 'Client', 72 | component: Client, 73 | icon: GoPerson, 74 | visible: true 75 | }, 76 | { 77 | path: '/observer', 78 | nameCode: 'menu.observer', 79 | description: 'Observer', 80 | component: Observer, 81 | icon: GoEye, 82 | visible: true 83 | }, 84 | { 85 | path: '/issuesreport', 86 | nameCode: 'text.issues.report', 87 | description: 'Issues Report', 88 | component: IssuesReport, 89 | icon: GoGraph, 90 | visible: true 91 | }, 92 | { 93 | path: '/users', 94 | nameCode: 'text.users', 95 | description: 'User', 96 | component: User, 97 | icon: GoOrganization, 98 | visible: true 99 | }, 100 | { 101 | path: '/permissions', 102 | nameCode: 'text.permissions', 103 | description: 'Permission', 104 | component: Permission, 105 | icon: GoLock, 106 | visible: true 107 | }, 108 | { 109 | path: '/permissions/setting', 110 | description: 'Permission Setting', 111 | component: PermissionSetting, 112 | visible: false 113 | }, 114 | { 115 | path: '/rootcause', 116 | nameCode: 'text.rootcauses', 117 | description: 'Root Cause', 118 | component: RootCause, 119 | icon: GoGear, 120 | visible: true 121 | }, 122 | { 123 | path: '/processes/:processId/event', 124 | description: 'Add Event', 125 | component: AddEditEvent, 126 | visible: false 127 | }, 128 | { 129 | path: '/processes/:processId/event/:eventId', 130 | description: 'Edit Event', 131 | component: AddEditEvent, 132 | visible: false 133 | } 134 | ]; 135 | 136 | /** 137 | * Manager group routes 138 | */ 139 | export const managerRoutes: IRoute[] = [ 140 | { 141 | path: '/client', 142 | nameCode: 'menu.client', 143 | description: 'Client', 144 | component: Client, 145 | icon: GoPerson, 146 | visible: true 147 | }, 148 | { 149 | path: '/observer', 150 | nameCode: 'menu.observer', 151 | description: 'Observer', 152 | component: Observer, 153 | icon: GoEye, 154 | visible: true 155 | }, 156 | { 157 | path: '/issuesreport', 158 | nameCode: 'text.issues.report', 159 | description: 'Issues Report', 160 | component: IssuesReport, 161 | icon: GoGraph, 162 | visible: true 163 | } 164 | ]; 165 | 166 | /** 167 | * Engineer group routes 168 | */ 169 | export const engineerRoutes: IRoute[] = [ 170 | { 171 | path: '/client', 172 | nameCode: 'menu.client', 173 | description: 'Client', 174 | component: Client, 175 | icon: GoPerson, 176 | visible: true 177 | }, 178 | { 179 | path: '/observer', 180 | nameCode: 'menu.observer', 181 | description: 'Observer', 182 | component: Observer, 183 | icon: GoEye, 184 | visible: true 185 | } 186 | ]; 187 | 188 | /** 189 | * Associate group routes 190 | */ 191 | export const associateRoutes: IRoute[] = [ 192 | { 193 | path: '/client', 194 | nameCode: 'menu.client', 195 | description: 'Client', 196 | component: Client, 197 | icon: GoPerson, 198 | visible: true 199 | } 200 | ]; 201 | -------------------------------------------------------------------------------- /source/console/src/graphql/mutations.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | /* eslint-disable */ 4 | 5 | export const createSite = `mutation CreateSite($id: ID, $name: String!, $description: String!) { 6 | createSite(id: $id, type: "SITE", name: $name, description: $description) { 7 | id 8 | name 9 | description 10 | version 11 | } 12 | }`; 13 | export const deleteSite = `mutation DeleteSite($siteId: ID!) { 14 | deleteSite(id: $siteId, type: "SITE") { 15 | id 16 | } 17 | }`; 18 | export const createArea = `mutation CreateArea($id: ID, $areaSiteId: ID!, $name: String!, $description: String!) { 19 | createArea(id: $id, type: "AREA", areaSiteId: $areaSiteId, name: $name, description: $description) { 20 | id 21 | name 22 | description 23 | version 24 | } 25 | }`; 26 | export const deleteArea = `mutation DeleteArea($areaId: ID!) { 27 | deleteArea(id: $areaId, type: "AREA") { 28 | id 29 | } 30 | }`; 31 | export const createProcess = `mutation CreateProcess($id: ID, $processAreaId: ID!, $name: String!, $description: String!) { 32 | createProcess(id: $id, type: "PROCESS", processAreaId: $processAreaId, name: $name, description: $description) { 33 | id 34 | name 35 | description 36 | version 37 | } 38 | }`; 39 | export const deleteProcess = `mutation DeleteProcess($processId: ID!) { 40 | deleteProcess(id: $processId, type: "PROCESS") { 41 | id 42 | } 43 | } 44 | `; 45 | export const createEvent = `mutation CreateEvent( 46 | $id: ID, 47 | $eventProcessId: ID, 48 | $parentId: ID, 49 | $name: String!, 50 | $description: String!, 51 | $priority: Priority!, 52 | $sms: String, 53 | $email: String, 54 | $rootCauses: [String], 55 | $eventImgKey: String, 56 | $eventType: String, 57 | $alias: String, 58 | ) { 59 | createEvent( 60 | id: $id, 61 | type: "EVENT", 62 | eventProcessId: $eventProcessId, 63 | parentId: $parentId, 64 | name: $name, 65 | description: $description, 66 | eventType: $eventType, 67 | priority: $priority, 68 | sms: $sms, 69 | email: $email, 70 | rootCauses: $rootCauses, 71 | eventImgKey: $eventImgKey 72 | alias: $alias 73 | ) { 74 | id 75 | name 76 | description 77 | priority 78 | sms 79 | email 80 | rootCauses 81 | version 82 | eventImgKey 83 | eventType, 84 | alias, 85 | eventProcessId, 86 | parentId 87 | } 88 | }`; 89 | export const updateEvent = `mutation UpdateEvent( 90 | $id: ID!, 91 | $sms: String, 92 | $email: String, 93 | $previousSms: String, 94 | $previousEmail: String, 95 | $rootCauses: [String], 96 | $eventImgKey: String, 97 | $alias: String 98 | ) { 99 | updateEvent( 100 | id: $id, 101 | sms: $sms, 102 | email: $email, 103 | previousSms: $previousSms, 104 | previousEmail: $previousEmail, 105 | rootCauses: $rootCauses, 106 | eventImgKey: $eventImgKey, 107 | alias: $alias 108 | ) { 109 | id 110 | name 111 | description 112 | type 113 | priority 114 | sms 115 | email 116 | rootCauses 117 | version 118 | eventImgKey 119 | alias 120 | } 121 | }`; 122 | export const deleteEvent = `mutation DeleteEvent($eventId: ID!) { 123 | deleteEvent(id: $eventId, type: "EVENT") { 124 | id 125 | } 126 | }`; 127 | export const createStation = `mutation CreateStation($id: ID, $stationAreaId: ID!, $name: String!, $description: String!) { 128 | createStation(id: $id, type: "STATION", stationAreaId: $stationAreaId, name: $name, description: $description) { 129 | id 130 | name 131 | description 132 | version 133 | } 134 | }`; 135 | export const deleteStation = `mutation DeleteStation($stationId: ID!) { 136 | deleteStation(id: $stationId, type: "STATION") { 137 | id 138 | } 139 | }`; 140 | export const createDevice = `mutation CreateDevice($id: ID, $deviceStationId: ID!, $name: String!, $description: String!, $alias: String) { 141 | createDevice(id: $id, type: "DEVICE", deviceStationId: $deviceStationId, name: $name, description: $description, alias: $alias) { 142 | id 143 | name 144 | description 145 | version 146 | alias 147 | } 148 | }`; 149 | export const deleteDevice = `mutation DeleteDevice($deviceId: ID!) { 150 | deleteDevice(id: $deviceId, type: "DEVICE") { 151 | id 152 | } 153 | }`; 154 | export const updateIssue = `mutation UpdateIssue($input: UpdateIssueInput!) { 155 | updateIssue(input: $input) { 156 | id 157 | eventId 158 | eventDescription 159 | type 160 | priority 161 | siteName 162 | processName 163 | areaName 164 | stationName 165 | deviceName 166 | created 167 | createdAt 168 | acknowledged 169 | closed 170 | resolutionTime 171 | acknowledgedTime 172 | status 173 | version 174 | rootCause 175 | comment 176 | } 177 | }`; 178 | export const putPermission = `mutation PutPermission($input: PermissionInput!) { 179 | putPermission(input: $input) { 180 | id 181 | sites { 182 | id 183 | name 184 | } 185 | areas { 186 | id 187 | name 188 | parentId 189 | } 190 | processes { 191 | id 192 | name 193 | parentId 194 | } 195 | stations { 196 | id 197 | name 198 | parentId 199 | } 200 | devices { 201 | id 202 | name 203 | parentId 204 | } 205 | version 206 | } 207 | }`; 208 | export const deletePermission = `mutation DeletePermission($id: ID!) { 209 | deletePermission(id: $id, type: "PERMISSION") { 210 | id 211 | } 212 | }`; 213 | export const createRootCause = `mutation CreateRootCause($id: ID, $rootCause: String!) { 214 | createRootCause(id: $id, type: "ROOT_CAUSE", name: $rootCause) { 215 | id 216 | type 217 | name 218 | } 219 | }`; 220 | export const deleteRootCause = `mutation DeleteRootCause($id: ID!) { 221 | deleteRootCause(id: $id, type: "ROOT_CAUSE") { 222 | id 223 | name 224 | } 225 | }`; -------------------------------------------------------------------------------- /source/console/src/graphql/subscriptions.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | /* eslint-disable */ 4 | 5 | export const onCreateIssue = `subscription OnCreateIssue { 6 | onCreateIssue { 7 | id 8 | eventId 9 | eventDescription 10 | type 11 | priority 12 | siteName 13 | processName 14 | areaName 15 | stationName 16 | deviceName 17 | created 18 | createdAt 19 | acknowledged 20 | closed 21 | resolutionTime 22 | acknowledgedTime 23 | status 24 | version 25 | additionalDetails 26 | } 27 | } 28 | `; 29 | export const onUpdateIssue = `subscription OnUpdateIssue { 30 | onUpdateIssue { 31 | id 32 | eventId 33 | eventDescription 34 | type 35 | priority 36 | siteName 37 | processName 38 | areaName 39 | stationName 40 | deviceName 41 | created 42 | createdAt 43 | acknowledged 44 | closed 45 | resolutionTime 46 | acknowledgedTime 47 | status 48 | version 49 | rootCause 50 | comment 51 | } 52 | } 53 | `; 54 | export const onPutPermission = `subscription OnPutPermission { 55 | onPutPermission { 56 | id 57 | sites { 58 | id 59 | name 60 | parentId 61 | } 62 | areas { 63 | id 64 | name 65 | parentId 66 | } 67 | processes { 68 | id 69 | name 70 | parentId 71 | } 72 | stations { 73 | id 74 | name 75 | parentId 76 | } 77 | devices { 78 | id 79 | name 80 | parentId 81 | } 82 | } 83 | }`; 84 | export const onDeletePermission = `subscription OnDeletePermission { 85 | onDeletePermission { 86 | id 87 | } 88 | }`; 89 | export const onCreateRootCause = `subscription OnCreateRootCause { 90 | onCreateRootCause { 91 | id 92 | name 93 | } 94 | }`; 95 | export const onDeleteRootCause = `subscription OnDeleteRootCause { 96 | onDeleteRootCause { 97 | id 98 | name 99 | } 100 | }`; -------------------------------------------------------------------------------- /source/console/src/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Import React and Amplify packages 5 | import { CookiesProvider, Cookies } from 'react-cookie'; 6 | import ReactDOM from 'react-dom'; 7 | import { I18n } from 'aws-amplify'; 8 | 9 | // Import App 10 | import App from './App'; 11 | 12 | // Import style sheets 13 | import './assets/css/style.scss'; 14 | import 'bootstrap/dist/css/bootstrap.min.css'; 15 | 16 | // Import language files 17 | import de from './util/lang/de.json'; // German 18 | import en from './util/lang/en.json'; // English 19 | import es from './util/lang/es.json'; // Spanish (Spain) 20 | import fr from './util/lang/fr.json'; // French (France) 21 | import ja from './util/lang/ja.json'; // Japanese 22 | import ko from './util/lang/ko.json'; // Korean 23 | import zh from './util/lang/zh.json'; // Chinese (Simplified) 24 | import th from './util/lang/th.json'; // Thai 25 | 26 | // Create dictionary for translated strings. Default to English if a string is 27 | // missing a translation 28 | const dict: any = { 29 | en, 30 | de: { ...en, ...de }, 31 | es: { ...en, ...es }, 32 | fr: { ...en, ...fr }, 33 | ja: { ...en, ...ja }, 34 | ko: { ...en, ...ko }, 35 | zh: { ...en, ...zh }, 36 | th: { ...en, ...th } 37 | }; 38 | I18n.putVocabularies(dict); 39 | 40 | // Declare Amazon Virtual Andon console configuration 41 | declare let andon_config: any; 42 | 43 | // Set the default locale cookie 44 | const cookies = new Cookies(); 45 | const locale = cookies.get('ui_locale'); 46 | if (locale === undefined) { 47 | let defaultLanguageConfig = andon_config.default_language; 48 | let localLanguage = ''; 49 | 50 | switch (defaultLanguageConfig) { 51 | case 'Browser Default': 52 | localLanguage = navigator.language.slice(0, 2); 53 | break; 54 | case 'Chinese (Simplified)': 55 | localLanguage = 'zh'; 56 | break; 57 | case 'English': 58 | localLanguage = 'en'; 59 | break; 60 | case 'French (France)': 61 | localLanguage = 'fr'; 62 | break; 63 | case 'German': 64 | localLanguage = 'de'; 65 | break; 66 | case 'Japanese': 67 | localLanguage = 'ja'; 68 | break; 69 | case 'Korean': 70 | localLanguage = 'ko'; 71 | break; 72 | case 'Spanish (Spain)': 73 | localLanguage = 'es'; 74 | break; 75 | case 'Thai': 76 | localLanguage = 'th'; 77 | break; 78 | default: 79 | localLanguage = 'en'; 80 | break; 81 | } 82 | 83 | localLanguage = dict[localLanguage] ? localLanguage : 'en'; 84 | I18n.setLanguage(localLanguage); 85 | 86 | let cookieExpires = new Date(); 87 | cookieExpires.setFullYear(cookieExpires.getFullYear() + 20); 88 | cookies.set('ui_locale', localLanguage, { expires: cookieExpires, path: '/', secure: true }); 89 | window.location.reload(); 90 | } else { 91 | I18n.setLanguage(locale); 92 | } 93 | 94 | ReactDOM.render( 95 | 96 | 97 | , 98 | document.getElementById('root') 99 | ); -------------------------------------------------------------------------------- /source/console/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | /// 4 | 5 | // See https://github.com/facebook/create-react-app/issues/6560 for why this file exists. -------------------------------------------------------------------------------- /source/console/src/types/aws-amplify-react.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | declare module 'aws-amplify-react' { 4 | interface IAuthProps { 5 | authData?: any; 6 | } 7 | interface IForgotPasswordState { 8 | delivery: any; 9 | } 10 | interface IVerifyContactState { 11 | verifyAttr: any; 12 | } 13 | interface IAuthenticatorProps { 14 | hideDefault: boolean; 15 | amplifyConfig: any; 16 | errorMessage?: (message:string) => string; 17 | onStateChange: Function; 18 | } 19 | 20 | export declare class SignIn extends React.Component { 21 | _validAuthStates: string[]; 22 | handleInputChange: React.EventHandler; 23 | changeState(state: string, data?: any): void; 24 | signIn(): void; 25 | error(error: any): void; 26 | } 27 | 28 | export declare class RequireNewPassword extends React.Component { 29 | _validAuthStates: string[]; 30 | handleInputChange: React.EventHandler; 31 | changeState(state: string, data?: any): void; 32 | change(): void; 33 | error(error: any): void; 34 | } 35 | 36 | export declare class ForgotPassword extends React.Component { 37 | _validAuthStates: string[]; 38 | handleInputChange: React.EventHandler; 39 | changeState(state: string, data?: any): void; 40 | send(): void; 41 | submit(): void; 42 | error(error: any): void; 43 | } 44 | 45 | export declare class VerifyContact extends React.Component { 46 | _validAuthStates: string[]; 47 | handleInputChange: React.EventHandler; 48 | changeState(state: string, data?: any): void; 49 | verify(): void; 50 | submit(): void; 51 | error(error: any): void; 52 | } 53 | 54 | export declare class Authenticator extends React.Component { 55 | } 56 | } -------------------------------------------------------------------------------- /source/console/src/views/Home.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Import React and Amplify packages 5 | import React from 'react'; 6 | import { I18n } from 'aws-amplify'; 7 | 8 | // Import React Bootstrap components 9 | import Container from 'react-bootstrap/Container'; 10 | import Row from 'react-bootstrap/Row'; 11 | import Col from 'react-bootstrap/Col'; 12 | import Jumbotron from 'react-bootstrap/Jumbotron'; 13 | 14 | /** 15 | * Properties Interface 16 | * @interface IProps 17 | */ 18 | interface IProps {} 19 | 20 | /** 21 | * State Interface 22 | * @interface IState 23 | */ 24 | interface IState {} 25 | 26 | /** 27 | * The default home page 28 | * @class Home 29 | */ 30 | class Home extends React.Component { 31 | /** 32 | * Render this page. 33 | */ 34 | render() { 35 | return ( 36 | 37 | 38 | 39 | 40 |

Amazon Virtual Andon

41 |

42 | { I18n.get('text.user.guide.for.more.information') } { I18n.get('text.user.guide') } 43 |

44 |
45 | 46 |
47 |
48 | ) 49 | } 50 | } 51 | 52 | export default Home; -------------------------------------------------------------------------------- /source/console/src/views/Main.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Import React and Amplify packages 5 | import React from 'react'; 6 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 7 | import { LinkContainer } from 'react-router-bootstrap'; 8 | import { I18n } from 'aws-amplify'; 9 | import { Logger } from '@aws-amplify/core'; 10 | import Auth from '@aws-amplify/auth'; 11 | import { GoSignOut } from 'react-icons/go'; 12 | 13 | // Import React Bootstrap components 14 | import Navbar from 'react-bootstrap/Navbar'; 15 | import Nav from 'react-bootstrap/Nav'; 16 | import Button from 'react-bootstrap/Button'; 17 | 18 | // Import custom setting 19 | import { LOGGING_LEVEL } from '../util/CustomUtil'; 20 | import { IRoute } from '../components/Interfaces'; 21 | import NoMatch from '../components/NoMatch'; 22 | import EmptyCol from '../components/EmptyCol'; 23 | 24 | // Import views 25 | import Home from './Home'; 26 | 27 | /** 28 | * Properties Interface 29 | * @interface IProps 30 | */ 31 | interface IProps { 32 | authState?: any; 33 | routes: IRoute[]; 34 | handleNotification: Function; 35 | } 36 | 37 | /** 38 | * State Interface 39 | * @interface IState 40 | */ 41 | interface IState { } 42 | 43 | // Logging 44 | const LOGGER = new Logger('Main', LOGGING_LEVEL); 45 | 46 | /** 47 | * The main application including Amplify authentication and routers per user group 48 | * @class Main 49 | */ 50 | class Main extends React.Component { 51 | 52 | /** 53 | * Sign out the user. 54 | */ 55 | async signOut() { 56 | await Auth.signOut().catch((error) => { 57 | LOGGER.error('Error occurred while signing out.', error); 58 | }); 59 | 60 | window.location.reload(); 61 | } 62 | 63 | /** 64 | * Render this page. 65 | */ 66 | render() { 67 | if (this.props.authState === 'signedin') { 68 | return ( 69 |
70 | 71 | 72 | 73 | Amazon Virtual Andon 74 | 75 | 76 | 77 | 97 | 102 | 103 | 104 | 105 | ()} /> 106 | { 107 | this.props.routes.map((route: IRoute) => { 108 | return ( 109 | ()} /> 110 | ); 111 | }) 112 | } 113 | ()} /> 114 | 115 | 116 |
117 | ); 118 | } else { 119 | return null; 120 | } 121 | } 122 | } 123 | 124 | export default Main; -------------------------------------------------------------------------------- /source/console/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "es6", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "noFallthroughCasesInSwitch": true, 23 | "noImplicitAny": false 24 | }, 25 | "include": [ 26 | "src", 27 | "__tests__" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /source/external-integrations-handler/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [''], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | }, 7 | coverageReporters: [ 8 | 'text', 9 | ['lcov', { 'projectRoot': '../' }] 10 | ], 11 | setupFiles: ['./test/setJestEnvironmentVariables.ts'], 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 13 | }; 14 | -------------------------------------------------------------------------------- /source/external-integrations-handler/lib/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * Structure of a record representing an object being uploaded to the S3 bucket 6 | */ 7 | export interface IS3Record { 8 | eventSource: string; 9 | eventName: string; 10 | s3: { 11 | bucket: { name: string; } 12 | object: { key: string; } 13 | } 14 | } 15 | 16 | /** 17 | * Structure of a request coming from an S3 18 | */ 19 | export interface IS3Request { 20 | Records: IS3Record[]; 21 | } 22 | 23 | /** 24 | * Structure of a message sent to the IoT Devices topic 25 | */ 26 | export interface IIotMessage { 27 | messages: { 28 | name: string; 29 | timestamp: string; 30 | quality: string; 31 | value: string 32 | }[] 33 | } 34 | 35 | /** 36 | * Structure of the properties object used when publishing a new issue 37 | */ 38 | export interface IPublishIssueParams { 39 | eventId: string; 40 | eventDescription: string; 41 | priority: string; 42 | deviceName: string; 43 | stationName: string; 44 | areaName: string; 45 | siteName: string; 46 | processName: string; 47 | issueSource: 's3File' | 'device', 48 | createdBy: 'automatic-issue-detection' | 'device', 49 | additionalDetails?: string; 50 | } 51 | -------------------------------------------------------------------------------- /source/external-integrations-handler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "external-integrations-handler", 3 | "version": "3.0.6", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions" 7 | }, 8 | "description": "Handles issues created by external integrations", 9 | "main": "index.js", 10 | "dependencies": { 11 | "uuid": "^9.0.0", 12 | "aws-sdk": "2.1354.0" 13 | }, 14 | "devDependencies": { 15 | "@types/jest": "^29.5.0", 16 | "@types/node": "^18.15.11", 17 | "jest": "^29.5.0", 18 | "ts-jest": "^29.1.0", 19 | "ts-node": "^10.9.1", 20 | "typescript": "^5.0.3" 21 | }, 22 | "engines": { 23 | "node": ">=18.0.0" 24 | }, 25 | "scripts": { 26 | "clean": "rm -rf node_modules/ dist/ coverage/ package-lock.json", 27 | "prebuild": "npm run clean && npm install", 28 | "build": "tsc --build tsconfig.json", 29 | "package": "npm run prebuild && npm run build && npm prune --production && rsync -avrq ./node_modules ./dist && npm run include-solution-utils && npm run package:zip", 30 | "package:zip": "cd dist && zip -q -r9 ./package.zip * && cd ..", 31 | "pretest": "npm run clean && npm install", 32 | "test": "jest --coverage --silent", 33 | "include-solution-utils": "npm run solution-utils:prep && npm run solution-utils:package", 34 | "solution-utils:prep": "rm -rf dist/solution-utils && mkdir dist/solution-utils", 35 | "solution-utils:package": "cd ../solution-utils && npm run package && cd dist/ && rsync -avrq . ../../$npm_package_name/dist/solution-utils/ && cd ../../$npm_package_name" 36 | }, 37 | "license": "Apache-2.0" 38 | } -------------------------------------------------------------------------------- /source/external-integrations-handler/test/setJestEnvironmentVariables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env.LOGGING_LEVEL = 'VERBOSE'; 5 | process.env.AWS_LAMBDA_FUNCTION_NAME = 'fn-name'; 6 | process.env.IOT_MESSAGE_NAME_DELIMITER = '/'; 7 | -------------------------------------------------------------------------------- /source/external-integrations-handler/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "types": [ 12 | "node", 13 | "@types/jest" 14 | ] 15 | }, 16 | "include": [ 17 | "**/*.ts" 18 | ], 19 | "exclude": [ 20 | "package", 21 | "dist", 22 | "**/*.map" 23 | ] 24 | } -------------------------------------------------------------------------------- /source/glue-job-scripts/etl-cleanup.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import boto3 5 | import sys 6 | from botocore import config 7 | from datetime import datetime 8 | from itertools import islice 9 | from awsglue.utils import getResolvedOptions 10 | from pyspark.context import SparkContext 11 | from awsglue.context import GlueContext 12 | from awsglue.job import Job 13 | 14 | # System arguments 15 | args_list = ["glue_output_bucket", "glue_output_s3_key_prefix", "solution_id", "solution_version"] 16 | args = getResolvedOptions(sys.argv, args_list) # NOSONAR: python:S4823 17 | GLUE_OUTPUT_BUCKET = args["glue_output_bucket"] 18 | GLUE_OUTPUT_S3_KEY_PREFIX = args["glue_output_s3_key_prefix"] 19 | SOLUTION_ID = args["solution_id"] 20 | SOLUTION_VERSION = args["solution_version"] 21 | 22 | # Sets Glue context and logging 23 | spark_context = SparkContext() 24 | glue_context = GlueContext(spark_context) 25 | job = Job(glue_context) 26 | 27 | # AWS Clients 28 | config_json = {} 29 | if SOLUTION_ID.strip() != "" and SOLUTION_VERSION.strip() != "": 30 | config_json["user_agent_extra"] = f"AwsSolution/{SOLUTION_ID}/{SOLUTION_VERSION}" 31 | 32 | config = config.Config(**config_json) 33 | s3 = boto3.client('s3', config=config) 34 | 35 | class DataCleanupException(Exception): 36 | """Raised when there is an issue while cleaning previous data from S3""" 37 | pass 38 | 39 | def log_message(msg): 40 | msg_arr = [f'****** LOG_MSG {datetime.now()} ******'] 41 | 42 | if not isinstance(msg, list): 43 | msg = [msg] 44 | 45 | # Add some preceding whitespace to each line for the log message. 46 | # This makes it easier to read in the Glue logs on Cloudwatch 47 | msg = list(map(lambda x: f' {x}', msg)) 48 | 49 | msg_arr.extend(msg) 50 | msg_arr.append('') # empty line 51 | 52 | # Glue sends Python logging messages (using logger) to the error logs in CloudWatch. 53 | # Instead, we will use the print statement as they appear in the normal Logs section 54 | # of the Glue job. 55 | print('\n'.join(msg_arr)) 56 | 57 | def main(): 58 | """ 59 | Deletes any previous data that was exported from DynamoDB to S3 so 60 | the current ETL job will represent the current state of the DynamoDB tables 61 | """ 62 | log_message(f"Looking for previously generated output files: s3://{GLUE_OUTPUT_BUCKET}/{GLUE_OUTPUT_S3_KEY_PREFIX}") 63 | 64 | list_params = { 65 | "Bucket": GLUE_OUTPUT_BUCKET, 66 | "Prefix": GLUE_OUTPUT_S3_KEY_PREFIX 67 | } 68 | 69 | previous_job_output_data = set() 70 | 71 | while True: 72 | response = s3.list_objects_v2(**list_params) 73 | if response["KeyCount"] > 0: 74 | # Extract only a list of Keys from the Contents returned by S3 75 | previous_job_output_data.update(list(map(lambda x: x["Key"], response["Contents"]))) 76 | 77 | if "NextContinuationToken" not in response: 78 | # Exit the `while` loop if there are no more objects in the S3 bucket 79 | break 80 | else: 81 | # Search again if there are more items in the S3 bucket 82 | list_params["ContinuationToken"] = response["NextContinuationToken"] 83 | 84 | log_message(f"Number of previously generated output files: {len(previous_job_output_data)}") 85 | 86 | while len(previous_job_output_data) > 0: 87 | # Delete up to 500 objects at a time until the list of previously 88 | # generated output files is empty 89 | objects_to_delete = list(islice(previous_job_output_data, 500)) 90 | 91 | log_message(f"Attempting to delete batch of previously generated data. Number of objects to delete: {len(objects_to_delete)}") 92 | 93 | delete_params = { 94 | "Bucket": GLUE_OUTPUT_BUCKET, 95 | "Delete": { 96 | "Objects": list(map(lambda x: { "Key": x }, objects_to_delete)) 97 | } 98 | } 99 | 100 | delete_response = s3.delete_objects(**delete_params) 101 | 102 | if "Errors" in delete_response and len(delete_response["Errors"]) > 0: 103 | raise DataCleanupException(f"Error while cleaning previous job output: {str(delete_response['Errors'][0])}") 104 | 105 | if "Deleted" not in delete_response or len(delete_response["Deleted"]) != len(objects_to_delete): 106 | raise DataCleanupException(f"Error while cleaning previous job output. Expecting {len(objects_to_delete)} to be deleted but S3 reported {len(delete_response['Deleted'])} were deleted") 107 | 108 | # Remove the objects that were deleted from the 'previous_job_output_data' set 109 | previous_job_output_data = (previous_job_output_data - set(objects_to_delete)) 110 | log_message(f"Successfully deleted {len(objects_to_delete)} objects. Number still left to delete: {len(previous_job_output_data)}") 111 | 112 | job.commit() 113 | 114 | if __name__ == '__main__': 115 | main() -------------------------------------------------------------------------------- /source/glue-job-scripts/etl-data-export.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import sys 5 | from datetime import datetime 6 | from awsglue.transforms import ApplyMapping, SelectFields, ResolveChoice 7 | from awsglue.utils import getResolvedOptions 8 | from pyspark.context import SparkContext 9 | from awsglue.context import GlueContext 10 | from awsglue.job import Job 11 | 12 | # System arguments 13 | args_list = ["job_type", "ddb_issues_table_name", "ddb_data_hierarchy_table_name", "glue_db_name", "glue_issues_table_name", "glue_data_hierarchy_table_name", "glue_output_bucket"] 14 | args = getResolvedOptions(sys.argv, args_list) # NOSONAR: python:S4823 15 | JOB_TYPE = args["job_type"] 16 | DDB_ISSUES_TABLE_NAME = args["ddb_issues_table_name"] 17 | DDB_DATA_HIERARCHY_TABLE_NAME = args["ddb_data_hierarchy_table_name"] 18 | GLUE_ISSUES_TABLE_NAME = args["glue_issues_table_name"] 19 | GLUE_DATA_HIERARCHY_TABLE_NAME = args["glue_data_hierarchy_table_name"] 20 | GLUE_DB_NAME = args["glue_db_name"] 21 | GLUE_OUTPUT_BUCKET = args["glue_output_bucket"] 22 | 23 | # Sets Glue context and logging 24 | spark_context = SparkContext() 25 | glue_context = GlueContext(spark_context) 26 | job = Job(glue_context) 27 | 28 | class JobInputException(Exception): 29 | """Raised when input to the job is not valid""" 30 | pass 31 | 32 | def log_message(msg): 33 | msg_arr = [f'****** LOG_MSG {datetime.now()} ******'] 34 | 35 | if not isinstance(msg, list): 36 | msg = [msg] 37 | 38 | # Add some preceding whitespace to each line for the log message. 39 | # This makes it easier to read in the Glue logs on Cloudwatch 40 | msg = list(map(lambda x: f' {x}', msg)) 41 | 42 | msg_arr.extend(msg) 43 | msg_arr.append('') # empty line 44 | 45 | # Glue sends Python logging messages (using logger) to the error logs in CloudWatch. 46 | # Instead, we will use the print statement as they appear in the normal Logs section 47 | # of the Glue job. 48 | print('\n'.join(msg_arr)) 49 | 50 | def get_column_mapping(column_name): 51 | """ 52 | Maps the columns from the Glue Data Catalog that was generated by Crawling the 53 | DynamoDB table with the column in the table that was crated in Glue. Defaults to string 54 | if no other data type is specified 55 | """ 56 | 57 | bigint_const = "bigint##bigint" 58 | 59 | if (JOB_TYPE == "issues"): 60 | data_types = { 61 | "version": bigint_const, 62 | "createddateutc": "date##date", 63 | "acknowledgedtime": bigint_const, 64 | "resolutiontime": bigint_const 65 | } 66 | elif (JOB_TYPE == "hierarchy"): 67 | data_types = { 68 | "version": bigint_const, 69 | "rootcauses": "array##string", 70 | "filterPolicy": "struct##string" 71 | } 72 | 73 | if column_name in data_types is not None: 74 | split_type = data_types[column_name].split("##") 75 | return (column_name, split_type[0], column_name, split_type[1]) 76 | else: 77 | return (column_name, "string", column_name, "string") 78 | 79 | def main(): 80 | """This script will load data from the supplied DynamoDB Table to S3 so it can be analyzed with Athena""" 81 | if (JOB_TYPE == "issues"): 82 | DDB_TABLE_NAME = DDB_ISSUES_TABLE_NAME 83 | GLUE_TABLE_NAME = GLUE_ISSUES_TABLE_NAME 84 | FIELD_PATHS = [ 85 | "eventid", 86 | "acknowledged", 87 | "created", 88 | "sitename", 89 | "issuesource", 90 | "priority", 91 | "areaname#status#processname#eventdescription#stationname#devicename#created", 92 | "version", 93 | "devicename", 94 | "devicename#eventid", 95 | "createdat", 96 | "areaname", 97 | "processname", 98 | "createddateutc", 99 | "eventdescription", 100 | "areaname#status#processname#stationname#devicename#created", 101 | "stationname", 102 | "id", 103 | "acknowledgedtime", 104 | "status", 105 | "updatedat", 106 | "closed", 107 | "resolutiontime", 108 | "createdby", 109 | "acknowledgedby", 110 | "closedby", 111 | "rejectedby", 112 | "additionaldetails" 113 | 114 | ] 115 | elif (JOB_TYPE == "hierarchy"): 116 | DDB_TABLE_NAME = DDB_DATA_HIERARCHY_TABLE_NAME 117 | GLUE_TABLE_NAME = GLUE_DATA_HIERARCHY_TABLE_NAME 118 | FIELD_PATHS = [ 119 | "createdat", 120 | "name", 121 | "description", 122 | "id", 123 | "devicestationid", 124 | "type", 125 | "version", 126 | "parentid", 127 | "updatedat", 128 | "areasiteid", 129 | "eventprocessid", 130 | "eventtype", 131 | "priority", 132 | "rootcauses", 133 | "sms", 134 | "eventimgkey", 135 | "email", 136 | "protocol", 137 | "endpoint", 138 | "filterpolicy", 139 | "subscriptionarn", 140 | "stationareaid", 141 | "processareaid", 142 | "alias" 143 | ] 144 | else: 145 | raise JobInputException(f"JOB_TYPE was invalid ({JOB_TYPE}). Expecting either \"issues\" or \"hierarchy\"") 146 | 147 | log_message([ 148 | "Running with the following context:", 149 | f"DDB_TABLE_NAME: {DDB_TABLE_NAME}", 150 | f"GLUE_TABLE_NAME: {GLUE_TABLE_NAME}", 151 | f"GLUE_DB_NAME: {GLUE_DB_NAME}", 152 | f"GLUE_OUTPUT_BUCKET: {GLUE_OUTPUT_BUCKET}" 153 | ]) 154 | 155 | DDB_TABLE_NAME_FORMATTED = DDB_TABLE_NAME.lower().replace('-', '_') 156 | 157 | log_message("Mapping columns") 158 | COLUMN_MAPPINGS = list(map(lambda x: get_column_mapping(x), FIELD_PATHS)) 159 | 160 | log_message("Creating a Dynamic Frame from the DynamoDB table schema") 161 | datasource0 = glue_context.create_dynamic_frame.from_catalog( 162 | database = GLUE_DB_NAME, 163 | table_name = DDB_TABLE_NAME_FORMATTED, 164 | transformation_ctx = "datasource0" 165 | ) 166 | 167 | log_message("Applying column mappings") 168 | applymapping1 = ApplyMapping.apply( 169 | frame = datasource0, 170 | mappings = COLUMN_MAPPINGS, 171 | transformation_ctx = "applymapping1" 172 | ) 173 | 174 | log_message("Selecting fields") 175 | selectfields2 = SelectFields.apply( 176 | frame = applymapping1, 177 | paths = FIELD_PATHS, 178 | transformation_ctx = "selectfields2" 179 | ) 180 | 181 | log_message("Resolving") 182 | resolvechoice3 = ResolveChoice.apply( 183 | frame = selectfields2, 184 | choice = "MATCH_CATALOG", 185 | database = GLUE_DB_NAME, 186 | table_name = GLUE_TABLE_NAME, 187 | transformation_ctx = "resolvechoice3" 188 | ) 189 | 190 | resolvechoice4 = ResolveChoice.apply( 191 | frame = resolvechoice3, 192 | choice = "make_struct", 193 | transformation_ctx = "resolvechoice4" 194 | ) 195 | 196 | log_message("Persisting data in S3") 197 | glue_context.write_dynamic_frame.from_catalog( 198 | frame = resolvechoice4, 199 | database = GLUE_DB_NAME, 200 | table_name = GLUE_TABLE_NAME, 201 | transformation_ctx = "datasink5" 202 | ) 203 | 204 | job.commit() 205 | log_message("Done") 206 | 207 | if __name__ == '__main__': 208 | main() -------------------------------------------------------------------------------- /source/solution-helper/generate-solution-constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { ICustomResourceRequest } from './lib/utils'; 5 | import { v4 as uuidV4 } from 'uuid'; 6 | import { getOptions } from '../solution-utils/get-options'; 7 | import Logger, { LoggingLevel as LogLevel } from '../solution-utils/logger'; 8 | 9 | // Import AWS SDK 10 | import Iot from 'aws-sdk/clients/iot'; 11 | const awsSdkOptions = getOptions(); 12 | const iot = new Iot(awsSdkOptions); 13 | 14 | interface HandlerOutput { 15 | anonymousUUID?: string; 16 | iotEndpointAddress?: string; 17 | } 18 | const { LOGGING_LEVEL, AWS_LAMBDA_FUNCTION_NAME } = process.env; 19 | const logger = new Logger(AWS_LAMBDA_FUNCTION_NAME, LOGGING_LEVEL); 20 | 21 | export async function handleGenerateSolutionConstants(event: ICustomResourceRequest): Promise { 22 | if (event.RequestType === 'Create') { 23 | return { 24 | anonymousUUID: uuidV4(), 25 | iotEndpointAddress: await getIotEndpoint() 26 | }; 27 | } 28 | 29 | return {}; 30 | } 31 | 32 | /** 33 | * Get IoT endpoint. 34 | * @return {Promise} - IoT endpoint 35 | */ 36 | async function getIotEndpoint(): Promise { 37 | const params = { 38 | endpointType: 'iot:Data-ATS' 39 | }; 40 | 41 | try { 42 | const response = await iot.describeEndpoint(params).promise(); 43 | return response.endpointAddress; 44 | } catch (error) { 45 | logger.log(LogLevel.ERROR, 'Error getting IoT endpoint.', error); 46 | throw error; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /source/solution-helper/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [''], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | }, 7 | coverageReporters: [ 8 | 'text', 9 | ['lcov', { 'projectRoot': '../' }] 10 | ], 11 | setupFiles: ['./test/setJestEnvironmentVariables.ts'], 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 13 | }; 14 | -------------------------------------------------------------------------------- /source/solution-helper/lib/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | /** 4 | * The actions this custom resource handler can support 5 | */ 6 | export enum CustomResourceActions { 7 | GENERATE_SOLUTION_CONSTANTS = 'GENERATE_SOLUTION_CONSTANTS', 8 | COPY_WEBSITE = 'COPY_WEBSITE', 9 | PUT_WEBSITE_CONFIG = 'PUT_WEBSITE_CONFIG', 10 | SOLUTION_LIFECYCLE = 'SOLUTION_LIFECYCLE', 11 | CONFIGURE_BUCKET_NOTIFICATION = 'CONFIGURE_BUCKET_NOTIFICATION' 12 | } 13 | 14 | /** 15 | * The CloudFormation lifecycle type for this custom resource 16 | */ 17 | export enum CustomResourceRequestTypes { 18 | CREATE = 'Create', 19 | UPDATE = 'Update', 20 | DELETE = 'Delete' 21 | } 22 | 23 | /** 24 | * Possible return values to the CloudFormation custom resource request. 25 | */ 26 | export enum StatusTypes { 27 | Success = 'SUCCESS', 28 | Failed = 'FAILED' 29 | } 30 | 31 | /** 32 | * Base interface for custom resource request properties 33 | * Action is required 34 | */ 35 | export interface ICustomResourceRequestProps { 36 | Action: CustomResourceActions; 37 | } 38 | 39 | /** 40 | * Request properties for the COPY_WEBSITE Custom Resource 41 | */ 42 | export interface ICopyWebsiteRequestProps extends ICustomResourceRequestProps { 43 | SourceBucket: string; 44 | SourceKey: string; 45 | SourceManifest: string; 46 | DestinationBucket: string; 47 | WebsiteDistributionDomain: string; 48 | } 49 | 50 | /** 51 | * Request properties for the SOLUTION_LIFECYCLE Custom Resource 52 | */ 53 | export interface ISolutionLifecycleRequestProps extends ICustomResourceRequestProps { 54 | IotPolicyName: string; 55 | SolutionParameters: { 56 | DefaultLanguage: string; 57 | StartGlueWorkflow: string; 58 | LoggingLevel: string; 59 | AnomalyDetectionBucketParameterSet: string; 60 | CognitoDomainPrefixParameterSet: string; 61 | CognitoSAMLProviderMetadataUrlParameterSet: string; 62 | CognitoSAMLProviderNameParameterSet: string; 63 | } 64 | } 65 | 66 | /** 67 | * Request properties for the PUT_WEBSITE_CONFIG Custom Resource 68 | */ 69 | export interface IPutWebsiteConfigRequestProps extends ICustomResourceRequestProps { 70 | S3Bucket: string; 71 | AndonWebsiteConfigFileBaseName: string; 72 | AndonWebsiteConfig: IAndonWebsiteConfig; 73 | } 74 | 75 | /** 76 | * Structure of the Amplify configuration object for the Amazon Virtual Andon web console 77 | */ 78 | export interface IAndonWebsiteConfig { 79 | aws_project_region: string; 80 | aws_cognito_identity_pool_id: string; 81 | aws_cognito_region: string; 82 | aws_user_pools_id: string; 83 | aws_user_pools_web_client_id: string; 84 | aws_appsync_graphqlEndpoint: string; 85 | aws_appsync_region: string; 86 | aws_appsync_authenticationType: 'AMAZON_COGNITO_USER_POOLS'; 87 | aws_iot_endpoint: string; 88 | aws_iot_policy_name: string; 89 | solutions_send_metrics: string; 90 | solutions_metrics_endpoint: 'https://metrics.awssolutionsbuilder.com/page'; 91 | solutions_solutionId: string; 92 | solutions_solutionUuId: string; 93 | solutions_version: string; 94 | default_language: string; 95 | website_bucket: string; 96 | oauth?: { 97 | domain: string; 98 | scope: ['phone', 'email', 'openid', 'profile', 'aws.cognito.signin.user.admin']; 99 | redirectSignIn: string; 100 | redirectSignOut: string; 101 | responseType: 'code'; 102 | }; 103 | } 104 | 105 | /** 106 | * Request properties for the CONFIGURE_BUCKET_NOTIFICATION Custom Resource 107 | */ 108 | export interface IConfigureBucketNotificationRequestProps extends ICustomResourceRequestProps { 109 | BucketName: string; 110 | FunctionArn: string; 111 | } 112 | 113 | /** 114 | * The request object coming from CloudFormation 115 | */ 116 | export interface ICustomResourceRequest { 117 | RequestType: CustomResourceRequestTypes; 118 | PhysicalResourceId: string; 119 | StackId: string; 120 | ServiceToken: string; 121 | RequestId: string; 122 | LogicalResourceId: string; 123 | ResponseURL: string; 124 | ResourceType: string; 125 | ResourceProperties: ICustomResourceRequestProps | ICopyWebsiteRequestProps | IPutWebsiteConfigRequestProps | ISolutionLifecycleRequestProps | IConfigureBucketNotificationRequestProps; 126 | } 127 | 128 | /** 129 | * Returned from custom resource handler methods representing both the Status 130 | * and any corresponding data to include in the response. 131 | */ 132 | export interface ICompletionStatus { 133 | Status: StatusTypes 134 | Data: any 135 | } 136 | 137 | /** 138 | * The Lambda function context 139 | */ 140 | export interface ILambdaContext { 141 | getRemainingTimeInMillis: Function; 142 | functionName: string; 143 | functionVersion: string; 144 | invokedFunctionArn: string; 145 | memoryLimitInMB: number; 146 | awsRequestId: string; 147 | logGroupName: string; 148 | logStreamName: string; 149 | identity: any; 150 | clientContext: any; 151 | callbackWaitsForEmptyEventLoop: boolean; 152 | } 153 | -------------------------------------------------------------------------------- /source/solution-helper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solution-helper", 3 | "version": "3.0.6", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions" 7 | }, 8 | "description": "CloudFormation Custom Resources for use during solution lifecycle events", 9 | "main": "index.js", 10 | "dependencies": { 11 | "axios": "~1.3.5", 12 | "uuid": "^9.0.0", 13 | "aws-sdk": "2.1354.0" 14 | }, 15 | "engines": { 16 | "node": ">=18.0.0" 17 | }, 18 | "devDependencies": { 19 | "@types/jest": "^29.5.0", 20 | "@types/node": "^18.15.11", 21 | "@types/uuid": "^9.0.1", 22 | "jest": "^29.5.0", 23 | "ts-jest": "^29.1.0", 24 | "ts-node": "^10.9.1", 25 | "typescript": "^5.0.3" 26 | }, 27 | "scripts": { 28 | "clean": "rm -rf node_modules/ dist/ coverage/ package-lock.json", 29 | "prebuild": "npm run clean && npm install", 30 | "build": "tsc --build tsconfig.json", 31 | "package": "npm run prebuild && npm run build && npm prune --omit=dev && rsync -avrq ./node_modules ./dist && npm run include-solution-utils && npm run package:zip", 32 | "package:zip": "cd dist && zip -q -r9 ./package.zip * && cd ..", 33 | "pretest": "npm run clean && npm install", 34 | "test": "jest --coverage --silent", 35 | "include-solution-utils": "npm run solution-utils:prep && npm run solution-utils:package", 36 | "solution-utils:prep": "rm -rf dist/solution-utils && mkdir dist/solution-utils", 37 | "solution-utils:package": "cd ../solution-utils && npm run package && cd dist/ && rsync -avrq . ../../$npm_package_name/dist/solution-utils/ && cd ../../$npm_package_name" 38 | }, 39 | "license": "Apache-2.0" 40 | } -------------------------------------------------------------------------------- /source/solution-helper/test/setJestEnvironmentVariables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env.AWS_REGION = 'mock-region-1'; 5 | process.env.RETRY_SECONDS = '0.01'; 6 | process.env.ANONYMOUS_DATA_UUID = 'mock-metrics-uuid'; 7 | process.env.SOLUTION_ID = 'mock-solution-id'; 8 | process.env.SOLUTION_VERSION = 'mock-solution-version'; 9 | process.env.SEND_ANONYMOUS_DATA = 'Yes'; 10 | process.env.AWS_LAMBDA_FUNCTION_NAME = 'solution-helper-fn-name'; 11 | process.env.LOGGING_LEVEL = 'VERBOSE'; 12 | -------------------------------------------------------------------------------- /source/solution-helper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "types": [ 12 | "node", 13 | "@types/jest" 14 | ] 15 | }, 16 | "include": [ 17 | "**/*.ts" 18 | ], 19 | "exclude": [ 20 | "package", 21 | "dist", 22 | "**/*.map" 23 | ] 24 | } -------------------------------------------------------------------------------- /source/solution-utils/get-options.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | /** 4 | * If the SOLUTION_ID and SOLUTION_VERSION environment variables are set, this will return 5 | * an object with a custom user agent string. Otherwise, the object returned will be empty 6 | * @returns {object} Either object with `customUserAgent` string or an empty object 7 | */ 8 | export function getOptions(options: any = {}): any { 9 | const { SOLUTION_ID, SOLUTION_VERSION } = process.env; 10 | if (SOLUTION_ID && SOLUTION_VERSION) { 11 | if (SOLUTION_ID.trim() !== '' && SOLUTION_VERSION.trim() !== '') { 12 | options.customUserAgent = `AwsSolution/${SOLUTION_ID}/${SOLUTION_VERSION}`; 13 | } 14 | } 15 | 16 | return options; 17 | } 18 | -------------------------------------------------------------------------------- /source/solution-utils/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | }, 7 | coverageReporters: [ 8 | 'text', 9 | ['lcov', { 'projectRoot': '../' }] 10 | ], 11 | setupFiles: ['./test/setJestEnvironmentVariables.ts'] 12 | }; 13 | -------------------------------------------------------------------------------- /source/solution-utils/logger.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * @enum The supported logging level 6 | */ 7 | export enum LoggingLevel { 8 | ERROR = 1, 9 | WARN = 2, 10 | INFO = 3, 11 | DEBUG = 4, 12 | VERBOSE = 5 13 | } 14 | 15 | /** 16 | * @class Logger class 17 | */ 18 | export default class Logger { 19 | private readonly name: string; 20 | private readonly loggingLevel: LoggingLevel; 21 | 22 | /** 23 | * Sets up the default properties. 24 | * @param name The logger name which will be shown in the log. 25 | * @param loggingLevel The logging level to show the minimum logs. 26 | */ 27 | constructor(name: string, loggingLevel: string | LoggingLevel) { 28 | this.name = name; 29 | 30 | if (typeof loggingLevel === 'string' || !loggingLevel) { 31 | this.loggingLevel = LoggingLevel[loggingLevel] || LoggingLevel.ERROR; 32 | } else { 33 | this.loggingLevel = loggingLevel; 34 | } 35 | } 36 | 37 | /** 38 | * Logs when the logging level is lower than the default logging level. 39 | * @param loggingLevel The logging level of the log 40 | * @param messages The log messages 41 | */ 42 | public log(loggingLevel: LoggingLevel, ...messages: any[]): void { 43 | if (loggingLevel <= this.loggingLevel) { 44 | this._log(loggingLevel, ...messages); 45 | } 46 | } 47 | 48 | /** 49 | * Logs based on the logging level. 50 | * @param loggingLevel The logging level of the log 51 | * @param messages The log messages 52 | */ 53 | private _log(loggingLevel: LoggingLevel, ...messages: any[]): void { 54 | switch (loggingLevel) { 55 | case LoggingLevel.VERBOSE: 56 | case LoggingLevel.DEBUG: 57 | console.debug(`[${this.name}]`, ...messages); 58 | break; 59 | case LoggingLevel.INFO: 60 | console.info(`[${this.name}]`, ...messages); 61 | break; 62 | case LoggingLevel.WARN: 63 | console.warn(`[${this.name}]`, ...messages); 64 | break; 65 | default: 66 | console.error(`[${this.name}]`, ...messages); 67 | break; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /source/solution-utils/metrics.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import axios, { AxiosRequestConfig } from 'axios'; 5 | import moment from 'moment'; 6 | 7 | const METRICS_ENDPOINT = 'https://metrics.awssolutionsbuilder.com/generic'; 8 | export const METRICS_ENDPOINT_PAGE = 'https://metrics.awssolutionsbuilder.com/page'; 9 | const { ANONYMOUS_DATA_UUID, SOLUTION_ID, SOLUTION_VERSION } = process.env; 10 | 11 | interface IMetricPayload { 12 | Solution: string; 13 | Version: string; 14 | UUID: string; 15 | TimeStamp: string; 16 | Data: any; 17 | } 18 | 19 | export async function sendAnonymousMetric(data: any): Promise { 20 | try { 21 | const payload: IMetricPayload = { 22 | Solution: SOLUTION_ID, 23 | Version: SOLUTION_VERSION, 24 | UUID: ANONYMOUS_DATA_UUID, 25 | TimeStamp: moment.utc().format('YYYY-MM-DD HH:mm:ss.S'), 26 | Data: data 27 | }; 28 | 29 | validatePayload(payload); 30 | const payloadStr = JSON.stringify(payload); 31 | 32 | const config: AxiosRequestConfig = { 33 | headers: { 34 | 'content-type': '', 35 | 'content-length': payloadStr.length 36 | } 37 | }; 38 | 39 | console.log('Sending anonymous metric', payloadStr); 40 | const response = await axios.post(METRICS_ENDPOINT, payloadStr, config); 41 | console.log(`Anonymous metric response: ${response.statusText} (${response.status})`); 42 | } catch (err) { 43 | // Log the error 44 | console.error('Error sending anonymous metric'); 45 | console.error(err); 46 | } 47 | } 48 | 49 | export function validatePayload(payload: IMetricPayload): void { 50 | if (!payload.Solution || payload.Solution.trim() === '') { throw new Error('Solution ID was not supplied'); } 51 | if (!payload.Version || payload.Version.trim() === '') { throw new Error('Solution version was not supplied'); } 52 | if (!payload.TimeStamp || payload.TimeStamp.trim() === '') { throw new Error('TimeStamp was not supplied'); } 53 | if (!payload.UUID || payload.UUID.trim() === '') { throw new Error('Anonymous UUID was not supplied'); } 54 | if (typeof payload.Data !== 'object') { throw new Error('Data was not an object'); } 55 | if (payload.Data === null) { throw new Error('Data was not supplied'); } 56 | if (Object.keys(payload.Data).length === 0) { throw new Error('Data was an empty object'); } 57 | } 58 | -------------------------------------------------------------------------------- /source/solution-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solution-utils", 3 | "version": "3.0.6", 4 | "author": { 5 | "name": "Amazon Web Services", 6 | "url": "https://aws.amazon.com/solutions" 7 | }, 8 | "license": "Apache-2.0", 9 | "private": true, 10 | "description": "Utilities to be used within this solution", 11 | "main": "get-options", 12 | "typings": "index", 13 | "scripts": { 14 | "clean": "rm -rf node_modules/ dist/ coverage/ package-lock.json", 15 | "build:tsc": "tsc --project tsconfig.json", 16 | "build": "npm run clean && npm install && npm run build:tsc", 17 | "package": "npm run build && npm prune --production && rsync -avrq ./node_modules ./dist", 18 | "pretest": "npm run clean && npm install", 19 | "test": "jest --coverage --silent" 20 | }, 21 | "engines": { 22 | "node": ">=18.0.0" 23 | }, 24 | "files": [ 25 | "get-options.js", 26 | "logger.js", 27 | "metrics.js" 28 | ], 29 | "devDependencies": { 30 | "@types/jest": "^29.5.0", 31 | "@types/node": "^18.15.11", 32 | "@types/uuid": "^9.0.1", 33 | "jest": "^29.5.0", 34 | "ts-jest": "^29.1.0", 35 | "ts-node": "^10.9.1", 36 | "typescript": "^5.0.3" 37 | }, 38 | "dependencies": { 39 | "axios": "^1.3.5", 40 | "moment": "^2.29.4" 41 | } 42 | } -------------------------------------------------------------------------------- /source/solution-utils/test/get-options.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Spy on the console messages 5 | const consoleLogSpy = jest.spyOn(console, 'log'); 6 | const consoleErrorSpy = jest.spyOn(console, 'error'); 7 | 8 | describe('getOptions', () => { 9 | const OLD_ENV = process.env; 10 | 11 | beforeEach(() => { 12 | process.env = { ...OLD_ENV }; 13 | jest.resetModules(); 14 | consoleLogSpy.mockClear(); 15 | consoleErrorSpy.mockClear(); 16 | }); 17 | 18 | afterEach(() => { 19 | jest.clearAllMocks(); 20 | }); 21 | 22 | afterAll(() => { 23 | process.env = OLD_ENV; 24 | }); 25 | 26 | it('will return an empty object when environment variables are missing', () => { 27 | const { getOptions } = require('../get-options'); 28 | expect.assertions(4); 29 | 30 | process.env.SOLUTION_ID = ' '; // whitespace 31 | expect(getOptions()).toEqual({}); 32 | 33 | delete process.env.SOLUTION_ID; 34 | expect(getOptions()).toEqual({}); 35 | 36 | process.env.SOLUTION_ID = 'foo'; 37 | process.env.SOLUTION_VERSION = ' '; // whitespace 38 | expect(getOptions()).toEqual({}); 39 | 40 | delete process.env.SOLUTION_VERSION; 41 | expect(getOptions()).toEqual({}); 42 | }); 43 | 44 | it('will return an object with the custom user agent string', () => { 45 | const { getOptions } = require('../get-options'); 46 | expect.assertions(1); 47 | expect(getOptions()).toEqual({ customUserAgent: `AwsSolution/solution-id/solution-version` }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /source/solution-utils/test/metrics.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { sendAnonymousMetric } from '../metrics'; 5 | import axios from 'axios'; 6 | 7 | // Mock Axios 8 | jest.mock('axios'); 9 | const mockedAxios = axios as jest.Mocked; 10 | 11 | // Spy on the console messages 12 | const consoleLogSpy = jest.spyOn(console, 'log'); 13 | const consoleErrorSpy = jest.spyOn(console, 'error'); 14 | 15 | describe('sendAnonymousMetric', () => { 16 | const OLD_ENV = process.env; 17 | 18 | beforeEach(() => { 19 | process.env = { ...OLD_ENV }; 20 | jest.resetModules(); 21 | consoleLogSpy.mockClear(); 22 | consoleErrorSpy.mockClear(); 23 | }); 24 | 25 | afterEach(() => { 26 | jest.clearAllMocks(); 27 | }); 28 | 29 | afterAll(() => { 30 | process.env = OLD_ENV; 31 | }); 32 | 33 | test('Valid metric is sent successfully', async () => { 34 | mockedAxios.post.mockResolvedValue({ status: 200, statusText: 'OK' }); 35 | 36 | expect.assertions(7); 37 | const payload = { Event: 'Valid Metric' }; 38 | await expect(sendAnonymousMetric(payload)).resolves.not.toThrow(); 39 | expect(consoleLogSpy).toHaveBeenCalledWith('Sending anonymous metric', expect.stringContaining('"Solution":"solution-id"')); 40 | expect(consoleLogSpy).toHaveBeenCalledWith('Sending anonymous metric', expect.stringContaining('"Version":"solution-version"')); 41 | expect(consoleLogSpy).toHaveBeenCalledWith('Sending anonymous metric', expect.stringContaining('"UUID":"anonymous-uuid"')); 42 | expect(consoleLogSpy).toHaveBeenCalledWith('Sending anonymous metric', expect.stringContaining('"TimeStamp":')); 43 | expect(consoleLogSpy).toHaveBeenCalledWith('Sending anonymous metric', expect.stringContaining('"Data":{"Event":"Valid Metric"')); 44 | expect(consoleLogSpy).toHaveBeenCalledWith('Anonymous metric response: OK (200)'); 45 | }); 46 | 47 | test('Exception is logged but function still returns', async () => { 48 | mockedAxios.post.mockRejectedValue('Error'); 49 | const metrics = require('../metrics'); 50 | 51 | expect.assertions(2); 52 | const payload = { Event: 'Valid Metric' }; 53 | await expect(metrics.sendAnonymousMetric(payload)).resolves.not.toThrow(); 54 | expect(consoleErrorSpy).toHaveBeenCalledWith('Error sending anonymous metric'); 55 | }); 56 | 57 | test('Payload validation - missing anonymous UUID', async () => { 58 | delete process.env.ANONYMOUS_DATA_UUID; 59 | 60 | const metrics = require('../metrics'); 61 | mockedAxios.post.mockResolvedValue({ status: 200, statusText: 'OK' }); 62 | 63 | expect.assertions(3); 64 | const payload = { Event: 'Valid Metric' }; 65 | await expect(metrics.sendAnonymousMetric(payload)).resolves.not.toThrow(); 66 | expect(consoleErrorSpy).toHaveBeenCalledWith('Error sending anonymous metric'); 67 | expect(consoleErrorSpy).toHaveBeenCalledWith(new Error('Anonymous UUID was not supplied')); 68 | }); 69 | 70 | test('Payload validation - solution ID', async () => { 71 | delete process.env.SOLUTION_ID; 72 | 73 | const metrics = require('../metrics'); 74 | mockedAxios.post.mockResolvedValue({ status: 200, statusText: 'OK' }); 75 | 76 | expect.assertions(3); 77 | const payload = { Event: 'Valid Metric' }; 78 | await expect(metrics.sendAnonymousMetric(payload)).resolves.not.toThrow(); 79 | expect(consoleErrorSpy).toHaveBeenCalledWith('Error sending anonymous metric'); 80 | expect(consoleErrorSpy).toHaveBeenCalledWith(new Error('Solution ID was not supplied')); 81 | }); 82 | 83 | test('Payload validation - solution version', async () => { 84 | delete process.env.SOLUTION_VERSION; 85 | 86 | const metrics = require('../metrics'); 87 | mockedAxios.post.mockResolvedValue({ status: 200, statusText: 'OK' }); 88 | 89 | expect.assertions(3); 90 | const payload = { Event: 'Valid Metric' }; 91 | await expect(metrics.sendAnonymousMetric(payload)).resolves.not.toThrow(); 92 | expect(consoleErrorSpy).toHaveBeenCalledWith('Error sending anonymous metric'); 93 | expect(consoleErrorSpy).toHaveBeenCalledWith(new Error('Solution version was not supplied')); 94 | }); 95 | 96 | test('Payload validation - data was not an object', async () => { 97 | const metrics = require('../metrics'); 98 | mockedAxios.post.mockResolvedValue({ status: 200, statusText: 'OK' }); 99 | 100 | expect.assertions(3); 101 | const payload = 'test'; 102 | await expect(metrics.sendAnonymousMetric(payload)).resolves.not.toThrow(); 103 | expect(consoleErrorSpy).toHaveBeenCalledWith('Error sending anonymous metric'); 104 | expect(consoleErrorSpy).toHaveBeenCalledWith(new Error('Data was not an object')); 105 | }); 106 | 107 | test('Payload validation - data was null', async () => { 108 | const metrics = require('../metrics'); 109 | mockedAxios.post.mockResolvedValue({ status: 200, statusText: 'OK' }); 110 | 111 | expect.assertions(3); 112 | const payload = null; 113 | await expect(metrics.sendAnonymousMetric(payload)).resolves.not.toThrow(); 114 | expect(consoleErrorSpy).toHaveBeenCalledWith('Error sending anonymous metric'); 115 | expect(consoleErrorSpy).toHaveBeenCalledWith(new Error('Data was not supplied')); 116 | }); 117 | 118 | test('Payload validation - data was null', async () => { 119 | const metrics = require('../metrics'); 120 | mockedAxios.post.mockResolvedValue({ status: 200, statusText: 'OK' }); 121 | 122 | expect.assertions(3); 123 | const payload = {}; 124 | await expect(metrics.sendAnonymousMetric(payload)).resolves.not.toThrow(); 125 | expect(consoleErrorSpy).toHaveBeenCalledWith('Error sending anonymous metric'); 126 | expect(consoleErrorSpy).toHaveBeenCalledWith(new Error('Data was an empty object')); 127 | }); 128 | 129 | test('Payload validation - Timestamp was not set', async () => { 130 | const metrics = require('../metrics'); 131 | mockedAxios.post.mockResolvedValue({ status: 200, statusText: 'OK' }); 132 | 133 | expect.assertions(1); 134 | try { 135 | const payload = { 136 | Solution: 'id', 137 | Version: 'version', 138 | UUID: 'anonymous-id', 139 | Data: { Event: 'no-timestamp' } 140 | }; 141 | await expect(metrics.validatePayload(payload)).resolves.not.toThrow(); 142 | } catch (err) { 143 | expect(err.message).toBe('TimeStamp was not supplied') 144 | } 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /source/solution-utils/test/setJestEnvironmentVariables.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | process.env.SOLUTION_ID = 'solution-id'; 5 | process.env.SOLUTION_VERSION = 'solution-version'; 6 | process.env.ANONYMOUS_DATA_UUID = 'anonymous-uuid'; 7 | -------------------------------------------------------------------------------- /source/solution-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "emitDecoratorMetadata": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "types": [ 12 | "node", 13 | "@types/jest" 14 | ] 15 | }, 16 | "include": [ 17 | "**/*.ts" 18 | ], 19 | "exclude": [ 20 | "package", 21 | "dist", 22 | "**/*.map" 23 | ] 24 | } --------------------------------------------------------------------------------