├── .dockerignore
├── .editorconfig
├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
└── workflows
│ ├── build.yml
│ ├── codeql.yml
│ ├── dependabot-hook.yml
│ ├── deployment-cleanup-pr.yml
│ ├── deployment-demo-env.yml
│ ├── deployment-test.yml
│ ├── diagram.yml
│ ├── lint.yml
│ ├── playwright.yml
│ └── test.yml
├── .gitignore
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── NOTICE
├── README.md
├── app-deployment-cfn.template.yaml
├── apps
├── client
│ ├── .babelrc
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── README.md
│ ├── index.html
│ ├── lang
│ │ └── en.json
│ ├── package.json
│ ├── service-worker.js
│ ├── src
│ │ ├── auth
│ │ │ ├── auth-service.interface.ts
│ │ │ ├── auth-service.ts
│ │ │ ├── cognito-auth-service.ts
│ │ │ └── edge-auth-service.ts
│ │ ├── constants
│ │ │ ├── format.ts
│ │ │ ├── index.ts
│ │ │ └── time.ts
│ │ ├── data
│ │ │ ├── dashboards.ts
│ │ │ ├── edge-login.ts
│ │ │ ├── migration.ts
│ │ │ └── query-client.ts
│ │ ├── helpers
│ │ │ ├── authMode.ts
│ │ │ ├── dates
│ │ │ │ ├── index.ts
│ │ │ │ └── utcDateString.ts
│ │ │ ├── events.ts
│ │ │ ├── featureFlag
│ │ │ │ ├── featureFlag.spec.ts
│ │ │ │ └── featureFlag.ts
│ │ │ ├── lists
│ │ │ │ ├── index.ts
│ │ │ │ ├── last.ts
│ │ │ │ ├── without-identifiable.ts
│ │ │ │ └── without.ts
│ │ │ ├── meta-tags.spec.ts
│ │ │ ├── meta-tags.ts
│ │ │ ├── predicates.ts
│ │ │ ├── predicates
│ │ │ │ ├── is-api-error.ts
│ │ │ │ ├── is-fatal.ts
│ │ │ │ ├── is-just.spec-d.ts
│ │ │ │ ├── is-just.spec.ts
│ │ │ │ ├── is-just.ts
│ │ │ │ ├── is-not-fatal.ts
│ │ │ │ ├── is-nothing.spec-d.ts
│ │ │ │ ├── is-nothing.spec.ts
│ │ │ │ └── is-nothing.ts
│ │ │ ├── strings
│ │ │ │ └── is-string-with-value.ts
│ │ │ └── tests
│ │ │ │ ├── error-boundary-tester.tsx
│ │ │ │ └── testing-library.tsx
│ │ ├── hooks
│ │ │ ├── application
│ │ │ │ ├── use-application.ts
│ │ │ │ ├── use-detect-401-unauthorized.spec.ts
│ │ │ │ └── use-detect-401-unauthorized.ts
│ │ │ ├── dashboard
│ │ │ │ ├── use-displaySettings.ts
│ │ │ │ └── use-viewport.ts
│ │ │ └── notifications
│ │ │ │ ├── use-dismiss-all-notifications.ts
│ │ │ │ ├── use-emit-notification.ts
│ │ │ │ └── use-notifications.ts
│ │ ├── index.tsx
│ │ ├── initialize-auth-dependents.tsx
│ │ ├── layout
│ │ │ ├── components
│ │ │ │ ├── breadcrumbs
│ │ │ │ │ ├── breadcrumbs.tsx
│ │ │ │ │ ├── hooks
│ │ │ │ │ │ └── use-crumbs.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── footer
│ │ │ │ │ ├── footer.css
│ │ │ │ │ ├── footer.spec.tsx
│ │ │ │ │ ├── footer.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── notifications
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── notifications.spec.tsx
│ │ │ │ │ └── notifications.tsx
│ │ │ │ └── top-navigation
│ │ │ │ │ ├── components
│ │ │ │ │ └── settings-modal
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ ├── content-density-toggle.tsx
│ │ │ │ │ │ └── settings-modal-footer.tsx
│ │ │ │ │ │ ├── helpers
│ │ │ │ │ │ └── is-comfortable.ts
│ │ │ │ │ │ ├── hooks
│ │ │ │ │ │ ├── use-density-toggle.ts
│ │ │ │ │ │ └── use-density.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── settings-modal.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── top-navigation.tsx
│ │ │ ├── hooks
│ │ │ │ ├── use-format.ts
│ │ │ │ ├── use-full-width.spec.ts
│ │ │ │ └── use-full-width.ts
│ │ │ └── layout.tsx
│ │ ├── logging
│ │ │ ├── cloud-watch-logger.spec.ts
│ │ │ └── cloud-watch-logger.ts
│ │ ├── metrics
│ │ │ ├── cloud-watch-metrics-recorder.spec.ts
│ │ │ ├── cloud-watch-metrics-recorder.ts
│ │ │ ├── metric-handler.spec.ts
│ │ │ └── metric-handler.ts
│ │ ├── register-loggers.ts
│ │ ├── register-metrics-recorder.ts
│ │ ├── register-service-worker.ts
│ │ ├── router.tsx
│ │ ├── routes
│ │ │ ├── dashboards
│ │ │ │ ├── create
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── create-dashboard-form-actions.tsx
│ │ │ │ │ │ ├── dashboard-description-field.tsx
│ │ │ │ │ │ └── dashboard-name-field.tsx
│ │ │ │ │ ├── create-dashboard-page.tsx
│ │ │ │ │ ├── create-dashboard-route.tsx
│ │ │ │ │ ├── hooks
│ │ │ │ │ │ ├── use-create-dashboard-form.ts
│ │ │ │ │ │ └── use-create-dashboard-mutation.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── types
│ │ │ │ │ │ └── create-dashboard-form-values.ts
│ │ │ │ ├── dashboard
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── dashboard-error-boundary.spec.tsx
│ │ │ │ │ │ ├── dashboard-error-boundary.tsx
│ │ │ │ │ │ ├── dashboard-loading-state.spec.tsx
│ │ │ │ │ │ └── dashboard-loading-state.tsx
│ │ │ │ │ ├── dashboard-configuration.ts
│ │ │ │ │ ├── dashboard-page.tsx
│ │ │ │ │ ├── dashboard-route.tsx
│ │ │ │ │ ├── hooks
│ │ │ │ │ │ ├── use-dashboard-query.ts
│ │ │ │ │ │ └── use-update-dashboard-mutation.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── styles.css
│ │ │ │ ├── dashboards-index
│ │ │ │ │ ├── assets
│ │ │ │ │ │ ├── AssetsSearch.svg
│ │ │ │ │ │ ├── Dashboard.svg
│ │ │ │ │ │ └── Widget.svg
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── delete-dashboard-modal.spec.tsx
│ │ │ │ │ │ ├── delete-dashboard-modal.tsx
│ │ │ │ │ │ ├── empty-dashboards-table.tsx
│ │ │ │ │ │ ├── getting-started.tsx
│ │ │ │ │ │ ├── migration.spec.tsx
│ │ │ │ │ │ ├── migration.tsx
│ │ │ │ │ │ ├── no-matches-dashboards-table.tsx
│ │ │ │ │ │ ├── styles.css
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── constants
│ │ │ │ │ │ └── text.ts
│ │ │ │ │ ├── dashboards-index-page.spec.tsx
│ │ │ │ │ ├── dashboards-index-page.tsx
│ │ │ │ │ ├── dashboards-index-route.tsx
│ │ │ │ │ ├── hooks
│ │ │ │ │ │ ├── use-dashboards-query.ts
│ │ │ │ │ │ ├── use-delete-dashboard-mutation.ts
│ │ │ │ │ │ ├── use-delete-modal-visibility.ts
│ │ │ │ │ │ ├── use-migration-query.ts
│ │ │ │ │ │ ├── use-migration-status-query.ts
│ │ │ │ │ │ ├── use-partial-update-dashboard-mutation.ts
│ │ │ │ │ │ └── use-table-preferences.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── styles.css
│ │ │ │ └── root-dashboards
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── root-dashboards-page.tsx
│ │ │ │ │ └── root-dashboards-route.tsx
│ │ │ ├── edge-login
│ │ │ │ ├── components
│ │ │ │ │ ├── edge-mechanism-field.tsx
│ │ │ │ │ ├── edge-password-field.tsx
│ │ │ │ │ └── edge-username-field.tsx
│ │ │ │ ├── edge-login-page.spec.tsx
│ │ │ │ ├── edge-login-page.tsx
│ │ │ │ ├── edge-login-route.tsx
│ │ │ │ └── hooks
│ │ │ │ │ ├── use-edge-login-form.ts
│ │ │ │ │ └── use-edge-login-query.ts
│ │ │ ├── root-index
│ │ │ │ ├── index.ts
│ │ │ │ └── root-index-route.tsx
│ │ │ └── root
│ │ │ │ ├── components
│ │ │ │ ├── root-error-boundary.spec.tsx
│ │ │ │ └── root-error-boundary.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── root-error-page.spec.tsx
│ │ │ │ ├── root-error-page.tsx
│ │ │ │ ├── root-page.spec.tsx
│ │ │ │ ├── root-page.tsx
│ │ │ │ └── root-route.tsx
│ │ ├── services
│ │ │ ├── generated
│ │ │ │ ├── core
│ │ │ │ │ ├── ApiError.ts
│ │ │ │ │ ├── ApiRequestOptions.ts
│ │ │ │ │ ├── ApiResult.ts
│ │ │ │ │ ├── CancelablePromise.ts
│ │ │ │ │ ├── OpenAPI.ts
│ │ │ │ │ └── request.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── BulkDeleteDashboardDto.ts
│ │ │ │ │ ├── CreateDashboardDto.ts
│ │ │ │ │ ├── Dashboard.ts
│ │ │ │ │ ├── DashboardDefinition.ts
│ │ │ │ │ ├── DashboardSummary.ts
│ │ │ │ │ ├── DashboardWidget.ts
│ │ │ │ │ ├── EdgeCredentials.ts
│ │ │ │ │ ├── EdgeLoginBody.ts
│ │ │ │ │ ├── MigrationStatus.ts
│ │ │ │ │ └── UpdateDashboardDto.ts
│ │ │ │ ├── schemas
│ │ │ │ │ ├── $BulkDeleteDashboardDto.ts
│ │ │ │ │ ├── $CreateDashboardDto.ts
│ │ │ │ │ ├── $Dashboard.ts
│ │ │ │ │ ├── $DashboardDefinition.ts
│ │ │ │ │ ├── $DashboardSummary.ts
│ │ │ │ │ ├── $DashboardWidget.ts
│ │ │ │ │ └── $UpdateDashboardDto.ts
│ │ │ │ └── services
│ │ │ │ │ ├── DashboardsService.ts
│ │ │ │ │ ├── DefaultService.ts
│ │ │ │ │ ├── EdgeLoginService.ts
│ │ │ │ │ └── MigrationService.ts
│ │ │ ├── index.ts
│ │ │ ├── intl.ts
│ │ │ ├── local-storage-dashboard
│ │ │ │ └── dashboard-service.ts
│ │ │ └── types.ts
│ │ ├── store
│ │ │ ├── navigation
│ │ │ │ └── index.ts
│ │ │ ├── notifications
│ │ │ │ └── index.ts
│ │ │ └── viewMode
│ │ │ │ └── index.ts
│ │ ├── structures
│ │ │ └── notifications
│ │ │ │ ├── error-notification.spec.ts
│ │ │ │ ├── error-notification.ts
│ │ │ │ ├── generic-error-notification.spec.ts
│ │ │ │ ├── generic-error-notification.ts
│ │ │ │ ├── info-notification.spec.ts
│ │ │ │ ├── info-notification.ts
│ │ │ │ ├── loading-notification.spec.ts
│ │ │ │ ├── loading-notification.ts
│ │ │ │ ├── notification.spec.ts
│ │ │ │ ├── notification.ts
│ │ │ │ ├── success-notification.spec.ts
│ │ │ │ └── success-notification.ts
│ │ ├── styles
│ │ │ └── variables.css
│ │ ├── test
│ │ │ └── setup.ts
│ │ └── types
│ │ │ ├── content-density.ts
│ │ │ ├── crumb.ts
│ │ │ ├── data.ts
│ │ │ ├── dimensional.ts
│ │ │ ├── fatal-status-code.ts
│ │ │ ├── format.ts
│ │ │ ├── handle.ts
│ │ │ ├── href.ts
│ │ │ ├── identifiable.ts
│ │ │ ├── index.ts
│ │ │ ├── lists.ts
│ │ │ ├── maybe.ts
│ │ │ ├── metadata.ts
│ │ │ ├── notification-view-model.ts
│ │ │ └── predicates.ts
│ ├── tsconfig.json
│ ├── vite-env.d.ts
│ ├── vite.config.ts
│ └── vitest.config.ts
└── core
│ ├── .cognito
│ ├── README.md
│ ├── config.json
│ └── db
│ │ ├── clients.json
│ │ └── us-west-2_h23TJjQR9.json
│ ├── .env
│ ├── .eslintrc.js
│ ├── .example.edge.env
│ ├── README.md
│ ├── api-resource-table-properties.js
│ ├── dynamodb-local-dir.js
│ ├── jest-dynamodb-config.js
│ ├── jest.config.ts
│ ├── nest-cli.json
│ ├── package.json
│ ├── src
│ ├── app.module.ts
│ ├── auth
│ │ ├── cognito-jwt-auth.guard.spec.ts
│ │ ├── cognito-jwt-auth.guard.ts
│ │ └── public.decorator.ts
│ ├── bootstrap
│ │ ├── docs.bootstrap.ts
│ │ ├── hmr.bootstrap.ts
│ │ ├── index.ts
│ │ ├── logger.bootstrap.ts
│ │ ├── mvc.bootstrap.ts
│ │ ├── security.bootstrap.e2e.spec.ts
│ │ ├── security.bootstrap.ts
│ │ ├── server.bootstrap.ts
│ │ └── validation.bootstrap.ts
│ ├── config
│ │ ├── auth.config.spec.ts
│ │ ├── auth.config.ts
│ │ ├── database.config.spec.ts
│ │ ├── database.config.ts
│ │ ├── edge.config.ts
│ │ ├── environment.ts
│ │ ├── global.config.spec.ts
│ │ ├── global.config.ts
│ │ ├── jwt.config.spec.ts
│ │ ├── jwt.config.ts
│ │ └── local-cognito-jwt-verifier.ts
│ ├── dashboards
│ │ ├── README.md
│ │ ├── dashboard.constants.ts
│ │ ├── dashboards.controller.ts
│ │ ├── dashboards.e2e.spec.ts
│ │ ├── dashboards.module.ts
│ │ ├── dashboards.repository.ts
│ │ ├── dashboards.service.ts
│ │ ├── dto
│ │ │ ├── bulk-delete-dashboards.dto.ts
│ │ │ ├── create-dashboard.dto.ts
│ │ │ └── update-dashboard.dto.ts
│ │ ├── entities
│ │ │ ├── dashboard-definition.entity.ts
│ │ │ ├── dashboard-summary.entity.ts
│ │ │ ├── dashboard-widget.entity.ts
│ │ │ └── dashboard.entity.ts
│ │ └── params
│ │ │ ├── delete-dashboard.params.ts
│ │ │ ├── read-dashboard.params.ts
│ │ │ └── update-dashboard.params.ts
│ ├── edge-login
│ │ ├── README.md
│ │ ├── edge-login.controller.ts
│ │ ├── edge-login.e2e.spec.ts
│ │ ├── edge-login.module.ts
│ │ ├── edge-login.service.ts
│ │ └── entities
│ │ │ ├── edge-credentials.entity.ts
│ │ │ └── edge-login-body.entity.ts
│ ├── health
│ │ ├── README.md
│ │ ├── health.controller.spec.ts
│ │ ├── health.controller.ts
│ │ ├── health.module.ts
│ │ └── indicators
│ │ │ ├── dynamodb.health.spec.ts
│ │ │ └── dynamodb.health.ts
│ ├── lifecycle-hooks
│ │ └── dynamodb-local-setup.ts
│ ├── logging
│ │ ├── README.md
│ │ ├── dev-logger.provider.ts
│ │ ├── logger.constants.ts
│ │ ├── logger.module.ts
│ │ ├── pino-http.configs.ts
│ │ ├── prod-logger.provider.ts
│ │ └── prod-logger.service.ts
│ ├── main.ts
│ ├── migration
│ │ ├── README.md
│ │ ├── entities
│ │ │ └── migration-status.entity.ts
│ │ ├── migration.controller.ts
│ │ ├── migration.e2e.spec.ts
│ │ ├── migration.module.ts
│ │ ├── service
│ │ │ ├── convert-monitor-to-app-definition.spec.ts
│ │ │ ├── convert-monitor-to-app-definition.ts
│ │ │ ├── migration.service.ts
│ │ │ └── monitor-dashboard-definition.ts
│ │ └── util
│ │ │ └── colorPalette.ts
│ ├── mvc
│ │ ├── mvc.controller.ts
│ │ └── mvc.module.ts
│ ├── repl.ts
│ ├── testing
│ │ ├── aws-configuration.ts
│ │ └── jwt-generator.ts
│ └── types
│ │ ├── environment.ts
│ │ ├── index.ts
│ │ └── strings
│ │ └── is-string-with-value.ts
│ ├── tsconfig.build.json
│ ├── tsconfig.json
│ └── webpack-hmr.config.js
├── cdk
├── .eslintrc.js
├── README.md
├── bin
│ └── cdk.ts
├── cdk.json
├── lib
│ ├── auth
│ │ ├── auth-stack.ts
│ │ └── sso-auth-stack.ts
│ ├── core
│ │ ├── core-service-web-acl.ts
│ │ ├── core-service.ts
│ │ └── core-stack.ts
│ ├── csp
│ │ └── public-asset-directives.ts
│ ├── database
│ │ └── database-stack.ts
│ ├── endpoints
│ │ └── aws-endpoints.ts
│ ├── iot-application-stack.ts
│ └── logging
│ │ └── logging-stack.ts
├── package.json
└── tsconfig.json
├── deployCdk.js
├── deploymentguide
└── README.md
├── developmentguide
└── README.md
├── install-deploy-unix.sh
├── install-prepreqs-unix.sh
├── package.json
├── packages
├── eslint-config-custom
│ ├── index.js
│ └── package.json
├── jest-config
│ ├── base.ts
│ └── package.json
└── tsconfig
│ ├── base.json
│ └── package.json
├── playwright.config.ts
├── playwright
└── .auth
│ └── user.json
├── tests
├── auth.setup.ts
├── create-dashboard-page.spec.ts
├── create-dashboard-page.spec.ts-snapshots
│ ├── empty-page-chromium-darwin.png
│ ├── empty-page-firefox-darwin.png
│ ├── empty-page-webkit-darwin.png
│ ├── filled-fields-chromium-darwin.png
│ ├── filled-fields-firefox-darwin.png
│ ├── filled-fields-webkit-darwin.png
│ ├── max-length-errors-chromium-darwin
│ ├── max-length-errors-chromium-darwin.png
│ ├── max-length-errors-firefox-darwin
│ ├── max-length-errors-firefox-darwin.png
│ ├── max-length-errors-webkit-darwin
│ ├── max-length-errors-webkit-darwin.png
│ ├── required-field-errors-chromium-darwin
│ ├── required-field-errors-chromium-darwin.png
│ ├── required-field-errors-firefox-darwin
│ ├── required-field-errors-firefox-darwin.png
│ ├── required-field-errors-webkit-darwin
│ └── required-field-errors-webkit-darwin.png
├── dashboard-list-page.spec.ts
├── dashboard-list-page.spec.ts-snapshots
│ ├── dashboard-list-page-accessibility-scan-results-chromium-darwin
│ ├── dashboard-list-page-accessibility-scan-results-firefox-darwin
│ └── dashboard-list-page-accessibility-scan-results-webkit-darwin
├── dashboard-page.spec.ts
├── dashboard-page.spec.ts-snapshots
│ ├── dashboard-page-chromium-darwin.png
│ ├── dashboard-page-firefox-darwin.png
│ └── dashboard-page-webkit-darwin.png
├── dashboards
│ ├── dashboard-management.spec.ts
│ └── dashboard-management.spec.ts-snapshots
│ │ ├── create-dashboard-page-accessibility-scan-results-chromium-darwin
│ │ ├── create-dashboard-page-accessibility-scan-results-firefox-darwin
│ │ ├── create-dashboard-page-accessibility-scan-results-webkit-darwin
│ │ ├── dashboard-page-accessibility-scan-results-chromium-darwin
│ │ ├── dashboard-page-accessibility-scan-results-firefox-darwin
│ │ ├── dashboard-page-accessibility-scan-results-webkit-darwin
│ │ ├── dashboards-page-accessibility-scan-results-chromium-darwin
│ │ ├── dashboards-page-accessibility-scan-results-firefox-darwin
│ │ └── dashboards-page-accessibility-scan-results-webkit-darwin
├── functional-homepage-iot-application
│ └── add-footer-throughout.spec.ts
├── helpers.ts
├── pages
│ ├── application-frame.page.ts
│ ├── create-dashboard.page.ts
│ ├── dashboard-page.page.ts
│ └── dashboards-index.page.ts
└── settings.spec.ts
├── tsconfig.json
├── turbo.json
├── userguide
├── README.md
└── imgs
│ ├── app-sign-in-screen.png
│ ├── app-sign-out-screen.png
│ ├── dashboard-creation-screen.png
│ ├── dashboard-edit-screen.png
│ ├── dashboard-screen.png
│ ├── dashboards-list-screen.png
│ ├── user-pool-resource-on-cfn.png
│ └── users-tab-on-cognito.png
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | **/logs
3 | **/*.log
4 | **/npm-debug.log*
5 | **/yarn-debug.log*
6 | **/yarn-error.log*
7 | **/pnpm-debug.log*
8 | **/lerna-debug.log*
9 |
10 | **/node_modules
11 | **/build
12 | **/dist
13 | **/dist-ssr
14 | **/*.local
15 | **/*.tsbuildinfo
16 | **/.turbo
17 | **/.nestjs_repl_history
18 | **/Dockerfile
19 | **/.dockerignore
20 | cdk/**
21 | !cdk/package.json
22 |
23 | # Editor directories and files
24 | **/.vscode/*
25 | **/!.vscode/extensions.json
26 | **/.idea
27 | **/.DS_Store
28 | **/*.suo
29 | **/*.ntvs*
30 | **/*.njsproj
31 | **/*.sln
32 | **/*.sw?
33 | **/test-results/
34 | **/playwright-report/
35 | **/playwright/.cache/
36 | **/coverage
37 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | insert_final_newline = false
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | node: true,
4 | },
5 | extends: ['custom', 'plugin:playwright/playwright-test'],
6 | parserOptions: {
7 | ecmaVersion: 2020,
8 | project: 'tsconfig.json',
9 | tsconfigRootDir: __dirname,
10 | sourceType: 'module',
11 | ecmaFeatures: {
12 | jsx: false,
13 | },
14 | },
15 | root: true,
16 | };
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 |
28 | - OS: [e.g. iOS]
29 | - Browser [e.g. chrome, safari]
30 | - Version [e.g. 22]
31 |
32 | **Smartphone (please complete the following information):**
33 |
34 | - Device: [e.g. iPhone6]
35 | - OS: [e.g. iOS8.1]
36 | - Browser [e.g. stock browser, safari]
37 | - Version [e.g. 22]
38 |
39 | **Additional context**
40 | Add any other context about the problem here.
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Description
2 |
3 | Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
4 |
5 | Fixes # (issue)
6 |
7 | ## Type of change
8 |
9 | Please delete options that are not relevant.
10 |
11 | - [ ] Bug fix (non-breaking change which fixes an issue)
12 | - [ ] New feature (non-breaking change which adds functionality)
13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
14 | - [ ] This change requires a documentation update
15 |
16 | # How Has This Been Tested?
17 |
18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
19 |
20 | - [ ] Test A
21 | - [ ] Test B
22 |
23 | # Checklist:
24 |
25 | - [ ] My code follows the style guidelines of this project
26 | - [ ] I have performed a self-review of my code
27 | - [ ] I have commented my code, particularly in hard-to-understand areas
28 | - [ ] I have made corresponding changes to the documentation
29 | - [ ] I have added tests that prove my fix is effective or that my feature works
30 | - [ ] New and existing unit tests pass locally with my changes
31 |
32 | ## Legal
33 |
34 | This project is available under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0.html).
35 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Please see the documentation for all configuration options:
2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
3 |
4 | version: 2
5 | updates:
6 | - package-ecosystem: npm
7 | directory: /
8 | versioning-strategy: increase
9 | schedule:
10 | interval: weekly
11 | target-branch: rc
12 | open-pull-requests-limit: 10
13 | groups:
14 | aws-sdk:
15 | patterns:
16 | - '@aws-sdk*'
17 | tanstack:
18 | patterns:
19 | - '@tanstack*'
20 | - package-ecosystem: github-actions
21 | directory: /
22 | schedule:
23 | interval: weekly
24 | target-branch: rc
25 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | run-name: Build action initiated by ${{ github.actor }}
4 |
5 | on:
6 | push:
7 | branches: [main, rc]
8 | pull_request:
9 | branches: [main, rc]
10 |
11 | jobs:
12 | build:
13 | timeout-minutes: 15
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout 🛎️
18 | uses: actions/checkout@v4
19 | with:
20 | fetch-depth: 2
21 |
22 | - name: Install Java 🔧
23 | uses: actions/setup-java@v4
24 | with:
25 | distribution: corretto
26 | java-version: 17
27 |
28 | - name: Install Node.js 🔧
29 | uses: actions/setup-node@v4
30 | with:
31 | node-version: 18
32 | cache: yarn
33 |
34 | - name: Install Dependencies 🔩
35 | run: yarn install --frozen-lockfile
36 |
37 | - name: Build Application 👷
38 | env:
39 | NODE_OPTIONS: '--max_old_space_size=4096'
40 | run: yarn build
41 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: CodeQL
2 |
3 | run-name: CodeQL action initiated by ${{ github.actor }}
4 |
5 | on:
6 | push:
7 | branches: [main, rc]
8 | pull_request:
9 | branches: [main, rc]
10 | schedule:
11 | - cron: '26 3 * * 0'
12 |
13 | jobs:
14 | analyze:
15 | timeout-minutes: 10
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - name: Checkout 🛎️
20 | uses: actions/checkout@v4
21 | with:
22 | fetch-depth: 2
23 |
24 | - name: Initialize CodeQL 🫣
25 | uses: github/codeql-action/init@v3
26 | with:
27 | languages: javascript
28 |
29 | - name: Autobuild 🤖
30 | uses: github/codeql-action/autobuild@v3
31 |
32 | - name: Perform CodeQL Analysis 🔬
33 | uses: github/codeql-action/analyze@v3
34 |
--------------------------------------------------------------------------------
/.github/workflows/diagram.yml:
--------------------------------------------------------------------------------
1 | name: Create Diagram
2 |
3 | run-name: Create Diagram action initiated by ${{ github.actor }}
4 |
5 | on:
6 | push:
7 | branches: main
8 |
9 | jobs:
10 | diagram:
11 | timeout-minutes: 10
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout 🛎️
16 | uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 2
19 |
20 | - name: Create Diagram 🎨
21 | uses: githubocto/repo-visualizer@0.9.1
22 | with:
23 | artifact_name: diagram
24 | should_push: false
25 | output_file: diagram.svg
26 | excluded_paths: 'dist,node_modules,ignore,.github'
27 |
28 | # retrieve diagram from workflow output
29 | # diagram in README.md is updated manually
30 | - name: Download Diagram 📝
31 | uses: actions/download-artifact@v4
32 | with:
33 | name: diagram
34 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | run-name: Lint action initiated by ${{ github.actor }}
4 |
5 | on:
6 | push:
7 | branches: [main, rc]
8 | pull_request:
9 | branches: [main, rc]
10 |
11 | jobs:
12 | lint:
13 | timeout-minutes: 20
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout 🛎️
18 | uses: actions/checkout@v4
19 | with:
20 | fetch-depth: 2
21 |
22 | - name: Install Java 🔧
23 | uses: actions/setup-java@v4
24 | with:
25 | distribution: corretto
26 | java-version: 17
27 |
28 | - name: Install Node.js 🔧
29 | uses: actions/setup-node@v4
30 | with:
31 | node-version: 18
32 | cache: yarn
33 |
34 | - name: Install Dependencies 🔩
35 | run: yarn install --frozen-lockfile
36 |
37 | - name: Lint Application 🐰
38 | run: yarn lint
39 |
--------------------------------------------------------------------------------
/.github/workflows/playwright.yml:
--------------------------------------------------------------------------------
1 | name: Playwright Test
2 |
3 | run-name: Playwright Test action initiated by ${{ github.actor }}
4 |
5 | on:
6 | push:
7 | branches: [main, rc]
8 | pull_request:
9 | branches: [main, rc]
10 |
11 | jobs:
12 | playwright:
13 | timeout-minutes: 30
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout 🛎️
18 | uses: actions/checkout@v4
19 | with:
20 | fetch-depth: 2
21 |
22 | - name: Install Java 🔧
23 | uses: actions/setup-java@v4
24 | with:
25 | distribution: corretto
26 | java-version: 17
27 |
28 | - name: Install Node.js 🔧
29 | uses: actions/setup-node@v4
30 | with:
31 | node-version: 18
32 | cache: yarn
33 |
34 | - name: Install Dependencies 🔩
35 | run: yarn install --frozen-lockfile
36 |
37 | - name: Install Playwright Browsers 🔩
38 | run: yarn playwright install --with-deps
39 |
40 | - name: Build packages
41 | run: yarn build
42 |
43 | - name: Run Playwright tests 🧑🔬
44 | run: yarn playwright test
45 |
46 | - name: Store Test Artifacts on Failure 🥲
47 | if: failure()
48 | uses: actions/upload-artifact@v4
49 | with:
50 | name: playwright-report
51 | path: playwright-report/
52 | retention-days: 30
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | cdk.out
11 | node_modules
12 | build
13 | dist
14 | dist-ssr
15 | *.local
16 | *.tsbuildinfo
17 | .turbo
18 | .nestjs_repl_history
19 |
20 | # Editor directories and files
21 | .vscode/*
22 | !.vscode/extensions.json
23 | .idea
24 | .DS_Store
25 | *.suo
26 | *.ntvs*
27 | *.njsproj
28 | *.sln
29 | *.sw?
30 | /test-results/
31 | /playwright-report/
32 | /playwright/.cache/
33 | /playwright/.auth/
34 | coverage
35 | /.cognito/db/*
36 | /apps/client/html/*
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
5 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 |
3 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
4 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
5 | opensource-codeofconduct@amazon.com with any additional questions or comments.
6 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 |
--------------------------------------------------------------------------------
/apps/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | [
4 | "formatjs",
5 | {
6 | "idInterpolationPattern": "[sha512:contenthash:base64:6]",
7 | "ast": true
8 | }
9 | ]
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/apps/client/.eslintignore:
--------------------------------------------------------------------------------
1 | src/services/generated/*
2 |
--------------------------------------------------------------------------------
/apps/client/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | },
5 | extends: [
6 | 'custom',
7 | 'plugin:@tanstack/eslint-plugin-query/recommended',
8 | 'plugin:testing-library/react',
9 | 'plugin:jest-dom/recommended',
10 | 'plugin:react/recommended',
11 | 'plugin:react/jsx-runtime',
12 | 'plugin:react-hooks/recommended',
13 | 'plugin:jsx-a11y/recommended',
14 | ],
15 | parserOptions: {
16 | ecmaVersion: 2020,
17 | project: 'tsconfig.json',
18 | tsconfigRootDir: __dirname,
19 | sourceType: 'module',
20 | ecmaFeatures: {
21 | jsx: true,
22 | },
23 | },
24 | // https://formatjs.io/docs/tooling/linter
25 | plugins: [
26 | 'formatjs',
27 | '@tanstack/query',
28 | 'testing-library',
29 | 'jest-dom',
30 | 'react',
31 | 'react-hooks',
32 | 'jsx-a11y',
33 | ],
34 | rules: {
35 | 'formatjs/no-offset': 'error',
36 | '@typescript-eslint/no-throw-literal': 'off',
37 | },
38 | root: true,
39 | settings: {
40 | react: {
41 | version: 'detect',
42 | },
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/apps/client/README.md:
--------------------------------------------------------------------------------
1 | # Client
2 |
3 | # Guidelines
4 |
5 | Please follow Cloudscape best practices https://cloudscape.design/
6 |
7 | # Authentication
8 |
9 | After creating the CFN stack create a user in the user pool, so you can log into the application.
10 |
11 | https://docs.aws.amazon.com/cognito/latest/developerguide/how-to-create-user-accounts.html
12 |
--------------------------------------------------------------------------------
/apps/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | IoT Application
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/apps/client/lang/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "KBBRBq": {
3 | "defaultMessage": "Dashboards",
4 | "description": "dashboards list header"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/apps/client/service-worker.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file will not automatically update when you change it in development.
3 | * Restart the development server to see changes.
4 | */
5 |
6 | self.addEventListener('install', () => {
7 | self.skipWaiting();
8 | });
9 |
10 | self.addEventListener('activate', (event) => {
11 | event.waitUntil(self.clients.claim());
12 | });
13 |
14 | // listen for 401s to tell the client to redirect to login
15 | self.addEventListener('fetch', (event) => {
16 | event.respondWith(
17 | fetch(event.request).then((response) => {
18 | if (response.status === 401) {
19 | self.clients.matchAll().then((clients) => {
20 | clients.forEach((client) => {
21 | client.postMessage({
22 | type: '401_UNAUTHORIZED'
23 | });
24 | });
25 | });
26 | }
27 |
28 | return response;
29 | })
30 | )
31 | });
32 |
--------------------------------------------------------------------------------
/apps/client/src/auth/auth-service.interface.ts:
--------------------------------------------------------------------------------
1 | import { type AwsCredentialIdentity } from '@smithy/types';
2 |
3 | /**
4 | * AuthService handles user's credentials.
5 | */
6 | export interface AuthService {
7 | /**
8 | * Returns the user's AWS Credentials.
9 | * @return - the user's AWS Credentials
10 | */
11 | getAwsCredentials(): Promise;
12 |
13 | /**
14 | * Sets the user's AWS Credentials.
15 | */
16 | setAwsCredentials(credentials: AwsCredentialIdentity): void;
17 |
18 | /**
19 | * AWS Region to obtain the AWS Credentials from.
20 | */
21 | get awsRegion(): string;
22 |
23 | /**
24 | * Returns the user's session token.
25 | * @return - the user's session token
26 | */
27 | getToken(): Promise;
28 |
29 | /**
30 | * This function accepts a callback function to call when application is signed-in or already signed-in
31 | * @param callback the callback function to call when application is signed-in
32 | */
33 | onSignedIn(callback: () => unknown): void;
34 |
35 | // TODO: Refactor interface to only include shared methods
36 | getEdgeEndpoint?(): string;
37 |
38 | setEdgeEndpoint?(endpoint: string): void;
39 | }
40 |
--------------------------------------------------------------------------------
/apps/client/src/auth/auth-service.ts:
--------------------------------------------------------------------------------
1 | import { AuthService } from './auth-service.interface';
2 | import { EdgeAuthService } from './edge-auth-service';
3 | import { CognitoAuthService } from './cognito-auth-service';
4 | import { type AwsCredentialIdentity } from '@smithy/types';
5 | import { getAuthMode } from '~/helpers/authMode';
6 |
7 | class ClientAuthService {
8 | private authService: AuthService;
9 |
10 | constructor() {
11 | if (getAuthMode() === 'edge') {
12 | this.authService = new EdgeAuthService();
13 | } else {
14 | this.authService = new CognitoAuthService();
15 | }
16 | }
17 |
18 | setAwsCredentials(credentials: AwsCredentialIdentity) {
19 | this.authService.setAwsCredentials(credentials);
20 | }
21 |
22 | getAwsCredentials() {
23 | return this.authService.getAwsCredentials();
24 | }
25 |
26 | get awsRegion() {
27 | return this.authService.awsRegion;
28 | }
29 |
30 | getToken() {
31 | return this.authService.getToken();
32 | }
33 |
34 | onSignedIn(callback: () => unknown) {
35 | return this.authService.onSignedIn(callback);
36 | }
37 |
38 | setEdgeEndpoint(endpoint: string) {
39 | if (this.authService.setEdgeEndpoint) {
40 | this.authService.setEdgeEndpoint(endpoint);
41 | }
42 | }
43 |
44 | getEdgeEndpoint() {
45 | if (this.authService.getEdgeEndpoint) {
46 | return this.authService.getEdgeEndpoint();
47 | }
48 | return '';
49 | }
50 | }
51 |
52 | export const authService = new ClientAuthService();
53 |
--------------------------------------------------------------------------------
/apps/client/src/auth/edge-auth-service.ts:
--------------------------------------------------------------------------------
1 | import { AuthService } from './auth-service.interface';
2 | import { type AwsCredentialIdentity } from '@smithy/types';
3 |
4 | export class EdgeAuthService implements AuthService {
5 | private region = 'edge';
6 | private credentials: AwsCredentialIdentity = {
7 | accessKeyId: '',
8 | secretAccessKey: '',
9 | };
10 | private edgeEndpoint = '0.0.0.0';
11 |
12 | setEdgeEndpoint(endpoint: string) {
13 | this.edgeEndpoint = endpoint;
14 | }
15 |
16 | getEdgeEndpoint() {
17 | return this.edgeEndpoint;
18 | }
19 |
20 | setAwsCredentials(credentials: AwsCredentialIdentity) {
21 | this.credentials = credentials;
22 | }
23 |
24 | getAwsCredentials() {
25 | return Promise.resolve(this.credentials);
26 | }
27 |
28 | get awsRegion() {
29 | return this.region;
30 | }
31 |
32 | getToken() {
33 | if (this.credentials.sessionToken) {
34 | return Promise.resolve(this.credentials.sessionToken);
35 | }
36 |
37 | // TODO: Handle session token in edge mode
38 | return Promise.resolve('');
39 | }
40 |
41 | /**
42 | * This function accepts a callback function to call when application is signed-in or already signed-in
43 | * @param callback the callback function to call when application is signed-in
44 | */
45 | onSignedIn(callback: () => unknown) {
46 | if (
47 | this.credentials.accessKeyId !== '' &&
48 | this.credentials.secretAccessKey !== ''
49 | ) {
50 | callback();
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/apps/client/src/constants/format.ts:
--------------------------------------------------------------------------------
1 | import type { Format } from '~/types';
2 |
3 | export const ROOT_INDEX_PAGE_FORMAT: Format = 'default';
4 | export const DASHBOARDS_INDEX_PAGE_FORMAT: Format = 'table';
5 | export const CREATE_DASHBOARD_PAGE_FORMAT: Format = 'form';
6 | export const DASHBOARD_PAGE_FORMAT: Format = 'default';
7 | export const EDGE_LOGIN_PAGE_FORMAT: Format = 'form';
8 |
--------------------------------------------------------------------------------
/apps/client/src/constants/index.ts:
--------------------------------------------------------------------------------
1 | import { ContentDensity } from '~/types';
2 |
3 | export const DEFAULT_LOCALE = 'en';
4 |
5 | export const ROOT_PATH = '/';
6 | export const DASHBOARDS_PATH = 'dashboards';
7 | export const CREATE_PATH = 'create';
8 | export const DASHBOARD_PATH = ':dashboardId';
9 |
10 | // Add the corresponding routes under apps/core/src/mvc/mvc.controller.ts to support browser router.
11 | export const ROOT_HREF = '/';
12 | export const DASHBOARDS_HREF = '/dashboards';
13 | export const CREATE_DASHBOARD_HREF = '/dashboards/create';
14 | export const EDGE_LOGIN_HREF = '/edge-login';
15 |
16 | export const DEFAULT_CONTENT_DENSITY: ContentDensity = 'comfortable';
17 | export const CONTENT_DENSITY_KEY = 'content-density';
18 |
--------------------------------------------------------------------------------
/apps/client/src/constants/time.ts:
--------------------------------------------------------------------------------
1 | export const SECONDS_IN_MS = 1000 as const;
2 | export const MINUTE_IN_MS = SECONDS_IN_MS * 60;
3 |
--------------------------------------------------------------------------------
/apps/client/src/data/edge-login.ts:
--------------------------------------------------------------------------------
1 | import { edgeLogin } from '~/services';
2 | import { EdgeLoginBody } from '~/services/generated/models/EdgeLoginBody';
3 |
4 | export const EDGE_LOGIN_QUERY_KEY = ['edge-login'];
5 |
6 | // TODO: Invalidate when aws credentials expire
7 | export function createEdgeLoginQuery(body: EdgeLoginBody) {
8 | return {
9 | queryKey: [EDGE_LOGIN_QUERY_KEY, { body }],
10 | queryFn: () => edgeLogin(body),
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/apps/client/src/data/migration.ts:
--------------------------------------------------------------------------------
1 | import { dashboardMigration, dashboardMigrationStatus } from '~/services';
2 |
3 | export const MIGRATION_QUERY_KEY = ['migration'];
4 |
5 | export const MIGRATION_START_QUERY_KEY = [...MIGRATION_QUERY_KEY, 'start'];
6 |
7 | export const MIGRATION_STATUS_QUERY_KEY = [...MIGRATION_QUERY_KEY, 'status'];
8 |
9 | export const MIGRATION_QUERY = {
10 | queryKey: MIGRATION_START_QUERY_KEY,
11 | queryFn: async () => {
12 | await dashboardMigration();
13 | return {};
14 | },
15 | };
16 |
17 | export const MIGRATION_STATUS_QUERY = {
18 | queryKey: MIGRATION_STATUS_QUERY_KEY,
19 | queryFn: dashboardMigrationStatus,
20 | };
21 |
--------------------------------------------------------------------------------
/apps/client/src/data/query-client.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from '@tanstack/react-query';
2 |
3 | import { MINUTE_IN_MS } from '~/constants/time';
4 |
5 | export const queryClient = new QueryClient({
6 | defaultOptions: {
7 | queries: {
8 | staleTime: MINUTE_IN_MS * 10,
9 | },
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/authMode.ts:
--------------------------------------------------------------------------------
1 | import { extractedMetaTags } from './meta-tags';
2 | import once from 'lodash/once';
3 |
4 | export const getAuthMode = once(() => {
5 | const tags = Array.from(document.getElementsByTagName('meta'));
6 | const { authMode } = extractedMetaTags(tags);
7 | return authMode;
8 | });
9 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/dates/index.ts:
--------------------------------------------------------------------------------
1 | export { utcDateString } from './utcDateString';
2 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/dates/utcDateString.ts:
--------------------------------------------------------------------------------
1 | export function utcDateString(date: Date) {
2 | return [
3 | date.getUTCFullYear(),
4 | (date.getUTCMonth() + 1).toString().padStart(2, '0'),
5 | date.getUTCDate().toString().padStart(2, '0'),
6 | ].join('-');
7 | }
8 |
9 | if (import.meta.vitest) {
10 | const { it, expect } = import.meta.vitest;
11 |
12 | it('returns date string in UTC time', () => {
13 | const date = new Date('05 October 2011 14:48 UTC');
14 |
15 | expect(utcDateString(date)).toBe('2011-10-05');
16 | });
17 | }
18 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/events.ts:
--------------------------------------------------------------------------------
1 | export function preventFullPageLoad(event: Event) {
2 | event.preventDefault();
3 | }
4 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/featureFlag/featureFlag.spec.ts:
--------------------------------------------------------------------------------
1 | import { featureEnabled } from './featureFlag';
2 |
3 | describe('featureFlag', () => {
4 | test('Migration feature is disabled', () => {
5 | const isMigrationEnabled = featureEnabled('Migration');
6 | expect(isMigrationEnabled).toEqual(false);
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/featureFlag/featureFlag.ts:
--------------------------------------------------------------------------------
1 | export const Features = {};
2 |
3 | /**
4 | * Caution:
5 | * This array shows/hides features that customers see.
6 | * We should use this for in-development features and remove the flags when releasing.
7 | */
8 | export const supportedFeatures: string[] = [];
9 |
10 | export const featureEnabled = (feature: string) => {
11 | return supportedFeatures.includes(feature);
12 | };
13 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/lists/index.ts:
--------------------------------------------------------------------------------
1 | export { last } from './last';
2 | export { without } from './without';
3 | export { withoutIdentifiable } from './without-identifiable';
4 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/lists/last.ts:
--------------------------------------------------------------------------------
1 | import type { Maybe } from '~/types';
2 |
3 | export function last(as: T[]): Maybe {
4 | return as.at(-1);
5 | }
6 |
7 | if (import.meta.vitest) {
8 | const { it, expect } = import.meta.vitest;
9 |
10 | it('returns the last list element', () => {
11 | const list = [1, 2, 3];
12 |
13 | expect(last(list)).toBe(3);
14 | });
15 |
16 | it('returns undefined when given an empty list', () => {
17 | const list: unknown[] = [];
18 |
19 | expect(last(list)).toBe(undefined);
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/lists/without-identifiable.ts:
--------------------------------------------------------------------------------
1 | import { without } from './without';
2 | import { isSameIdentifiable } from '../predicates';
3 | import type { Identifiable } from '~/types';
4 |
5 | export function withoutIdentifiable(a: T) {
6 | return without(isSameIdentifiable(a));
7 | }
8 |
9 | if (import.meta.vitest) {
10 | const { expect, it } = import.meta.vitest;
11 |
12 | it('returns a list without the identifiable members matching the predicate', () => {
13 | const list = [
14 | { id: '1' },
15 | { id: '2' },
16 | { id: '3' },
17 | { id: '2' },
18 | { id: '1' },
19 | ];
20 |
21 | expect(withoutIdentifiable({ id: '2' })(list)).toEqual([
22 | { id: '1' },
23 | { id: '3' },
24 | { id: '1' },
25 | ]);
26 | });
27 |
28 | it('returns the same list when no members match the predicate', () => {
29 | const list = [{ id: '1' }, { id: '2' }, { id: '3' }];
30 |
31 | expect(withoutIdentifiable({ id: '4' })(list)).toEqual(list);
32 | });
33 |
34 | it('returns an empty list when given an empty list', () => {
35 | const list: Identifiable[] = [];
36 |
37 | expect(withoutIdentifiable({ id: '2' })(list)).toEqual(list);
38 | });
39 | }
40 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/lists/without.ts:
--------------------------------------------------------------------------------
1 | import type { Predicate } from '~/types';
2 |
3 | export function without(p: Predicate) {
4 | return (as: T[]): T[] => {
5 | return as.filter((a) => !p(a));
6 | };
7 | }
8 |
9 | if (import.meta.vitest) {
10 | const { expect, it } = import.meta.vitest;
11 |
12 | it('returns a list without the members matching the predicate', () => {
13 | const list = [1, 2, 3, 4, 5, 4, 3, 2, 1];
14 |
15 | expect(without((n: number) => n > 3)(list)).toEqual([1, 2, 3, 3, 2, 1]);
16 | });
17 |
18 | it('returns the same list when no members match the predicate', () => {
19 | const list = [1, 2, 3, 4, 5];
20 |
21 | expect(without((n: number) => n > 6)(list)).toEqual(list);
22 | });
23 |
24 | it('returns an empty list when given an empty list', () => {
25 | const list: number[] = [];
26 |
27 | expect(without((n: number) => n > 6)(list)).toEqual(list);
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/meta-tags.spec.ts:
--------------------------------------------------------------------------------
1 | import { extractedMetaTags } from './meta-tags';
2 |
3 | describe('extractedMetaTags', () => {
4 | it('returns extracted meta tag values', () => {
5 | const mockMetaTag1 = document.createElement('meta');
6 | const mockMetaTag2 = document.createElement('meta');
7 |
8 | const mockMetaTag1Name = 'authenticationFlowType';
9 | const mockMetaTag1Content = 'authenticationFlowTypeContent';
10 | const mockMetaTag2Name = 'cognitoEndpoint';
11 | const mockMetaTag2Content = 'cognitoEndpointContent';
12 |
13 | mockMetaTag1.name = mockMetaTag1Name;
14 | mockMetaTag1.content = mockMetaTag1Content;
15 | mockMetaTag2.name = mockMetaTag2Name;
16 | mockMetaTag2.content = mockMetaTag2Content;
17 |
18 | const metaTags = extractedMetaTags([mockMetaTag1, mockMetaTag2]);
19 |
20 | expect(metaTags).toMatchObject({
21 | [mockMetaTag1Name]: mockMetaTag1Content,
22 | [mockMetaTag2Name]: mockMetaTag2Content,
23 | });
24 | });
25 |
26 | it('does not contain meta tag values not specified', () => {
27 | const mockMetaTag = document.createElement('meta');
28 |
29 | const mockMetaTagName = 'doesNotExist';
30 | const mockMetaTagContent = 'doesNotExistContent';
31 |
32 | mockMetaTag.name = mockMetaTagName;
33 | mockMetaTag.content = mockMetaTagContent;
34 |
35 | const metaTags = extractedMetaTags([mockMetaTag]);
36 |
37 | expect(metaTags).not.toMatchObject({
38 | [mockMetaTagName]: mockMetaTagContent,
39 | });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/meta-tags.ts:
--------------------------------------------------------------------------------
1 | import { Metadata } from '~/types';
2 |
3 | export const extractedMetaTags = (
4 | metaElements: HTMLMetaElement[],
5 | ): Metadata => {
6 | const metaTags: Metadata = {
7 | awsAccessKeyId: '',
8 | awsSecretAccessKey: '',
9 | awsSessionToken: '',
10 | applicationName: '',
11 | authenticationFlowType: '',
12 | cognitoEndpoint: '',
13 | edgeEndpoint: '',
14 | identityPoolId: '',
15 | region: '',
16 | userPoolId: '',
17 | userPoolWebClientId: '',
18 | logMode: '',
19 | metricsMode: '',
20 | domainName: '',
21 | authMode: '',
22 | };
23 |
24 | metaElements.forEach(
25 | ({ name, content }: { name: string; content: string }) => {
26 | if (name in metaTags) {
27 | (metaTags as unknown as Record)[name] = content;
28 | }
29 | },
30 | );
31 |
32 | return metaTags;
33 | };
34 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/predicates.ts:
--------------------------------------------------------------------------------
1 | import type { ReadonlyTuple } from 'type-fest';
2 | import type { Identifiable, NonEmptyList } from '~/types';
3 |
4 | export function isListWithSingleItem(
5 | list: Readonly,
6 | ): list is ReadonlyTuple {
7 | return list.length === 1;
8 | }
9 |
10 | export function isSameIdentifiable(a: Identifiable) {
11 | return (b: Identifiable) => {
12 | return a.id === b.id;
13 | };
14 | }
15 |
16 | export function isNonEmptyList(maybe: T[]): maybe is NonEmptyList {
17 | return maybe.length >= 1;
18 | }
19 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/predicates/is-api-error.ts:
--------------------------------------------------------------------------------
1 | import { ApiError } from '~/services';
2 |
3 | export function isApiError(error: unknown): error is ApiError {
4 | return error instanceof ApiError;
5 | }
6 |
7 | if (import.meta.vitest) {
8 | const { expect, it } = import.meta.vitest;
9 |
10 | it('should return true when given an ApiError instance', () => {
11 | expect(
12 | isApiError(
13 | new ApiError(
14 | { method: 'GET', url: 'test' },
15 | {
16 | url: 'test',
17 | ok: false,
18 | status: 500,
19 | statusText: 'test',
20 | body: 'test',
21 | },
22 | 'test',
23 | ),
24 | ),
25 | ).toBe(true);
26 | });
27 |
28 | it('should return false when given a non-ApiError error instance', () => {
29 | expect(isApiError(new Error('test'))).toBe(false);
30 | });
31 |
32 | it('should return false when called with null', () => {
33 | expect(isApiError(null)).toBe(false);
34 | });
35 |
36 | it('should return false when called with undefined', () => {
37 | expect(isApiError(undefined)).toBe(false);
38 | });
39 |
40 | it('should return false when called with a non-ApiError object', () => {
41 | expect(isApiError({})).toBe(false);
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/predicates/is-just.spec-d.ts:
--------------------------------------------------------------------------------
1 | import { expectTypeOf } from 'vitest';
2 | import { isJust } from './is-just';
3 | import type { Maybe, Just, Nothing } from '~/types';
4 |
5 | describe('isJust', () => {
6 | test('isJust widens to Maybe', () => {
7 | expectTypeOf(isJust).guards.toMatchTypeOf>();
8 | expectTypeOf(isJust).guards.toMatchTypeOf | Nothing>();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/predicates/is-just.spec.ts:
--------------------------------------------------------------------------------
1 | import { isJust } from './is-just';
2 |
3 | describe('isJust', () => {
4 | it.each([1, 0, 'a', '', true, false, [1], [], { k: 'v' }, {}])(
5 | 'should return true when the value is Just',
6 | (value) => {
7 | expect(isJust(value)).toBe(true);
8 | },
9 | );
10 |
11 | it.each([undefined, null])(
12 | 'should return false when the value is Nothing',
13 | (value) => {
14 | expect(isJust(value)).toBe(false);
15 | },
16 | );
17 | });
18 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/predicates/is-just.ts:
--------------------------------------------------------------------------------
1 | import type { Just, Maybe } from '~/types';
2 |
3 | export function isJust(maybe: Maybe): maybe is Just {
4 | return maybe != null;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/predicates/is-nothing.spec-d.ts:
--------------------------------------------------------------------------------
1 | import { expectTypeOf } from 'vitest';
2 | import { isNothing } from './is-nothing';
3 | import type { Maybe, Just, Nothing } from '~/types';
4 |
5 | describe('isNothing', () => {
6 | test('isNothing widens to Maybe', () => {
7 | expectTypeOf(isNothing).guards.toMatchTypeOf>();
8 | expectTypeOf(isNothing).guards.toMatchTypeOf<
9 | Just | Nothing
10 | >();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/predicates/is-nothing.spec.ts:
--------------------------------------------------------------------------------
1 | import { isNothing } from './is-nothing';
2 |
3 | describe('isNothing', () => {
4 | it.each([null, undefined])(
5 | 'should return true when the value is Nothing',
6 | (value) => {
7 | expect(isNothing(value)).toBe(true);
8 | },
9 | );
10 |
11 | it.each([1, 0, 'a', '', true, false, [1], [], { k: 'v' }, {}])(
12 | 'should return false when the value is Just',
13 | (value) => {
14 | expect(isNothing(value)).toBe(false);
15 | },
16 | );
17 | });
18 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/predicates/is-nothing.ts:
--------------------------------------------------------------------------------
1 | import { isJust } from '~/helpers/predicates/is-just';
2 | import type { Maybe, Nothing } from '~/types';
3 |
4 | export function isNothing(maybe: Maybe): maybe is Nothing {
5 | return !isJust(maybe);
6 | }
7 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/strings/is-string-with-value.ts:
--------------------------------------------------------------------------------
1 | const isString = (value: unknown): value is string => {
2 | return typeof value === 'string';
3 | };
4 |
5 | export const isStringWithValue = (value: unknown): value is string => {
6 | return isString(value) && value !== '';
7 | };
8 |
9 | if (import.meta.vitest) {
10 | it('returns true for string with value', () => {
11 | expect(isStringWithValue('abc')).toBe(true);
12 | });
13 |
14 | it('returns false for string with no value', () => {
15 | expect(isStringWithValue('')).toBe(false);
16 | });
17 |
18 | it('returns false for non-string value', () => {
19 | expect(isStringWithValue({})).toBe(false);
20 | });
21 |
22 | it('returns false for undefined value', () => {
23 | expect(isStringWithValue(undefined)).toBe(false);
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/tests/error-boundary-tester.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export function ErrorBoundaryTester() {
4 | const [throwError, setThrowError] = useState(false);
5 |
6 | if (throwError) {
7 | throw new Error();
8 | }
9 |
10 | return (
11 | <>
12 | not an error fallback
13 |
14 | >
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/apps/client/src/helpers/tests/testing-library.tsx:
--------------------------------------------------------------------------------
1 | import { render as rtlRender } from '@testing-library/react';
2 | import { PropsWithChildren, ReactElement } from 'react';
3 | import { IntlProvider } from 'react-intl';
4 |
5 | function render(ui: ReactElement, { locale = 'en', ...renderOptions } = {}) {
6 | function Wrapper(props: PropsWithChildren) {
7 | return {props.children};
8 | }
9 | return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
10 | }
11 |
12 | // re-export everything
13 | export * from '@testing-library/react';
14 |
15 | // override render method
16 | export { render };
17 |
--------------------------------------------------------------------------------
/apps/client/src/hooks/application/use-application.ts:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router-dom';
2 | import { useDismissAllNotifications } from '~/hooks/notifications/use-dismiss-all-notifications';
3 |
4 | /** Use to control the application. */
5 | export function useApplication() {
6 | const navigateClient = useNavigate();
7 | const dismissNotifications = useDismissAllNotifications();
8 |
9 | /**
10 | * Navigate the client to a new page.
11 | *
12 | * @regards
13 | * Beware of the order you call this function! It produces side-effects!
14 | * If you intend to emit a notification at the same time, you should call this function first.
15 | *
16 | * Do not use any other method to navigate the client!
17 | */
18 | function navigate(href: string) {
19 | dismissNotifications();
20 | navigateClient(href);
21 | }
22 |
23 | return {
24 | navigate,
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/apps/client/src/hooks/application/use-detect-401-unauthorized.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { Auth } from 'aws-amplify';
3 |
4 | export function useDetect401Unauthorized() {
5 | useEffect(() => {
6 | navigator.serviceWorker.addEventListener(
7 | 'message',
8 | handleServiceWorkerMessage,
9 | );
10 |
11 | return () => {
12 | navigator.serviceWorker.removeEventListener(
13 | 'message',
14 | handleServiceWorkerMessage,
15 | );
16 | };
17 | }, []);
18 | }
19 |
20 | function handleServiceWorkerMessage(event: MessageEvent<{ type?: string }>) {
21 | if (event.data.type === '401_UNAUTHORIZED') {
22 | void Auth.signOut();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/apps/client/src/hooks/dashboard/use-displaySettings.ts:
--------------------------------------------------------------------------------
1 | import { useLocalStorage } from 'react-use';
2 | import { DashboardDisplaySettings } from '@iot-app-kit/dashboard';
3 |
4 | const DEFAULT_DISPLAY_SETTINGS: DashboardDisplaySettings = {
5 | numRows: 1000,
6 | numColumns: 200,
7 | cellSize: 20,
8 | };
9 | export const useDisplaySettings = (dashboardId: string) => {
10 | return useLocalStorage(
11 | `dashboard-${dashboardId}-display-settings`,
12 | DEFAULT_DISPLAY_SETTINGS,
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/apps/client/src/hooks/dashboard/use-viewport.ts:
--------------------------------------------------------------------------------
1 | import { useLocalStorage } from 'react-use';
2 | import type { Viewport } from '@iot-app-kit/core';
3 |
4 | const DEFAULT_VIEWPORT: Viewport = { duration: '5 minute' };
5 | export const useViewport = (dashboardId: string) => {
6 | return useLocalStorage(
7 | `dashboard-${dashboardId}-viewport`,
8 | DEFAULT_VIEWPORT,
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/apps/client/src/hooks/notifications/use-dismiss-all-notifications.ts:
--------------------------------------------------------------------------------
1 | import { useSetAtom } from 'jotai';
2 | import { dismissAllNotificationsAtom } from '~/store/notifications';
3 |
4 | /** Use to dismiss all notifications from global store. */
5 | export function useDismissAllNotifications() {
6 | return useSetAtom(dismissAllNotificationsAtom);
7 | }
8 |
--------------------------------------------------------------------------------
/apps/client/src/hooks/notifications/use-emit-notification.ts:
--------------------------------------------------------------------------------
1 | import { emitNotificationAtom } from '~/store/notifications';
2 | import { useSetAtom } from 'jotai';
3 |
4 | /** Use write-only notification global store. */
5 | export function useEmitNotification() {
6 | return useSetAtom(emitNotificationAtom);
7 | }
8 |
--------------------------------------------------------------------------------
/apps/client/src/hooks/notifications/use-notifications.ts:
--------------------------------------------------------------------------------
1 | import { notificationsAtom } from '~/store/notifications';
2 | import { useAtomValue } from 'jotai';
3 |
4 | /** Use readonly notification global store. */
5 | export function useNotifications() {
6 | return useAtomValue(notificationsAtom);
7 | }
8 |
--------------------------------------------------------------------------------
/apps/client/src/initialize-auth-dependents.tsx:
--------------------------------------------------------------------------------
1 | import { v4 as uuid } from 'uuid';
2 | import { utcDateString } from './helpers/dates';
3 | import { cloudWatchMetricsRecorder } from './metrics/cloud-watch-metrics-recorder';
4 | import { cloudWatchLogger } from './logging/cloud-watch-logger';
5 |
6 | /**
7 | * This function initializes the services that depends on authentication to work
8 | * @param applicationName the name of this application
9 | */
10 | export async function initializeAuthDependents(applicationName: string) {
11 | const logStreamName = `${utcDateString(new Date())}-${uuid()}`;
12 | cloudWatchMetricsRecorder.init(applicationName);
13 | try {
14 | await cloudWatchLogger.init(applicationName, logStreamName);
15 | } catch (e) {
16 | // NOOP; application should proceed;
17 | console.error(
18 | 'Error during CloudWatch logger initialization, no logs are sent to CloudWatch',
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/breadcrumbs/breadcrumbs.tsx:
--------------------------------------------------------------------------------
1 | import BreadcrumbGroup from '@cloudscape-design/components/breadcrumb-group';
2 | import { useIntl } from 'react-intl';
3 |
4 | import { preventFullPageLoad } from '~/helpers/events';
5 | import { useCrumbs } from './hooks/use-crumbs';
6 | import { useApplication } from '~/hooks/application/use-application';
7 |
8 | export function Breadcrumbs() {
9 | const crumbs = useCrumbs();
10 | const intl = useIntl();
11 | const { navigate } = useApplication();
12 |
13 | return (
14 | {
21 | preventFullPageLoad(event);
22 | navigate(event.detail.href);
23 | }}
24 | />
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/breadcrumbs/hooks/use-crumbs.ts:
--------------------------------------------------------------------------------
1 | import invariant from 'tiny-invariant';
2 | import { useMatches } from 'react-router-dom';
3 |
4 | import { isJust } from '~/helpers/predicates/is-just';
5 |
6 | import type { Crumbly, DataBound, Handleable, Maybe } from '~/types';
7 |
8 | type MaybeMatch = Maybe> & Maybe>>>;
9 |
10 | export function useCrumbs() {
11 | const matches = useMatches() as MaybeMatch[];
12 |
13 | invariant(matches.length >= 1, 'Expected at least 1 matching route');
14 |
15 | const matchesWithCrumbs = matches.filter(
16 | (m): m is Maybe> & Handleable> =>
17 | isJust(m?.handle?.crumb),
18 | );
19 |
20 | invariant(
21 | matchesWithCrumbs.length >= 1,
22 | 'Expected at least 1 matching route with crumbs',
23 | );
24 |
25 | const crumbs = matchesWithCrumbs.map((m) =>
26 | isJust(m.data) ? m.handle.crumb(m.data) : m.handle.crumb(),
27 | );
28 |
29 | return crumbs;
30 | }
31 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/breadcrumbs/index.ts:
--------------------------------------------------------------------------------
1 | export { Breadcrumbs } from './breadcrumbs';
2 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/footer/footer.css:
--------------------------------------------------------------------------------
1 | /**Custom footer component CSS*/
2 |
3 | .dashboard-footer-container {
4 | z-index: 1002;
5 | position:sticky;
6 | bottom:0;
7 | width: 100%;
8 | overflow: hidden;
9 | align-items: center;
10 | }
11 |
12 | .dashboard-footer-right-content {
13 | float: right;
14 | }
--------------------------------------------------------------------------------
/apps/client/src/layout/components/footer/footer.spec.tsx:
--------------------------------------------------------------------------------
1 | import { Footer } from './footer';
2 | import { render, screen } from '~/helpers/tests/testing-library';
3 |
4 | describe('footer', () => {
5 | it('should load footer on page render', () => {
6 | render();
7 | expect(screen.getByTestId('footer-component')).toBeInTheDocument();
8 | });
9 |
10 | it('should have copy right description in the footer', () => {
11 | render();
12 |
13 | expect(screen.getByTestId('copy-right')).toBeInTheDocument();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/footer/footer.tsx:
--------------------------------------------------------------------------------
1 | import { FormattedMessage } from 'react-intl';
2 | import {
3 | colorBackgroundControlDefault,
4 | colorBackgroundLayoutToggleDefault,
5 | spaceScaledXs,
6 | spaceScaledS,
7 | spaceScaledL,
8 | } from '@cloudscape-design/design-tokens';
9 | import './footer.css';
10 | import { useParams } from 'react-router-dom';
11 |
12 | export const Footer = () => {
13 | const params = useParams();
14 |
15 | const footerStyle = {
16 | fontSize: spaceScaledS,
17 | color: colorBackgroundControlDefault,
18 | backgroundColor: colorBackgroundLayoutToggleDefault,
19 | padding: `${spaceScaledXs} ${spaceScaledL}`,
20 | };
21 |
22 | return (
23 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/footer/index.ts:
--------------------------------------------------------------------------------
1 | export { Footer } from './footer';
2 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/notifications/index.ts:
--------------------------------------------------------------------------------
1 | export { Notifications } from './notifications';
2 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/top-navigation/components/settings-modal/components/content-density-toggle.tsx:
--------------------------------------------------------------------------------
1 | import FormField from '@cloudscape-design/components/form-field';
2 | import Toggle from '@cloudscape-design/components/toggle';
3 | import { FormattedMessage, useIntl } from 'react-intl';
4 |
5 | interface ContentDensityToggleProps {
6 | onToggle: (toggled: boolean) => void;
7 | toggled: boolean;
8 | }
9 |
10 | export function ContentDensityToggle(props: ContentDensityToggleProps) {
11 | const intl = useIntl();
12 |
13 | return (
14 |
25 | props.onToggle(event.detail.checked)}
27 | checked={props.toggled}
28 | >
29 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/top-navigation/components/settings-modal/components/settings-modal-footer.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@cloudscape-design/components/box';
2 | import Button from '@cloudscape-design/components/button';
3 | import SpaceBetween from '@cloudscape-design/components/space-between';
4 | import { FormattedMessage } from 'react-intl';
5 |
6 | interface SettingsModalFooterProps {
7 | onCancel: () => void;
8 | onConfirm: () => void;
9 | }
10 |
11 | export function SettingsModalFooter(props: SettingsModalFooterProps) {
12 | return (
13 |
14 |
15 |
21 |
22 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/top-navigation/components/settings-modal/helpers/is-comfortable.ts:
--------------------------------------------------------------------------------
1 | import type { Comfortable, ContentDensity } from '~/types';
2 |
3 | export function isComfortable(density: ContentDensity): density is Comfortable {
4 | return density === 'comfortable';
5 | }
6 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/top-navigation/components/settings-modal/hooks/use-density-toggle.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import { isComfortable } from '../helpers/is-comfortable';
4 |
5 | import type { ContentDensity } from '~/types';
6 |
7 | export function useDensityToggle(density: ContentDensity) {
8 | const [toggled, setToggled] = useState(isComfortable(density));
9 |
10 | return [toggled, setToggled] as const;
11 | }
12 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/top-navigation/components/settings-modal/hooks/use-density.ts:
--------------------------------------------------------------------------------
1 | import { applyDensity, Density } from '@cloudscape-design/global-styles';
2 | import { useLocalStorage } from 'react-use';
3 |
4 | import { isComfortable } from '../helpers/is-comfortable';
5 | import { CONTENT_DENSITY_KEY, DEFAULT_CONTENT_DENSITY } from '~/constants';
6 |
7 | import type { ContentDensity } from '~/types';
8 |
9 | const CONTENT_DENSITY_INITIALIZER = DEFAULT_CONTENT_DENSITY;
10 |
11 | export function useDensity() {
12 | const [density = DEFAULT_CONTENT_DENSITY, setDensity] =
13 | useLocalStorage(
14 | CONTENT_DENSITY_KEY,
15 | CONTENT_DENSITY_INITIALIZER,
16 | );
17 |
18 | const densitySetting = isComfortable(density)
19 | ? Density.Comfortable
20 | : Density.Compact;
21 |
22 | applyDensity(densitySetting);
23 |
24 | return [density, setDensity] as const;
25 | }
26 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/top-navigation/components/settings-modal/index.ts:
--------------------------------------------------------------------------------
1 | export { SettingsModal } from './settings-modal';
2 |
--------------------------------------------------------------------------------
/apps/client/src/layout/components/top-navigation/index.ts:
--------------------------------------------------------------------------------
1 | export { TopNavigation } from './top-navigation';
2 |
--------------------------------------------------------------------------------
/apps/client/src/layout/hooks/use-format.ts:
--------------------------------------------------------------------------------
1 | import { useMatches } from 'react-router-dom';
2 | import invariant from 'tiny-invariant';
3 |
4 | import { isJust } from '~/helpers/predicates/is-just';
5 |
6 | import type { Formatted, Handleable, Maybe } from '~/types';
7 |
8 | /** Use to know what format to render to current page. */
9 | export function useFormat() {
10 | const matches = useMatches() as Maybe>>[];
11 |
12 | invariant(matches.length >= 1, 'Expected at least 1 match');
13 |
14 | const matchesWithFormat = matches.filter((m): m is Handleable =>
15 | isJust(m?.handle?.format),
16 | );
17 |
18 | return isJust(matchesWithFormat[0])
19 | ? matchesWithFormat[0].handle.format
20 | : 'default';
21 | }
22 |
--------------------------------------------------------------------------------
/apps/client/src/layout/hooks/use-full-width.ts:
--------------------------------------------------------------------------------
1 | import { useMatches } from 'react-router-dom';
2 | import invariant from 'tiny-invariant';
3 |
4 | import { isJust } from '~/helpers/predicates/is-just';
5 |
6 | import type {
7 | DataBound,
8 | Dimensional,
9 | FullWidth,
10 | Handleable,
11 | Maybe,
12 | } from '~/types';
13 |
14 | type MaybeMatchWithDimensions = Maybe> &
15 | Maybe>>>;
16 |
17 | /** Use to know if the current page should be rendered at full-width. */
18 | export function useFullWidth(): FullWidth {
19 | const matches = useMatches() as MaybeMatchWithDimensions[];
20 |
21 | invariant(matches.length >= 1, 'Expected at least 1 match');
22 |
23 | const matchesWithFullWidth = matches.filter(
24 | (m): m is Maybe> & Handleable> =>
25 | isJust(m?.handle?.fullWidth),
26 | );
27 |
28 | invariant(
29 | matchesWithFullWidth.length <= 1,
30 | 'Expected at most 1 match with dimensions',
31 | );
32 |
33 | const match = matchesWithFullWidth[0];
34 |
35 | return match?.handle.fullWidth(match.data) ?? false;
36 | }
37 |
--------------------------------------------------------------------------------
/apps/client/src/metrics/metric-handler.spec.ts:
--------------------------------------------------------------------------------
1 | import { vi } from 'vitest';
2 | import metricHandler from './metric-handler';
3 |
4 | describe('MetricHandler', () => {
5 | const spy = vi.spyOn(metricHandler, 'sendMetrics');
6 |
7 | beforeAll(() => {
8 | vi.stubGlobal('document', { visibilityState: 'hidden' });
9 |
10 | vi.mock('web-vitals', () => ({
11 | onCLS: (cb: (metric: string) => void) => cb('CLS'),
12 | onFID: (cb: (metric: string) => void) => cb('FID'),
13 | onFCP: (cb: (metric: string) => void) => cb('FCP'),
14 | onLCP: (cb: (metric: string) => void) => cb('LCP'),
15 | onTTFB: (cb: (metric: string) => void) => cb('TTFB'),
16 | }));
17 | });
18 |
19 | afterAll(() => {
20 | vi.clearAllMocks();
21 | vi.unstubAllGlobals();
22 | });
23 |
24 | it('should report batches of metrics', () => {
25 | metricHandler.reportWebVitals();
26 |
27 | metricHandler.flush();
28 |
29 | expect(spy).toHaveBeenCalledWith('["CLS","FID","FCP","LCP","TTFB"]');
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/apps/client/src/register-loggers.ts:
--------------------------------------------------------------------------------
1 | import { registerPlugin } from '@iot-app-kit/core';
2 | import { cloudWatchLogger } from './logging/cloud-watch-logger';
3 |
4 | const LogModes = {
5 | Local: 'local',
6 | Cloud: 'cloud',
7 | };
8 |
9 | export function registerLogger(logMode: string) {
10 | if (logMode === LogModes.Local) {
11 | registerPlugin('logger', {
12 | provider: () => ({
13 | /* eslint-disable no-console */
14 | log: (...args) => console.log('logging:', ...args),
15 | error: (...args) => console.error('logging:', ...args),
16 | warn: (...args) => console.warn('logging:', ...args),
17 | }),
18 | });
19 | } else {
20 | registerPlugin('logger', {
21 | provider: () => cloudWatchLogger,
22 | });
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/apps/client/src/register-metrics-recorder.ts:
--------------------------------------------------------------------------------
1 | import { registerPlugin } from '@iot-app-kit/core';
2 | import { cloudWatchMetricsRecorder } from './metrics/cloud-watch-metrics-recorder';
3 |
4 | const MetricModes = {
5 | Local: 'local',
6 | Cloud: 'cloud',
7 | };
8 |
9 | export function registerMetricsRecorder(metricsMode: string) {
10 | if (metricsMode === MetricModes.Local) {
11 | registerPlugin('metricsRecorder', {
12 | provider: () => ({
13 | /* eslint-disable no-console */
14 | record: (...args) => console.log('record metric:', ...args),
15 | }),
16 | });
17 | } else {
18 | registerPlugin('metricsRecorder', {
19 | provider: () => cloudWatchMetricsRecorder,
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/apps/client/src/register-service-worker.ts:
--------------------------------------------------------------------------------
1 | export function registerServiceWorker() {
2 | const swUrl = '/service-worker.js';
3 |
4 | navigator.serviceWorker.register(swUrl).catch((error) => {
5 | console.error('Error during service worker registration:', error);
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/apps/client/src/router.tsx:
--------------------------------------------------------------------------------
1 | import { createBrowserRouter } from 'react-router-dom';
2 |
3 | import { rootRoute } from './routes/root';
4 |
5 | export const routes = [rootRoute];
6 |
7 | export const router = createBrowserRouter(routes);
8 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/create/components/create-dashboard-form-actions.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@cloudscape-design/components/button';
2 | import SpaceBetween from '@cloudscape-design/components/space-between';
3 | import { colorBackgroundHomeHeader } from '@cloudscape-design/design-tokens';
4 | import { FormattedMessage } from 'react-intl';
5 |
6 | import { DASHBOARDS_HREF } from '~/constants';
7 | import { useApplication } from '~/hooks/application/use-application';
8 |
9 | interface CreateDashboardFormActionsProps {
10 | isLoading: boolean;
11 | }
12 |
13 | export function CreateDashboardFormActions(
14 | props: CreateDashboardFormActionsProps,
15 | ) {
16 | const { navigate } = useApplication();
17 |
18 | return (
19 |
20 |
30 |
31 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/create/create-dashboard-route.tsx:
--------------------------------------------------------------------------------
1 | import { CreateDashboardPage } from './create-dashboard-page';
2 | import { CREATE_PATH, CREATE_DASHBOARD_HREF } from '~/constants';
3 | import { CREATE_DASHBOARD_PAGE_FORMAT } from '~/constants/format';
4 | import { intl } from '~/services';
5 |
6 | import type { RouteObject } from 'react-router-dom';
7 |
8 | export const createDashboardRoute = {
9 | path: CREATE_PATH,
10 | element: ,
11 | handle: {
12 | crumb: () => ({
13 | text: intl.formatMessage({
14 | defaultMessage: 'Create dashboard',
15 | description: 'create dashboard route breadcrumb text',
16 | }),
17 | href: CREATE_DASHBOARD_HREF,
18 | }),
19 | format: CREATE_DASHBOARD_PAGE_FORMAT,
20 | },
21 | } satisfies RouteObject;
22 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/create/hooks/use-create-dashboard-form.ts:
--------------------------------------------------------------------------------
1 | import { useForm } from 'react-hook-form';
2 |
3 | import type { CreateDashboardFormValues } from '../types/create-dashboard-form-values';
4 |
5 | const DEFAULT_VALUES: CreateDashboardFormValues = {
6 | name: '',
7 | description: '',
8 | };
9 |
10 | export function useCreateDashboardForm() {
11 | return useForm({ defaultValues: DEFAULT_VALUES });
12 | }
13 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/create/index.ts:
--------------------------------------------------------------------------------
1 | export { createDashboardRoute } from './create-dashboard-route';
2 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/create/types/create-dashboard-form-values.ts:
--------------------------------------------------------------------------------
1 | import type { Dashboard } from '~/services';
2 |
3 | export type CreateDashboardFormValues = Pick;
4 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboard/components/dashboard-error-boundary.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@cloudscape-design/components/box';
2 | import Button from '@cloudscape-design/components/button';
3 | import { useQueryErrorResetBoundary } from '@tanstack/react-query';
4 | import { ErrorBoundary } from 'react-error-boundary';
5 | import { FormattedMessage } from 'react-intl';
6 |
7 | import type { PropsWithChildren } from 'react';
8 |
9 | export function DashboardErrorBoundary(props: PropsWithChildren) {
10 | const { reset } = useQueryErrorResetBoundary();
11 |
12 | return (
13 | (
16 |
17 |
23 |
27 |
28 |
34 |
35 | )}
36 | >
37 | {props.children}
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboard/components/dashboard-loading-state.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '~/helpers/tests/testing-library';
2 | import { DashboardLoadingState } from './dashboard-loading-state';
3 |
4 | describe('', () => {
5 | test('renders loading state with Spinner and message', () => {
6 | render();
7 |
8 | const spinner = screen.getByRole('progressbar');
9 | const loadingMessage = screen.getByText('Loading dashboard');
10 |
11 | expect(spinner).toBeInTheDocument();
12 | expect(loadingMessage).toBeInTheDocument();
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboard/components/dashboard-loading-state.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@cloudscape-design/components/box';
2 | import Spinner from '@cloudscape-design/components/spinner';
3 | import { FormattedMessage } from 'react-intl';
4 |
5 | export function DashboardLoadingState() {
6 | return (
7 |
8 |
9 | {/* does not have a role="progressbar" attribute */}
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboard/dashboard-configuration.ts:
--------------------------------------------------------------------------------
1 | import { type DashboardClientConfiguration } from '@iot-app-kit/dashboard';
2 | import { IoTSiteWiseClient } from '@aws-sdk/client-iotsitewise';
3 | import { IoTEventsClient } from '@aws-sdk/client-iot-events';
4 | import { IoTTwinMakerClient } from '@aws-sdk/client-iottwinmaker';
5 | import { getAuthMode } from '~/helpers/authMode';
6 |
7 | import { authService } from '~/auth/auth-service';
8 |
9 | export function getDashboardClientConfiguration(): DashboardClientConfiguration {
10 | if (getAuthMode() === 'edge') {
11 | return getEdgeClientConfig();
12 | }
13 |
14 | return getCloudClientConfig();
15 | }
16 |
17 | function getCloudClientConfig(): DashboardClientConfiguration {
18 | return {
19 | awsCredentials: () => authService.getAwsCredentials(),
20 | awsRegion: authService.awsRegion,
21 | };
22 | }
23 |
24 | function getEdgeClientConfig(): DashboardClientConfiguration {
25 | const clientConfig = {
26 | endpoint: authService.getEdgeEndpoint(),
27 | credentials: () => authService.getAwsCredentials(),
28 | region: authService.awsRegion,
29 | disableHostPrefix: true,
30 | };
31 |
32 | const iotSiteWiseClient = new IoTSiteWiseClient(clientConfig);
33 | const iotEventsClient = new IoTEventsClient(clientConfig);
34 | const iotTwinMakerClient = new IoTTwinMakerClient(clientConfig);
35 |
36 | const clients = {
37 | iotSiteWiseClient,
38 | iotEventsClient,
39 | iotTwinMakerClient,
40 | };
41 |
42 | return clients;
43 | }
44 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboard/hooks/use-dashboard-query.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 |
3 | import { createDashboardQuery } from '~/data/dashboards';
4 | import { isFatal } from '~/helpers/predicates/is-fatal';
5 |
6 | import type { Dashboard } from '~/services';
7 |
8 | export function useDashboardQuery(dashboardId: Dashboard['id']) {
9 | return useQuery({
10 | ...createDashboardQuery(dashboardId),
11 | throwOnError: isFatal,
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboard/index.ts:
--------------------------------------------------------------------------------
1 | export { dashboardRoute } from './dashboard-route';
2 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboard/styles.css:
--------------------------------------------------------------------------------
1 | /* Prevent conflicting box sizing coming from Amplify */
2 | .dashboard * {
3 | box-sizing: initial;
4 | }
5 |
6 | .dashboard {
7 | height: 100% !important; /* TODO: add this to dashboard */
8 | }
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/components/empty-dashboards-table.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@cloudscape-design/components/box';
2 | import Button from '@cloudscape-design/components/button';
3 | import { FormattedMessage, useIntl } from 'react-intl';
4 | import { CREATE_DASHBOARD_HREF } from '~/constants';
5 | import { useApplication } from '~/hooks/application/use-application';
6 |
7 | export function EmptyDashboardsTable() {
8 | const { navigate } = useApplication();
9 | const intl = useIntl();
10 |
11 | return (
12 |
13 |
14 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/components/no-matches-dashboards-table.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@cloudscape-design/components/box';
2 | import Button from '@cloudscape-design/components/button';
3 | import { FormattedMessage } from 'react-intl';
4 |
5 | interface NoMatchDashboardsTableProps {
6 | onClick: () => void;
7 | }
8 |
9 | export function NoMatchDashboardsTable(props: NoMatchDashboardsTableProps) {
10 | return (
11 |
12 |
13 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/components/styles.css:
--------------------------------------------------------------------------------
1 | .getting-started-step1-icon {
2 | margin-top: 21px;
3 | }
4 |
5 | .getting-started-step2-icon {
6 | margin-top: 0px;
7 | }
8 |
9 | .getting-started-step3-icon {
10 | margin-top: 13px;
11 | }
12 |
13 | .getting-started-col-btn {
14 | position: absolute;
15 | bottom: 0;
16 | }
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/components/types.ts:
--------------------------------------------------------------------------------
1 | export interface gettingStartedColumnsTypes {
2 | columnTitle: string;
3 | icon: string;
4 | className: string;
5 | columnDescription: string;
6 | }
7 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/constants/text.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/apps/client/src/routes/dashboards/dashboards-index/constants/text.ts
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/dashboards-index-route.tsx:
--------------------------------------------------------------------------------
1 | import { DASHBOARDS_INDEX_PAGE_FORMAT } from '~/constants/format';
2 | import { DashboardsIndexPage } from './dashboards-index-page';
3 |
4 | import type { RouteObject } from 'react-router-dom';
5 |
6 | export const dashboardsIndexRoute = {
7 | index: true,
8 | element: ,
9 | handle: {
10 | format: DASHBOARDS_INDEX_PAGE_FORMAT,
11 | },
12 | } satisfies RouteObject;
13 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/hooks/use-dashboards-query.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 |
3 | import { DASHBOARDS_QUERY } from '~/data/dashboards';
4 |
5 | export function useDashboardsQuery() {
6 | return useQuery({
7 | ...DASHBOARDS_QUERY,
8 | });
9 | }
10 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/hooks/use-delete-dashboard-mutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query';
2 | import { bulkDeleteDashboards } from '~/services';
3 | import { invalidateDashboards } from '~/data/dashboards';
4 |
5 | export function useDeleteDashboardMutation() {
6 | return useMutation({
7 | mutationFn: bulkDeleteDashboards,
8 | onSuccess: () => void invalidateDashboards(),
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/hooks/use-delete-modal-visibility.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export function useDeleteModalVisibility() {
4 | return useState(false);
5 | }
6 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/hooks/use-migration-query.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 | import { MIGRATION_QUERY } from '~/data/migration';
3 |
4 | export function useMigrationQuery() {
5 | return useQuery({
6 | ...MIGRATION_QUERY,
7 | refetchOnWindowFocus: false,
8 | enabled: false, // only call this API on button click once using refetch()
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/hooks/use-migration-status-query.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 | import { MIGRATION_STATUS_QUERY } from '~/data/migration';
3 |
4 | export function useMigrationStatusQuery() {
5 | return useQuery({
6 | ...MIGRATION_STATUS_QUERY,
7 | refetchInterval: 5000,
8 | staleTime: 0,
9 | gcTime: 0,
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/hooks/use-table-preferences.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionPreferencesProps } from '@cloudscape-design/components';
2 | import useLocalStorage from 'react-use/lib/useLocalStorage';
3 |
4 | const PREFERENCES_KEY = 'table-preferences';
5 | const DEFAULT_PREFERENCES = {
6 | pageSize: 10,
7 | wrapLines: true,
8 | stripedRows: false,
9 | visibleContent: ['name', 'description', 'lastUpdateDate', 'creationDate'],
10 | };
11 | const NON_PREFERENCES = {
12 | // ensure that the id column is always visible
13 | visibleContent: ['id'],
14 | };
15 |
16 | /**
17 | * Hook to get and set table preferences in local storage.
18 | */
19 | export function useTablePreferences() {
20 | const [preferences = DEFAULT_PREFERENCES, setPreferences] = useLocalStorage<
21 | CollectionPreferencesProps['preferences']
22 | >(PREFERENCES_KEY, DEFAULT_PREFERENCES);
23 |
24 | return [
25 | {
26 | ...preferences,
27 | visibleContent: [
28 | ...NON_PREFERENCES.visibleContent,
29 | ...(preferences.visibleContent ?? []),
30 | ],
31 | },
32 | setPreferences,
33 | ] as const;
34 | }
35 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/index.ts:
--------------------------------------------------------------------------------
1 | export { dashboardsIndexRoute } from './dashboards-index-route';
2 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/dashboards-index/styles.css:
--------------------------------------------------------------------------------
1 | @import '../../../styles/variables.css';
2 |
3 | .btn-custom-primary {
4 | background: var(--colors-background-button) !important;
5 | border-color: var(--colors-background-button) !important;
6 | }
7 |
8 | .btn-custom-primary:hover {
9 | background: var(--colors-button-hover) !important;
10 | border-color: var(--colors-button-hover) !important;
11 | }
12 |
13 | .dashboard-table-filter div div {
14 | max-width: 100% !important;
15 | }
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/root-dashboards/index.ts:
--------------------------------------------------------------------------------
1 | export { rootDashboardsRoute } from './root-dashboards-route';
2 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/root-dashboards/root-dashboards-page.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 |
3 | export function RootDashboardsPage() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/client/src/routes/dashboards/root-dashboards/root-dashboards-route.tsx:
--------------------------------------------------------------------------------
1 | import { RootDashboardsPage } from './root-dashboards-page';
2 | import { dashboardsIndexRoute } from '../dashboards-index';
3 | import { dashboardRoute } from '../dashboard';
4 | import { createDashboardRoute } from '../create';
5 | import { DASHBOARDS_PATH, DASHBOARDS_HREF } from '~/constants';
6 |
7 | import type { RouteObject } from 'react-router-dom';
8 |
9 | export const rootDashboardsRoute = {
10 | path: DASHBOARDS_PATH,
11 | element: ,
12 | handle: {
13 | activeHref: DASHBOARDS_HREF,
14 | },
15 | children: [dashboardsIndexRoute, dashboardRoute, createDashboardRoute],
16 | } satisfies RouteObject;
17 |
--------------------------------------------------------------------------------
/apps/client/src/routes/edge-login/components/edge-password-field.tsx:
--------------------------------------------------------------------------------
1 | import FormField from '@cloudscape-design/components/form-field';
2 | import Input from '@cloudscape-design/components/input';
3 | import { Controller } from 'react-hook-form';
4 |
5 | import type { Control } from 'react-hook-form';
6 | import type { EdgeLoginFormValues } from '../hooks/use-edge-login-form';
7 | import { isJust } from '~/helpers/predicates/is-just';
8 |
9 | interface EdgePasswordFieldProps {
10 | control: Control;
11 | }
12 |
13 | export function EdgePasswordField(props: EdgePasswordFieldProps) {
14 | return (
15 | (
26 |
31 | field.onChange(event.detail.value)}
34 | value={field.value}
35 | type="password"
36 | placeholder="Enter password"
37 | />
38 |
39 | )}
40 | />
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/apps/client/src/routes/edge-login/components/edge-username-field.tsx:
--------------------------------------------------------------------------------
1 | import FormField from '@cloudscape-design/components/form-field';
2 | import Input from '@cloudscape-design/components/input';
3 | import { Controller } from 'react-hook-form';
4 |
5 | import type { Control } from 'react-hook-form';
6 | import type { EdgeLoginFormValues } from '../hooks/use-edge-login-form';
7 | import { isJust } from '~/helpers/predicates/is-just';
8 |
9 | interface EdgeUsernameFieldProps {
10 | control: Control;
11 | }
12 |
13 | export function EdgeUsernameField(props: EdgeUsernameFieldProps) {
14 | return (
15 | (
26 |
31 | field.onChange(event.detail.value)}
34 | value={field.value}
35 | placeholder="Enter username"
36 | />
37 |
38 | )}
39 | />
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/apps/client/src/routes/edge-login/edge-login-route.tsx:
--------------------------------------------------------------------------------
1 | import { type RouteObject } from 'react-router-dom';
2 | import { EDGE_LOGIN_PAGE_FORMAT } from '~/constants/format';
3 | import { EdgeLoginPage } from './edge-login-page';
4 |
5 | export const edgeLoginRoute = {
6 | path: 'edge-login',
7 | element: ,
8 | handle: {
9 | crumb: () => ({
10 | text: 'Edge login',
11 | href: '/edge-login',
12 | }),
13 | format: EDGE_LOGIN_PAGE_FORMAT,
14 | },
15 | } satisfies RouteObject;
16 |
--------------------------------------------------------------------------------
/apps/client/src/routes/edge-login/hooks/use-edge-login-form.ts:
--------------------------------------------------------------------------------
1 | import { useForm } from 'react-hook-form';
2 |
3 | export type EdgeAuthMechanisms = 'linux' | 'ldap';
4 |
5 | export interface EdgeLoginFormValues {
6 | username: string;
7 | password: string;
8 | authMechanism: EdgeAuthMechanisms;
9 | }
10 |
11 | export const DEFAULT_VALUES: EdgeLoginFormValues = {
12 | username: '',
13 | password: '',
14 | authMechanism: 'linux',
15 | };
16 |
17 | export function useEdgeLoginForm() {
18 | return useForm({ defaultValues: DEFAULT_VALUES });
19 | }
20 |
--------------------------------------------------------------------------------
/apps/client/src/routes/edge-login/hooks/use-edge-login-query.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 | import { createEdgeLoginQuery } from '~/data/edge-login';
3 | import { EdgeLoginBody } from '~/services/generated/models/EdgeLoginBody';
4 |
5 | export function useEdgeLoginQuery(requestBody: EdgeLoginBody) {
6 | return useQuery({
7 | ...createEdgeLoginQuery(requestBody),
8 | refetchOnWindowFocus: false,
9 | enabled: false, // only call this API on button click using refetch()
10 | retry: false,
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/apps/client/src/routes/root-index/index.ts:
--------------------------------------------------------------------------------
1 | export { rootIndexRoute, rootIndexEdgeRoute } from './root-index-route';
2 |
--------------------------------------------------------------------------------
/apps/client/src/routes/root-index/root-index-route.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate, type RouteObject } from 'react-router-dom';
2 | import {
3 | ROOT_INDEX_PAGE_FORMAT,
4 | EDGE_LOGIN_PAGE_FORMAT,
5 | } from '~/constants/format';
6 |
7 | export const rootIndexRoute = {
8 | index: true,
9 | element: ,
10 | handle: {
11 | format: ROOT_INDEX_PAGE_FORMAT,
12 | },
13 | } satisfies RouteObject;
14 |
15 | export const rootIndexEdgeRoute = {
16 | index: true,
17 | element: ,
18 | handle: {
19 | format: EDGE_LOGIN_PAGE_FORMAT,
20 | },
21 | } satisfies RouteObject;
22 |
--------------------------------------------------------------------------------
/apps/client/src/routes/root/components/root-error-boundary.spec.tsx:
--------------------------------------------------------------------------------
1 | import { vi } from 'vitest';
2 | import { render, screen } from '~/helpers/tests/testing-library';
3 | import userEvent from '@testing-library/user-event';
4 |
5 | import { RootErrorBoundary } from './root-error-boundary';
6 | import { ErrorBoundaryTester } from '~/helpers/tests/error-boundary-tester';
7 |
8 | describe('', () => {
9 | test('e2e error boundary usage', async () => {
10 | // prevent console.error from being displayed in the test output
11 | console.error = vi.fn();
12 | const user = userEvent.setup();
13 | render(
14 |
15 |
16 | ,
17 | );
18 |
19 | // children rendered as expected
20 | expect(screen.getByText('not an error fallback')).toBeVisible();
21 | expect(screen.queryByText('Something went wrong.')).not.toBeInTheDocument();
22 |
23 | // an error occurs (simulated)
24 | await user.click(screen.getByRole('button', { name: 'throw error' }));
25 |
26 | // error fallback is rendered
27 | expect(screen.getByText('Something went wrong.')).toBeVisible();
28 | expect(screen.queryByText('not an error fallback')).not.toBeInTheDocument();
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/apps/client/src/routes/root/components/root-error-boundary.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@cloudscape-design/components/box';
2 | import { ErrorBoundary } from 'react-error-boundary';
3 | import { FormattedMessage } from 'react-intl';
4 |
5 | import type { PropsWithChildren } from 'react';
6 |
7 | export function RootErrorBoundary(props: PropsWithChildren) {
8 | return (
9 | (
11 |
12 |
13 |
17 |
18 |
19 | )}
20 | >
21 | {props.children}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/apps/client/src/routes/root/index.ts:
--------------------------------------------------------------------------------
1 | export { rootRoute } from './root-route';
2 |
--------------------------------------------------------------------------------
/apps/client/src/routes/root/root-error-page.spec.tsx:
--------------------------------------------------------------------------------
1 | import { vi } from 'vitest';
2 | import { render, screen } from '~/helpers/tests/testing-library';
3 | import userEvent from '@testing-library/user-event';
4 |
5 | import { RootErrorPage } from './root-error-page';
6 |
7 | const navigateMock = vi.fn();
8 | vi.mock('~/hooks/application/use-application', () => ({
9 | useApplication: () => ({
10 | navigate: navigateMock,
11 | }),
12 | }));
13 |
14 | describe('', () => {
15 | it('should allow user to navigate to home page', async () => {
16 | const user = userEvent.setup();
17 | render();
18 |
19 | expect(
20 | screen.getByRole('heading', { name: 'Page not found' }),
21 | ).toBeVisible();
22 |
23 | await user.click(
24 | screen.getByRole('link', { name: 'IoT dashboard application home page' }),
25 | );
26 |
27 | expect(navigateMock).toHaveBeenCalledWith('/');
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/apps/client/src/routes/root/root-error-page.tsx:
--------------------------------------------------------------------------------
1 | import Container from '@cloudscape-design/components/container';
2 | import ContentLayout from '@cloudscape-design/components/content-layout';
3 | import Header from '@cloudscape-design/components/header';
4 | import Link from '@cloudscape-design/components/link';
5 | import { useIntl, FormattedMessage } from 'react-intl';
6 |
7 | import { useApplication } from '~/hooks/application/use-application';
8 |
9 | export function RootErrorPage() {
10 | const intl = useIntl();
11 | const { navigate } = useApplication();
12 |
13 | return (
14 |
23 |
27 |
28 | }
29 | >
30 |
31 | {
34 | event.preventDefault();
35 | navigate('/');
36 | }}
37 | >
38 |
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/apps/client/src/routes/root/root-page.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '~/helpers/tests/testing-library';
2 |
3 | import { RootPage } from './root-page';
4 | import { MemoryRouter, Route, Routes } from 'react-router-dom';
5 |
6 | describe('', () => {
7 | it('should render outlet', () => {
8 | render(
9 |
10 |
11 | }>
12 | test page} />
13 |
14 |
15 | ,
16 | );
17 |
18 | expect(screen.getByText('test page')).toBeVisible();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/apps/client/src/routes/root/root-page.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 |
3 | import { RootErrorBoundary } from './components/root-error-boundary';
4 | import { useDetect401Unauthorized } from '~/hooks/application/use-detect-401-unauthorized';
5 |
6 | export function RootPage() {
7 | useDetect401Unauthorized();
8 |
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/apps/client/src/routes/root/root-route.tsx:
--------------------------------------------------------------------------------
1 | import { RootPage } from './root-page';
2 | import { rootIndexRoute, rootIndexEdgeRoute } from '../root-index/index';
3 | import { rootDashboardsRoute } from '../dashboards/root-dashboards';
4 | import { edgeLoginRoute } from '../edge-login/edge-login-route';
5 | import { ROOT_PATH, ROOT_HREF } from '~/constants';
6 | import { getAuthMode } from '~/helpers/authMode';
7 |
8 | import type { RouteObject } from 'react-router-dom';
9 | import { RootErrorPage } from './root-error-page';
10 | import { Layout } from '~/layout/layout';
11 | import { intl } from '~/services';
12 |
13 | let children: RouteObject[] = [rootIndexRoute, rootDashboardsRoute];
14 |
15 | if (getAuthMode() === 'edge') {
16 | children = [
17 | rootIndexEdgeRoute,
18 | edgeLoginRoute,
19 | rootIndexRoute,
20 | rootDashboardsRoute,
21 | ];
22 | }
23 |
24 | export const rootRoute = {
25 | path: ROOT_PATH,
26 | element: (
27 |
28 |
29 |
30 | ),
31 | errorElement: (
32 |
33 |
34 |
35 | ),
36 | handle: {
37 | activeHref: ROOT_HREF,
38 | crumb: () => ({
39 | text: intl.formatMessage({
40 | defaultMessage: 'Home',
41 | description: 'root route breadcrumb text',
42 | }),
43 | href: ROOT_HREF,
44 | }),
45 | },
46 | children,
47 | } satisfies RouteObject;
48 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/core/ApiError.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | import type { ApiRequestOptions } from './ApiRequestOptions';
5 | import type { ApiResult } from './ApiResult';
6 |
7 | export class ApiError extends Error {
8 | public readonly url: string;
9 | public readonly status: number;
10 | public readonly statusText: string;
11 | public readonly body: any;
12 | public readonly request: ApiRequestOptions;
13 |
14 | constructor(
15 | request: ApiRequestOptions,
16 | response: ApiResult,
17 | message: string,
18 | ) {
19 | super(message);
20 |
21 | this.name = 'ApiError';
22 | this.url = response.url;
23 | this.status = response.status;
24 | this.statusText = response.statusText;
25 | this.body = response.body;
26 | this.request = request;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/core/ApiRequestOptions.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | export type ApiRequestOptions = {
5 | readonly method:
6 | | 'GET'
7 | | 'PUT'
8 | | 'POST'
9 | | 'DELETE'
10 | | 'OPTIONS'
11 | | 'HEAD'
12 | | 'PATCH';
13 | readonly url: string;
14 | readonly path?: Record;
15 | readonly cookies?: Record;
16 | readonly headers?: Record;
17 | readonly query?: Record;
18 | readonly formData?: Record;
19 | readonly body?: any;
20 | readonly mediaType?: string;
21 | readonly responseHeader?: string;
22 | readonly errors?: Record;
23 | };
24 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/core/ApiResult.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | export type ApiResult = {
5 | readonly url: string;
6 | readonly ok: boolean;
7 | readonly status: number;
8 | readonly statusText: string;
9 | readonly body: any;
10 | };
11 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/core/OpenAPI.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | import type { ApiRequestOptions } from './ApiRequestOptions';
5 |
6 | type Resolver = (options: ApiRequestOptions) => Promise;
7 | type Headers = Record;
8 |
9 | export type OpenAPIConfig = {
10 | BASE: string;
11 | VERSION: string;
12 | WITH_CREDENTIALS: boolean;
13 | CREDENTIALS: 'include' | 'omit' | 'same-origin';
14 | TOKEN?: string | Resolver;
15 | USERNAME?: string | Resolver;
16 | PASSWORD?: string | Resolver;
17 | HEADERS?: Headers | Resolver;
18 | ENCODE_PATH?: (path: string) => string;
19 | };
20 |
21 | export const OpenAPI: OpenAPIConfig = {
22 | BASE: '',
23 | VERSION: '1.0',
24 | WITH_CREDENTIALS: false,
25 | CREDENTIALS: 'include',
26 | TOKEN: undefined,
27 | USERNAME: undefined,
28 | PASSWORD: undefined,
29 | HEADERS: undefined,
30 | ENCODE_PATH: undefined,
31 | };
32 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/models/BulkDeleteDashboardDto.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 |
5 | export type BulkDeleteDashboardDto = {
6 | ids: Array;
7 | };
8 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/models/CreateDashboardDto.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 |
5 | import type { DashboardDefinition } from './DashboardDefinition';
6 |
7 | export type CreateDashboardDto = {
8 | name: string;
9 | description: string;
10 | definition: DashboardDefinition;
11 | };
12 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/models/Dashboard.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 |
5 | import type { DashboardDefinition } from './DashboardDefinition';
6 |
7 | export type Dashboard = {
8 | id: string;
9 | name: string;
10 | description: string;
11 | definition: DashboardDefinition;
12 | creationDate: string;
13 | lastUpdateDate: string;
14 | };
15 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/models/DashboardDefinition.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 |
5 | import type { DashboardWidget } from './DashboardWidget';
6 |
7 | export type DashboardDefinition = {
8 | widgets: Array;
9 | };
10 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/models/DashboardSummary.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 |
5 | export type DashboardSummary = {
6 | id: string;
7 | name: string;
8 | description: string;
9 | creationDate: string;
10 | lastUpdateDate: string;
11 | };
12 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/models/DashboardWidget.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 |
5 | export type DashboardWidget = {
6 | type:
7 | | 'xy-plot'
8 | | 'line-scatter-chart'
9 | | 'line-chart'
10 | | 'scatter-chart'
11 | | 'bar-chart'
12 | | 'kpi'
13 | | 'gauge'
14 | | 'status'
15 | | 'status-timeline'
16 | | 'table'
17 | | 'text';
18 | /**
19 | * Unique identifier of the widget.
20 | */
21 | id: string;
22 | /**
23 | * X position of the widget relative to grid.
24 | */
25 | x: number;
26 | /**
27 | * Y position of the widget relative to grid.
28 | */
29 | y: number;
30 | /**
31 | * Z position of the widget relative to grid.
32 | */
33 | z: number;
34 | /**
35 | * Width of the widget.
36 | */
37 | width: number;
38 | /**
39 | * Height of the widget.
40 | */
41 | height: number;
42 | /**
43 | * Widget properties. Depends on the widget type. Currently, it is not
44 | * validated.
45 | */
46 | properties: Record;
47 | };
48 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/models/EdgeCredentials.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 |
5 | export type EdgeCredentials = {
6 | accessKeyId: string;
7 | secretAccessKey: string;
8 | sessionToken?: string;
9 | sessionExpiryTime?: string;
10 | }
11 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/models/EdgeLoginBody.ts:
--------------------------------------------------------------------------------
1 | export type EdgeLoginBody = {
2 | username: string;
3 | password: string;
4 | authMechanism: string;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/models/MigrationStatus.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 |
5 | export enum Status {
6 | NOT_STARTED = 'not-started',
7 | IN_PROGRESS = 'in-progress',
8 | COMPLETE = 'complete',
9 | COMPLETE_NONE_CREATED = 'complete-none-created',
10 | ERROR = 'error'
11 | }
12 |
13 | export type MigrationStatus = {
14 | status: Status;
15 | message?: string;
16 | }
17 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/models/UpdateDashboardDto.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 |
5 | import type { DashboardDefinition } from './DashboardDefinition';
6 |
7 | export type UpdateDashboardDto = {
8 | name?: string;
9 | description?: string;
10 | definition?: DashboardDefinition;
11 | };
12 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/schemas/$BulkDeleteDashboardDto.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | export const $BulkDeleteDashboardDto = {
5 | properties: {
6 | ids: {
7 | type: 'array',
8 | contains: {
9 | type: 'string',
10 | },
11 | isRequired: true,
12 | },
13 | },
14 | } as const;
15 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/schemas/$CreateDashboardDto.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | export const $CreateDashboardDto = {
5 | properties: {
6 | name: {
7 | type: 'string',
8 | isRequired: true,
9 | maxLength: 256,
10 | minLength: 1,
11 | },
12 | description: {
13 | type: 'string',
14 | isRequired: true,
15 | maxLength: 200,
16 | minLength: 1,
17 | },
18 | definition: {
19 | type: 'DashboardDefinition',
20 | isRequired: true,
21 | },
22 | },
23 | } as const;
24 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/schemas/$Dashboard.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | export const $Dashboard = {
5 | properties: {
6 | id: {
7 | type: 'string',
8 | isRequired: true,
9 | maxLength: 12,
10 | minLength: 12,
11 | },
12 | name: {
13 | type: 'string',
14 | isRequired: true,
15 | maxLength: 256,
16 | minLength: 1,
17 | },
18 | description: {
19 | type: 'string',
20 | isRequired: true,
21 | maxLength: 200,
22 | minLength: 1,
23 | },
24 | definition: {
25 | type: 'DashboardDefinition',
26 | isRequired: true,
27 | },
28 | creationDate: {
29 | type: 'string',
30 | isRequired: true,
31 | },
32 | lastUpdateDate: {
33 | type: 'string',
34 | isRequired: true,
35 | },
36 | },
37 | } as const;
38 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/schemas/$DashboardDefinition.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | export const $DashboardDefinition = {
5 | properties: {
6 | widgets: {
7 | type: 'array',
8 | contains: {
9 | type: 'DashboardWidget',
10 | },
11 | isRequired: true,
12 | },
13 | },
14 | } as const;
15 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/schemas/$DashboardSummary.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | export const $DashboardSummary = {
5 | properties: {
6 | id: {
7 | type: 'string',
8 | isRequired: true,
9 | maxLength: 12,
10 | minLength: 12,
11 | },
12 | name: {
13 | type: 'string',
14 | isRequired: true,
15 | maxLength: 256,
16 | minLength: 1,
17 | },
18 | description: {
19 | type: 'string',
20 | isRequired: true,
21 | maxLength: 200,
22 | minLength: 1,
23 | },
24 | creationDate: {
25 | type: 'string',
26 | isRequired: true,
27 | },
28 | lastUpdateDate: {
29 | type: 'string',
30 | isRequired: true,
31 | },
32 | },
33 | } as const;
34 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/schemas/$DashboardWidget.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | export const $DashboardWidget = {
5 | properties: {
6 | type: {
7 | type: 'Enum',
8 | isRequired: true,
9 | },
10 | id: {
11 | type: 'string',
12 | description: `Unique identifier of the widget.`,
13 | isRequired: true,
14 | },
15 | x: {
16 | type: 'number',
17 | description: `X position of the widget relative to grid.`,
18 | isRequired: true,
19 | },
20 | y: {
21 | type: 'number',
22 | description: `Y position of the widget relative to grid.`,
23 | isRequired: true,
24 | },
25 | z: {
26 | type: 'number',
27 | description: `Z position of the widget relative to grid.`,
28 | isRequired: true,
29 | },
30 | width: {
31 | type: 'number',
32 | description: `Width of the widget.`,
33 | isRequired: true,
34 | },
35 | height: {
36 | type: 'number',
37 | description: `Height of the widget.`,
38 | isRequired: true,
39 | },
40 | properties: {
41 | type: 'dictionary',
42 | contains: {
43 | properties: {},
44 | },
45 | isRequired: true,
46 | },
47 | },
48 | } as const;
49 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/schemas/$UpdateDashboardDto.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | export const $UpdateDashboardDto = {
5 | properties: {
6 | name: {
7 | type: 'string',
8 | maxLength: 256,
9 | minLength: 1,
10 | },
11 | description: {
12 | type: 'string',
13 | maxLength: 200,
14 | minLength: 1,
15 | },
16 | definition: {
17 | type: 'DashboardDefinition',
18 | },
19 | },
20 | } as const;
21 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/services/DefaultService.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | import type { CancelablePromise } from '../core/CancelablePromise';
5 | import { OpenAPI } from '../core/OpenAPI';
6 | import { request as __request } from '../core/request';
7 |
8 | export class DefaultService {
9 | /**
10 | * @returns any The Health Check is successful
11 | * @throws ApiError
12 | */
13 | public static healthControllerCheck(): CancelablePromise<{
14 | status?: string;
15 | info?: Record> | null;
16 | error?: Record> | null;
17 | details?: Record>;
18 | }> {
19 | return __request(OpenAPI, {
20 | method: 'GET',
21 | url: '/health',
22 | errors: {
23 | 503: `The Health Check is not successful`,
24 | },
25 | });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/services/EdgeLoginService.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | import type { CancelablePromise } from '../core/CancelablePromise';
5 | import { OpenAPI } from '../core/OpenAPI';
6 | import { request as __request } from '../core/request';
7 | import { EdgeCredentials } from '../models/EdgeCredentials';
8 | import { EdgeLoginBody } from '../models/EdgeLoginBody';
9 |
10 | export class EdgeLoginService {
11 | /**
12 | * @returns void
13 | * @throws ApiError
14 | */
15 | public static edgeLogin(requestBody: EdgeLoginBody): CancelablePromise {
16 | return __request(OpenAPI, {
17 | method: 'POST',
18 | url: '/edge-login',
19 | body: requestBody,
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/apps/client/src/services/generated/services/MigrationService.ts:
--------------------------------------------------------------------------------
1 | /* istanbul ignore file */
2 | /* tslint:disable */
3 | /* eslint-disable */
4 | import type { CancelablePromise } from '../core/CancelablePromise';
5 | import { OpenAPI } from '../core/OpenAPI';
6 | import { request as __request } from '../core/request';
7 | import { MigrationStatus } from '../models/MigrationStatus';
8 |
9 | export class MigrationService {
10 | /**
11 | * @returns void
12 | * @throws ApiError
13 | */
14 | public static migrationControllerMigration(): CancelablePromise {
15 | return __request(OpenAPI, {
16 | method: 'POST',
17 | url: '/migration',
18 | });
19 | }
20 |
21 | /**
22 | * @returns MigrationStatus
23 | * @throws ApiError
24 | */
25 | public static migrationControllerGetMigrationStatus(): CancelablePromise {
26 | return __request(OpenAPI, {
27 | method: 'GET',
28 | url: '/migration',
29 | });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/apps/client/src/services/intl.ts:
--------------------------------------------------------------------------------
1 | import { createIntl, createIntlCache } from 'react-intl';
2 |
3 | const cache = createIntlCache();
4 |
5 | export const intl = createIntl(
6 | {
7 | locale: 'en',
8 | messages: {},
9 | },
10 | cache,
11 | );
12 |
--------------------------------------------------------------------------------
/apps/client/src/services/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DashboardsService,
3 | EdgeLoginService,
4 | MigrationService,
5 | } from './generated';
6 |
7 | // Dashboard API
8 | export type ListDashboards = typeof DashboardsService.dashboardsControllerList;
9 | export type CreateDashboard =
10 | typeof DashboardsService.dashboardsControllerCreate;
11 | export type ReadDashboard = typeof DashboardsService.dashboardsControllerRead;
12 | export type UpdateDashboard =
13 | typeof DashboardsService.dashboardsControllerUpdate;
14 | export type DeleteDashboard =
15 | typeof DashboardsService.dashboardsControllerDelete;
16 | export type BulkDeleteDashboards =
17 | typeof DashboardsService.dashboardsControllerBulkDelete;
18 |
19 | // Migration API
20 | export type DashboardMigration =
21 | typeof MigrationService.migrationControllerMigration;
22 | export type DashboardMigrationStatus =
23 | typeof MigrationService.migrationControllerGetMigrationStatus;
24 |
25 | // EdgeLogin API
26 | export type EdgeLogin = typeof EdgeLoginService.edgeLogin;
27 |
--------------------------------------------------------------------------------
/apps/client/src/store/navigation/index.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai';
2 |
3 | /**
4 | * Navigation visibility store
5 | *
6 | * Do not export.
7 | */
8 | const isNavigationVisibleBaseAtom = atom(false);
9 |
10 | /**
11 | * Readonly navigation visibility
12 | *
13 | * Use for all navigation visibility checks.
14 | */
15 | export const isNavigationVisibleAtom = atom((get) =>
16 | get(isNavigationVisibleBaseAtom),
17 | );
18 |
19 | /** Set isNavigationVisible in store */
20 | export const setIsNavigationVisibleAtom = atom(
21 | null,
22 | (_get, set, isVisible: boolean) => {
23 | set(isNavigationVisibleBaseAtom, isVisible);
24 | },
25 | );
26 |
--------------------------------------------------------------------------------
/apps/client/src/store/viewMode/index.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai';
2 |
3 | /**
4 | * edit mode visibility store
5 | */
6 | const isEditModeVisible = atom(false);
7 |
8 | /**
9 | * Readonly edit mode visibility
10 | */
11 | export const getDashboardEditMode = atom((get) => get(isEditModeVisible));
12 |
13 | /** Set isEditModeVisible in store */
14 | export const setDashboardEditMode = atom(
15 | null,
16 | (_get, set, isVisible: boolean) => {
17 | set(isEditModeVisible, isVisible);
18 | },
19 | );
20 |
--------------------------------------------------------------------------------
/apps/client/src/structures/notifications/error-notification.spec.ts:
--------------------------------------------------------------------------------
1 | import { ErrorNotification } from './error-notification';
2 | import { Notification } from './notification';
3 |
4 | describe('ErrorNotification', () => {
5 | it('should create an error notification', () => {
6 | const notification = new ErrorNotification('test');
7 |
8 | expect(notification).toBeInstanceOf(Notification);
9 | expect(notification.type).toBe('error');
10 | expect(notification.content).toBe('test');
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/apps/client/src/structures/notifications/error-notification.ts:
--------------------------------------------------------------------------------
1 | import { Notification } from './notification';
2 |
3 | export class ErrorNotification extends Notification {
4 | constructor(content: string) {
5 | super('error', content);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/apps/client/src/structures/notifications/generic-error-notification.spec.ts:
--------------------------------------------------------------------------------
1 | import { ErrorNotification } from './error-notification';
2 | import { GenericErrorNotification } from './generic-error-notification';
3 | import { Notification } from './notification';
4 |
5 | describe('GenericErrorNotification', () => {
6 | it('should create a generic error notification', () => {
7 | const notification = new GenericErrorNotification(new Error('test'));
8 |
9 | expect(notification).toBeInstanceOf(ErrorNotification);
10 | expect(notification).toBeInstanceOf(Notification);
11 | expect(notification.type).toBe('error');
12 | expect(notification.content).toBe('test');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/apps/client/src/structures/notifications/generic-error-notification.ts:
--------------------------------------------------------------------------------
1 | import { ErrorNotification } from './error-notification';
2 |
3 | export class GenericErrorNotification extends ErrorNotification {
4 | constructor(error: Error) {
5 | super(error.message);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/apps/client/src/structures/notifications/info-notification.spec.ts:
--------------------------------------------------------------------------------
1 | import { Notification } from './notification';
2 | import { InfoNotification } from './info-notification';
3 |
4 | describe('InfoNotification', () => {
5 | it('should create an info notification', () => {
6 | const notification = new InfoNotification('test');
7 |
8 | expect(notification).toBeInstanceOf(Notification);
9 | expect(notification.type).toBe('info');
10 | expect(notification.content).toBe('test');
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/apps/client/src/structures/notifications/info-notification.ts:
--------------------------------------------------------------------------------
1 | import { Notification } from './notification';
2 |
3 | export class InfoNotification extends Notification {
4 | constructor(content: string) {
5 | super('info', content);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/apps/client/src/structures/notifications/loading-notification.spec.ts:
--------------------------------------------------------------------------------
1 | import { Notification } from './notification';
2 | import { LoadingNotification } from './loading-notification';
3 |
4 | describe('LoadingNotification', () => {
5 | it('should create a loading notification', () => {
6 | const notification = new LoadingNotification('test');
7 |
8 | expect(notification).toBeInstanceOf(Notification);
9 | expect(notification.type).toBe('success');
10 | expect(notification.loading).toBe(true);
11 | expect(notification.content).toBe('test');
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/apps/client/src/structures/notifications/loading-notification.ts:
--------------------------------------------------------------------------------
1 | import { Notification } from './notification';
2 |
3 | export class LoadingNotification extends Notification {
4 | constructor(content: string) {
5 | super('success', content, true);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/apps/client/src/structures/notifications/notification.spec.ts:
--------------------------------------------------------------------------------
1 | import { Notification } from './notification';
2 |
3 | describe('Notification', () => {
4 | it.each(['info', 'success', 'warning', 'error'] as const)(
5 | 'should create a notification with type %s',
6 | (type) => {
7 | const notification = new Notification(type, 'test');
8 |
9 | expect(notification.type).toBe(type);
10 | expect(notification.content).toBe('test');
11 | },
12 | );
13 | });
14 |
--------------------------------------------------------------------------------
/apps/client/src/structures/notifications/notification.ts:
--------------------------------------------------------------------------------
1 | import { NotificationViewModel } from '~/types/notification-view-model';
2 |
3 | export class Notification {
4 | constructor(
5 | public readonly type: NotificationViewModel['type'],
6 | public readonly content: NotificationViewModel['content'],
7 | public readonly loading?: NotificationViewModel['loading'],
8 | ) {}
9 | }
10 |
--------------------------------------------------------------------------------
/apps/client/src/structures/notifications/success-notification.spec.ts:
--------------------------------------------------------------------------------
1 | import { Notification } from './notification';
2 | import { SuccessNotification } from './success-notification';
3 |
4 | describe('SuccessNotification', () => {
5 | it('should create a success notification', () => {
6 | const notification = new SuccessNotification('test');
7 |
8 | expect(notification).toBeInstanceOf(Notification);
9 | expect(notification.type).toBe('success');
10 | expect(notification.content).toBe('test');
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/apps/client/src/structures/notifications/success-notification.ts:
--------------------------------------------------------------------------------
1 | import { Notification } from './notification';
2 |
3 | export class SuccessNotification extends Notification {
4 | constructor(content: string) {
5 | super('success', content);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/apps/client/src/styles/variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --colors-background-button: #ff9900;
3 | --colors-button-hover: #EC7211;
4 | }
5 |
--------------------------------------------------------------------------------
/apps/client/src/test/setup.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { afterEach } from 'vitest';
3 | import { cleanup } from '@testing-library/react';
4 | import 'aws-sdk-client-mock-jest';
5 |
6 | afterEach(() => {
7 | cleanup();
8 | });
9 |
--------------------------------------------------------------------------------
/apps/client/src/types/content-density.ts:
--------------------------------------------------------------------------------
1 | export type Comfortable = 'comfortable';
2 | export type Compact = 'compact';
3 |
4 | export type ContentDensity = Comfortable | Compact;
5 |
--------------------------------------------------------------------------------
/apps/client/src/types/crumb.ts:
--------------------------------------------------------------------------------
1 | export interface Crumb {
2 | text: string;
3 | href: string;
4 | }
5 |
6 | export type GetCrumb = (data?: T) => Crumb;
7 |
8 | export interface Crumbly {
9 | crumb: GetCrumb;
10 | }
11 |
--------------------------------------------------------------------------------
/apps/client/src/types/data.ts:
--------------------------------------------------------------------------------
1 | export interface DataBound {
2 | data: T;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/client/src/types/dimensional.ts:
--------------------------------------------------------------------------------
1 | export type FullWidth = boolean;
2 |
3 | type GetFullWidth = (data?: T) => FullWidth;
4 |
5 | export interface Dimensional {
6 | fullWidth: GetFullWidth;
7 | }
8 |
--------------------------------------------------------------------------------
/apps/client/src/types/fatal-status-code.ts:
--------------------------------------------------------------------------------
1 | export type FatalStatusCode =
2 | | 500
3 | | 501
4 | | 502
5 | | 503
6 | | 504
7 | | 505
8 | | 506
9 | | 507
10 | | 508
11 | | 510
12 | | 511;
13 |
--------------------------------------------------------------------------------
/apps/client/src/types/format.ts:
--------------------------------------------------------------------------------
1 | export type Format = 'default' | 'cards' | 'form' | 'table';
2 |
3 | export interface Formatted {
4 | format: Format;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/client/src/types/handle.ts:
--------------------------------------------------------------------------------
1 | export type Handle = T;
2 |
3 | export interface Handleable {
4 | handle: Handle;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/client/src/types/href.ts:
--------------------------------------------------------------------------------
1 | export interface WithActiveHref {
2 | activeHref: string;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/client/src/types/identifiable.ts:
--------------------------------------------------------------------------------
1 | export interface Identifiable {
2 | id: string;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/client/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './content-density';
2 | export * from './crumb';
3 | export * from './data';
4 | export * from './dimensional';
5 | export * from './format';
6 | export * from './handle';
7 | export * from './href';
8 | export * from './identifiable';
9 | export * from './lists';
10 | export * from './maybe';
11 | export * from './notification-view-model';
12 | export * from './predicates';
13 | export * from './metadata';
14 |
--------------------------------------------------------------------------------
/apps/client/src/types/lists.ts:
--------------------------------------------------------------------------------
1 | export type NonEmptyList = [T, ...T[]];
2 |
--------------------------------------------------------------------------------
/apps/client/src/types/maybe.ts:
--------------------------------------------------------------------------------
1 | export type Just = T;
2 | export type Nothing = null | undefined;
3 |
4 | export type Maybe = Just | Nothing;
5 |
--------------------------------------------------------------------------------
/apps/client/src/types/metadata.ts:
--------------------------------------------------------------------------------
1 | export interface Metadata {
2 | applicationName: string;
3 | authenticationFlowType: string;
4 | authMode: string;
5 | awsAccessKeyId: string;
6 | awsSecretAccessKey: string;
7 | awsSessionToken: string;
8 | cognitoEndpoint: string;
9 | domainName?: string;
10 | edgeEndpoint?: string;
11 | identityPoolId: string;
12 | logMode: string;
13 | metricsMode: string;
14 | region: string;
15 | userPoolId: string;
16 | userPoolWebClientId: string;
17 | }
18 |
--------------------------------------------------------------------------------
/apps/client/src/types/notification-view-model.ts:
--------------------------------------------------------------------------------
1 | import type { FlashbarProps } from '@cloudscape-design/components/flashbar';
2 | import type { SetRequired } from 'type-fest';
3 |
4 | type FlashbarItem = FlashbarProps['items'][number];
5 |
6 | // ID made required as it is utilized in notification features
7 | export type NotificationViewModel = SetRequired;
8 |
--------------------------------------------------------------------------------
/apps/client/src/types/predicates.ts:
--------------------------------------------------------------------------------
1 | export type TypePredicate = (a: T) => a is U;
2 | export type NonTypePredicate = (a: T) => boolean;
3 |
4 | export type Predicate =
5 | | TypePredicate
6 | | NonTypePredicate;
7 |
--------------------------------------------------------------------------------
/apps/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "jsx": "react-jsx",
6 | "lib": ["dom", "dom.iterable", "es2020"],
7 | "outDir": "./dist",
8 | "module": "es2020",
9 | "rootDir": "./src",
10 | "types": ["node", "vite/client", "vitest/importMeta"],
11 | "paths": {
12 | "~/*": ["src/*"]
13 | }
14 | },
15 | "display": "Client",
16 | "exclude": ["node_modules"],
17 | "extends": "tsconfig/base.json",
18 | "include": ["src", "src/test/setup.ts"]
19 | }
20 |
--------------------------------------------------------------------------------
/apps/client/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/apps/core/.cognito/README.md:
--------------------------------------------------------------------------------
1 | # cognito-local configuration
2 |
3 | This directory provides static cognito-local configurations for local development purpose. To learn more about the configurations, see [cognito-local readme](https://www.npmjs.com/package/cognito-local?activeTab=readme#configuration).
4 |
5 | ## Preconfigured resources
6 |
7 | 1. a User Pool with ID `us-west-2_h23TJjQR9`, see file `./db/us-west-2_h23TJjQR9.json`
8 | 1. a User with username `test-user`, see file `./db/us-west-2_h23TJjQR9.json`
9 | 1. a User Pool Client with ID `9cehli62qxmki9mg5adjmucuq`, see file `./db/clients.json`
--------------------------------------------------------------------------------
/apps/core/.cognito/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "TokenConfig": {
3 | "IssuerDomain": "https://cognito-idp.us-west-2.amazonaws.com"
4 | }
5 | }
--------------------------------------------------------------------------------
/apps/core/.cognito/db/clients.json:
--------------------------------------------------------------------------------
1 | {
2 | "Clients": {
3 | "9cehli62qxmki9mg5adjmucuq": {
4 | "ClientId": "9cehli62qxmki9mg5adjmucuq",
5 | "ClientName": "test-client",
6 | "CreationDate": "2023-02-18T10:21:19.518Z",
7 | "LastModifiedDate": "2023-02-18T10:21:19.518Z",
8 | "TokenValidityUnits": {
9 | "AccessToken": "hours",
10 | "IdToken": "minutes",
11 | "RefreshToken": "days"
12 | },
13 | "UserPoolId": "us-west-2_h23TJjQR9"
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/apps/core/.env:
--------------------------------------------------------------------------------
1 | APPLICATION_NAME=IotApp
2 | AWS_ACCESS_KEY_ID=fakeMyKeyId
3 | AWS_REGION=us-west-2
4 | AWS_SECRET_ACCESS_KEY=fakeSecretAccessKey
5 | AWS_SESSION_TOKEN=
6 | COGNITO_IDENTITY_POOL_ID=fakeIdentiyPoolId
7 | COGNITO_USE_LOCAL_VERIFIER=true
8 | COGNITO_USER_POOL_ID=us-west-2_h23TJjQR9
9 | COGNITO_USER_POOL_CLIENT_ID=9cehli62qxmki9mg5adjmucuq
10 | COGNITO_DOMAIN_NAME=https://test.auth.us-west-2.amazoncognito.com
11 | DATABASE_ENDPOINT=http://127.0.0.1:8000
12 | DATABASE_LAUNCH_LOCAL=true
13 | DATABASE_PORT=8000
14 | DATABASE_TABLE_NAME=ApiResourceTable
15 | NODE_ENV=development
16 | AUTH_MODE=cognito
17 | # WebPack Server, Local Cognito and DDB endpoints, and AWS endpoints
18 | SERVICE_ENDPOINTS='ws://localhost:3001 http://localhost:9229 http://localhost:8000 https://*.amazonaws.com'
19 |
--------------------------------------------------------------------------------
/apps/core/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | node: true,
4 | },
5 | extends: ['custom', 'plugin:jest/recommended'],
6 | parserOptions: {
7 | ecmaVersion: 2020,
8 | project: 'tsconfig.json',
9 | tsconfigRootDir: __dirname,
10 | sourceType: 'module',
11 | ecmaFeatures: {
12 | jsx: false,
13 | },
14 | },
15 | plugins: ['jest'],
16 | overrides: [
17 | // NestJS modules are often empty
18 | {
19 | files: ['*.module.ts'],
20 | rules: {
21 | '@typescript-eslint/no-extraneous-class': 'off',
22 | },
23 | },
24 | ],
25 | root: true,
26 | };
27 |
--------------------------------------------------------------------------------
/apps/core/.example.edge.env:
--------------------------------------------------------------------------------
1 | EDGE_ENDPOINT=xxxx
2 |
3 | APPLICATION_NAME=IotApp
4 | AUTH_MODE=edge
5 | AWS_ACCESS_KEY_ID=fakeMyKeyId
6 | AWS_REGION=edge
7 | AWS_SECRET_ACCESS_KEY=fakeSecretAccessKey
8 | AWS_SESSION_TOKEN=
9 | COGNITO_IDENTITY_POOL_ID=fakeIdentiyPoolId
10 | COGNITO_USE_LOCAL_VERIFIER=true
11 | COGNITO_USER_POOL_ID=us-west-2_h23TJjQR9
12 | COGNITO_USER_POOL_CLIENT_ID=9cehli62qxmki9mg5adjmucuq
13 | COGNITO_DOMAIN_NAME=https://test.auth.us-west-2.amazoncognito.com
14 | DATABASE_ENDPOINT=http://127.0.0.1:8000
15 | DATABASE_LAUNCH_LOCAL=true
16 | DATABASE_PORT=8000
17 | DATABASE_TABLE_NAME=ApiResourceTable
18 | NODE_ENV=development
19 | # WebPack Server, Local Cognito and DDB endpoints, and AWS endpoints
20 | SERVICE_ENDPOINTS='ws://localhost:3001 http://localhost:9229 http://localhost:8000 https://*.amazonaws.com'
21 |
--------------------------------------------------------------------------------
/apps/core/api-resource-table-properties.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | TableName: 'ApiResourceTable',
3 | KeySchema: [
4 | { AttributeName: 'id', KeyType: 'HASH' },
5 | { AttributeName: 'resourceType', KeyType: 'RANGE' }
6 | ],
7 | AttributeDefinitions: [
8 | { AttributeName: 'id', AttributeType: 'S' },
9 | { AttributeName: 'resourceType', AttributeType: 'S' },
10 | ],
11 | BillingMode: 'PAY_PER_REQUEST',
12 | GlobalSecondaryIndexes: [
13 | {
14 | IndexName: 'resourceTypeIndex',
15 | KeySchema: [
16 | { AttributeName: 'resourceType', KeyType: 'HASH' },
17 | ],
18 | Projection: {
19 | ProjectionType: 'ALL',
20 | },
21 | },
22 | ],
23 | };
24 |
--------------------------------------------------------------------------------
/apps/core/dynamodb-local-dir.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This script prints the installation directory of dynamodb-local to `stdout`.
3 | * It allows us to use the output in command substitution. For example, the
4 | * command `ls $(node dynamodb-local-dir.js)` lists contents under the
5 | * installation directory of dynamodb-local.
6 | */
7 |
8 | 'use strict';
9 |
10 | const os = require('os');
11 | const path = require('path');
12 |
13 | // reference to https://github.com/rynop/dynamodb-local/blob/1cca305c077bd600ad972569e42647d17782e921/index.js#L17
14 | console.log(path.join(os.tmpdir(), 'dynamodb-local'));
15 |
--------------------------------------------------------------------------------
/apps/core/jest-dynamodb-config.js:
--------------------------------------------------------------------------------
1 | const apiResourceTable = require('./api-resource-table-properties.js');
2 |
3 | module.exports = {
4 | port: 8001,
5 | tables: [
6 | {
7 | ...apiResourceTable,
8 | TableName: 'dashboard-api-e2e-test',
9 | }
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/apps/core/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'jest';
2 |
3 | import baseConfig from 'jest-config/base';
4 |
5 | const jestDynamodb = require('@shelf/jest-dynamodb/jest-preset');
6 |
7 | const config: Config = {
8 | ...baseConfig,
9 | //https://jestjs.io/docs/dynamodb
10 | ...jestDynamodb,
11 | collectCoverageFrom: [
12 | '**/src/**/*.{js,ts}',
13 | '!./src/main.ts',
14 | '!./src/repl.ts',
15 | '!./src/app.module.ts',
16 | '!./src/lifecycle-hooks/dynamodb-local-setup.ts',
17 | '!./src/config/local-cognito-jwt-verifier.ts',
18 | '!./src/testing/**/*',
19 | ],
20 | displayName: 'Core',
21 | moduleFileExtensions: ['js', 'json', 'ts'],
22 | testEnvironment: 'node',
23 | transform: {
24 | '^.*\\.ts$': 'ts-jest',
25 | },
26 | moduleNameMapper: {
27 | // Force module uuid to resolve with the CJS entry point, because Jest does not support package.json.exports. See https://github.com/uuidjs/uuid/issues/451
28 | "^uuid$": "uuid",
29 | },
30 |
31 | };
32 |
33 | export default config;
34 |
--------------------------------------------------------------------------------
/apps/core/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/nest-cli",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src",
5 | "compilerOptions": {
6 | "deleteOutDir": true,
7 | "plugins": [
8 | {
9 | "name": "@nestjs/swagger",
10 | "options": {
11 | "introspectComments": true
12 | }
13 | }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/apps/core/src/auth/public.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 |
3 | export const isPublicMetadataKey = 'isPublic';
4 |
5 | /**
6 | * Decorator to mark controller classes or handlers as publicly accessible without authorization.
7 | * @returns
8 | */
9 | export const Public = () => SetMetadata(isPublicMetadataKey, true);
10 |
--------------------------------------------------------------------------------
/apps/core/src/bootstrap/docs.bootstrap.ts:
--------------------------------------------------------------------------------
1 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
2 | import { NestFastifyApplication } from '@nestjs/platform-fastify';
3 |
4 | /**
5 | * Setup Core documentation
6 | *
7 | * @remarks
8 | *
9 | * Core API documentation is accessible by visiting
10 | * {@link http://localhost:3000/docs} in your browser.
11 | *
12 | * The API documentation additionally serves as a REST client for manual testing.
13 | *
14 | * @see {@link https://docs.nestjs.com/openapi/introduction}.
15 | *
16 | * @internal
17 | */
18 | export const bootstrapDocs = (app: NestFastifyApplication) => {
19 | const config = new DocumentBuilder()
20 | .setTitle('IoT Application Core')
21 | .setDescription('Core API documentation')
22 | .setVersion('1.0')
23 | .addTag('dashboards')
24 | .build();
25 | const document = SwaggerModule.createDocument(app, config);
26 | SwaggerModule.setup('/docs', app, document);
27 | };
28 |
--------------------------------------------------------------------------------
/apps/core/src/bootstrap/hmr.bootstrap.ts:
--------------------------------------------------------------------------------
1 | import { NestFastifyApplication } from '@nestjs/platform-fastify';
2 |
3 | interface MaybeHotModule {
4 | hot?: {
5 | accept(): void;
6 | dispose(cb: () => Promise): void;
7 | };
8 | }
9 |
10 | type HotModule = {
11 | [P in keyof MaybeHotModule]-?: MaybeHotModule['hot'];
12 | };
13 |
14 | const isHotModule = (module: MaybeHotModule): module is HotModule => {
15 | return module.hot != null;
16 | };
17 |
18 | const handleHotModule = (module: HotModule, app: NestFastifyApplication) => {
19 | module.hot.accept();
20 | module.hot.dispose(() => app.close());
21 | };
22 |
23 | /** Webpack module (value is injected at runtime by Webpack) */
24 | declare const module: MaybeHotModule;
25 |
26 | /**
27 | * HMR bootup script
28 | *
29 | * @regards
30 | *
31 | * @see {@link http://docs.nestjs.com/recipes/hot-reload | Hot Reload}
32 | *
33 | * @internal
34 | */
35 | export const bootstrapHmr = (app: NestFastifyApplication) => {
36 | if (isHotModule(module)) {
37 | handleHotModule(module, app);
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/apps/core/src/bootstrap/index.ts:
--------------------------------------------------------------------------------
1 | import { NestFastifyApplication } from '@nestjs/platform-fastify';
2 |
3 | import { bootstrapDocs } from './docs.bootstrap';
4 | import { bootstrapHmr } from './hmr.bootstrap';
5 | import { bootstrapLogger } from './logger.bootstrap';
6 | import { bootstrapMvc } from './mvc.bootstrap';
7 | import { bootstrapSecurity } from './security.bootstrap';
8 | import { bootstrapServer } from './server.bootstrap';
9 | import { bootstrapValidation } from './validation.bootstrap';
10 |
11 | /** Core bootup script */
12 | export const bootstrap = async (app: NestFastifyApplication) => {
13 | bootstrapLogger(app);
14 | await bootstrapSecurity(app);
15 | bootstrapValidation(app);
16 | bootstrapDocs(app);
17 | bootstrapMvc(app);
18 | await bootstrapServer(app);
19 | bootstrapHmr(app);
20 | };
21 |
--------------------------------------------------------------------------------
/apps/core/src/bootstrap/logger.bootstrap.ts:
--------------------------------------------------------------------------------
1 | import { NestFastifyApplication } from '@nestjs/platform-fastify';
2 | import { LOGGER_PROVIDER_TOKEN } from '../logging/logger.constants';
3 |
4 | export const bootstrapLogger = (app: NestFastifyApplication) => {
5 | app.useLogger(app.get(LOGGER_PROVIDER_TOKEN));
6 | };
7 |
--------------------------------------------------------------------------------
/apps/core/src/bootstrap/mvc.bootstrap.ts:
--------------------------------------------------------------------------------
1 | import { NestFastifyApplication } from '@nestjs/platform-fastify';
2 | import { join } from 'path';
3 | import Handlebars from 'handlebars';
4 |
5 | const CLIENT_BUILD_FOLDER = join(process.cwd(), '..', 'client', 'build');
6 |
7 | export const bootstrapMvc = (app: NestFastifyApplication) => {
8 | app.useStaticAssets({
9 | root: join(CLIENT_BUILD_FOLDER),
10 | });
11 |
12 | app.setViewEngine({
13 | engine: {
14 | handlebars: Handlebars,
15 | },
16 | templates: CLIENT_BUILD_FOLDER,
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/apps/core/src/bootstrap/server.bootstrap.ts:
--------------------------------------------------------------------------------
1 | import { NestFastifyApplication } from '@nestjs/platform-fastify';
2 |
3 | /** Server bootup script */
4 | export const bootstrapServer = async (app: NestFastifyApplication) => {
5 | await app.listen(3000, '0.0.0.0');
6 | };
7 |
--------------------------------------------------------------------------------
/apps/core/src/config/database.config.spec.ts:
--------------------------------------------------------------------------------
1 | import { configFactory } from './database.config';
2 | import { envVarRequiredMsg } from './environment';
3 |
4 | describe('databaseConfig', () => {
5 | describe('configFactory', () => {
6 | test('returns cloud values', () => {
7 | process.env.DATABASE_LAUNCH_LOCAL = 'false';
8 | process.env.DATABASE_TABLE_NAME = 'CloudApiResourceTable';
9 |
10 | expect(configFactory()).toEqual({
11 | launchLocal: false,
12 | port: 8000,
13 | endpoint: undefined,
14 | tableName: 'CloudApiResourceTable',
15 | });
16 | });
17 |
18 | test('throws DATABASE_TABLE_NAME_MISSING_ERROR', () => {
19 | process.env.DATABASE_TABLE_NAME = 'undefined';
20 |
21 | expect(() => configFactory()).toThrow(
22 | envVarRequiredMsg('DATABASE_TABLE_NAME'),
23 | );
24 | });
25 |
26 | test('returns local values', () => {
27 | process.env.DATABASE_LAUNCH_LOCAL = 'true';
28 | process.env.DATABASE_PORT = '1234';
29 | process.env.DATABASE_ENDPOINT = 'http://overriden-endpoint:1234';
30 | process.env.DATABASE_TABLE_NAME = 'LocalApiResourceTable';
31 |
32 | expect(configFactory()).toEqual({
33 | launchLocal: true,
34 | port: 1234,
35 | endpoint: 'http://overriden-endpoint:1234',
36 | tableName: 'LocalApiResourceTable',
37 | });
38 | });
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/apps/core/src/config/database.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 | import { isDefined } from '../types/environment';
3 | import invariant from 'tiny-invariant';
4 | import { envVarRequiredMsg } from './environment';
5 |
6 | export const configFactory = () => {
7 | const {
8 | DATABASE_LAUNCH_LOCAL,
9 | DATABASE_ENDPOINT: endpoint,
10 | DATABASE_TABLE_NAME: tableName,
11 | DATABASE_PORT,
12 | } = process.env;
13 |
14 | const launchLocal = DATABASE_LAUNCH_LOCAL === 'true';
15 | // port is for local consumption only; default to 8000 to work with local environments
16 | const port = DATABASE_PORT !== undefined ? parseInt(DATABASE_PORT) : 8000;
17 | invariant(isDefined(tableName), envVarRequiredMsg('DATABASE_TABLE_NAME'));
18 |
19 | return {
20 | launchLocal,
21 | port,
22 | endpoint,
23 | tableName,
24 | };
25 | };
26 |
27 | export const databaseConfig = registerAs('database', configFactory);
28 |
--------------------------------------------------------------------------------
/apps/core/src/config/edge.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 |
3 | export const configFactory = () => {
4 | const { EDGE_ENDPOINT: edgeEndpoint } = process.env;
5 |
6 | return {
7 | edgeEndpoint,
8 | };
9 | };
10 |
11 | export const edgeConfig = registerAs('edge', configFactory);
12 |
--------------------------------------------------------------------------------
/apps/core/src/config/environment.ts:
--------------------------------------------------------------------------------
1 | export const envVarRequiredMsg = (envVarName: string) =>
2 | `Environment variable "${envVarName}" is required.`;
3 |
--------------------------------------------------------------------------------
/apps/core/src/config/global.config.spec.ts:
--------------------------------------------------------------------------------
1 | import { configFactory } from './global.config';
2 | import { envVarRequiredMsg } from './environment';
3 | import { MetricModes, LogModes } from '../types/environment';
4 |
5 | describe('globalConfig', () => {
6 | describe('configFactory', () => {
7 | test('returns environment values in local mode', () => {
8 | const applicationName = 'ApplicationName';
9 |
10 | process.env.APPLICATION_NAME = applicationName;
11 | process.env.NODE_ENV = 'development';
12 |
13 | expect(configFactory()).toEqual({
14 | applicationName: applicationName,
15 | logMode: LogModes.Local,
16 | metricsMode: MetricModes.Local,
17 | });
18 | });
19 |
20 | test('returns environment values in cloud mode', () => {
21 | const applicationName = 'ApplicationName';
22 |
23 | process.env.APPLICATION_NAME = applicationName;
24 | process.env.NODE_ENV = 'production';
25 |
26 | expect(configFactory()).toEqual({
27 | applicationName: applicationName,
28 | logMode: LogModes.Cloud,
29 | metricsMode: MetricModes.Cloud,
30 | });
31 | });
32 |
33 | test('throws APPLICATION_NAME_MISSING_ERROR', () => {
34 | process.env.APPLICATION_NAME = 'undefined';
35 |
36 | expect(() => configFactory()).toThrow(
37 | envVarRequiredMsg('APPLICATION_NAME'),
38 | );
39 | });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/apps/core/src/config/global.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 | import { isDefined } from '../types/environment';
3 | import invariant from 'tiny-invariant';
4 | import { envVarRequiredMsg } from './environment';
5 | import { getLogMode, getMetricsMode } from '../types/environment';
6 |
7 | export const configFactory = () => {
8 | const { APPLICATION_NAME: applicationName } = process.env;
9 |
10 | invariant(isDefined(applicationName), envVarRequiredMsg('APPLICATION_NAME'));
11 |
12 | const logMode = getLogMode();
13 | const metricsMode = getMetricsMode();
14 |
15 | invariant(isDefined(logMode), 'Something went wrong getting log mode.');
16 | invariant(
17 | isDefined(metricsMode),
18 | 'Something went wrong getting metrics mode.',
19 | );
20 |
21 | return {
22 | applicationName,
23 | logMode,
24 | metricsMode,
25 | };
26 | };
27 |
28 | export const globalConfig = registerAs('global', configFactory);
29 |
--------------------------------------------------------------------------------
/apps/core/src/config/jwt.config.ts:
--------------------------------------------------------------------------------
1 | import { registerAs } from '@nestjs/config';
2 | import { CognitoJwtVerifier } from 'aws-jwt-verify';
3 | import { localCognitoJwtVerifier } from './local-cognito-jwt-verifier';
4 | import invariant from 'tiny-invariant';
5 | import { isDefined } from '../types/environment';
6 | import { envVarRequiredMsg } from './environment';
7 |
8 | const COGNITO_JWT_TOKEN_USE = 'access';
9 |
10 | export const configFactory = () => {
11 | const {
12 | COGNITO_USE_LOCAL_VERIFIER: useLocalVerifier,
13 | COGNITO_USER_POOL_ID: userPoolId,
14 | COGNITO_USER_POOL_CLIENT_ID: userPoolWebClientId,
15 | } = process.env;
16 |
17 | invariant(isDefined(userPoolId), envVarRequiredMsg('COGNITO_USER_POOL_ID'));
18 | invariant(
19 | isDefined(userPoolWebClientId),
20 | envVarRequiredMsg('COGNITO_USER_POOL_CLIENT_ID'),
21 | );
22 |
23 | if (useLocalVerifier === 'true') {
24 | return {
25 | cognitoJwtVerifier: localCognitoJwtVerifier,
26 | };
27 | }
28 |
29 | const cloudCognitoJwtVerifier = CognitoJwtVerifier.create({
30 | clientId: userPoolWebClientId,
31 | userPoolId,
32 | tokenUse: COGNITO_JWT_TOKEN_USE,
33 | });
34 |
35 | return {
36 | cognitoJwtVerifier: cloudCognitoJwtVerifier,
37 | };
38 | };
39 |
40 | export const jwtConfig = registerAs('jwt', configFactory);
41 |
--------------------------------------------------------------------------------
/apps/core/src/config/local-cognito-jwt-verifier.ts:
--------------------------------------------------------------------------------
1 | import { CognitoJwtVerifier } from 'aws-jwt-verify';
2 | import { JwksCache, JwkWithKid, Jwks } from 'aws-jwt-verify/jwk';
3 |
4 | // JwksCache built for local development, works in conjunction with cognito-local
5 | const localJwksCache: JwksCache = {
6 | getJwk: (): Promise => Promise.resolve(localJwk),
7 | getCachedJwk: (): JwkWithKid => localJwk,
8 | addJwks: (): void => {
9 | /** NOOP **/
10 | },
11 | getJwks: (): Promise => Promise.resolve(localJwks),
12 | };
13 | // jwk copied from https://github.com/jagregory/cognito-local/blob/master/src/keys/cognitoLocal.public.json
14 | const localJwk = {
15 | kty: 'RSA',
16 | e: 'AQAB',
17 | use: 'sig',
18 | kid: 'CognitoLocal',
19 | alg: 'RS256',
20 | n: '2uLO7yh1_6Icfd89V3nNTc_qhfpDN7vEmOYlmJQlc9_RmOns26lg88fXXFntZESwHOm7_homO2Ih6NOtu4P5eskGs8d8VQMOQfF4YrP-pawVz-gh1S7eSvzZRDHBT4ItUuoiVP1B9HN_uScKxIqjmitpPqEQB_o2NJv8npCfqUAU-4KmxquGtjdmfctswSZGdz59M3CAYKDfuvLH9_vV6TRGgbUaUAXWC2WJrbbEXzK3XUDBrmF3Xo-yw8f3SgD3JOPl3HaaWMKL1zGVAsge7gQaGiJBzBurg5vwN61uDGGz0QZC1JqcUTl3cZnrx_L8isIR7074SJEuljIZRnCcjQ',
21 | };
22 | const localJwks = { keys: [localJwk] };
23 | export const localUserPoolId = 'us-west-2_h23TJjQR9';
24 | export const localClientId = '9cehli62qxmki9mg5adjmucuq';
25 |
26 | export const localCognitoJwtVerifier = CognitoJwtVerifier.create(
27 | {
28 | userPoolId: localUserPoolId,
29 | tokenUse: 'access',
30 | clientId: localClientId,
31 | },
32 | {
33 | jwksCache: localJwksCache,
34 | },
35 | );
36 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/README.md:
--------------------------------------------------------------------------------
1 | # Dashboards
2 |
3 | `DashboardsModule` extends Core with the `/dashboards` REST API.
4 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/dashboard.constants.ts:
--------------------------------------------------------------------------------
1 | export const RESOURCE_TYPES = {
2 | DASHBOARD_DATA: 'DashboardData',
3 | DASHBOARD_DEFINITION: 'DashboardDefinition',
4 | };
5 |
6 | export const DATABASE_GSI = {
7 | RESOURCE_TYPE: 'resourceTypeIndex',
8 | };
9 |
10 | export const MESSAGES = {
11 | ITEM_NOT_FOUND_ERROR: 'Missing dashboard data or definition',
12 | UNKNOWN_ERROR: 'Unknown error',
13 | };
14 |
15 | // SiteWise Monitor max dashboard name length is 256
16 | export const DASHBOARD_NAME_MAX_LENGTH = 256;
17 |
18 | export const DASHBOARD_DESCRIPTION_MAX_LENGTH = 200;
19 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/dashboards.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, ModuleMetadata } from '@nestjs/common';
2 |
3 | import { DashboardsController } from './dashboards.controller';
4 | import { DashboardsRepository } from './dashboards.repository';
5 | import { DashboardsService } from './dashboards.service';
6 |
7 | export const dashboardsModuleMetadata: ModuleMetadata = {
8 | controllers: [DashboardsController],
9 | providers: [DashboardsRepository, DashboardsService],
10 | exports: [DashboardsService],
11 | };
12 |
13 | /** Core Dashboards Module */
14 | @Module(dashboardsModuleMetadata)
15 | export class DashboardsModule {}
16 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/dashboards.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | import { DashboardsRepository } from './dashboards.repository';
4 | import { CreateDashboardDto } from './dto/create-dashboard.dto';
5 | import { Dashboard } from './entities/dashboard.entity';
6 |
7 | @Injectable()
8 | export class DashboardsService {
9 | constructor(private readonly repository: DashboardsRepository) {}
10 |
11 | public async read(id: Dashboard['id']) {
12 | return this.repository.find(id);
13 | }
14 |
15 | public async list() {
16 | return this.repository.findAll();
17 | }
18 |
19 | public async create(createDashboardDto: CreateDashboardDto) {
20 | return this.repository.create(createDashboardDto);
21 | }
22 |
23 | public async update(
24 | dashboard: Pick &
25 | Partial>,
26 | ) {
27 | return this.repository.update(dashboard);
28 | }
29 |
30 | public async delete(id: Dashboard['id']) {
31 | return this.repository.delete(id);
32 | }
33 |
34 | public async bulkDelete(ids: Dashboard['id'][]) {
35 | // TODO: use BatchWriteItem instead of Promise.all
36 | return Promise.all(ids.map((id) => this.repository.delete(id)));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/dto/bulk-delete-dashboards.dto.ts:
--------------------------------------------------------------------------------
1 | import { Dashboard } from '../entities/dashboard.entity';
2 |
3 | export class BulkDeleteDashboardDto {
4 | ids: Dashboard['id'][];
5 | }
6 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/dto/create-dashboard.dto.ts:
--------------------------------------------------------------------------------
1 | import { OmitType } from '@nestjs/swagger';
2 |
3 | import { Dashboard } from '../entities/dashboard.entity';
4 |
5 | /** POST /api/dashboards HTTP/1.1 request body */
6 | export class CreateDashboardDto extends OmitType(Dashboard, [
7 | 'id',
8 | 'creationDate',
9 | 'lastUpdateDate',
10 | ] as const) {}
11 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/dto/update-dashboard.dto.ts:
--------------------------------------------------------------------------------
1 | import { OmitType, PartialType } from '@nestjs/swagger';
2 |
3 | import { Dashboard } from '../entities/dashboard.entity';
4 |
5 | /** PATCH /api/dashboards/{dashboardId} HTTP/1.1 request body */
6 | export class UpdateDashboardDto extends OmitType(PartialType(Dashboard), [
7 | 'id',
8 | 'creationDate',
9 | 'lastUpdateDate',
10 | 'sitewiseMonitorId',
11 | ] as const) {}
12 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/entities/dashboard-definition.entity.ts:
--------------------------------------------------------------------------------
1 | import { Type } from 'class-transformer';
2 | import {
3 | ArrayMaxSize,
4 | IsArray,
5 | IsObject,
6 | ValidateNested,
7 | } from 'class-validator';
8 |
9 | import { DashboardWidget } from './dashboard-widget.entity';
10 |
11 | export class DashboardDefinition {
12 | @IsArray()
13 | @ArrayMaxSize(1000)
14 | @ValidateNested({ each: true })
15 | @IsObject({ each: true })
16 | @Type(() => DashboardWidget)
17 | public readonly widgets: DashboardWidget[];
18 | }
19 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/entities/dashboard-summary.entity.ts:
--------------------------------------------------------------------------------
1 | import { OmitType } from '@nestjs/swagger';
2 |
3 | import { Dashboard } from '../entities/dashboard.entity';
4 |
5 | /** POST /api/dashboards HTTP/1.1 request body */
6 | export class DashboardSummary extends OmitType(Dashboard, [
7 | 'definition',
8 | ] as const) {}
9 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/entities/dashboard.entity.ts:
--------------------------------------------------------------------------------
1 | import { Type } from 'class-transformer';
2 | import {
3 | IsDateString,
4 | IsObject,
5 | IsString,
6 | Length,
7 | ValidateNested,
8 | IsOptional,
9 | } from 'class-validator';
10 |
11 | import { DashboardDefinition } from './dashboard-definition.entity';
12 | import {
13 | DASHBOARD_NAME_MAX_LENGTH,
14 | DASHBOARD_DESCRIPTION_MAX_LENGTH,
15 | } from '../dashboard.constants';
16 |
17 | export type DashboardId = string;
18 | export type DashboardName = string;
19 | export type DashboardDescription = string;
20 | export type SiteWiseMonitorDashboardId = string;
21 |
22 | export class Dashboard {
23 | /**
24 | * @example "zckYx-InI8_f"
25 | */
26 | @IsString()
27 | @Length(12, 12)
28 | public readonly id: DashboardId;
29 |
30 | /**
31 | * @example "Wind Farm 4"
32 | */
33 | @IsString()
34 | @Length(1, DASHBOARD_NAME_MAX_LENGTH)
35 | public readonly name: DashboardName;
36 |
37 | /**
38 | * @example "Wind Farm 4 Description"
39 | */
40 | @IsString()
41 | @Length(1, DASHBOARD_DESCRIPTION_MAX_LENGTH)
42 | public readonly description: DashboardDescription;
43 |
44 | @IsObject()
45 | @ValidateNested()
46 | @Type(() => DashboardDefinition)
47 | public readonly definition: DashboardDefinition;
48 |
49 | @IsDateString()
50 | public readonly creationDate: string;
51 |
52 | @IsDateString()
53 | public readonly lastUpdateDate: string;
54 |
55 | @IsString()
56 | @IsOptional()
57 | public readonly sitewiseMonitorId?: SiteWiseMonitorDashboardId;
58 | }
59 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/params/delete-dashboard.params.ts:
--------------------------------------------------------------------------------
1 | import { PickType } from '@nestjs/swagger';
2 |
3 | import { Dashboard } from '../entities/dashboard.entity';
4 |
5 | /** DELETE /api/dashboards/{dashboardId} HTTP/1.1 request params */
6 | export class DeleteDashboardParams extends PickType(Dashboard, [
7 | 'id',
8 | ] as const) {}
9 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/params/read-dashboard.params.ts:
--------------------------------------------------------------------------------
1 | import { PickType } from '@nestjs/swagger';
2 |
3 | import { Dashboard } from '../entities/dashboard.entity';
4 |
5 | /** GET /api/dashboards/{dashboardId} HTTP/1.1 request params */
6 | export class ReadDashboardParams extends PickType(Dashboard, ['id'] as const) {}
7 |
--------------------------------------------------------------------------------
/apps/core/src/dashboards/params/update-dashboard.params.ts:
--------------------------------------------------------------------------------
1 | import { PickType } from '@nestjs/swagger';
2 |
3 | import { Dashboard } from '../entities/dashboard.entity';
4 |
5 | /** PUT /api/dashboards/{dashboardId} HTTP/1.1 request params */
6 | export class UpdateDashboardParams extends PickType(Dashboard, [
7 | 'id',
8 | ] as const) {}
9 |
--------------------------------------------------------------------------------
/apps/core/src/edge-login/README.md:
--------------------------------------------------------------------------------
1 | # EdgeLogin
2 |
3 | `EdgeLoginModule` extends Core with the `/edge-login` REST API.
4 |
5 | This module is for calling the `/authenticate` endpoint of a SiteWise Edge Gateway. The endpoint does not have CORS support, so this endpoint is used to proxy the call from the client.
--------------------------------------------------------------------------------
/apps/core/src/edge-login/edge-login.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Body } from '@nestjs/common';
2 | import { ApiTags } from '@nestjs/swagger';
3 | import { EdgeLoginService } from './edge-login.service';
4 | import { Public } from '../auth/public.decorator';
5 | import { EdgeCredentials } from './entities/edge-credentials.entity';
6 | import { EdgeLoginBody } from './entities/edge-login-body.entity';
7 | import { isErr } from '../types';
8 |
9 | @ApiTags('edge-login')
10 | @Controller('api/edge-login')
11 | export class EdgeLoginController {
12 | constructor(private readonly edgeLoginService: EdgeLoginService) {}
13 |
14 | @Public()
15 | @Post()
16 | public async edgeLogin(
17 | @Body() edgeLoginBody: EdgeLoginBody,
18 | ): Promise {
19 | const result = await this.edgeLoginService.login(edgeLoginBody);
20 |
21 | if (isErr(result)) {
22 | throw result.err;
23 | }
24 |
25 | return result.ok;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/apps/core/src/edge-login/edge-login.module.ts:
--------------------------------------------------------------------------------
1 | import { HttpModule } from '@nestjs/axios';
2 | import { Module, ModuleMetadata } from '@nestjs/common';
3 | import { ConfigModule } from '@nestjs/config';
4 |
5 | import { EdgeLoginController } from './edge-login.controller';
6 | import { EdgeLoginService } from './edge-login.service';
7 | import { edgeConfig } from '../config/edge.config';
8 |
9 | export const edgeLoginModuleMetadata: ModuleMetadata = {
10 | imports: [ConfigModule.forFeature(edgeConfig), HttpModule],
11 | controllers: [EdgeLoginController],
12 | providers: [EdgeLoginService],
13 | };
14 |
15 | /** Core Dashboards Module */
16 | @Module(edgeLoginModuleMetadata)
17 | export class EdgeLoginModule {}
18 |
--------------------------------------------------------------------------------
/apps/core/src/edge-login/entities/edge-credentials.entity.ts:
--------------------------------------------------------------------------------
1 | export interface EdgeCredentials {
2 | accessKeyId: string;
3 | secretAccessKey: string;
4 | sessionToken?: string;
5 | sessionExpiryTime?: string;
6 | }
7 |
--------------------------------------------------------------------------------
/apps/core/src/edge-login/entities/edge-login-body.entity.ts:
--------------------------------------------------------------------------------
1 | import { IsString } from 'class-validator';
2 |
3 | export class EdgeLoginBody {
4 | /**
5 | * @example "user"
6 | */
7 | @IsString()
8 | public readonly username: string;
9 |
10 | /**
11 | * @example "password"
12 | */
13 | @IsString()
14 | public readonly password: string;
15 |
16 | /**
17 | * @example "linux"
18 | */
19 | @IsString()
20 | public readonly authMechanism: string;
21 | }
22 |
--------------------------------------------------------------------------------
/apps/core/src/health/README.md:
--------------------------------------------------------------------------------
1 | # Health
2 |
3 | `HealthModule` extends the Core System to add the `/health` endpoint.
4 |
5 | When `GET /health HTTP/1.1` is requested by the client, application status
6 | information is returned.
7 |
8 | When the application is healthy, the client is returned:
9 |
10 | ```
11 | HTTP/1.1 200 OK
12 |
13 | {
14 | "status": "ok",
15 | "info": {
16 | "core": {
17 | "status": "up"
18 | }
19 | },
20 | "error": {},
21 | "details": {
22 | "core": {
23 | "status": "up"
24 | }
25 | }
26 | }
27 | ```
28 |
29 | When the application is not healthy, the application is returned:
30 |
31 | ```
32 | HTTP/1.1 503 Service Unavailable
33 |
34 | {
35 | "status": "error",
36 | "info": {},
37 | "error": {
38 | "core": {
39 | "status": "down"
40 | }
41 | },
42 | "details": {
43 | "core": {
44 | "status": "down"
45 | }
46 | }
47 | }
48 | ```
49 |
--------------------------------------------------------------------------------
/apps/core/src/health/health.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 | import { HealthCheck, HealthCheckService } from '@nestjs/terminus';
3 | import { Public } from '../auth/public.decorator';
4 | import { DynamoDbHealthIndicator } from './indicators/dynamodb.health';
5 |
6 | /** HTTP API for application health status */
7 | @Controller('health')
8 | export class HealthController {
9 | constructor(
10 | private dynamoDbHealthIndicator: DynamoDbHealthIndicator,
11 | private readonly health: HealthCheckService,
12 | ) {}
13 |
14 | @Public()
15 | @Get() // GET /health
16 | @HealthCheck()
17 | async check() {
18 | return this.health.check([() => this.dynamoDbHealthIndicator.check('db')]);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/apps/core/src/health/health.module.ts:
--------------------------------------------------------------------------------
1 | import { HttpModule } from '@nestjs/axios';
2 | import { Module } from '@nestjs/common';
3 | import { TerminusModule } from '@nestjs/terminus';
4 |
5 | import { HealthController } from './health.controller';
6 | import { DynamoDbHealthIndicator } from './indicators/dynamodb.health';
7 |
8 | /** Application health status */
9 | @Module({
10 | imports: [TerminusModule, HttpModule],
11 | controllers: [HealthController],
12 | providers: [DynamoDbHealthIndicator],
13 | })
14 | export class HealthModule {}
15 |
--------------------------------------------------------------------------------
/apps/core/src/health/indicators/dynamodb.health.ts:
--------------------------------------------------------------------------------
1 | import { DescribeTableCommand, DynamoDBClient } from '@aws-sdk/client-dynamodb';
2 | import { Inject, Injectable, Logger } from '@nestjs/common';
3 | import { ConfigType } from '@nestjs/config';
4 | import { HealthCheckError, HealthIndicator } from '@nestjs/terminus';
5 | import { databaseConfig } from '../../config/database.config';
6 |
7 | @Injectable()
8 | export class DynamoDbHealthIndicator extends HealthIndicator {
9 | private readonly logger = new Logger(DynamoDbHealthIndicator.name);
10 |
11 | constructor(
12 | @Inject(databaseConfig.KEY)
13 | private readonly dbConfig: ConfigType,
14 | ) {
15 | super();
16 | }
17 |
18 | async check(key: string) {
19 | const { endpoint, tableName } = this.dbConfig;
20 |
21 | try {
22 | await new DynamoDBClient({
23 | endpoint,
24 | }).send(
25 | new DescribeTableCommand({
26 | TableName: tableName,
27 | }),
28 | );
29 |
30 | return this.getStatus(key, true);
31 | } catch (e) {
32 | this.logger.error('DynamoDB check failed:');
33 | this.logger.error(e);
34 |
35 | throw new HealthCheckError(
36 | 'DynamoDB check failed',
37 | this.getStatus(key, false),
38 | );
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/apps/core/src/logging/dev-logger.provider.ts:
--------------------------------------------------------------------------------
1 | import { ConsoleLogger } from '@nestjs/common';
2 | import { LOGGER_PROVIDER_TOKEN } from './logger.constants';
3 |
4 | export const DevloggerFactoryProvider = {
5 | provide: LOGGER_PROVIDER_TOKEN,
6 | useFactory: () => {
7 | return new ConsoleLogger();
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/apps/core/src/logging/logger.constants.ts:
--------------------------------------------------------------------------------
1 | export const LOGGER_PROVIDER_TOKEN = 'CoreLoggerService';
2 |
--------------------------------------------------------------------------------
/apps/core/src/logging/logger.module.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Module } from '@nestjs/common';
2 | import { LoggerModule as PinoLoggerModule } from 'nestjs-pino';
3 | import { ProdLoggerFactoryProvider } from './prod-logger.provider';
4 | import { isDevEnv } from '../types/environment';
5 | import { DevloggerFactoryProvider } from './dev-logger.provider';
6 | import { pinoHttpConfigs } from './pino-http.configs';
7 |
8 | @Module({})
9 | export class LoggerModule {
10 | static forRoot(): DynamicModule {
11 | if (isDevEnv()) {
12 | return this.developmentLoggerForRoot();
13 | }
14 |
15 | return this.productionLoggerForRoot();
16 | }
17 |
18 | private static developmentLoggerForRoot(): DynamicModule {
19 | return {
20 | global: true,
21 | module: LoggerModule,
22 | providers: [DevloggerFactoryProvider],
23 | };
24 | }
25 |
26 | private static productionLoggerForRoot(): DynamicModule {
27 | return {
28 | global: true,
29 | imports: [
30 | PinoLoggerModule.forRoot({
31 | pinoHttp: pinoHttpConfigs,
32 | }),
33 | ],
34 | module: LoggerModule,
35 | providers: [ProdLoggerFactoryProvider],
36 | };
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/apps/core/src/logging/pino-http.configs.ts:
--------------------------------------------------------------------------------
1 | import { IncomingMessage } from 'http';
2 | import { ServerResponse } from 'http';
3 | import { Options } from 'pino-http';
4 |
5 | /**
6 | * Custom request serializer to include necessary informations only (omitted sensitive properties like headers).
7 | */
8 | const requestSerializer = (req: IncomingMessage) => ({
9 | id: req.id,
10 | method: req.method,
11 | url: req.url,
12 | });
13 |
14 | /**
15 | * Custom response serializer to include necessary informations only (omitted sensitive properties like headers).
16 | */
17 | const responseSerializer = (res: ServerResponse) => ({
18 | err: res.err,
19 | statusCode: res.statusCode,
20 | statusMessage: res.statusMessage,
21 | });
22 |
23 | const customReceivedObject = (req: IncomingMessage) => ({
24 | context: 'HttpRequestReceived',
25 | req,
26 | });
27 |
28 | const customSuccessObject = (
29 | _req: IncomingMessage,
30 | _res: ServerResponse,
31 | loggableObject: object,
32 | ) => ({
33 | ...loggableObject,
34 | context: 'HttpRequestCompleted',
35 | });
36 |
37 | const customErrorObject = (
38 | _req: IncomingMessage,
39 | _res: ServerResponse,
40 | _err: Error,
41 | loggableObject: object,
42 | ) => ({
43 | ...loggableObject,
44 | context: 'HttpRequestErrored',
45 | });
46 |
47 | export const pinoHttpConfigs: Options = {
48 | quietReqLogger: true,
49 | customReceivedObject,
50 | customSuccessObject,
51 | customErrorObject,
52 | serializers: {
53 | req: requestSerializer,
54 | res: responseSerializer,
55 | },
56 | };
57 |
--------------------------------------------------------------------------------
/apps/core/src/logging/prod-logger.provider.ts:
--------------------------------------------------------------------------------
1 | import { ProdLoggerService } from './prod-logger.service';
2 | import { Params, PARAMS_PROVIDER_TOKEN } from 'nestjs-pino';
3 | import { LOGGER_PROVIDER_TOKEN } from './logger.constants';
4 |
5 | export const ProdLoggerFactoryProvider = {
6 | provide: LOGGER_PROVIDER_TOKEN,
7 | useFactory: (params: Params) => {
8 | return new ProdLoggerService(params);
9 | },
10 | inject: [{ token: PARAMS_PROVIDER_TOKEN, optional: false }],
11 | };
12 |
--------------------------------------------------------------------------------
/apps/core/src/logging/prod-logger.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@nestjs/common';
2 | import { Logger, Params, PARAMS_PROVIDER_TOKEN, PinoLogger } from 'nestjs-pino';
3 |
4 | @Injectable()
5 | export class ProdLoggerService extends Logger {
6 | constructor(@Inject(PARAMS_PROVIDER_TOKEN) params: Params) {
7 | const pinoLogger = new PinoLogger(params);
8 | super(pinoLogger, params);
9 | }
10 |
11 | /**
12 | * No overrides currently; this class exists to allow for future extention;
13 | */
14 | }
15 |
--------------------------------------------------------------------------------
/apps/core/src/migration/entities/migration-status.entity.ts:
--------------------------------------------------------------------------------
1 | import { IsString } from 'class-validator';
2 |
3 | export enum Status {
4 | NOT_STARTED = 'not-started',
5 | IN_PROGRESS = 'in-progress',
6 | COMPLETE = 'complete',
7 | COMPLETE_NONE_CREATED = 'complete-none-created',
8 | ERROR = 'error',
9 | }
10 |
11 | export class MigrationStatus {
12 | /**
13 | * @example "in-progress"
14 | */
15 | @IsString()
16 | public readonly status: Status;
17 |
18 | /**
19 | * @example "error calling API ListPortals: you do not have permission."
20 | */
21 | @IsString()
22 | public readonly message?: string;
23 | }
24 |
--------------------------------------------------------------------------------
/apps/core/src/migration/migration.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, HttpCode, Get, Post } from '@nestjs/common';
2 | import { ApiTags } from '@nestjs/swagger';
3 | import { MigrationService } from './service/migration.service';
4 | import { MigrationStatus } from './entities/migration-status.entity';
5 |
6 | @ApiTags('migration')
7 | @Controller('api/migration')
8 | export class MigrationController {
9 | constructor(private readonly migrationService: MigrationService) {}
10 |
11 | @Post()
12 | @HttpCode(202)
13 | public migration() {
14 | void this.migrationService.migrate();
15 | }
16 |
17 | @Get()
18 | public getMigrationStatus(): MigrationStatus {
19 | return this.migrationService.getMigrationStatus();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/apps/core/src/migration/migration.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, ModuleMetadata } from '@nestjs/common';
2 |
3 | import { MigrationController } from './migration.controller';
4 | import { MigrationService } from './service/migration.service';
5 | import { DashboardsModule } from '../dashboards/dashboards.module';
6 |
7 | export const migrationModuleMetadata: ModuleMetadata = {
8 | imports: [DashboardsModule],
9 | controllers: [MigrationController],
10 | providers: [MigrationService],
11 | };
12 |
13 | /** Core Dashboards Module */
14 | @Module(migrationModuleMetadata)
15 | export class MigrationModule {}
16 |
--------------------------------------------------------------------------------
/apps/core/src/migration/util/colorPalette.ts:
--------------------------------------------------------------------------------
1 | // color palette copied from https://cloudscape.design/foundation/visual-foundation/data-vis-colors/
2 | export const colorPalette = [
3 | '#7d2105',
4 | '#3184c2',
5 | '#7d8998',
6 | '#b2911c',
7 | '#67a353',
8 | '#ba2e0f',
9 | '#ce567c',
10 | '#1c8e81',
11 | '#9469d6',
12 | '#cc5f21',
13 | '#273ea5',
14 | '#962249',
15 | '#03524a',
16 | '#6237a7',
17 | '#7e3103',
18 | '#6f062f',
19 | '#003e38',
20 | '#431d84',
21 | '#037f0c',
22 | '#602400',
23 | '#125502',
24 | '#314fbf',
25 | '#4d3901',
26 | '#a32952',
27 | '#06645a',
28 | '#6b40b2',
29 | '#983c02',
30 | '#6f5504',
31 | '#1f3191',
32 | '#780d35',
33 | '#01443e',
34 | '#4a238b',
35 | '#692801',
36 | '#5f6b7a',
37 | '#3759ce',
38 | '#b1325c',
39 | '#5978e3',
40 | '#096f64',
41 | '#7749bf',
42 | '#a84401',
43 | '#9c7b0b',
44 | '#23379b',
45 | '#8b1b42',
46 | '#014b44',
47 | '#59309d',
48 | '#732c02',
49 | '#1f8104',
50 | '#4066df',
51 | '#c33d69',
52 | '#0d7d70',
53 | '#8456ce',
54 | '#bc4d01',
55 | ];
56 |
--------------------------------------------------------------------------------
/apps/core/src/mvc/mvc.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigModule } from '@nestjs/config';
3 |
4 | import { MvcController } from './mvc.controller';
5 | import { authConfig } from '../config/auth.config';
6 | import { edgeConfig } from '../config/edge.config';
7 | import { globalConfig } from '../config/global.config';
8 |
9 | @Module({
10 | imports: [
11 | ConfigModule.forFeature(authConfig),
12 | ConfigModule.forFeature(edgeConfig),
13 | ConfigModule.forFeature(globalConfig),
14 | ],
15 | controllers: [MvcController],
16 | providers: [],
17 | })
18 | export class MvcModule {}
19 |
--------------------------------------------------------------------------------
/apps/core/src/repl.ts:
--------------------------------------------------------------------------------
1 | import { repl } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 |
4 | /**
5 | * Main() for Core REPL
6 | *
7 | * @regards
8 | *
9 | * With the IoT Application Core REPL, the internals of Core are directly
10 | * accessible.
11 | *
12 | * To get started, run `pnpm repl` and the Core REPL will boot.
13 | *
14 | * @see {@link https://docs.nestjs.com/recipes/repl}
15 | * @see {@link https://docs.nestjs.com/recipes/repl#watch-mode}
16 | *
17 | * @internal
18 | */
19 | async function bootstrap() {
20 | const replServer = await repl(AppModule);
21 |
22 | // enables persistence of REPL commands
23 | replServer.setupHistory('.nestjs_repl_history', (err) => {
24 | if (err) {
25 | console.error(err);
26 | }
27 | });
28 | }
29 |
30 | void bootstrap();
31 |
--------------------------------------------------------------------------------
/apps/core/src/testing/aws-configuration.ts:
--------------------------------------------------------------------------------
1 | export const accessKeyId = 'fakeMyKeyId';
2 | export const secretAccessKey = 'fakeSecretAccessKey';
3 | export const credentials = {
4 | accessKeyId,
5 | secretAccessKey,
6 | };
7 |
8 | export const region = 'us-west-2';
9 |
10 | export const databaseEndpoint = 'http://localhost:8001';
11 | export const databaseLaunchLocal = 'false';
12 | export const databasePort = '8001';
13 | export const databaseTableName = 'dashboard-api-e2e-test';
14 |
15 | export const configureTestProcessEnv = (env: NodeJS.ProcessEnv) => {
16 | env.AWS_ACCESS_KEY_ID = accessKeyId;
17 | env.AWS_SECRET_ACCESS_KEY = secretAccessKey;
18 | env.AWS_REGION = region;
19 | env.DATABASE_PORT = databasePort;
20 | env.DATABASE_ENDPOINT = databaseEndpoint;
21 | env.DATABASE_TABLE_NAME = databaseTableName;
22 | env.DATABASE_LAUNCH_LOCAL = databaseLaunchLocal;
23 | };
24 |
25 | export const configureEdgeTestProcessEnv = (env: NodeJS.ProcessEnv) => {
26 | configureTestProcessEnv(env);
27 | env.AUTH_MODE = 'edge';
28 | env.EDGE_ENDPOINT = 'https://1.2.3.4';
29 | };
30 |
--------------------------------------------------------------------------------
/apps/core/src/types/environment.ts:
--------------------------------------------------------------------------------
1 | export type Undefined = undefined | 'undefined';
2 | export type Defined = T;
3 | export type Maybe = Defined | Undefined;
4 |
5 | export function isUndefined(maybe: Maybe): maybe is Undefined {
6 | return maybe === undefined || maybe === 'undefined';
7 | }
8 |
9 | export function isDefined(maybe: Maybe): maybe is Defined {
10 | return !isUndefined(maybe);
11 | }
12 |
13 | export function isDevEnv() {
14 | return process.env.NODE_ENV === 'development';
15 | }
16 |
17 | export const MetricModes = {
18 | Local: 'local',
19 | Cloud: 'cloud',
20 | };
21 |
22 | export const LogModes = {
23 | Local: 'local',
24 | Cloud: 'cloud',
25 | };
26 |
27 | export function getMetricsMode() {
28 | if (isDevEnv()) {
29 | return MetricModes.Local;
30 | }
31 | return MetricModes.Cloud;
32 | }
33 |
34 | export function getLogMode() {
35 | if (isDevEnv()) {
36 | return LogModes.Local;
37 | }
38 | return LogModes.Cloud;
39 | }
40 |
--------------------------------------------------------------------------------
/apps/core/src/types/index.ts:
--------------------------------------------------------------------------------
1 | // TODO: Move ADTs to shared package
2 | export type Nothing = null | undefined;
3 | export type Just = T;
4 | export type Maybe = Just | Nothing;
5 |
6 | export function isNothing(maybe: Maybe): maybe is Nothing {
7 | return maybe == null;
8 | }
9 |
10 | export function isJust(maybe: Maybe): maybe is Just {
11 | return !isNothing(maybe);
12 | }
13 |
14 | export interface Err {
15 | readonly _tag: 'Err';
16 | readonly err: E;
17 | }
18 | export interface Ok {
19 | readonly _tag: 'Ok';
20 | readonly ok: A;
21 | }
22 | export type Result = Err | Ok;
23 |
24 | export function err(err: E): Result {
25 | return { _tag: 'Err', err };
26 | }
27 |
28 | export function ok(ok: A): Result {
29 | return { _tag: 'Ok', ok };
30 | }
31 |
32 | export function isErr(either: Result): either is Err {
33 | return either._tag === 'Err';
34 | }
35 |
36 | export function isOk(either: Result): either is Ok {
37 | return either._tag === 'Ok';
38 | }
39 |
--------------------------------------------------------------------------------
/apps/core/src/types/strings/is-string-with-value.ts:
--------------------------------------------------------------------------------
1 | const isString = (value: unknown): value is string => {
2 | return typeof value === 'string';
3 | };
4 |
5 | export const isStringWithValue = (value: unknown): value is string => {
6 | return isString(value) && value !== '';
7 | };
8 |
--------------------------------------------------------------------------------
/apps/core/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/apps/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Core",
4 | "exclude": ["node_modules"],
5 | "extends": "tsconfig/base.json",
6 | "compilerOptions": {
7 | "baseUrl": "./",
8 | "lib": ["es2022"],
9 | "outDir": "./dist",
10 | "rootDir": "./src",
11 | "strictPropertyInitialization": false,
12 | "target": "es2022"
13 | },
14 | "include": ["src"]
15 | }
16 |
--------------------------------------------------------------------------------
/apps/core/webpack-hmr.config.js:
--------------------------------------------------------------------------------
1 | const nodeExternals = require('webpack-node-externals');
2 | const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');
3 |
4 | module.exports = function (options, webpack) {
5 | return {
6 | ...options,
7 | entry: ['webpack/hot/poll?100', options.entry],
8 | externals: [
9 | nodeExternals({
10 | allowlist: ['webpack/hot/poll?100'],
11 | modulesDir: '../../node_modules',
12 | }),
13 | ],
14 | plugins: [
15 | ...options.plugins,
16 | new webpack.HotModuleReplacementPlugin(),
17 | new webpack.WatchIgnorePlugin({
18 | paths: [/\.js$/, /\.d\.ts$/],
19 | }),
20 | new RunScriptWebpackPlugin({
21 | name: options.output.filename,
22 | autoRestart: false,
23 | }),
24 | ],
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/cdk/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | node: true,
4 | },
5 | extends: ['custom'],
6 | parserOptions: {
7 | ecmaVersion: 2020,
8 | project: 'tsconfig.json',
9 | sourceType: 'module',
10 | ecmaFeatures: {
11 | jsx: false,
12 | },
13 | },
14 | root: true,
15 | };
16 |
--------------------------------------------------------------------------------
/cdk/README.md:
--------------------------------------------------------------------------------
1 | # CDK
2 |
3 | This is a CDK package for application deployment.
4 |
5 | The `cdk.json` file tells the CDK Toolkit how to execute your app.
6 |
7 | ## Useful commands
8 |
9 | * `npm run build` compile typescript to js
10 | * `npm run watch` watch for changes and compile
11 | * `cdk deploy` deploy this stack to your default AWS account/region
12 | * `cdk diff` compare deployed stack with current state
13 | * `cdk synth` emits the synthesized CloudFormation template
14 |
--------------------------------------------------------------------------------
/cdk/bin/cdk.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import 'source-map-support/register';
3 | import { App, RemovalPolicy } from 'aws-cdk-lib';
4 | import { IotApplicationStack } from '../lib/iot-application-stack';
5 |
6 | const app = new App();
7 | const stackName = app.node.tryGetContext('stackName') as string;
8 | const cleanupRetainedResources = app.node.tryGetContext(
9 | 'cleanupRetainedResources',
10 | ) as boolean;
11 | const removalPolicyOverride = cleanupRetainedResources
12 | ? RemovalPolicy.DESTROY
13 | : undefined;
14 |
15 | new IotApplicationStack(app, stackName, {
16 | removalPolicyOverride,
17 | /* If you don't specify 'env', this stack will be environment-agnostic.
18 | * Account/Region-dependent features and context lookups will not work,
19 | * but a single synthesized template can be deployed anywhere. */
20 | /* Uncomment the next line to specialize this stack for the AWS Account
21 | * and Region that are implied by the current CLI configuration. */
22 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
23 | /* Uncomment the next line if you know exactly what Account and Region you
24 | * want to deploy the stack to. */
25 | // env: { account: '123456789012', region: 'us-east-1' },
26 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
27 | });
28 |
--------------------------------------------------------------------------------
/cdk/lib/auth/sso-auth-stack.ts:
--------------------------------------------------------------------------------
1 | import { UserPoolDomain } from 'aws-cdk-lib/aws-cognito';
2 | import { Construct } from 'constructs';
3 | import { AuthStackProps, AuthStack } from './auth-stack';
4 |
5 | export class SsoAuthStack extends AuthStack {
6 | readonly domain: UserPoolDomain;
7 |
8 | constructor(scope: Construct, id: string, props: AuthStackProps) {
9 | super(scope, id, props);
10 |
11 | this.domain = this.userPool.addDomain('Domain', {
12 | cognitoDomain: {
13 | // Use the unique id for this cdk construct for naming
14 | domainPrefix: `sitewise-${this.node.addr.substring(0, 6)}`,
15 | },
16 | });
17 |
18 | this.domain.signInUrl(this.userPoolClient, {
19 | redirectUri: 'https://.awsapprunner.com',
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/cdk/lib/core/core-service-web-acl.ts:
--------------------------------------------------------------------------------
1 | import { CfnWebACL, CfnWebACLAssociation } from 'aws-cdk-lib/aws-wafv2';
2 | import { Construct } from 'constructs';
3 |
4 | export class CoreServiceWebACL extends Construct {
5 | readonly webACL: CfnWebACL;
6 |
7 | constructor(scope: Construct, id: string) {
8 | super(scope, id);
9 |
10 | this.webACL = new CfnWebACL(this, 'WebACL', {
11 | defaultAction: { allow: {} },
12 | scope: 'REGIONAL',
13 | visibilityConfig: {
14 | cloudWatchMetricsEnabled: false,
15 | metricName: 'IoTAppCoreServiceWebACL',
16 | sampledRequestsEnabled: true,
17 | },
18 | rules: [
19 | {
20 | action: { block: {} },
21 | name: 'IoTAppCoreServiceRateLimit',
22 | priority: 0,
23 | statement: {
24 | rateBasedStatement: {
25 | aggregateKeyType: 'IP',
26 | limit: 3000, // 10 TPS over 5 minutes
27 | },
28 | },
29 | visibilityConfig: {
30 | cloudWatchMetricsEnabled: true,
31 | metricName: 'IoTAppCoreServiceRateLimit',
32 | sampledRequestsEnabled: true,
33 | },
34 | },
35 | ],
36 | });
37 | }
38 |
39 | public associate(resourceArn: string) {
40 | new CfnWebACLAssociation(this, 'WebACLAssociation', {
41 | resourceArn,
42 | webAclArn: this.webACL.attrArn,
43 | });
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/cdk/lib/core/core-stack.ts:
--------------------------------------------------------------------------------
1 | import { Stack, StackProps } from 'aws-cdk-lib';
2 | import { Construct } from 'constructs';
3 | import { CoreService, CoreServiceProps } from './core-service';
4 | import { CoreServiceWebACL } from './core-service-web-acl';
5 |
6 | export interface CoreStackProps extends StackProps {
7 | readonly coreServiceProps: CoreServiceProps;
8 | }
9 |
10 | export class CoreStack extends Stack {
11 | readonly coreService: CoreService;
12 |
13 | constructor(scope: Construct, id: string, props: CoreStackProps) {
14 | super(scope, id, props);
15 |
16 | this.coreService = new CoreService(this, 'Service', props.coreServiceProps);
17 |
18 | const {
19 | service: { attrServiceArn: coreServiceArn },
20 | } = this.coreService;
21 | const coreServiceWebACL = new CoreServiceWebACL(this, 'WebACL');
22 | coreServiceWebACL.associate(coreServiceArn);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/cdk/lib/csp/public-asset-directives.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getCloudWatchEndpoint,
3 | getCloudWatchLogsEndpoint,
4 | getCognitoIdenityPoolEndpoint,
5 | getCognitoUserPoolEndpoint,
6 | getEventsControlPlaneEndpoint,
7 | getEventsDataPlaneEndpoint,
8 | getSiteWiseControlPlaneEndpoint,
9 | getSiteWiseDataPlaneEndpoint,
10 | getTwinMakerControlPlaneEndpoint,
11 | getTwinMakerDataPlaneEndpoint,
12 | } from '../endpoints/aws-endpoints';
13 |
14 | export function getServicesEndpoints(region: string): string[] {
15 | return [
16 | getCloudWatchEndpoint(region),
17 | getCloudWatchLogsEndpoint(region),
18 | getCognitoIdenityPoolEndpoint(region),
19 | getCognitoUserPoolEndpoint(region),
20 | getEventsControlPlaneEndpoint(region),
21 | getEventsDataPlaneEndpoint(region),
22 | getSiteWiseControlPlaneEndpoint(region),
23 | getSiteWiseDataPlaneEndpoint(region),
24 | getTwinMakerControlPlaneEndpoint(region),
25 | getTwinMakerDataPlaneEndpoint(region),
26 | ];
27 | }
28 |
--------------------------------------------------------------------------------
/cdk/lib/database/database-stack.ts:
--------------------------------------------------------------------------------
1 | import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
2 | import {
3 | AttributeType,
4 | BillingMode,
5 | ProjectionType,
6 | Table,
7 | } from 'aws-cdk-lib/aws-dynamodb';
8 | import { Construct } from 'constructs';
9 |
10 | export interface DatabaseStackProps extends StackProps {
11 | readonly removalPolicyOverride?: RemovalPolicy;
12 | }
13 |
14 | export class DatabaseStack extends Stack {
15 | readonly resourceTable: Table;
16 |
17 | constructor(scope: Construct, id: string, props: DatabaseStackProps) {
18 | super(scope, id, props);
19 |
20 | const { removalPolicyOverride } = props;
21 |
22 | this.resourceTable = new Table(this, 'ResourceTable', {
23 | pointInTimeRecovery: true,
24 | partitionKey: {
25 | name: 'id',
26 | type: AttributeType.STRING,
27 | },
28 | sortKey: {
29 | name: 'resourceType',
30 | type: AttributeType.STRING,
31 | },
32 | billingMode: BillingMode.PAY_PER_REQUEST,
33 | removalPolicy: removalPolicyOverride,
34 | });
35 | this.resourceTable.addGlobalSecondaryIndex({
36 | indexName: 'resourceTypeIndex',
37 | partitionKey: {
38 | name: 'resourceType',
39 | type: AttributeType.STRING,
40 | },
41 | projectionType: ProjectionType.ALL,
42 | });
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/cdk/lib/logging/logging-stack.ts:
--------------------------------------------------------------------------------
1 | import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
2 | import { LogGroup } from 'aws-cdk-lib/aws-logs';
3 | import { Construct } from 'constructs';
4 |
5 | export interface LoggingStackProps extends StackProps {
6 | readonly applicationName: string;
7 | readonly removalPolicyOverride?: RemovalPolicy;
8 | }
9 |
10 | export class LoggingStack extends Stack {
11 | readonly logGroup: LogGroup;
12 |
13 | constructor(scope: Construct, id: string, props: LoggingStackProps) {
14 | super(scope, id, props);
15 |
16 | const { applicationName, removalPolicyOverride } = props;
17 |
18 | this.logGroup = new LogGroup(this, 'ApplicationLogGroup', {
19 | logGroupName: applicationName,
20 | removalPolicy: removalPolicyOverride,
21 | });
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/cdk/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cdk",
3 | "version": "0.0.0",
4 | "private": true,
5 | "bin": {
6 | "cdk": "bin/cdk.js"
7 | },
8 | "scripts": {
9 | "build": "tsc",
10 | "clean": "rimraf .turbo && rimraf dist",
11 | "clean:full": "rimraf .turbo && rimraf node_modules && rimraf dist",
12 | "watch": "tsc -w",
13 | "bootstrap": "cdk bootstrap",
14 | "cdk": "cdk",
15 | "deploy": "cdk deploy --all",
16 | "deploy:no-review:cognito": "cdk deploy -c authMode=cognito --all --require-approval never",
17 | "deploy:no-review:sso": "cdk deploy -c authMode=sso --all --require-approval never",
18 | "lint": "TIMING=1 eslint \"{bin,lib}/**/*.ts\" --max-warnings 0",
19 | "lint:commit": "tsc --noEmit && TIMING=1 eslint $(git diff --name-only HEAD HEAD~1 | grep -E \"{bin,lib}/**/*.ts\" | xargs) --max-warnings 0",
20 | "lint:fix": "eslint \"{bin,lib}/**/*.ts\" --fix"
21 | },
22 | "dependencies": {
23 | "aws-cdk-lib": "2.131.0",
24 | "constructs": "^10.2.70",
25 | "source-map-support": "^0.5.21"
26 | },
27 | "devDependencies": {
28 | "@types/node": "20.11.10",
29 | "aws-cdk": "2.130.0",
30 | "tsconfig": "*"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/cdk/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "cdk",
4 | "compilerOptions": {
5 | "target": "ES2020",
6 | "module": "commonjs",
7 | "lib": [
8 | "es2020"
9 | ],
10 | "outDir": "./dist",
11 | "strictPropertyInitialization": false
12 | },
13 | "exclude": [
14 | "node_modules",
15 | "cdk.out"
16 | ],
17 | "extends": "tsconfig/base.json"
18 | }
19 |
--------------------------------------------------------------------------------
/install-deploy-unix.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script installs prerequesites and deploy the application for Unix based
4 | # system.
5 |
6 | # Install prerequesites
7 | . install-prepreqs-unix.sh
8 |
9 | # Deploy the application
10 | echo "Deploying the application..."
11 | yarn deploy
12 | echo
13 |
--------------------------------------------------------------------------------
/install-prepreqs-unix.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script installs Node.JS related prerequesites for Unix based system.
4 |
5 | # Install Volta
6 | echo "Installing Volta..."
7 | curl https://get.volta.sh | bash
8 | export VOLTA_HOME="$HOME/.volta"
9 | export PATH="$VOLTA_HOME/bin:$PATH"
10 | echo
11 |
12 | # Install Node.js@18 with Volta
13 | echo "Installing Node.js@18..."
14 | volta install node@18
15 | echo
16 |
17 | # Install Yarn with Volta
18 | echo "Install Yarn..."
19 | volta install yarn
20 | echo
21 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | },
5 | extends: [
6 | 'eslint:recommended',
7 | 'plugin:@typescript-eslint/recommended',
8 | 'plugin:@typescript-eslint/recommended-requiring-type-checking',
9 | 'plugin:@typescript-eslint/strict',
10 | 'plugin:prettier/recommended',
11 | 'plugin:turbo/recommended',
12 | ],
13 | ignorePatterns: ['.eslintrc.js'],
14 | parser: '@typescript-eslint/parser',
15 | plugins: ['@typescript-eslint', 'prettier', 'turbo'],
16 | rules: {
17 | 'no-console': ['error', { allow: ['warn', 'error'] }],
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-config-custom",
3 | "version": "0.0.0",
4 | "private": true,
5 | "main": "index.js",
6 | "scripts": {
7 | "clean": "rimraf .turbo",
8 | "clean:full": "rimraf .turbo && rimraf node_modules"
9 | },
10 | "dependencies": {
11 | "eslint-config-prettier": "^9.1.0",
12 | "eslint-config-react": "^1.1.7",
13 | "eslint-config-turbo": "latest"
14 | },
15 | "publishConfig": {
16 | "access": "public"
17 | },
18 | "devDependencies": {
19 | "eslint-plugin-prettier": "^5.1.3"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/jest-config/base.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'jest';
2 |
3 | const config: Config = {
4 | collectCoverage: true,
5 | coverageDirectory: './coverage',
6 | coverageThreshold: {
7 | global: {
8 | branches: 55,
9 | functions: 80,
10 | lines: 80,
11 | statements: 80,
12 | },
13 | },
14 | notify: true,
15 | };
16 |
17 | export default config;
18 |
--------------------------------------------------------------------------------
/packages/jest-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jest-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "publishConfig": {
6 | "access": "public"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/tsconfig/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Base",
4 | "compilerOptions": {
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "target": "es2015",
8 |
9 | "allowSyntheticDefaultImports": true,
10 | "allowUnreachableCode": false,
11 | "allowUnusedLabels": false,
12 | "checkJs": true,
13 | "composite": true,
14 | "declaration": true,
15 | "declarationMap": true,
16 | "emitDecoratorMetadata": true,
17 | "esModuleInterop": true,
18 | "experimentalDecorators": true,
19 | "forceConsistentCasingInFileNames": true,
20 | "verbatimModuleSyntax": false,
21 | "incremental": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noImplicitOverride": true,
24 | "noImplicitReturns": true,
25 | "noUncheckedIndexedAccess": true,
26 | "noUnusedLocals": true,
27 | "noUnusedParameters": true,
28 | "preserveWatchOutput": true,
29 | "skipLibCheck": true,
30 | "sourceMap": true,
31 | "strict": true
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/tsconfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tsconfig",
3 | "version": "0.0.0",
4 | "private": true,
5 | "publishConfig": {
6 | "access": "public"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tests/auth.setup.ts:
--------------------------------------------------------------------------------
1 | import { test as setup, expect } from './helpers';
2 |
3 | // Read environment variables
4 | const userPassword = process.env.USER_PASSWORD ?? 'test-Password!';
5 |
6 | const authFile = 'playwright/.auth/user.json';
7 |
8 | setup('authenticate', async ({ page }) => {
9 | // increase timeout for authentication
10 | setup.slow();
11 |
12 | // user enters application at Amplify login page
13 | await page.goto('');
14 |
15 | // user enters their credentials
16 | await page.getByLabel('Username').fill('test-user');
17 | await page.getByLabel('Password').nth(0).fill(userPassword);
18 |
19 | // user clicks sign-in
20 | await page.getByRole('button', { name: 'Sign in' }).click();
21 |
22 | // user lands at home page
23 | await expect(
24 | page.getByRole('heading', { name: 'Dashboards' }).first(),
25 | ).toBeVisible();
26 |
27 | // storage of authentication state for tests
28 | await page.context().storageState({ path: authFile });
29 | });
30 |
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/empty-page-chromium-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/create-dashboard-page.spec.ts-snapshots/empty-page-chromium-darwin.png
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/empty-page-firefox-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/create-dashboard-page.spec.ts-snapshots/empty-page-firefox-darwin.png
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/empty-page-webkit-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/create-dashboard-page.spec.ts-snapshots/empty-page-webkit-darwin.png
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/filled-fields-chromium-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/create-dashboard-page.spec.ts-snapshots/filled-fields-chromium-darwin.png
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/filled-fields-firefox-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/create-dashboard-page.spec.ts-snapshots/filled-fields-firefox-darwin.png
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/filled-fields-webkit-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/create-dashboard-page.spec.ts-snapshots/filled-fields-webkit-darwin.png
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/max-length-errors-chromium-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "role-img-alt",
4 | "targets": [
5 | [
6 | "#formField\\:rk\\:-error > .awsui_error-icon-shake-wrapper_14mhv_d3ru4_97 > .awsui_error-icon-scale-wrapper_14mhv_d3ru4_124[role=\"img\"]"
7 | ],
8 | [
9 | "#formField\\:rl\\:-error > .awsui_error-icon-shake-wrapper_14mhv_d3ru4_97 > .awsui_error-icon-scale-wrapper_14mhv_d3ru4_124[role=\"img\"]"
10 | ]
11 | ]
12 | }
13 | ]
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/max-length-errors-chromium-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/create-dashboard-page.spec.ts-snapshots/max-length-errors-chromium-darwin.png
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/max-length-errors-firefox-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "role-img-alt",
4 | "targets": [
5 | [
6 | "#formField\\:rk\\:-error > .awsui_error-icon-shake-wrapper_14mhv_d3ru4_97 > .awsui_error-icon-scale-wrapper_14mhv_d3ru4_124[role=\"img\"]"
7 | ],
8 | [
9 | "#formField\\:rl\\:-error > .awsui_error-icon-shake-wrapper_14mhv_d3ru4_97 > .awsui_error-icon-scale-wrapper_14mhv_d3ru4_124[role=\"img\"]"
10 | ]
11 | ]
12 | }
13 | ]
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/max-length-errors-firefox-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/create-dashboard-page.spec.ts-snapshots/max-length-errors-firefox-darwin.png
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/max-length-errors-webkit-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "role-img-alt",
4 | "targets": [
5 | [
6 | "#formField\\:rk\\:-error > .awsui_error-icon-shake-wrapper_14mhv_d3ru4_97 > .awsui_error-icon-scale-wrapper_14mhv_d3ru4_124[role=\"img\"]"
7 | ],
8 | [
9 | "#formField\\:rl\\:-error > .awsui_error-icon-shake-wrapper_14mhv_d3ru4_97 > .awsui_error-icon-scale-wrapper_14mhv_d3ru4_124[role=\"img\"]"
10 | ]
11 | ]
12 | }
13 | ]
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/max-length-errors-webkit-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/create-dashboard-page.spec.ts-snapshots/max-length-errors-webkit-darwin.png
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/required-field-errors-chromium-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "role-img-alt",
4 | "targets": [
5 | [
6 | "#formField\\:rk\\:-error > .awsui_error-icon-shake-wrapper_14mhv_d3ru4_97 > .awsui_error-icon-scale-wrapper_14mhv_d3ru4_124[role=\"img\"]"
7 | ],
8 | [
9 | "#formField\\:rl\\:-error > .awsui_error-icon-shake-wrapper_14mhv_d3ru4_97 > .awsui_error-icon-scale-wrapper_14mhv_d3ru4_124[role=\"img\"]"
10 | ]
11 | ]
12 | }
13 | ]
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/required-field-errors-chromium-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/create-dashboard-page.spec.ts-snapshots/required-field-errors-chromium-darwin.png
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/required-field-errors-firefox-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "role-img-alt",
4 | "targets": [
5 | [
6 | "#formField\\:rk\\:-error > .awsui_error-icon-shake-wrapper_14mhv_d3ru4_97 > .awsui_error-icon-scale-wrapper_14mhv_d3ru4_124[role=\"img\"]"
7 | ],
8 | [
9 | "#formField\\:rl\\:-error > .awsui_error-icon-shake-wrapper_14mhv_d3ru4_97 > .awsui_error-icon-scale-wrapper_14mhv_d3ru4_124[role=\"img\"]"
10 | ]
11 | ]
12 | }
13 | ]
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/required-field-errors-firefox-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/create-dashboard-page.spec.ts-snapshots/required-field-errors-firefox-darwin.png
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/required-field-errors-webkit-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "role-img-alt",
4 | "targets": [
5 | [
6 | "#formField\\:rk\\:-error > .awsui_error-icon-shake-wrapper_14mhv_d3ru4_97 > .awsui_error-icon-scale-wrapper_14mhv_d3ru4_124[role=\"img\"]"
7 | ],
8 | [
9 | "#formField\\:rl\\:-error > .awsui_error-icon-shake-wrapper_14mhv_d3ru4_97 > .awsui_error-icon-scale-wrapper_14mhv_d3ru4_124[role=\"img\"]"
10 | ]
11 | ]
12 | }
13 | ]
--------------------------------------------------------------------------------
/tests/create-dashboard-page.spec.ts-snapshots/required-field-errors-webkit-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/create-dashboard-page.spec.ts-snapshots/required-field-errors-webkit-darwin.png
--------------------------------------------------------------------------------
/tests/dashboard-list-page.spec.ts-snapshots/dashboard-list-page-accessibility-scan-results-chromium-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "target-size",
4 | "targets": [
5 | [
6 | ".awsui_page-number_fvjdu_ziaze_151"
7 | ],
8 | [
9 | "#link-self\\:r3l\\:"
10 | ],
11 | [
12 | "#link-self\\:r3q\\:"
13 | ]
14 | ]
15 | }
16 | ]
--------------------------------------------------------------------------------
/tests/dashboard-list-page.spec.ts-snapshots/dashboard-list-page-accessibility-scan-results-firefox-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "target-size",
4 | "targets": [
5 | [
6 | "#link-self\\:r3l\\:"
7 | ],
8 | [
9 | "#link-self\\:r3q\\:"
10 | ]
11 | ]
12 | }
13 | ]
--------------------------------------------------------------------------------
/tests/dashboard-list-page.spec.ts-snapshots/dashboard-list-page-accessibility-scan-results-webkit-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "target-size",
4 | "targets": [
5 | [
6 | "#link-self\\:r3l\\:"
7 | ],
8 | [
9 | "#link-self\\:r3q\\:"
10 | ]
11 | ]
12 | }
13 | ]
--------------------------------------------------------------------------------
/tests/dashboard-page.spec.ts-snapshots/dashboard-page-chromium-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/dashboard-page.spec.ts-snapshots/dashboard-page-chromium-darwin.png
--------------------------------------------------------------------------------
/tests/dashboard-page.spec.ts-snapshots/dashboard-page-firefox-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/dashboard-page.spec.ts-snapshots/dashboard-page-firefox-darwin.png
--------------------------------------------------------------------------------
/tests/dashboard-page.spec.ts-snapshots/dashboard-page-webkit-darwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/tests/dashboard-page.spec.ts-snapshots/dashboard-page-webkit-darwin.png
--------------------------------------------------------------------------------
/tests/dashboards/dashboard-management.spec.ts-snapshots/create-dashboard-page-accessibility-scan-results-chromium-darwin:
--------------------------------------------------------------------------------
1 | []
--------------------------------------------------------------------------------
/tests/dashboards/dashboard-management.spec.ts-snapshots/create-dashboard-page-accessibility-scan-results-firefox-darwin:
--------------------------------------------------------------------------------
1 | []
--------------------------------------------------------------------------------
/tests/dashboards/dashboard-management.spec.ts-snapshots/create-dashboard-page-accessibility-scan-results-webkit-darwin:
--------------------------------------------------------------------------------
1 | []
--------------------------------------------------------------------------------
/tests/dashboards/dashboard-management.spec.ts-snapshots/dashboard-page-accessibility-scan-results-chromium-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "empty-heading",
4 | "targets": [
5 | [
6 | "h1"
7 | ]
8 | ]
9 | },
10 | {
11 | "rule": "empty-table-header",
12 | "targets": [
13 | [
14 | ".awsui_selection-control_wih1l_1km9g_221"
15 | ]
16 | ]
17 | },
18 | {
19 | "rule": "heading-order",
20 | "targets": [
21 | [
22 | ".widget-panel > h4"
23 | ]
24 | ]
25 | },
26 | {
27 | "rule": "target-size",
28 | "targets": [
29 | [
30 | ".awsui_page-number_fvjdu_m8qbw_151"
31 | ],
32 | [
33 | ".awsui_resizer_x7peu_19okv_98"
34 | ]
35 | ]
36 | }
37 | ]
--------------------------------------------------------------------------------
/tests/dashboards/dashboard-management.spec.ts-snapshots/dashboard-page-accessibility-scan-results-firefox-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "empty-heading",
4 | "targets": [
5 | [
6 | "h1"
7 | ]
8 | ]
9 | },
10 | {
11 | "rule": "empty-table-header",
12 | "targets": [
13 | [
14 | ".awsui_selection-control_wih1l_1km9g_221"
15 | ]
16 | ]
17 | },
18 | {
19 | "rule": "heading-order",
20 | "targets": [
21 | [
22 | ".widget-panel > h4"
23 | ]
24 | ]
25 | },
26 | {
27 | "rule": "target-size",
28 | "targets": [
29 | [
30 | ".awsui_page-number_fvjdu_m8qbw_151"
31 | ],
32 | [
33 | ".awsui_resizer_x7peu_19okv_98"
34 | ]
35 | ]
36 | }
37 | ]
--------------------------------------------------------------------------------
/tests/dashboards/dashboard-management.spec.ts-snapshots/dashboard-page-accessibility-scan-results-webkit-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "empty-heading",
4 | "targets": [
5 | [
6 | "h1"
7 | ]
8 | ]
9 | },
10 | {
11 | "rule": "empty-table-header",
12 | "targets": [
13 | [
14 | ".awsui_selection-control_wih1l_1km9g_221"
15 | ]
16 | ]
17 | },
18 | {
19 | "rule": "heading-order",
20 | "targets": [
21 | [
22 | ".widget-panel > h4"
23 | ]
24 | ]
25 | },
26 | {
27 | "rule": "target-size",
28 | "targets": [
29 | [
30 | ".awsui_page-number_fvjdu_m8qbw_151"
31 | ],
32 | [
33 | ".awsui_resizer_x7peu_19okv_98"
34 | ]
35 | ]
36 | }
37 | ]
--------------------------------------------------------------------------------
/tests/dashboards/dashboard-management.spec.ts-snapshots/dashboards-page-accessibility-scan-results-chromium-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "target-size",
4 | "targets": [
5 | [
6 | ".awsui_page-number_fvjdu_m8qbw_151"
7 | ],
8 | [
9 | "#link-self\\:r3l\\:"
10 | ]
11 | ]
12 | }
13 | ]
--------------------------------------------------------------------------------
/tests/dashboards/dashboard-management.spec.ts-snapshots/dashboards-page-accessibility-scan-results-firefox-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "target-size",
4 | "targets": [
5 | [
6 | ".awsui_page-number_fvjdu_m8qbw_151"
7 | ],
8 | [
9 | "#link-self\\:r3l\\:"
10 | ]
11 | ]
12 | }
13 | ]
--------------------------------------------------------------------------------
/tests/dashboards/dashboard-management.spec.ts-snapshots/dashboards-page-accessibility-scan-results-webkit-darwin:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "rule": "target-size",
4 | "targets": [
5 | [
6 | ".awsui_page-number_fvjdu_m8qbw_151"
7 | ],
8 | [
9 | "#link-self\\:r3l\\:"
10 | ]
11 | ]
12 | }
13 | ]
--------------------------------------------------------------------------------
/tests/functional-homepage-iot-application/add-footer-throughout.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '../helpers';
2 | import { DashboardsIndexPage } from '../pages/dashboards-index.page';
3 | import { Footer } from '../pages/application-frame.page';
4 |
5 | test.describe('Homepage For Iot Application - Footer Testing', () => {
6 | // Positive Scenario: Verify Footer Rendering
7 | test('Verify Footer Rendering', async ({ page }) => {
8 | const dashboardsPage = new DashboardsIndexPage(page);
9 |
10 | // Navigate to the dashboard page where the footer should be displayed.
11 | await dashboardsPage.goto();
12 | await dashboardsPage.expectIsCurrentPage();
13 |
14 | // Initialize the Footer object.
15 | const footer = new Footer(page);
16 |
17 | // Check if the footer is visible.
18 | await footer.isFooterVisible();
19 |
20 | // Assert that the Footer is visible.
21 | await expect(footer.footerElement).toBeVisible();
22 |
23 | // Get the copyright text from the footer.
24 | const copyrightText = await footer.getCopyrightText();
25 |
26 | // Assert that the copyright text contains the expected content.
27 | expect(copyrightText).toContain('© 2023, Amazon Web Services, Inc.');
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/tests/pages/dashboard-page.page.ts:
--------------------------------------------------------------------------------
1 | import { type Page, type Locator } from '@playwright/test';
2 | import { expect, type Dashboard } from '../helpers';
3 |
4 | export class DashboardPage {
5 | readonly #page: Page;
6 | readonly #dashboard: Dashboard;
7 | public readonly heading: Locator;
8 | public readonly editButton: Locator;
9 | public readonly saveButton: Locator;
10 |
11 | constructor({ page, dashboard }: { page: Page; dashboard: Dashboard }) {
12 | this.#page = page;
13 | this.#dashboard = dashboard;
14 | this.heading = page.getByRole('heading', { name: dashboard.name });
15 | this.editButton = page.getByRole('button', { name: 'Edit' });
16 | this.saveButton = page.getByRole('button', { name: 'Save' });
17 | }
18 |
19 | public async goto() {
20 | await this.#page.goto(`dashboards/${this.#dashboard.id}`);
21 | }
22 |
23 | public async expectIsCurrentPage() {
24 | await expect(this.#page).toHaveURL(`dashboards/${this.#dashboard.id}`);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/tsconfig",
3 | "display": "IoT Application",
4 | "exclude": ["node_modules"],
5 | "extends": "tsconfig/base.json",
6 | "include": ["tests"]
7 | }
8 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "pipeline": {
4 | "build": {
5 | "outputs": [
6 | "build/**",
7 | "dist/**"
8 | ]
9 | },
10 | "clean": {
11 | "cache": false
12 | },
13 | "clean:full": {
14 | "cache": false
15 | },
16 | "dev": {
17 | "cache": false,
18 | "persistent": true,
19 | "env": [
20 | "APPLICATION_NAME",
21 | "AWS_ACCESS_KEY_ID",
22 | "AWS_REGION",
23 | "AWS_SECRET_ACCESS_KEY",
24 | "AWS_SESSION_TOKEN",
25 | "COGNITO_IDENTITY_POOL_ID",
26 | "COGNITO_USE_LOCAL_VERIFIER",
27 | "COGNITO_USER_POOL_CLIENT_ID",
28 | "COGNITO_USER_POOL_ID",
29 | "COGNITO_DOMAIN_NAME",
30 | "DATABASE_ENDPOINT",
31 | "DATABASE_TABLE_NAME",
32 | "DATABASE_PORT",
33 | "DATABASE_LAUNCH_LOCAL",
34 | "EDGE_ENDPOINT",
35 | "PUBLIC_URL",
36 | "NODE_ENV",
37 | "SERVICE_ENDPOINTS",
38 | "AUTH_MODE"
39 | ]
40 | },
41 | "lint": {},
42 | "lint:commit": {},
43 | "lint:fix": {},
44 | "playwright": {
45 | "env": [
46 | "ENDPOINT",
47 | "LAUNCH_WEB_SERVER",
48 | "USER_PASSWORD"
49 | ]
50 | },
51 | "test": {
52 | "outputs": [
53 | "coverage/**"
54 | ]
55 | },
56 | "test:commit": {},
57 | "test:watch": {
58 | "cache": false
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/userguide/imgs/app-sign-in-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/userguide/imgs/app-sign-in-screen.png
--------------------------------------------------------------------------------
/userguide/imgs/app-sign-out-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/userguide/imgs/app-sign-out-screen.png
--------------------------------------------------------------------------------
/userguide/imgs/dashboard-creation-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/userguide/imgs/dashboard-creation-screen.png
--------------------------------------------------------------------------------
/userguide/imgs/dashboard-edit-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/userguide/imgs/dashboard-edit-screen.png
--------------------------------------------------------------------------------
/userguide/imgs/dashboard-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/userguide/imgs/dashboard-screen.png
--------------------------------------------------------------------------------
/userguide/imgs/dashboards-list-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/userguide/imgs/dashboards-list-screen.png
--------------------------------------------------------------------------------
/userguide/imgs/user-pool-resource-on-cfn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/userguide/imgs/user-pool-resource-on-cfn.png
--------------------------------------------------------------------------------
/userguide/imgs/users-tab-on-cognito.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awslabs/iot-application/d554d74dbb1d9e100e2e62fdd74197dfafcc16bf/userguide/imgs/users-tab-on-cognito.png
--------------------------------------------------------------------------------