├── .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(