diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 49de5dac16..c1bcf5e41c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -30,7 +30,6 @@ /workspaces/orchestrator @redhat-developer/rhdh-plugins-maintainers @lholmquist @lokanandaprabhu @redhat-developer/rhdh-plugins @karthikjeeyar @christoph-jerolimov /workspaces/quickstart @redhat-developer/rhdh-plugins-maintainers @redhat-developer/rhdh-ui @divyanshiGupta @its-mitesh-kumar /workspaces/cost-management @redhat-developer/rhdh-plugins-maintainers @ydayagi @mareklibra @batzionb @PreetiW @asmasarw -/workspaces/sandbox @redhat-developer/rhdh-plugins-maintainers @lucifergene @rohitkrai03 @karthikjeeyar @xcoulon @alexeykazakov @mfrancisc @rsoaresd @jrosental @rajivnathan @MatousJobanek /workspaces/scorecard @redhat-developer/rhdh-plugins-maintainers @redhat-developer/rhdh-plugins @redhat-developer/rhdh-ui @christoph-jerolimov @its-mitesh-kumar @eswaraiahsapram /workspaces/theme @redhat-developer/rhdh-plugins-maintainers @redhat-developer/rhdh-ui @ciiay @logonoff /workspaces/translations @redhat-developer/rhdh-plugins-maintainers @redhat-developer/rhdh-ui @ciiay @debsmita1 @karthikjeeyar diff --git a/.github/archived-plugins.json b/.github/archived-plugins.json index 7b763f7ee9..3d0bdf08f4 100644 --- a/.github/archived-plugins.json +++ b/.github/archived-plugins.json @@ -8,6 +8,15 @@ "gitTag": "@red-hat-developer-hub/backstage-plugin-openshift-image-registry@1.18.0", "reason": "No longer maintained", "archivedDate": "2026-04-01" + }, + { + "pluginName": "@red-hat-developer-hub/backstage-plugin-sandbox", + "version": "0.1.0", + "workspace": "sandbox", + "plugin": "sandbox", + "gitTag": "@red-hat-developer-hub/backstage-plugin-sandbox@0.1.0", + "reason": "No longer maintained", + "archivedDate": "2026-06-10" } ] } diff --git a/.github/labeler.yml b/.github/labeler.yml index dd87015c7a..17cc15b416 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -52,9 +52,6 @@ workspace/quickstart: workspace/repo-tools: - 'Workspace\\s*repo-tools' -workspace/sandbox: - - 'Workspace\\s*sandbox' - workspace/scorecard: - 'Workspace\\s*scorecard' diff --git a/.github/pr-labeler.yml b/.github/pr-labeler.yml index 21f152eaf4..41fe8f2fc7 100644 --- a/.github/pr-labeler.yml +++ b/.github/pr-labeler.yml @@ -70,10 +70,6 @@ workspace/repo-tools: - changed-files: - any-glob-to-any-file: ['workspaces/repo-tools/**'] -workspace/sandbox: - - changed-files: - - any-glob-to-any-file: ['workspaces/sandbox/**'] - workspace/scorecard: - changed-files: - any-glob-to-any-file: ['workspaces/scorecard/**'] diff --git a/.github/renovate-presets/workspace/rhdh-sandbox-presets.json b/.github/renovate-presets/workspace/rhdh-sandbox-presets.json deleted file mode 100644 index 9e1544f244..0000000000 --- a/.github/renovate-presets/workspace/rhdh-sandbox-presets.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "packageRules": [ - { - "description": "all Sandbox minor updates", - "matchFileNames": ["workspaces/sandbox/**"], - "extends": [ - "github>redhat-developer/rhdh-plugins//.github/renovate-presets/base/rhdh-minor-presets(Sandbox)" - ], - "addLabels": ["team/sandbox", "sandbox"] - }, - { - "description": "all Sandbox patch updates", - "matchFileNames": ["workspaces/sandbox/**"], - "extends": [ - "github>redhat-developer/rhdh-plugins//.github/renovate-presets/base/rhdh-patch-presets(Sandbox)" - ], - "addLabels": ["team/sandbox", "sandbox"] - }, - { - "description": "all Sandbox dev dependency updates", - "matchFileNames": ["workspaces/sandbox/**"], - "extends": [ - "github>redhat-developer/rhdh-plugins//.github/renovate-presets/base/rhdh-devdependency-presets(Sandbox)" - ], - "addLabels": ["team/sandbox", "sandbox"] - } - ] -} diff --git a/.github/renovate.json b/.github/renovate.json index e6d4c007a3..2395aef51d 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -56,7 +56,6 @@ "github>redhat-developer/rhdh-plugins//.github/renovate-presets/workspace/rhdh-theme-presets", "github>redhat-developer/rhdh-plugins//.github/renovate-presets/workspace/rhdh-adoption-insights-presets", "github>redhat-developer/rhdh-plugins//.github/renovate-presets/workspace/rhdh-orchestrator-presets", - "github>redhat-developer/rhdh-plugins//.github/renovate-presets/workspace/rhdh-sandbox-presets", "github>redhat-developer/rhdh-plugins//.github/renovate-presets/workspace/rhdh-cost-management-presets", "github>redhat-developer/rhdh-plugins//.github/renovate-presets/workspace/rhdh-quickstart-presets", "github>redhat-developer/rhdh-plugins//.github/renovate-presets/workspace/rhdh-mcp-integrations-presets", diff --git a/ARCHIVED_WORKSPACES.md b/ARCHIVED_WORKSPACES.md index 9a9806faa9..fc829e4229 100644 --- a/ARCHIVED_WORKSPACES.md +++ b/ARCHIVED_WORKSPACES.md @@ -17,3 +17,4 @@ When a workspace or plugin is archived: | Workspace | Package | Reason | Source | | ------------------------ | ------------------------------------------------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | openshift-image-registry | `@red-hat-developer-hub/backstage-plugin-openshift-image-registry` | No longer maintained | [@red-hat-developer-hub/backstage-plugin-openshift-image-registry@1.18.0](https://github.com/redhat-developer/rhdh-plugins/tree/%40red-hat-developer-hub%2Fbackstage-plugin-openshift-image-registry%401.18.0/workspaces/openshift-image-registry) | +| sandbox | `@red-hat-developer-hub/backstage-plugin-sandbox` | No longer maintained | [@red-hat-developer-hub/backstage-plugin-sandbox@0.1.0](https://github.com/redhat-developer/rhdh-plugins/tree/%40red-hat-developer-hub%2Fbackstage-plugin-sandbox%400.1.0/workspaces/sandbox) | diff --git a/codecov.yml b/codecov.yml index 63a3452ac9..a7ab6717b2 100644 --- a/codecov.yml +++ b/codecov.yml @@ -139,9 +139,6 @@ flag_management: - name: repo-tools paths: - workspaces/repo-tools/ - - name: sandbox - paths: - - workspaces/sandbox/ - name: scorecard paths: - workspaces/scorecard/ diff --git a/workspaces/sandbox/.changeset/README.md b/workspaces/sandbox/.changeset/README.md deleted file mode 100644 index e5b6d8d6a6..0000000000 --- a/workspaces/sandbox/.changeset/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changesets - -Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works -with multi-package repos, or single-package repos to help you version and publish your code. You can -find the full documentation for it [in our repository](https://github.com/changesets/changesets) - -We have a quick list of common questions to get you started engaging with this project in -[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/workspaces/sandbox/.changeset/config.json b/workspaces/sandbox/.changeset/config.json deleted file mode 100644 index 8208df00f2..0000000000 --- a/workspaces/sandbox/.changeset/config.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", - "changelog": "@changesets/cli/changelog", - "commit": false, - "fixed": [], - "linked": [], - "access": "public", - "baseBranch": "main", - "updateInternalDependencies": "patch", - "privatePackages": { - "tag": false, - "version": false - } -} diff --git a/workspaces/sandbox/.dockerignore b/workspaces/sandbox/.dockerignore deleted file mode 100644 index 05edb62650..0000000000 --- a/workspaces/sandbox/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -.git -.yarn/cache -.yarn/install-state.gz -node_modules -packages/*/src -packages/*/node_modules -plugins -*.local.yaml diff --git a/workspaces/sandbox/.eslintignore b/workspaces/sandbox/.eslintignore deleted file mode 100644 index 129be7add2..0000000000 --- a/workspaces/sandbox/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -sandboxplaywright.config.ts diff --git a/workspaces/sandbox/.eslintrc.js b/workspaces/sandbox/.eslintrc.js deleted file mode 100644 index 59b86f8412..0000000000 --- a/workspaces/sandbox/.eslintrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../.eslintrc.cjs'); diff --git a/workspaces/sandbox/.gitignore b/workspaces/sandbox/.gitignore deleted file mode 100644 index b63d5a3359..0000000000 --- a/workspaces/sandbox/.gitignore +++ /dev/null @@ -1,68 +0,0 @@ -# macOS -.DS_Store - -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Coverage directory generated when running tests with coverage -coverage - -# Dependencies -node_modules/ - -# Yarn files -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions - -# Node version directives -.nvmrc - -# dotenv environment variables file -.env -.env.test - -# Build output -dist -dist-types - -# Temporary change files created by Vim -*.swp - -# MkDocs build output -site - -# Local configuration files -*.local.yaml - -# Sensitive credentials -*-credentials.yaml - -# vscode database functionality support files -*.session.sql - -# E2E test reports -e2e-test-report/ - -# generated by 'npx @janus-idp/cli@3.3.1 package package-dynamic-plugins --export-to .' -index.json - - -# Environment variables -**/.env - -# Playwright -**/node_modules/ -**/test-results/ -**/playwright-report/ -**/blob-report/ -**/playwright/.cache/ \ No newline at end of file diff --git a/workspaces/sandbox/.prettierignore b/workspaces/sandbox/.prettierignore deleted file mode 100644 index 4f5a69b9d0..0000000000 --- a/workspaces/sandbox/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -dist -dist-types -dist-dynamic -coverage -.vscode -.eslintrc.js diff --git a/workspaces/sandbox/Makefile b/workspaces/sandbox/Makefile deleted file mode 100644 index 4d53244a84..0000000000 --- a/workspaces/sandbox/Makefile +++ /dev/null @@ -1,5 +0,0 @@ -# It's necessary to set this because some environments don't link sh -> bash. -SHELL := /bin/bash - -include ./make/*.mk -.DEFAULT_GOAL := help \ No newline at end of file diff --git a/workspaces/sandbox/README.md b/workspaces/sandbox/README.md deleted file mode 100644 index 9a75e79385..0000000000 --- a/workspaces/sandbox/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Developer Sandbox Plugin for Red Hat Developer Hub - -This plugin provides the new developer sandbox experience for the Red Hat Developer Hub. - -## Configuration - -This plugin needs the following to be configured in the `app-config.local.yaml` file: - -1. The URL of the registration-service-api production server. -2. The URL of the kube-api production server. -3. The site key for Google Recaptcha. The keys can be obtained from the Google Recaptcha admin console. - -```sh -yarn install -yarn dev -``` - -To generate knip reports for this app, run: - -```sh -yarn backstage-repo-tools knip-reports -``` - -## Development - -To start the app locally, run: - -1. `cd workspaces/sandbox` -2. `yarn install` -3. `make start-rhdh-local` - -Please, note that every time you want to re deploy, you need to run: -`make stop-rhdh-local` - -NOTE: the app uses prod RH SSO as auth provider and sandbox stage backend by default -( those can be configured in `deploy/app-config.yaml` ) - -Known issues: - -- sometimes the local node build fails and you'll have to run `make stop-rhdh-local` and `make start-rhdh-local` again. - This is what you will see when the build fails: - ![Known Issues](./images/known_build_issue.png) - -## Upgrading backstage - -For upgrading backstage the `backstage-cli` should be used, for example: - -```shell -npx backstage-cli versions:bump --release 1.36.1 -``` - -More details on the upgrade process are available [keeping-backstage-updated](https://backstage.io/docs/getting-started/keeping-backstage-updated/) diff --git a/workspaces/sandbox/app-config.production.yaml b/workspaces/sandbox/app-config.production.yaml deleted file mode 100644 index 94f8f27f22..0000000000 --- a/workspaces/sandbox/app-config.production.yaml +++ /dev/null @@ -1,67 +0,0 @@ -app: - title: Developer Sandbox - branding: - fullLogo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJEAAAA8CAYAAABmWlgTAAAACXBIWXMAABCcAAAQnAEmzTo0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAApWSURBVHgB7V3hdds4DEbu3f/LBuUG9U1QdYL6Jog6QdwJrEyQZALnJog7gdwJkk4gd4L4JsARIRhBFClStiPbtb73+GxTIAlCEAiSoAwwYsSOuIARnUDETH9MdPqo0yUni7VOP3V6vri4WMGRQfNOvE6g5l85JM86/dJppfl/hhH7AymOTrc6vWA6Kp0WOik4ME6d/5MGCVCnEnfH4hA3g/lf4O5Y9OF/HM4YWmhT/bGA5nBFWOn0A4zplybfDm00VHzRKXPKrXX6Z6hhIsL/d+bH5V+B4fsTmH5IEP03zf8SRqRB34QH8STSMFCg8SlSyyunjtcnGgbCO/GPfeo4e2hhTXR60ulxF8HxzXjiGzmFgSD4L3GHoRSbQ3oFI0ZsCzTO+WiFuqAFdMVWZxBrwZZiuo8bw9ZihgM678y/ghEGdCPFmP/uJpvbs9Ptpz0Ml5X1fWAA6HZyIa/Mvf4HnC82/LmCYUGzoHIbRWJLUEK9aLiBYSB5ncMIA36icxxo3Hee5t4WybFAyN8VDAC2pLLtwSYMIxx4FKlMLHcwBRI85H35PjhY+3+72YFHkS4j9AdXIMHLS9A3QrMu0JVoFpPDQMB66b6I0M2Yv1uI13nLtAUcGGhmaTSczSJ0R6NAzE8heCnci6mohugE1otdRYTugemi5lXUudMqMhoLaRVSwTvh2BSIecoEP28y/9OhewCzT+Qi0+kKzKyAhPe33lMZamZwbKDhZwbvCPTPwmgfbg0HBIW7aN6IF7tn+ApXiX5owgdPeXriKd92LNfpDkbsHQEF+nxE8T5rMApEFlmRYv+ZWpK1kCpQYAKcGkDjaFH6ACbQaRnrOAuMpovWuVwdIrgL68CzD8zLL5cXplHQXDP5ovP/4+/rXXlH42QfswIRKAjPWqFaFmKcy7tKo3EGCXciT27YuVigZ/Yh/AofKtFOEeFnJ5+Ix/gnDKO0/KN/h7vRV9gBLBPJC82EJnBkcOSQUV6yJUKzwGQ79cx5Cuonh56aezDmLgPjQ+V87bNTXSnqWoGJdyFQXMuQC1nUDwV1zNCa8zMw/NMn+T+FTku+Tkp1zXTU342oaytgbYGsTI7RAnVDaFbBT6dMOTZDLStRbmHz0LE4aCyULZOL/LmryR3ligjf8qmoIunFZzEwMOPR+XfWInj4s1CwI/BELJAFNkedibyQitIKzhHmJNCgXVcoRV7FeUUCo0EaposNMT4kDTsoprNO/t6UCE9MgQgOv6+Gwx3O1lCbdIuMP++1eZVTW9lZWjzzDUOXsg4WkOK8JewPZPa/RWhoQTKk7BmYEFfil4aSn/DOwBMcwviheXNp7DKPq0Q37hSfrUgGRshSiXrt7DIDb2X2LKxNbGaEZn3Dx5OcDQ2CbRQI69nh8oBrdJn4/sZrimN9w4XJjOdCyWRHPscqofUEOTIgrzHAYWEViATy1d5Etpj0YOzdyfcoEOFbRIEUlyFc69+fD6RIV+L798YVMcblvpLCR6mwnvLKwK4MEoG1g3vbQWPbKyJ1WZ+o9xTf4X/iod+7T+TxgYIy95STm5+DHQAQPMh+NwL5UoPSbvhTAQ9p/CTcc/6jT6Doj9O1ddHm6cxXBgK+yzviEraDgn6gmy/79jWwQ9AAy1r6fPkBFEm6LP+2rqY8FeJJfnGsUSWfEDQ764WgbwVfYXOaWDL9LbYXLQvoAO6+2GjzKjRLGaT0NEmYo3jynXqkVaiYd0rTSPuF07ccegLbYSQFDACWCYo+Kx9RtGPY3MEtRP4lhqfaL6GOYr0O46LEhGUArmNXJVLYfAgk34/2h6eumadMEWlfKlEOW8Jpe5DgMKfN1uhxwUQZ/37uctgE3cZ1BNFoJ11Xti4w+09d9RHthNOG21+h8VHIeq27nG9Bt0nYpwvWifW+H4GuLfn769Djm/lhc9+PeF8l8DCFhJlkDKyENFu+GfCEbU6fKcPviBEjjgFofAgaDgc7GYHGrchxP1sxdqtLwYjhwDdxiu3JgYIBgM23gcy3aReN3yv5X8KIYaCFfY3+dwEN4vgyD4+e9hcYOXWL9Wna0lP+Dka8P7C5aGlBCjWDAYHht3pInkqR7Esn8Bj4P3vwzbM3ae9bJT15iSkTRhSNliJ6Lb6e1UuuSDjncsAA6yUXWgqg775dgNdlFTBRC8ttlx4u0KyfXHfQULzx8ym/MUv38RGMEBWY3fIVnCGw6Wxv9vZAYXMlugvVqY6TTj8yGLFX9HkriNLJ7m9tu2E54jeEL57oK9TRjaQs5ChSAL3ivEwnGh6iMUQjzgM+JVo5e0tLHkutX0GgIXCm6VrrCFiHmlIZUkJy3L57znBl/HPjq4fpcqiV1z0HRvlXzNPrnhi309t3Y8t6LeoiUNDV0rPPpsCcYnnlHcypYfptXzZO/f0R40PI6S3cFEwI8jqhPeJ1yr/vD+6venwiFaBzwz5ePDS3HT7VAusQkizmp2AzCKpBg80TIy5KbIeeBNvC8EKhxbxDXlSuCpSjfBWQY4nbtUd19g5qe3dgohIxbe7QTsS1W48Q3ZvzKOhLX764LsMP5DGluVOn70aWTl0SWUddIcw65NWFCh15YpvXCttymie0Z8vlcGhgPyVyV2dzzpfKRR3LRJkpet5rg+0n2rUelaedia99vqZ8Zfgaetp3Ld0D1pbSt2CnAvJ6FHXaTVDZX/ngzEV+hU05zRLbe3LKHX6Sgz2UiOklCs4rRV7uKSMj4+QZtBe3Lg+977Bkgz7QlzLAc+apy7vH5fTrztMGQppMrXJWIs8X1124/cP2w6bgyJB8jJrg6YANiMpE3pWmu3Lo5NMihUcx2tZ0k7NYiO8Wq0DZL7qdTxAGCb9rhdrlwwfKz/h7V1sNOC+/sLzI34Rbjw6G5GSxOYITMi30UiLw/39FjMaFFBQ93decd8lWYu3UcSO+TwLfu9radFyzWAdo1gH6FKyhVppLaJfPoBsf4UTQ54UOCpoR/2sRmkk3ygqJpptJJ0jJSuh66eSAtTxU/1qQPDhPHn1XPdrZJF4jhXz20Cin7T5QTtm1c12+DMKHrmvHBUzwiZimcuhycb0U+b2Osnjal+1kDq0M7k+O1XHqzzhP+h9PgXKVoOnjE107bVqfSPqAswTWW1N8OEZg+ybeYX0MxneMh1BE6lhgPbugYWqOaYcVsUtg2J6dUTsTp50FJqwTYfPtI8g8ZJyfYZOnN4cW232V5RS2lw3k6RJ3BjbH5gsySN6d60RwjMB+6x6EIlBPkVDW+w8+AR7yQDuzhHZoGqxEGYmsZ12EXdaJXIV+SCgXWic6eSUqMbIDjkaRqkD5zmg5bFqFKtJOHmmncOglsp51TSPyCpX1PjBcR+jMXatNPAEluuCOTjto7JmqJEcP62AoGmb+0oneaUhO6zLhTJuybSbsPVm+bTsEcrQf3HawadVWvmky00R5Zj7f/DF9/YJvesZlk/5YGOtza3YWRm0uwTn759yfqFxGnAAwwbE+N5zzvwyN2BNGJRqxM0YlGrEzxr803wIolg+OcS9raPwPmpHodKVwKUcAAAAASUVORK5CYII=' - fullLogoWidth: 100 - baseUrl: https://sandbox.redhat.com - sidebar: - search: false - administration: false - settings: false - -# Used for enabling the full page redirect flow rather than the popup flow for Red Hat SSO -enableExperimentalRedirectFlow: true - -organization: - name: Red Hat - -backend: - baseUrl: https://sandbox.redhat.com - csp: - img-src: - - "'self'" - - 'data:' - - 'https://catamphetamine.github.io' - script-src: - - "'self'" - - "'unsafe-eval'" - - 'https://cdn.segment.com' - - 'https://www.google.com' - - 'https://www.gstatic.com' - script-src-elem: - - "'self'" - - 'https://cdn.segment.com' - - 'https://www.google.com' - - 'https://www.gstatic.com' - frame-src: - - "'self'" - - 'https://www.google.com' - cors: - origin: https://sandbox.redhat.com - database: - client: better-sqlite3 - connection: ':memory:' - -auth: - environment: production - session: - secret: 'dummy' - providers: - oidc: - production: - metadataUrl: ${AUTH_OIDC_METADATA_URL} - clientId: ${AUTH_OIDC_CLIENT_ID} - clientSecret: ${AUTH_OIDC_CLIENT_SECRET} - prompt: auto - signIn: - resolvers: - - resolver: emailLocalPartMatchingUserEntityName - dangerouslyAllowSignInWithoutUserInCatalog: true - -signInPage: oidc - -sandbox: - signupAPI: ${SIGNUP_API} - kubeAPI: ${KUBE_API} - recaptcha: - siteKey: ${RECAPTCHA_SITE_KEY} diff --git a/workspaces/sandbox/app-config.yaml b/workspaces/sandbox/app-config.yaml deleted file mode 100644 index e234d6ce9d..0000000000 --- a/workspaces/sandbox/app-config.yaml +++ /dev/null @@ -1,88 +0,0 @@ -app: - title: Developer Sandbox - baseUrl: http://localhost:3000 - -# Used for enabling the full page redirect flow rather than the popup flow for Red Hat SSO -enableExperimentalRedirectFlow: true - -organization: - name: Red Hat - -sandbox: - signupAPI: ${SIGNUP_API} - kubeAPI: ${KUBE_API} - recaptcha: - siteKey: ${RECAPTCHA_SITE_KEY} - -backend: - # Used for enabling authentication, secret is shared by all backend plugins - # See https://backstage.io/docs/auth/service-to-service-auth for - # information on the format - # auth: - # keys: - # - secret: ${BACKEND_SECRET} - baseUrl: http://localhost:7007 - listen: - port: 7007 - # Uncomment the following host directive to bind to specific interfaces - # host: 127.0.0.1 - csp: - connect-src: ["'self'", 'http:', 'https:'] - # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference - # Default Helmet Content-Security-Policy values can be removed by setting the key to false - cors: - origin: http://localhost:3000 - methods: [GET, HEAD, PATCH, POST, PUT, DELETE] - credentials: true - # This is for local development only, it is not recommended to use this in production - # The production database configuration is stored in app-config.production.yaml - database: - client: better-sqlite3 - connection: ':memory:' - # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir - -integrations: - github: - - host: github.com - # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information - # about setting up the GitHub integration here: https://backstage.io/docs/integrations/github/locations#configuration - token: ${GITHUB_TOKEN} - ### Example for how to add your GitHub Enterprise instance using the API: - # - host: ghe.example.net - # apiBaseUrl: https://ghe.example.net/api/v3 - # token: ${GHE_TOKEN} - - ### Example for how to add a proxy endpoint for the frontend. - ### A typical reason to do this is to handle HTTPS and CORS for internal services. - # endpoints: - # '/test': - # target: 'https://example.com' - # changeOrigin: true - -# Reference documentation http://backstage.io/docs/features/techdocs/configuration -# Note: After experimenting with basic setup, use CI/CD to generate docs -# and an external cloud storage when deploying TechDocs for production use-case. -# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach -techdocs: - builder: 'local' # Alternatives - 'external' - generator: - runIn: 'docker' # Alternatives - 'local' - publisher: - type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives. - -auth: - # see https://backstage.io/docs/auth/ to learn about auth providers - providers: - # See https://backstage.io/docs/auth/guest/provider - guest: {} - -scaffolder: - {} - # see https://backstage.io/docs/features/software-templates/configuration for software template options - -catalog: - import: - entityFilename: catalog-info.yaml - pullRequestBranchName: backstage-integration - rules: - - allow: [Component, System, API, Resource, Location] diff --git a/workspaces/sandbox/backstage.json b/workspaces/sandbox/backstage.json deleted file mode 100644 index e235680d16..0000000000 --- a/workspaces/sandbox/backstage.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "version": "1.36.1" -} diff --git a/workspaces/sandbox/catalog-info.yaml b/workspaces/sandbox/catalog-info.yaml deleted file mode 100644 index 813533d6e4..0000000000 --- a/workspaces/sandbox/catalog-info.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - name: sandbox - title: Sandbox - description: An example of a Backstage application. - # Example for optional annotations - # annotations: - # github.com/project-slug: backstage/backstage - # backstage.io/techdocs-ref: dir:. -spec: - type: backstage-plugin - owner: john@example.com - lifecycle: active - system: rhdh - subcomponentOf: rhdh-plugins diff --git a/workspaces/sandbox/deploy/base/app-config.yaml b/workspaces/sandbox/deploy/base/app-config.yaml deleted file mode 100644 index 1e28fa4db2..0000000000 --- a/workspaces/sandbox/deploy/base/app-config.yaml +++ /dev/null @@ -1,82 +0,0 @@ -app: - title: Developer Sandbox - baseUrl: http://localhost:3000 - -# this is required in order to be able to open the SSO login in full page mode. -enableExperimentalRedirectFlow: true - -backend: - baseUrl: http://localhost:3000 - csp: - img-src: - - "'self'" - - 'data:' - - 'https://catamphetamine.github.io' - - 'https://consent.trustarc.com' - script-src: - - "'self'" - - "'unsafe-eval'" - - "'unsafe-inline'" - - 'https://cdn.segment.com' - - 'https://www.google.com' - - 'https://www.gstatic.com' - - 'https://static.redhat.com' - - 'https://consent.trustarc.com' - - 'https://www.redhat.com/ma/dpal.js' - - 'https://assets.adobedtm.com' - - 'https://www.youtube.com' - - 'https://s.ytimg.com' - - 'https://adobedc.demdex.net' - - 'https://smetrics.redhat.com' - script-src-elem: - - "'self'" - - "'unsafe-inline'" - - 'https://cdn.segment.com' - - 'https://www.google.com' - - 'https://www.gstatic.com' - - 'https://static.redhat.com' - - 'https://consent.trustarc.com' - - 'https://www.redhat.com/ma/dpal.js' - - 'https://assets.adobedtm.com' - - 'https://www.youtube.com' - - 'https://s.ytimg.com' - - 'https://adobedc.demdex.net' - - 'https://smetrics.redhat.com' - frame-src: - - "'self'" - - 'https://www.google.com' - - 'https://static.redhat.com' - - 'https://consent.trustarc.com' - - 'https://consent-pref.trustarc.com' - - 'https://www.youtube.com' - - 'https://www.youtube-nocookie.com' - listen: - port: 3000 - cors: - origin: http://localhost:3000 - methods: [GET, HEAD, PATCH, POST, PUT, DELETE] - credentials: true - database: - client: better-sqlite3 - connection: ':memory:' -auth: - environment: production - session: - secret: 'dummy' - providers: - oidc: - production: - metadataUrl: https://sso.redhat.com/auth/realms/redhat-external - clientId: crtoolchain-local - clientSecret: dummy - prompt: auto - signIn: - resolvers: - - resolver: emailLocalPartMatchingUserEntityName - dangerouslyAllowSignInWithoutUserInCatalog: true -signInPage: oidc -sandbox: - signupAPI: https://registration-service-toolchain-host-operator.apps.sandbox.x8i5.p1.openshiftapps.com/api/v1 - kubeAPI: https://api-toolchain-host-operator.apps.sandbox.x8i5.p1.openshiftapps.com - recaptcha: - siteKey: 6Lc_164lAAAAAPvrC0WO-XDljvZ2DZ3UQ38A4XR0 diff --git a/workspaces/sandbox/deploy/base/dynamic-plugins.yaml b/workspaces/sandbox/deploy/base/dynamic-plugins.yaml deleted file mode 100644 index 5e5696044d..0000000000 --- a/workspaces/sandbox/deploy/base/dynamic-plugins.yaml +++ /dev/null @@ -1,90 +0,0 @@ -includes: - - dynamic-plugins.default.yaml -plugins: - - disabled: false - package: ./local-plugins/red-hat-developer-hub-backstage-plugin-sandbox - pluginConfig: - dynamicPlugins: - frontend: - default.main-menu-items: - menuItems: - default.home: - title: Home - icon: homeIcon - to: '/' - priority: 200 - default.activities: - title: Activities - icon: activitiesIcon - to: '/activities' - priority: 100 - default.catalog: - title: '' - default.apis: - title: '' - default.learning-path: - title: '' - default.create: - title: '' - red-hat-developer-hub.backstage-plugin-sandbox: - signInPage: - importName: RHSSOSignInPage - appIcons: - - name: homeIcon - importName: SandboxHomeIcon - - name: activitiesIcon - importName: SandboxActivitiesIcon - dynamicRoutes: - - path: / - importName: SandboxPage - - path: /activities - importName: SandboxActivitiesPage - - # Global Header - - package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-global-header - disabled: false - pluginConfig: - dynamicPlugins: - frontend: - red-hat-developer-hub.backstage-plugin-global-header: - mountPoints: - - mountPoint: application/header - importName: GlobalHeader - config: - position: above-main-content - - mountPoint: global.header/component - importName: Spacer - config: - priority: 99 - props: - growFactor: 1 - minWidth: 1 - - mountPoint: global.header/component - importName: ProfileDropdown - config: - priority: 10 - - mountPoint: global.header/profile - importName: MenuItemLink - config: - priority: 100 - props: - title: Settings - link: /settings - icon: manageAccounts - - mountPoint: global.header/profile - importName: LogoutButton - config: - priority: 10 - - # Techdocs - - package: ./dynamic-plugins/dist/backstage-plugin-techdocs-backend-dynamic - disabled: true - - - package: ./dynamic-plugins/dist/backstage-plugin-techdocs - disabled: true - - - package: ./dynamic-plugins/dist/backstage-plugin-techdocs-module-addons-contrib - disabled: true - - - package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-dynamic-home-page - disabled: true diff --git a/workspaces/sandbox/deploy/base/kustomization.yaml b/workspaces/sandbox/deploy/base/kustomization.yaml deleted file mode 100644 index 760fcc9f5f..0000000000 --- a/workspaces/sandbox/deploy/base/kustomization.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - app-config.local.yaml - - dynamic-plugins.yaml diff --git a/workspaces/sandbox/images/know_build_issue.png b/workspaces/sandbox/images/know_build_issue.png deleted file mode 100644 index 1fd46ccd98..0000000000 Binary files a/workspaces/sandbox/images/know_build_issue.png and /dev/null differ diff --git a/workspaces/sandbox/make/sandbox.mk b/workspaces/sandbox/make/sandbox.mk deleted file mode 100644 index ee5dc7b0de..0000000000 --- a/workspaces/sandbox/make/sandbox.mk +++ /dev/null @@ -1,57 +0,0 @@ -TAG := $(shell date +'%d%H%M%S') -SANDBOX_RHDH_PLUGIN_IMAGE ?= quay.io/$(QUAY_NAMESPACE)/sandbox-rhdh-plugin:$(TAG) -OS := $(shell uname -s) -ARCH := $(shell uname -m) -PLATFORM ?= $(OS)/$(ARCH) - -.PHONY: push-plugin -push-plugin: - npx @red-hat-developer-hub/cli@latest plugin package \ - --tag $(SANDBOX_RHDH_PLUGIN_IMAGE) \ - --platform $(PLATFORM) && \ - podman push $(SANDBOX_RHDH_PLUGIN_IMAGE) - - -RHDH_LOCAL_DIR := "$(TMPDIR)rhdh-local" -.PHONY: clone-rhdh-local -clone-rhdh-local: - rm -rf ${RHDH_LOCAL_DIR}; \ - git clone --depth 1 https://github.com/redhat-developer/rhdh-local $(RHDH_LOCAL_DIR) && \ - cd $(RHDH_LOCAL_DIR) && \ - git fetch --unshallow && \ - git checkout 37be302480b1458c3dfd3270c25726d65f0ffe75 && \ - echo "cloned to $(RHDH_LOCAL_DIR)" - - -.PHONY: generate-env -generate-env: - cd $(RHDH_LOCAL_DIR) && \ - if [ $(OS) = "Darwin" ] && [ $(ARCH) = "arm64" ]; then \ - echo "# This is a nightly build. This image supports both amd64 and arm64" > .env; \ - echo "RHDH_IMAGE=quay.io/rhdh-community/rhdh:next" >> .env; \ - echo ".env file generated for macOS arm64"; \ - else \ - echo "Skipping .env generation: OS=$$OS, ARCH=$$ARCH"; \ - fi - -.PHONY: start-rhdh-local -start-rhdh-local: clone-rhdh-local generate-env - rm -rf plugins/sandbox/dist-dynamic - rm -rf red-hat-developer-hub-backstage-plugin-sandbox - podman run -it --rm -w /home -v $(PWD):/home node:22 bash -c "yarn install && npx --yes @red-hat-developer-hub/cli@latest plugin package --export-to . && exit" - cp -r red-hat-developer-hub-backstage-plugin-sandbox $(RHDH_LOCAL_DIR)/local-plugins/ - cp deploy/base/app-config.yaml $(RHDH_LOCAL_DIR)/configs/app-config/app-config.yaml - cp deploy/base/dynamic-plugins.yaml $(RHDH_LOCAL_DIR)/configs/dynamic-plugins/dynamic-plugins.override.yaml - cd $(RHDH_LOCAL_DIR) && \ - yq e '.services.rhdh.ports = ["3000:3000"] | (.services.rhdh.ports) |= map(. style="double")' -i compose.yaml && \ - podman-compose up -d && \ - echo "UI is up and running at: http://localhost:3000" - -.PHONY: stop-rhdh-local -stop-rhdh-local: - cd $(RHDH_LOCAL_DIR) && \ - podman-compose down -v && \ - rm -rf $(RHDH_LOCAL_DIR) - -.PHONY: restart-rhdh-local -restart-rhdh-local: stop-rhdh-local start-rhdh-local diff --git a/workspaces/sandbox/package.json b/workspaces/sandbox/package.json deleted file mode 100644 index baf9038a5c..0000000000 --- a/workspaces/sandbox/package.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "name": "@internal/sandbox", - "version": "1.0.0", - "private": true, - "engines": { - "node": "22 || 24" - }, - "scripts": { - "dev": "yarn workspaces foreach -A --include backend --include app --parallel -v -i run start", - "start": "yarn workspace app start", - "start-backend": "yarn workspace backend start", - "build:backend": "yarn workspace backend build", - "tsc": "tsc", - "tsc:full": "tsc --skipLibCheck true --incremental false", - "build:all": "backstage-cli repo build --all", - "build:api-reports": "yarn build:api-reports:only --tsc", - "build:api-reports:only": "backstage-repo-tools api-reports -o ae-wrong-input-file-type --validate-release-tags", - "build:knip-reports": "backstage-repo-tools knip-reports", - "clean": "backstage-cli repo clean", - "test": "backstage-cli repo test", - "test:all": "backstage-cli repo test --coverage", - "fix": "backstage-cli repo fix", - "lint": "backstage-cli repo lint --since origin/main", - "lint:all": "backstage-cli repo lint", - "lint:check": "backstage-cli package lint", - "lint:fix": "backstage-cli package lint --fix", - "prettier:check": "prettier --check .", - "new": "backstage-cli new --scope @red-hat-developer-hub", - "postinstall": "cd ../../ && yarn install", - "test:base": "playwright test --config=sandboxplaywright.config.ts", - "tests": "npm run test:base", - "test:ui": "npm run test:base -- --ui", - "test:headless": "npm run test:base -- --headless", - "test:headed": "npm run test:base -- --headed", - "test:debug": "npm run test:base -- --debug", - "test:chromium": "npm run test:base -- --project=chromium", - "test:firefox": "npm run test:base -- --project=firefox", - "test:webkit": "npm run test:base -- --project=webkit" - }, - "workspaces": { - "packages": [ - "packages/*", - "plugins/*" - ] - }, - "repository": { - "type": "git", - "url": "https://github.com/redhat-developer/rhdh-plugins", - "directory": "workspaces/sandbox" - }, - "devDependencies": { - "@backstage/cli": "^0.30.0", - "@backstage/e2e-test-utils": "^0.1.1", - "@backstage/repo-tools": "^0.13.0", - "@changesets/cli": "^2.27.1", - "@playwright/test": "^1.52.0", - "@spotify/prettier-config": "^12.0.0", - "@types/lodash": "^4.17.16", - "knip": "^5.27.4", - "node-gyp": "^9.0.0", - "prettier": "^2.3.2", - "typescript": "~5.3.0" - }, - "resolutions": { - "@types/react": "^18", - "@types/react-dom": "^18" - }, - "prettier": "@spotify/prettier-config", - "lint-staged": { - "*.{js,jsx,ts,tsx,mjs,cjs}": [ - "eslint --fix", - "prettier --write" - ], - "*.{json,md}": [ - "prettier --write" - ] - } -} diff --git a/workspaces/sandbox/plugins/README.md b/workspaces/sandbox/plugins/README.md deleted file mode 100644 index 4fc1756606..0000000000 --- a/workspaces/sandbox/plugins/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# The Plugins Folder - -This is where your own plugins and their associated modules live, each in a -separate folder of its own. - -If you want to create a new plugin here, go to your project root directory, run -the command `yarn new`, and follow the on-screen instructions. diff --git a/workspaces/sandbox/plugins/sandbox/.eslintrc.js b/workspaces/sandbox/plugins/sandbox/.eslintrc.js deleted file mode 100644 index e2a53a6ad2..0000000000 --- a/workspaces/sandbox/plugins/sandbox/.eslintrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/workspaces/sandbox/plugins/sandbox/@types/index.d.ts b/workspaces/sandbox/plugins/sandbox/@types/index.d.ts deleted file mode 100644 index 80a79a4747..0000000000 --- a/workspaces/sandbox/plugins/sandbox/@types/index.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -declare let grecaptcha: { - enterprise: ReCaptchaV3.ReCaptcha; -}; - -declare namespace ReCaptchaV3 { - interface ReCaptcha { - execute(siteKey: string, action: Action): PromiseLike; - ready(callback: () => void): void; - } - interface Action { - /** - * the name of the action. Actions may only contain alphanumeric characters and slashes, and must not be user-specific. - */ - action: string; - } -} diff --git a/workspaces/sandbox/plugins/sandbox/ANALYTICS-EVENTS.md b/workspaces/sandbox/plugins/sandbox/ANALYTICS-EVENTS.md deleted file mode 100644 index 4004d304a3..0000000000 --- a/workspaces/sandbox/plugins/sandbox/ANALYTICS-EVENTS.md +++ /dev/null @@ -1,79 +0,0 @@ -# Developer Sandbox - Analytics Events - -This document lists all analytics events sent to **Segment**, **Adobe Analytics (EDDL)**, and **Marketo**. - -## Track Events - -All events are sent to Segment and Adobe Analytics with the following patterns. Additionally, **Catalog clicks only** are sent to Marketo: - -- **Segment**: Event name = `{text} {verb}` where verb is `launched` (CTA) or `clicked` (non-CTA) -- **Adobe CTA**: Pushed to `window.appEventData` as `Master Link Clicked` -- **Adobe non-CTA**: Automatically tracked via data attributes -- **Marketo**: Catalog clicks only sent to Workato webhook for Marketo integration - -| Event Text | Section | Link Type | Segment Event Name | Adobe Tracking | Description | Intcmp ID | -| -------------------------------------------------------------------------------------- | ------------ | --------- | -------------------------------------------------------------------------------------------- | --------------- | ---------------------------------------------------------------------------- | ------------------ | -| **OpenShift** | Catalog | cta | `OpenShift launched` | Manual push | User clicks to launch OpenShift console for their sandbox environment | 701Pe00000dnCEYIA2 | -| **OpenShift AI** | Catalog | cta | `OpenShift AI launched` | Manual push | User clicks to launch OpenShift AI dashboard for ML/AI workloads | 701Pe00000do2uiIAA | -| **Dev Spaces** | Catalog | cta | `Dev Spaces launched` | Manual push | User clicks to launch OpenShift Dev Spaces cloud development environment | 701Pe00000doTQCIA2 | -| **Ansible Automation Platform** | Catalog | cta | `Ansible Automation Platform launched` | Manual push | User clicks to provision or access Ansible Automation Platform instance | 701Pe00000dowQXIAY | -| **OpenShift Virtualization** | Catalog | cta | `OpenShift Virtualization launched` | Manual push | User clicks to launch OpenShift Virtualization console for VM management | 701Pe00000dov6IIAQ | -| **Get Started - Ansible** | Catalog | cta | `Get Started - Ansible launched` | Manual push | User clicks to access their provisioned Ansible instance from the modal | 701Pe00000dowQXIAY | -| **Get started with your Developer Sandbox** | Activities | default | `Get started with your Developer Sandbox clicked` | Data attributes | User clicks to view the getting started tutorial for Developer Sandbox | - | -| **Streamline automation in OpenShift Dev Spaces with Ansible** | Activities | default | `Streamline automation in OpenShift Dev Spaces with Ansible clicked` | Data attributes | User clicks to view learning resource about Ansible automation in Dev Spaces | - | -| **How to deploy a Java application on Kubernetes in minutes** | Activities | default | `How to deploy a Java application on Kubernetes in minutes clicked` | Data attributes | User clicks to view Java application deployment tutorial | - | -| **Foundations of OpenShift** | Activities | default | `Foundations of OpenShift clicked` | Data attributes | User clicks to view foundational OpenShift learning resource | - | -| **Using OpenShift Pipelines** | Activities | default | `Using OpenShift Pipelines clicked` | Data attributes | User clicks to view CI/CD pipelines tutorial for OpenShift | - | -| **OpenShift virtualization and application modernization using the Developer Sandbox** | Activities | default | `OpenShift virtualization and application modernization using the Developer Sandbox clicked` | Data attributes | User clicks to view learning resource about VM management and modernization | - | -| **Contact Red Hat Sales** | Support | cta | `Contact Red Hat Sales launched` | Manual push | User clicks to contact Red Hat sales team for support or purchasing | - | -| **Send Code** | Verification | cta | `Send Code launched` | Manual push | User submits their phone number to receive a verification code via SMS | - | -| **Cancel Verification** | Verification | cta | `Cancel Verification launched` | Manual push | User cancels the phone verification process and closes the modal | - | -| **Start Trial** | Verification | cta | `Start Trial launched` | Manual push | User submits the verification code to complete signup and start their trial | - | -| **Resend Code** | Verification | cta | `Resend Code launched` | Manual push | User requests a new verification code to be sent to their phone | - | - -### Event Properties - -All events include these properties: - -| Property | Value Pattern | Example | -| ------------------ | ------------------------------ | ----------------------------- | -| `category` | `Developer Sandbox\|{Section}` | `Developer Sandbox\|Catalog` | -| `regions` | `sandbox-{section-lowercase}` | `sandbox-catalog` | -| `text` | Event text from table above | `OpenShift` | -| `href` | Destination URL | `https://console.example.com` | -| `linkType` | `cta` or `default` | `cta` | -| `internalCampaign` | Campaign ID (if applicable) | `701Pe00000dnCEYIA2` | - -## Identify & Group Calls - -| Call Type | Platform | Trigger | ID Used | Traits/Properties | -| ------------ | -------- | ------------------------------------------- | ----------- | ------------------------------------------------------------------------------- | -| **Identify** | Segment | First time `userID` available in session | `userID` | `{ company: [user company], email_domain: [user email domain] }` (when present) | -| **Group** | Segment | First time `accountID` available in session | `accountID` | `{ ebs: [account number] }` (when present) | - -## Marketo Integration (Catalog Clicks Only) - -Catalog clicks are sent to Marketo via a Workato webhook. The webhook URL is fetched dynamically from the `/uiconfig` API endpoint (`workatoWebHookURL` field). - -### Marketo Payload - -| Field | Source | Example | Description | -| --------------------- | --------------------------- | -------------------------------- | --------------------------------------------------- | -| `C_FirstName` | `userData.givenName` | `John` | User's first name | -| `C_LastName` | `userData.familyName` | `Doe` | User's last name | -| `C_EmailAddress` | `userData.email` | `jdtest@test.com` | User's email address (required for tracking) | -| `C_Company` | `userData.company` | `Red Hat` | User's company (empty if not available) | -| `A_Timestamp` | Current UTC timestamp | `2025/10/20 14:57:46` | Timestamp of the click in UTC (YYYY/MM/DD HH:mm:ss) | -| `F_FormData_Source` | Fixed value | `sandbox-redhat-com-integration` | Fixed identifier for this integration | -| `A_OfferID` | `internalCampaign` (Intcmp) | `701Pe00000dnCEYIA2` | Product offer ID from intcmp parameter | -| `A_TacticID_External` | Cookie: `rh_omni_tc` | `RHCTN1250000786344` | External tactic ID from Red Hat marketing cookie | -| `A_TacticID_Internal` | Cookie: `rh_omni_itc` | `RHCTE1250000786360` | Internal tactic ID from Red Hat marketing cookie | -| `Status` | Fixed value | `Engaged` | User engagement status | - -## Notes - -- **Segment** tracks all events (both CTA and non-CTA) -- **Adobe** manually tracks CTA events; non-CTA events use automatic tracking via dpal.js -- **Marketo** tracks Catalog clicks only via Workato webhook integration -- All tracking is non-blocking and includes error handling -- Scripts loaded: `trustarc.js` (privacy), `dpal.js` (Adobe tracking) diff --git a/workspaces/sandbox/plugins/sandbox/CHANGELOG.md b/workspaces/sandbox/plugins/sandbox/CHANGELOG.md deleted file mode 100644 index 28cea4e252..0000000000 --- a/workspaces/sandbox/plugins/sandbox/CHANGELOG.md +++ /dev/null @@ -1,18 +0,0 @@ -# @red-hat-developer-hub/backstage-plugin-sandbox - -## 0.1.0 - -### Minor Changes - -- cf219fd: Added a custom Sign in page which sends users directly the Red Hat account login, skipping the OIDC login page. - -### Patch Changes - -- 5e70ed1: Bug Fixes -- a9e5f32: Updated dependency `@openapitools/openapi-generator-cli` to `2.20.0`. - Updated dependency `prettier` to `3.5.3`. - Updated dependency `@redhat-developer/red-hat-developer-hub-theme` to `0.5.1`. - Updated dependency `@red-hat-developer-hub/backstage-plugin-theme` to `^0.6.0`. - Updated dependency `@janus-idp/cli` to `3.5.0`. -- 61dc50a: Add OAuth Header to the Sandbox API calls -- 5ad094e: Added Home and Activities pages diff --git a/workspaces/sandbox/plugins/sandbox/README.md b/workspaces/sandbox/plugins/sandbox/README.md deleted file mode 100644 index bb71b37397..0000000000 --- a/workspaces/sandbox/plugins/sandbox/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Sandbox Frontend Plugin - -Welcome to the sandbox plugin! - -_This plugin was created through the Backstage CLI_ - -## Getting started - -To start the frontend app, run: - -```sh -yarn install -yarn start -``` diff --git a/workspaces/sandbox/plugins/sandbox/app-config.dynamic.yaml b/workspaces/sandbox/plugins/sandbox/app-config.dynamic.yaml deleted file mode 100644 index 31929688cf..0000000000 --- a/workspaces/sandbox/plugins/sandbox/app-config.dynamic.yaml +++ /dev/null @@ -1,93 +0,0 @@ -includes: - - dynamic-plugins.default.yaml -plugins: - - disabled: false - package: oci://quay.io/!red-hat-developer-hub-backstage-plugin-sandbox - pluginConfig: - dynamicPlugins: - frontend: - default.main-menu-items: - menuItems: - default.home: - title: Home - icon: homeIcon - to: '/' - priority: 200 - default.activities: - title: Activities - icon: activitiesIcon - to: '/activities' - priority: 100 - default.catalog: - title: '' - default.apis: - title: '' - default.learning-path: - title: '' - default.create: - title: '' - red-hat-developer-hub.backstage-plugin-sandbox: - signInPage: - importName: RHSSOSignInPage - appIcons: - - name: homeIcon - importName: SandboxHomeIcon - - name: activitiesIcon - importName: SandboxActivitiesIcon - dynamicRoutes: - - path: / - importName: SandboxPage - - path: /activities - importName: SandboxActivitiesPage - - # Global Header - # Hidden the Search Bar and the Create Icon - # Moved the Profile Dropdown to the right - - package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-global-header - disabled: false - pluginConfig: - dynamicPlugins: - frontend: - red-hat-developer-hub.backstage-plugin-global-header: - mountPoints: - - mountPoint: application/header - importName: GlobalHeader - config: - position: above-main-content - - mountPoint: global.header/component - importName: Spacer - config: - priority: 99 - props: - growFactor: 1 - minWidth: 1 - - mountPoint: global.header/component - importName: ProfileDropdown - config: - priority: 10 - - mountPoint: global.header/profile - importName: MenuItemLink - config: - priority: 100 - props: - title: Settings - link: /settings - icon: manageAccounts - - mountPoint: global.header/profile - importName: LogoutButton - config: - priority: 10 - - # Techdocs: Disabled - - package: ./dynamic-plugins/dist/backstage-plugin-techdocs-backend-dynamic - disabled: true - - - package: ./dynamic-plugins/dist/backstage-plugin-techdocs - disabled: true - - - package: ./dynamic-plugins/dist/backstage-plugin-techdocs-module-addons-contrib - disabled: true - - # Dynamic Home Page: Disabled - - package: ./dynamic-plugins/dist/red-hat-developer-hub-backstage-plugin-dynamic-home-page - disabled: true diff --git a/workspaces/sandbox/plugins/sandbox/config.d.ts b/workspaces/sandbox/plugins/sandbox/config.d.ts deleted file mode 100644 index 948cda8dfd..0000000000 --- a/workspaces/sandbox/plugins/sandbox/config.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface Config { - /** - * Configuration required for using sandbox - * @visibility frontend - */ - sandbox: { - /** - * The URL of the sandbox service - * @visibility frontend - */ - signupAPI: string; - /** - * The URL of the sandbox service - * @visibility frontend - */ - kubeAPI: string; - /** - * @visibility frontend - */ - recaptcha: { - /** - * The recaptcha site key - * @visibility frontend - */ - siteKey: string; - }; - }; -} diff --git a/workspaces/sandbox/plugins/sandbox/dev/index.tsx b/workspaces/sandbox/plugins/sandbox/dev/index.tsx deleted file mode 100644 index cf3b5561e7..0000000000 --- a/workspaces/sandbox/plugins/sandbox/dev/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'; -import StarOutlineOutlinedIcon from '@mui/icons-material/StarOutlineOutlined'; -import { createDevApp } from '@backstage/dev-utils'; -import { getAllThemes } from '@red-hat-developer-hub/backstage-plugin-theme'; -import { - sandboxPlugin, - SandboxPage, - SandboxActivitiesPage, -} from '../src/plugin'; - -createDevApp() - .registerPlugin(sandboxPlugin) - .addThemes(getAllThemes()) - .addPage({ - element: , - title: 'Home', - icon: HomeOutlinedIcon, - path: '/', - }) - .addPage({ - element: , - title: 'Activities', - icon: StarOutlineOutlinedIcon, - path: '/activities', - }) - .render(); diff --git a/workspaces/sandbox/plugins/sandbox/knip-report.md b/workspaces/sandbox/plugins/sandbox/knip-report.md deleted file mode 100644 index ba11982646..0000000000 --- a/workspaces/sandbox/plugins/sandbox/knip-report.md +++ /dev/null @@ -1,16 +0,0 @@ -# Knip report - -## Unused dependencies (1) - -| Name | Location | Severity | -| :-------- | :---------------- | :------- | -| react-use | package.json:51:6 | error | - -## Unused devDependencies (4) - -| Name | Location | Severity | -| :-------------------------- | :---------------- | :------- | -| @testing-library/user-event | package.json:67:6 | error | -| @backstage/core-app-api | package.json:61:6 | error | -| react-router | package.json:71:6 | error | -| msw | package.json:68:6 | error | diff --git a/workspaces/sandbox/plugins/sandbox/package.json b/workspaces/sandbox/plugins/sandbox/package.json deleted file mode 100644 index aa4afdba61..0000000000 --- a/workspaces/sandbox/plugins/sandbox/package.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "name": "@red-hat-developer-hub/backstage-plugin-sandbox", - "version": "0.1.0", - "main": "src/index.ts", - "types": "src/index.ts", - "license": "Apache-2.0", - "private": true, - "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" - }, - "repository": { - "type": "git", - "url": "https://github.com/redhat-developer/rhdh-plugins", - "directory": "workspaces/sandbox/plugins/sandbox" - }, - "backstage": { - "role": "frontend-plugin", - "pluginId": "sandbox", - "pluginPackage": "@red-hat-developer-hub/backstage-plugin-sandbox", - "pluginPackages": [ - "@red-hat-developer-hub/backstage-plugin-sandbox" - ] - }, - "sideEffects": [ - "./**/*.css" - ], - "scripts": { - "build": "backstage-cli package build", - "clean": "backstage-cli package clean", - "lint:check": "backstage-cli package lint", - "lint:fix": "backstage-cli package lint --fix", - "postpack": "backstage-cli package postpack", - "prepack": "backstage-cli package prepack", - "start": "backstage-cli package start", - "test": "backstage-cli package test --passWithNoTests --coverage", - "tsc": "tsc", - "prettier:check": "prettier --ignore-unknown --check .", - "prettier:fix": "prettier --ignore-unknown --write ." - }, - "dependencies": { - "@backstage/core-app-api": "^1.15.5", - "@backstage/core-components": "^0.16.4", - "@backstage/core-plugin-api": "^1.10.4", - "@backstage/theme": "^0.6.4", - "@lit/react": "^1.0.5", - "@mui/icons-material": "^5.16.14", - "@mui/material": "^5.16.14", - "@rhds/elements": "^2.0.1", - "@rhds/icons": "^1.1.1", - "@segment/analytics-next": "^1.0.0", - "libphonenumber-js": "^1.12.8", - "lodash": "^4.17.21", - "react-phone-number-input": "^3.4.12", - "react-use": "^17.2.4" - }, - "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0", - "react-dom": "^18.0.2", - "react-router": "^6.3.0", - "react-router-dom": "^6.3.0" - }, - "devDependencies": { - "@backstage/cli": "^0.30.0", - "@backstage/dev-utils": "^1.1.7", - "@backstage/test-utils": "^1.7.5", - "@red-hat-developer-hub/backstage-plugin-theme": "^0.12.0", - "@testing-library/jest-dom": "^6.0.0", - "@testing-library/react": "^14.0.0", - "@testing-library/user-event": "^14.0.0", - "msw": "^1.0.0", - "react": "^16.13.1 || ^17.0.0 || ^18.0.0", - "react-dom": "^18.0.2", - "react-router": "^6.3.0", - "react-router-dom": "^6.3.0" - }, - "files": [ - "dist", - "config.d.ts", - "dist-scalprum", - "dist-dynamic/*.*", - "dist-dynamic/dist/*.*", - "dist-dynamic/dist-dynamic/*.*", - "dist-dynamic/dist-scalprum/*.*", - "app-config.dynamic.yaml" - ], - "scalprum": { - "name": "red-hat-developer-hub.backstage-plugin-sandbox", - "exposedModules": { - "PluginRoot": "./src/index.ts" - } - }, - "configSchema": "config.d.ts", - "keywords": [ - "backstage", - "plugin" - ], - "homepage": "https://red.ht/rhdh", - "bugs": "https://github.com/redhat-developer/rhdh-plugins/issues", - "maintainers": [ - "@lucifergene", - "@rohitkrai03" - ], - "author": "Red Hat" -} diff --git a/workspaces/sandbox/plugins/sandbox/report.api.md b/workspaces/sandbox/plugins/sandbox/report.api.md deleted file mode 100644 index 72ae47012a..0000000000 --- a/workspaces/sandbox/plugins/sandbox/report.api.md +++ /dev/null @@ -1,38 +0,0 @@ -## API Report File for "@red-hat-developer-hub/backstage-plugin-sandbox" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -/// - -import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { IconComponent } from '@backstage/core-plugin-api'; -import { JSX as JSX_2 } from 'react'; -import { RouteRef } from '@backstage/core-plugin-api'; - -// @public -export const RHSSOSignInPage: (props: any) => JSX_2.Element; - -// @public -export const SandboxActivitiesIcon: IconComponent; - -// @public -export const SandboxActivitiesPage: () => JSX_2.Element; - -// @public -export const SandboxHomeIcon: IconComponent; - -// @public -export const SandboxPage: () => JSX_2.Element; - -// @public -export const sandboxPlugin: BackstagePlugin< - { - root: RouteRef; - }, - {}, - {} ->; - -// (No @packageDocumentation comment for this package) -``` diff --git a/workspaces/sandbox/plugins/sandbox/src/api/AnsibleBackendClient.tsx b/workspaces/sandbox/plugins/sandbox/src/api/AnsibleBackendClient.tsx deleted file mode 100644 index dcc0e9d045..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/api/AnsibleBackendClient.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigApi } from '@backstage/core-plugin-api'; -import { errorMessage } from '../utils/common'; -import { AAPObject } from '../utils/aap-utils'; -import { AAPData } from '../types'; -import { SecureFetchApi } from './SecureFetchClient'; - -export type AnsibleBackendClientOptions = { - configApi: ConfigApi; - secureFetchApi: SecureFetchApi; -}; - -export interface AAPService { - getAAP(namespace: string): Promise; - createAAP(namespace: string): Promise; - unIdleAAP(namespace: string): Promise; - deleteAAPCR(namespace: string): Promise; -} - -export class AnsibleBackendClient implements AAPService { - private readonly configApi: ConfigApi; - private readonly secureFetchApi: SecureFetchApi; - - constructor(options: AnsibleBackendClientOptions) { - this.configApi = options.configApi; - this.secureFetchApi = options.secureFetchApi; - } - - private readonly kubeAPI = async (): Promise => { - const kubeAPI = this.configApi.getString('sandbox.kubeAPI'); - return kubeAPI; - }; - - getAAP = async (namespace: string): Promise => { - const kubeApi = await this.kubeAPI(); - const projectAAPUrl = `/apis/aap.ansible.com/v1alpha1/namespaces/${namespace}/ansibleautomationplatforms`; - const response = await this.secureFetchApi.fetch( - `${kubeApi}${projectAAPUrl}`, - { - method: 'GET', - }, - ); - - if (!response.ok) { - const error = await response.json(); - throw new Error(errorMessage(error)); - } - return response.json(); - }; - - createAAP = async (namespace: string): Promise => { - const kubeApi = await this.kubeAPI(); - const projectAAPUrl = `/apis/aap.ansible.com/v1alpha1/namespaces/${namespace}/ansibleautomationplatforms`; - const response = await this.secureFetchApi.fetch( - `${kubeApi}${projectAAPUrl}`, - { - method: 'POST', - body: AAPObject, - headers: { - 'Content-Type': 'application/yaml', - }, - }, - ); - - if (!response.ok && response.status !== 409) { - const error = await response.json(); - throw new Error(errorMessage(error)); - } - }; - - unIdleAAP = async (namespace: string): Promise => { - const kubeApi = await this.kubeAPI(); - const projectAAPUrl = `/apis/aap.ansible.com/v1alpha1/namespaces/${namespace}/ansibleautomationplatforms/sandbox-aap`; - const response = await this.secureFetchApi.fetch( - `${kubeApi}${projectAAPUrl}`, - { - method: 'PATCH', - body: JSON.stringify({ - spec: { - idle_aap: false, - }, - }), - headers: { - 'Content-Type': 'application/merge-patch+json', - }, - }, - ); - - if (!response.ok) { - const error = await response.json(); - throw new Error(errorMessage(error)); - } - }; - - deleteAAPCR = async (namespace: string): Promise => { - const kubeApi = await this.kubeAPI(); - const projectAAPUrl = `/apis/aap.ansible.com/v1alpha1/namespaces/${namespace}/ansibleautomationplatforms/sandbox-aap`; - const response = await this.secureFetchApi.fetch( - `${kubeApi}${projectAAPUrl}`, - { - method: 'DELETE', - }, - ); - - if (!response.ok && response.status !== 404) { - const error = await response.json(); - throw new Error(errorMessage(error)); - } - }; -} diff --git a/workspaces/sandbox/plugins/sandbox/src/api/KubeBackendClient.tsx b/workspaces/sandbox/plugins/sandbox/src/api/KubeBackendClient.tsx deleted file mode 100644 index cb0f08d92e..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/api/KubeBackendClient.tsx +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigApi } from '@backstage/core-plugin-api'; -import { errorMessage } from '../utils/common'; -import { - DeploymentData, - PersistentVolumeClaimData, - SecretItem, - StatefulSetData, -} from '../types'; -import { SecureFetchApi } from './SecureFetchClient'; - -export type KubeBackendClientOptions = { - configApi: ConfigApi; - secureFetchApi: SecureFetchApi; -}; - -export interface KubeAPIService { - deleteSecretsAndPVCs( - k8sObjects: StatefulSetData | DeploymentData | void, - userNamespace: string, - ): Promise; - deletePVCsForSTS( - k8sObjects: StatefulSetData | void, - userNamespace: string, - ): Promise; - getSecret( - namespace: string, - secretName: string, - ): Promise; - getPersistentVolumeClaims( - namespace: string, - labels?: string, - ): Promise; - getDeployments( - namespace: string, - labels?: string, - ): Promise; - getStatefulSets( - namespace: string, - labels?: string, - ): Promise; -} - -export class KubeBackendClient implements KubeAPIService { - private readonly configApi: ConfigApi; - private readonly secureFetchApi: SecureFetchApi; - - constructor(options: KubeBackendClientOptions) { - this.configApi = options.configApi; - this.secureFetchApi = options.secureFetchApi; - } - - private readonly kubeAPI = (): string => { - const kubeAPI = this.configApi.getString('sandbox.kubeAPI'); - return kubeAPI; - }; - - private readonly projectPersistentVolumeClaimUrl = ( - namespace: string, - labelSelector?: string, - ): string => { - let url = `/api/v1/namespaces/${namespace}/persistentvolumeclaims`; - if (labelSelector) { - url += `?labelSelector=${labelSelector}`; - } - return url; - }; - - private readonly projectDeploymentUrl = ( - namespace: string, - labelSelector?: string, - ) => { - let url = `/apis/apps/v1/namespaces/${namespace}/deployments`; - if (labelSelector) { - url += `?labelSelector=${labelSelector}`; - } - return url; - }; - - private readonly projectStatefulSetUrl = ( - namespace: string, - labelSelector?: string, - ) => { - let url = `/apis/apps/v1/namespaces/${namespace}/statefulsets`; - if (labelSelector) { - url += `?labelSelector=${labelSelector}`; - } - return url; - }; - - deleteSecretsAndPVCs = async ( - k8sObjects: StatefulSetData | DeploymentData | void, - userNamespace: string, - ): Promise => { - if (k8sObjects && k8sObjects.items.length > 0) { - const kubeApi = await this.kubeAPI(); - - for (const k8sObject of k8sObjects.items) { - const volumes = k8sObject?.spec?.template?.spec?.volumes; - if (!volumes) continue; - - for (const volume of volumes) { - // delete pvc if any - if (volume.persistentVolumeClaim?.claimName) { - const pvcURL = `/api/v1/namespaces/${userNamespace}/persistentvolumeclaims/${volume.persistentVolumeClaim.claimName}`; - const response = await this.secureFetchApi.fetch( - `${kubeApi}${pvcURL}`, - { - method: 'DELETE', - }, - ); - if (!response.ok && response.status !== 404) { - const error = await response.json(); - throw new Error(errorMessage(error)); - } - } - - // delete secret if any - if (volume.secret?.secretName) { - const secretURL = `/api/v1/namespaces/${userNamespace}/secrets/${volume.secret.secretName}`; - const response = await this.secureFetchApi.fetch( - `${kubeApi}${secretURL}`, - { - method: 'DELETE', - }, - ); - if (!response.ok && response.status !== 404) { - const error = await response.json(); - throw new Error(errorMessage(error)); - } - } - } - } - } - }; - - deletePVCsForSTS = async ( - k8sObjects: StatefulSetData | void, - userNamespace: string, - ): Promise => { - if (k8sObjects && k8sObjects.items.length > 0) { - const kubeApi = await this.kubeAPI(); - - for (const k8sObject of k8sObjects.items) { - const volumeClaimTemplates = k8sObject?.spec?.volumeClaimTemplates; - if (!volumeClaimTemplates) continue; - - for (const volumeClaim of volumeClaimTemplates) { - const pvcs = await this.getPersistentVolumeClaims( - userNamespace, - `app.kubernetes.io%2Fname%3D${volumeClaim.metadata.name}`, - ); - - if (pvcs && pvcs.items.length > 0) { - for (const pvc of pvcs.items) { - const pvcURL = `/api/v1/namespaces/${userNamespace}/persistentvolumeclaims/${pvc.metadata.name}`; - const response = await this.secureFetchApi.fetch( - `${kubeApi}${pvcURL}`, - { - method: 'DELETE', - }, - ); - if (!response.ok && response.status !== 404) { - const error = await response.json(); - throw new Error(errorMessage(error)); - } - } - } - } - } - } - }; - - getSecret = async ( - namespace: string, - secretName: string, - ): Promise => { - const kubeApi = await this.kubeAPI(); - const projectSecretURL = `/api/v1/namespaces/${namespace}/secrets/${secretName}`; - const response = await this.secureFetchApi.fetch( - `${kubeApi}${projectSecretURL}`, - { - method: 'GET', - }, - ); - - if (!response.ok) { - const error = await response.json(); - throw new Error(errorMessage(error)); - } - return response.json(); - }; - - getPersistentVolumeClaims = async ( - namespace: string, - labels?: string, - ): Promise => { - const kubeApi = await this.kubeAPI(); - const url = this.projectPersistentVolumeClaimUrl(namespace, labels); - const response = await this.secureFetchApi.fetch(`${kubeApi}${url}`, { - method: 'GET', - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(errorMessage(error)); - } - return response.json(); - }; - - getDeployments = async ( - namespace: string, - labels?: string, - ): Promise => { - const kubeApi = await this.kubeAPI(); - const url = this.projectDeploymentUrl(namespace, labels); - const response = await this.secureFetchApi.fetch(`${kubeApi}${url}`, { - method: 'GET', - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(errorMessage(error)); - } - return response.json(); - }; - - getStatefulSets = async ( - namespace: string, - labels?: string, - ): Promise => { - const kubeApi = await this.kubeAPI(); - const url = this.projectStatefulSetUrl(namespace, labels); - const response = await this.secureFetchApi.fetch(`${kubeApi}${url}`, { - method: 'GET', - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(errorMessage(error)); - } - return response.json(); - }; -} diff --git a/workspaces/sandbox/plugins/sandbox/src/api/RegistrationBackendClient.tsx b/workspaces/sandbox/plugins/sandbox/src/api/RegistrationBackendClient.tsx deleted file mode 100644 index 8f88be0a92..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/api/RegistrationBackendClient.tsx +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/// -import { ConfigApi } from '@backstage/core-plugin-api'; -import { isValidCountryCode, isValidPhoneNumber } from '../utils/phone-utils'; -import { CommonResponse, SignupData } from '../types'; -import { SecureFetchApi } from './SecureFetchClient'; - -export type RegistrationBackendClientOptions = { - configApi: ConfigApi; - secureFetchApi: SecureFetchApi; -}; - -export interface UIConfig { - workatoWebHookURL?: string; -} - -export interface RegistrationService { - getRecaptchaAPIKey(): string; - getSignUpData(): Promise; - signup(): Promise; - initiatePhoneVerification( - countryCode: string, - phoneNumber: string, - ): Promise; - completePhoneVerification(code: string): Promise; - verifyActivationCode(code: string): Promise; - getSegmentWriteKey(): Promise; - getUIConfig(): Promise; -} - -export class RegistrationBackendClient implements RegistrationService { - private readonly configApi: ConfigApi; - private readonly secureFetchApi: SecureFetchApi; - - constructor(options: RegistrationBackendClientOptions) { - this.configApi = options.configApi; - this.secureFetchApi = options.secureFetchApi; - } - - private readonly signupAPI = (): string => { - const signupAPI = this.configApi.getString('sandbox.signupAPI'); - return `${signupAPI}/signup`; - }; - - getRecaptchaAPIKey = (): string => { - return ( - this.configApi.getOptionalString('sandbox.recaptcha.siteKey') ?? - 'test-api-key' - ); - }; - - getSignUpData = async (): Promise => { - const signupURL = await this.signupAPI(); - const response = await this.secureFetchApi.fetch(signupURL, { - method: 'GET', - }); - if (!response.ok) { - if (response.status === 404) { - return undefined; - } - throw new Error( - `Unexpected status code: ${response.status} ${response.statusText}`, - ); - } - return response.json(); - }; - - getRecaptchaToken = async (): Promise => { - return new Promise((resolve, reject) => { - const apiKey = this.getRecaptchaAPIKey(); - let timeout = false; - const captchaTimeout = setTimeout(() => { - timeout = true; - reject(new Error('Recaptcha timeout.')); - }, 10000); - if (grecaptcha?.enterprise) { - grecaptcha.enterprise.ready(async () => { - if (!timeout) { - clearTimeout(captchaTimeout); - try { - resolve( - await grecaptcha.enterprise.execute(apiKey, { - action: 'SIGNUP', - }), - ); - } catch (e) { - reject(new Error('Recaptcha failure.')); - } - } - }); - } else { - reject(new Error('Recaptcha failure.')); - } - }); - }; - - signup = async (): Promise => { - let token = ''; - try { - token = await this.getRecaptchaToken(); - } catch (err) { - throw new Error(`Error getting recaptcha token: ${err}`); - } - const signupURL = await this.signupAPI(); - await this.secureFetchApi.fetch(signupURL, { - method: 'POST', - headers: { - 'Recaptcha-Token': token, - }, - body: null, - }); - }; - - initiatePhoneVerification = async ( - countryCode: string, - phoneNumber: string, - ): Promise => { - const verificationURL = `${await this.signupAPI()}/verification`; - if (!isValidCountryCode(countryCode)) { - throw new Error('Invalid country code.'); - } - if (!isValidPhoneNumber(phoneNumber)) { - throw new Error('Invalid phone number.'); - } - const response = await this.secureFetchApi.fetch(verificationURL, { - method: 'PUT', - body: JSON.stringify({ - country_code: countryCode, - phone_number: phoneNumber, - }), - }); - - if (!response.ok) { - const error: CommonResponse = await response.json(); - // handle backend error messages and make it more user-friendly - if ( - error?.message.includes("Invalid 'To' Phone Number") || - error?.message.includes("'To' number cannot be a Short Code:") || - error?.message.includes( - "Message cannot be sent with the current combination of 'To'", - ) || - error?.message.includes('is not a valid mobile number') - ) { - throw new Error( - 'Invalid phone number. Please verify the country code and number format, then try again.', - ); - } else if (error?.message.includes('phone number already in use')) { - throw new Error( - 'This phone number is already in use. Please use a different number.', - ); - } else { - throw new Error(error?.message); - } - } - }; - - completePhoneVerification = async (code: string): Promise => { - const verificationURL = `${await this.signupAPI()}/verification`; - const response = await this.secureFetchApi.fetch( - `${verificationURL}/${code}`, - { - method: 'GET', - }, - ); - - if (!response.ok) { - const error: CommonResponse = await response.json(); - throw new Error(error?.message); - } - }; - - verifyActivationCode = async (code: string): Promise => { - const verificationURL = `${await this.signupAPI()}/verification/activation-code`; - const response = await this.secureFetchApi.fetch(verificationURL, { - method: 'POST', - body: JSON.stringify({ - code: code, - }), - }); - - if (!response.ok) { - const error: CommonResponse = await response.json(); - throw new Error(error?.message); - } - }; - - getSegmentWriteKey = async (): Promise => { - const signupAPI = this.configApi.getString('sandbox.signupAPI'); - const response = await this.secureFetchApi.fetch( - `${signupAPI}/analytics/segment-write-key`, - { - method: 'GET', - }, - ); - - if (!response.ok) { - throw new Error(`Failed to fetch Segment write key: ${response.status}`); - } - - const writeKey = await response.text(); - return writeKey.trim(); - }; - - getUIConfig = async (): Promise => { - try { - const signupAPI = this.configApi.getString('sandbox.signupAPI'); - const response = await this.secureFetchApi.fetch( - `${signupAPI}/uiconfig`, - { - method: 'GET', - }, - ); - - if (!response.ok) { - // Return empty config if fetch fails - UI config is optional - return {}; - } - - return response.json(); - } catch (error) { - // Return empty config on any error - UI config is optional - return {}; - } - }; -} diff --git a/workspaces/sandbox/plugins/sandbox/src/api/SecureFetchClient.tsx b/workspaces/sandbox/plugins/sandbox/src/api/SecureFetchClient.tsx deleted file mode 100644 index 4fc4162575..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/api/SecureFetchClient.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { OAuthApi } from '@backstage/core-plugin-api'; - -export type SecureFetchClientOptions = { - oauthApi: OAuthApi; -}; - -export interface SecureFetchApi { - fetch(input: RequestInfo | URL, init?: RequestInit): Promise; -} - -export class SecureFetchClient implements SecureFetchApi { - private readonly oauthApi: OAuthApi; - - constructor(options: SecureFetchClientOptions) { - this.oauthApi = options.oauthApi; - } - - fetch = async ( - input: RequestInfo | URL, - init?: RequestInit, - ): Promise => { - const token = await this.oauthApi.getAccessToken(); - const headers = new Headers(init?.headers); - - if (token) { - headers.set('Accept', 'application/json'); - headers.set('Authorization', `Bearer ${token}`); - } - - return fetch(input, { - ...init, - headers, - }); - }; -} diff --git a/workspaces/sandbox/plugins/sandbox/src/api/__tests__/AnsibleBackendClient.test.tsx b/workspaces/sandbox/plugins/sandbox/src/api/__tests__/AnsibleBackendClient.test.tsx deleted file mode 100644 index 6d78dcc080..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/api/__tests__/AnsibleBackendClient.test.tsx +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigApi } from '@backstage/core-plugin-api'; -import { AnsibleBackendClient } from '../AnsibleBackendClient'; -import { SecureFetchApi } from '../SecureFetchClient'; - -// Helper to create a mock Response object -const createMockResponse = (options: { - ok: boolean; - status?: number; - statusText?: string; - json?: () => Promise; -}): Response => { - const { ok, status = 200, statusText = '', json } = options; - return { - ok, - status, - statusText, - headers: new Headers(), - redirected: false, - type: 'basic', - url: 'http://mock', - json: json || (() => Promise.resolve({})), - text: () => Promise.resolve(''), - blob: () => Promise.resolve(new Blob()), - arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), - formData: () => Promise.resolve(new FormData()), - bodyUsed: false, - body: null, - clone: function () { - return this; - }, - } as Response; -}; - -describe('AnsibleBackendClient', () => { - let mockConfigApi: jest.Mocked; - let mockSecureFetchApi: jest.Mocked; - let client: AnsibleBackendClient; - const testNamespace = 'test-namespace'; - const mockKubeApi = 'http://kube-api'; - - beforeEach(() => { - mockConfigApi = { - getString: jest.fn(), - getOptionalString: jest.fn(), - } as any; - - mockSecureFetchApi = { - fetch: jest.fn(), - } as any; - - client = new AnsibleBackendClient({ - configApi: mockConfigApi, - secureFetchApi: mockSecureFetchApi, - }); - - mockConfigApi.getString.mockReturnValue(mockKubeApi); - }); - - describe('getAAP', () => { - it('should return AAP data on successful response', async () => { - const mockData = { name: 'sandbox-aap', status: 'Running' }; - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - json: () => Promise.resolve(mockData), - }), - ); - - const result = await client.getAAP(testNamespace); - expect(result).toEqual(mockData); - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining( - `/apis/aap.ansible.com/v1alpha1/namespaces/${testNamespace}/ansibleautomationplatforms`, - ), - expect.objectContaining({ method: 'GET' }), - ); - }); - - it('should throw error on unsuccessful response', async () => { - const errorData = { message: 'Not found' }; - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 404, - json: () => Promise.resolve(errorData), - }), - ); - - await expect(client.getAAP(testNamespace)).rejects.toThrow(); - }); - }); - - describe('createAAP', () => { - const aapObject = '"kind":"AnsibleAutomationPlatform"'; - - it('should successfully create AAP', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - }), - ); - - await expect(client.createAAP(testNamespace)).resolves.not.toThrow(); - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining( - `/apis/aap.ansible.com/v1alpha1/namespaces/${testNamespace}/ansibleautomationplatforms`, - ), - expect.objectContaining({ - method: 'POST', - body: expect.stringContaining(aapObject), - headers: { - 'Content-Type': 'application/yaml', - }, - }), - ); - }); - - it('should not throw error on 409 Conflict response', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 409, - }), - ); - - await expect(client.createAAP(testNamespace)).resolves.not.toThrow(); - }); - - it('should throw error on other unsuccessful responses', async () => { - const errorData = { message: 'Internal error' }; - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 500, - json: () => Promise.resolve(errorData), - }), - ); - - await expect(client.createAAP(testNamespace)).rejects.toThrow(); - }); - }); - - describe('unIdleAAP', () => { - it('should successfully unidle AAP', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - }), - ); - - await expect(client.unIdleAAP(testNamespace)).resolves.not.toThrow(); - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining( - `/apis/aap.ansible.com/v1alpha1/namespaces/${testNamespace}/ansibleautomationplatforms/sandbox-aap`, - ), - expect.objectContaining({ - method: 'PATCH', - headers: { - 'Content-Type': 'application/merge-patch+json', - }, - body: JSON.stringify({ - spec: { - idle_aap: false, - }, - }), - }), - ); - }); - - it('should throw error on unsuccessful response', async () => { - const errorData = { message: 'Failed to unidle' }; - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 400, - json: () => Promise.resolve(errorData), - }), - ); - - await expect(client.unIdleAAP(testNamespace)).rejects.toThrow(); - }); - }); - - describe('deleteAAPCR', () => { - it('should successfully delete AAP CR', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - }), - ); - - await expect(client.deleteAAPCR(testNamespace)).resolves.not.toThrow(); - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining( - `/apis/aap.ansible.com/v1alpha1/namespaces/${testNamespace}/ansibleautomationplatforms/sandbox-aap`, - ), - expect.objectContaining({ - method: 'DELETE', - }), - ); - }); - - it('should not throw error on 404 Not Found response', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 404, - }), - ); - - await expect(client.deleteAAPCR(testNamespace)).resolves.not.toThrow(); - }); - - it('should throw error on other unsuccessful responses', async () => { - const errorData = { message: 'Internal error' }; - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 500, - json: () => Promise.resolve(errorData), - }), - ); - - await expect(client.deleteAAPCR(testNamespace)).rejects.toThrow(); - }); - }); -}); diff --git a/workspaces/sandbox/plugins/sandbox/src/api/__tests__/KubeBackendClient.test.tsx b/workspaces/sandbox/plugins/sandbox/src/api/__tests__/KubeBackendClient.test.tsx deleted file mode 100644 index 6c1b47e0ed..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/api/__tests__/KubeBackendClient.test.tsx +++ /dev/null @@ -1,502 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigApi } from '@backstage/core-plugin-api'; -import { KubeBackendClient } from '../KubeBackendClient'; -import { SecureFetchApi } from '../SecureFetchClient'; -import { - DeploymentData, - PersistentVolumeClaimData, - StatefulSetData, - SecretItem, -} from '../../types'; - -// Helper to create a mock Response object -const createMockResponse = (options: { - ok: boolean; - status?: number; - statusText?: string; - json?: () => Promise; -}): Response => { - const { ok, status = 200, statusText = '', json } = options; - return { - ok, - status, - statusText, - headers: new Headers(), - redirected: false, - type: 'basic', - url: 'http://mock', - json: json || (() => Promise.resolve({})), - text: () => Promise.resolve(''), - blob: () => Promise.resolve(new Blob()), - arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), - formData: () => Promise.resolve(new FormData()), - bodyUsed: false, - body: null, - clone: function () { - return this; - }, - } as Response; -}; - -const commonMetadata = { - uuid: 'test-uuid', - creationTimestamp: '2025-05-15T00:00:00Z', - labels: { - app: 'test', - }, -}; - -describe('KubeBackendClient', () => { - let mockConfigApi: jest.Mocked; - let mockSecureFetchApi: jest.Mocked; - let client: KubeBackendClient; - const testNamespace = 'test-namespace'; - const mockKubeApi = 'http://kube-api'; - - beforeEach(() => { - mockConfigApi = { - getString: jest.fn(), - getOptionalString: jest.fn(), - } as any; - - mockSecureFetchApi = { - fetch: jest.fn(), - } as any; - - client = new KubeBackendClient({ - configApi: mockConfigApi, - secureFetchApi: mockSecureFetchApi, - }); - - mockConfigApi.getString.mockReturnValue(mockKubeApi); - }); - - describe('deleteSecretsAndPVCs', () => { - const mockDeploymentData: DeploymentData = { - items: [ - { - metadata: { - name: 'test-deployment', - ...commonMetadata, - }, - status: { - conditions: [{ type: 'Available', status: 'True' }], - }, - spec: { - replicas: 1, - template: { - metadata: { - labels: { - app: 'test', - deployment: 'test', - }, - }, - spec: { - volumes: [ - { - persistentVolumeClaim: { claimName: 'test-pvc' }, - name: 'test-volume', - }, - { - secret: { secretName: 'test-secret' }, - name: 'test-secret-volume', - }, - ], - }, - }, - }, - }, - ], - }; - - it('should delete PVCs and secrets successfully', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - }), - ); - - await expect( - client.deleteSecretsAndPVCs(mockDeploymentData, testNamespace), - ).resolves.not.toThrow(); - - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining( - `/api/v1/namespaces/${testNamespace}/persistentvolumeclaims/test-pvc`, - ), - expect.objectContaining({ method: 'DELETE' }), - ); - - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining( - `/api/v1/namespaces/${testNamespace}/secrets/test-secret`, - ), - expect.objectContaining({ method: 'DELETE' }), - ); - }); - - it('should not throw on 404 responses', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 404, - }), - ); - - await expect( - client.deleteSecretsAndPVCs(mockDeploymentData, testNamespace), - ).resolves.not.toThrow(); - }); - - it('should throw on other error responses', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 500, - json: () => Promise.resolve({ message: 'Internal error' }), - }), - ); - - await expect( - client.deleteSecretsAndPVCs(mockDeploymentData, testNamespace), - ).rejects.toThrow(); - }); - }); - - describe('deletePVCsForSTS', () => { - const mockStatefulSetData: StatefulSetData = { - items: [ - { - metadata: { - name: 'test-statefulset', - ...commonMetadata, - }, - spec: { - replicas: 1, - template: { - metadata: { - labels: { - app: 'test', - deployment: 'test', - }, - }, - spec: {}, - }, - volumeClaimTemplates: [ - { - metadata: { - name: 'test-template', - ...commonMetadata, - }, - }, - ], - }, - status: { - conditions: [], - }, - }, - ], - }; - - const mockPVCData: PersistentVolumeClaimData = { - items: [ - { - metadata: { - name: 'test-pvc-1', - ...commonMetadata, - }, - }, - ], - }; - - it('should delete PVCs for StatefulSet successfully', async () => { - mockSecureFetchApi.fetch - .mockResolvedValueOnce( - createMockResponse({ - ok: true, - json: () => Promise.resolve(mockPVCData), - }), - ) - .mockResolvedValueOnce( - createMockResponse({ - ok: true, - }), - ); - - await expect( - client.deletePVCsForSTS(mockStatefulSetData, testNamespace), - ).resolves.not.toThrow(); - - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining( - `/api/v1/namespaces/${testNamespace}/persistentvolumeclaims/test-pvc-1`, - ), - expect.objectContaining({ method: 'DELETE' }), - ); - }); - }); - - describe('getSecret', () => { - const secretName = 'test-secret'; - const mockSecret: SecretItem = { - metadata: { - name: secretName, - uuid: 'test-secret-uuid', - creationTimestamp: '2025-05-15T00:00:00Z', - }, - data: { password: 'test-value' }, - }; - - it('should return secret data on successful response', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - json: () => Promise.resolve(mockSecret), - }), - ); - - const result = await client.getSecret(testNamespace, secretName); - expect(result).toEqual(mockSecret); - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining( - `/api/v1/namespaces/${testNamespace}/secrets/${secretName}`, - ), - expect.objectContaining({ method: 'GET' }), - ); - }); - }); - - describe('getPersistentVolumeClaims', () => { - const mockPVCs: PersistentVolumeClaimData = { - items: [ - { - metadata: { - name: 'pvc-1', - ...commonMetadata, - }, - }, - { - metadata: { - name: 'pvc-2', - ...commonMetadata, - }, - }, - ], - }; - - it('should return PVCs without labels', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - json: () => Promise.resolve(mockPVCs), - }), - ); - - const result = await client.getPersistentVolumeClaims(testNamespace); - expect(result).toEqual(mockPVCs); - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining( - `/api/v1/namespaces/${testNamespace}/persistentvolumeclaims`, - ), - expect.objectContaining({ method: 'GET' }), - ); - }); - - it('should return PVCs with label selector', async () => { - const labels = 'app=test'; - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - json: () => Promise.resolve(mockPVCs), - }), - ); - - await client.getPersistentVolumeClaims(testNamespace, labels); - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining(`labelSelector=${labels}`), - expect.any(Object), - ); - }); - }); - - describe('getDeployments', () => { - const mockDeployments: DeploymentData = { - items: [ - { - metadata: { - name: 'deployment-1', - ...commonMetadata, - }, - status: { - conditions: [{ type: 'Available', status: 'True' }], - }, - spec: { - replicas: 1, - template: { - metadata: { - labels: { - app: 'test', - deployment: 'test', - }, - }, - spec: {}, - }, - }, - }, - { - metadata: { - name: 'deployment-2', - ...commonMetadata, - }, - status: { - conditions: [{ type: 'Available', status: 'True' }], - }, - spec: { - replicas: 1, - template: { - metadata: { - labels: { - app: 'test', - deployment: 'test', - }, - }, - spec: {}, - }, - }, - }, - ], - }; - - it('should return deployments without labels', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - json: () => Promise.resolve(mockDeployments), - }), - ); - - const result = await client.getDeployments(testNamespace); - expect(result).toEqual(mockDeployments); - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining( - `/apis/apps/v1/namespaces/${testNamespace}/deployments`, - ), - expect.objectContaining({ method: 'GET' }), - ); - }); - - it('should return deployments with label selector', async () => { - const labels = 'app=test'; - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - json: () => Promise.resolve(mockDeployments), - }), - ); - - await client.getDeployments(testNamespace, labels); - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining(`labelSelector=${labels}`), - expect.any(Object), - ); - }); - }); - - describe('getStatefulSets', () => { - const mockStatefulSets: StatefulSetData = { - items: [ - { - metadata: { - name: 'statefulset-1', - ...commonMetadata, - }, - status: { - conditions: [{ type: 'Available', status: 'True' }], - }, - spec: { - replicas: 1, - template: { - metadata: { - labels: { - app: 'test', - deployment: 'test', - }, - }, - spec: {}, - }, - }, - }, - { - metadata: { - name: 'statefulset-2', - ...commonMetadata, - }, - status: { - conditions: [{ type: 'Available', status: 'True' }], - }, - spec: { - replicas: 1, - template: { - metadata: { - labels: { - app: 'test', - deployment: 'test', - }, - }, - spec: {}, - }, - }, - }, - ], - }; - - it('should return statefulsets without labels', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - json: () => Promise.resolve(mockStatefulSets), - }), - ); - - const result = await client.getStatefulSets(testNamespace); - expect(result).toEqual(mockStatefulSets); - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining( - `/apis/apps/v1/namespaces/${testNamespace}/statefulsets`, - ), - expect.objectContaining({ method: 'GET' }), - ); - }); - - it('should return statefulsets with label selector', async () => { - const labels = 'app=test'; - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - json: () => Promise.resolve(mockStatefulSets), - }), - ); - - await client.getStatefulSets(testNamespace, labels); - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining(`labelSelector=${labels}`), - expect.any(Object), - ); - }); - }); -}); diff --git a/workspaces/sandbox/plugins/sandbox/src/api/__tests__/RegistrationBackendClient.test.tsx b/workspaces/sandbox/plugins/sandbox/src/api/__tests__/RegistrationBackendClient.test.tsx deleted file mode 100644 index 421f781cd3..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/api/__tests__/RegistrationBackendClient.test.tsx +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigApi } from '@backstage/core-plugin-api'; -import { RegistrationBackendClient } from '../RegistrationBackendClient'; -import { SecureFetchApi } from '../SecureFetchClient'; - -// Helper to create a mock Response object -const createMockResponse = (options: { - ok: boolean; - status?: number; - statusText?: string; - json?: () => Promise; -}): Response => { - const { ok, status = 200, statusText = '', json } = options; - return { - ok, - status, - statusText, - headers: new Headers(), - redirected: false, - type: 'basic', - url: 'http://mock', - json: json || (() => Promise.resolve({})), - text: () => Promise.resolve(''), - blob: () => Promise.resolve(new Blob()), - arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), - formData: () => Promise.resolve(new FormData()), - bodyUsed: false, - body: null, - clone: function () { - return this; - }, - } as Response; -}; - -describe('RegistrationBackendClient', () => { - let mockConfigApi: jest.Mocked; - let mockSecureFetchApi: jest.Mocked; - let client: RegistrationBackendClient; - - beforeEach(() => { - mockConfigApi = { - getString: jest.fn(), - getOptionalString: jest.fn(), - } as any; - - mockSecureFetchApi = { - fetch: jest.fn(), - } as any; - - client = new RegistrationBackendClient({ - configApi: mockConfigApi, - secureFetchApi: mockSecureFetchApi, - }); - - // Mock the global grecaptcha object - (global as any).grecaptcha = { - enterprise: { - ready: jest.fn(callback => callback()), - execute: jest.fn(), - }, - }; - }); - - describe('getRecaptchaAPIKey', () => { - it('should return configured API key', () => { - mockConfigApi.getOptionalString.mockReturnValue('test-key'); - expect(client.getRecaptchaAPIKey()).toBe('test-key'); - }); - - it('should return default API key if not configured', () => { - mockConfigApi.getOptionalString.mockReturnValue(undefined); - expect(client.getRecaptchaAPIKey()).toBe('test-api-key'); - }); - }); - - describe('getSignUpData', () => { - it('should return signup data on successful response', async () => { - mockConfigApi.getString.mockReturnValue('http://api'); - const mockData = { someData: 'value' }; - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - json: () => Promise.resolve(mockData), - }), - ); - - const result = await client.getSignUpData(); - expect(result).toEqual(mockData); - }); - - it('should return undefined on 404 response', async () => { - mockConfigApi.getString.mockReturnValue('http://api'); - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 404, - }), - ); - - const result = await client.getSignUpData(); - expect(result).toBeUndefined(); - }); - - it('should throw error on other unsuccessful responses', async () => { - mockConfigApi.getString.mockReturnValue('http://api'); - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 500, - statusText: 'Server Error', - }), - ); - - await expect(client.getSignUpData()).rejects.toThrow( - 'Unexpected status code: 500 Server Error', - ); - }); - }); - - describe('initiatePhoneVerification', () => { - const validCountryCode = '+1'; - const validPhoneNumber = '1234567890'; - - beforeEach(() => { - mockConfigApi.getString.mockReturnValue('http://api'); - }); - - it('should successfully initiate phone verification', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - }), - ); - - await expect( - client.initiatePhoneVerification(validCountryCode, validPhoneNumber), - ).resolves.not.toThrow(); - }); - - it('should throw error for invalid phone number format', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 400, - json: () => - Promise.resolve({ - message: "Invalid 'To' Phone Number", - }), - }), - ); - - await expect( - client.initiatePhoneVerification(validCountryCode, validPhoneNumber), - ).rejects.toThrow( - 'Invalid phone number. Please verify the country code and number format, then try again.', - ); - }); - - it('should throw error for already used phone number', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 400, - json: () => - Promise.resolve({ - message: 'phone number already in use', - }), - }), - ); - - await expect( - client.initiatePhoneVerification(validCountryCode, validPhoneNumber), - ).rejects.toThrow( - 'This phone number is already in use. Please use a different number.', - ); - }); - }); - - describe('completePhoneVerification', () => { - const validCode = '123456'; - - beforeEach(() => { - mockConfigApi.getString.mockReturnValue('http://api'); - }); - - it('should successfully complete phone verification', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - }), - ); - - await expect( - client.completePhoneVerification(validCode), - ).resolves.not.toThrow(); - }); - - it('should throw error on unsuccessful verification', async () => { - const errorMessage = 'Invalid verification code'; - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 400, - json: () => - Promise.resolve({ - message: errorMessage, - }), - }), - ); - - await expect(client.completePhoneVerification(validCode)).rejects.toThrow( - errorMessage, - ); - }); - }); - - describe('verifyActivationCode', () => { - const validCode = '123456'; - - beforeEach(() => { - mockConfigApi.getString.mockReturnValue('http://api'); - }); - - it('should successfully verify activation code', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - }), - ); - - await expect( - client.verifyActivationCode(validCode), - ).resolves.not.toThrow(); - }); - - it('should throw error on unsuccessful verification', async () => { - const errorMessage = 'Invalid activation code'; - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 400, - json: () => - Promise.resolve({ - message: errorMessage, - }), - }), - ); - - await expect(client.verifyActivationCode(validCode)).rejects.toThrow( - errorMessage, - ); - }); - }); - - describe('getUIConfig', () => { - it('should return UI config with workatoWebHookURL', async () => { - const mockUIConfig = { - workatoWebHookURL: 'https://webhooks.test', - }; - - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - json: () => Promise.resolve(mockUIConfig), - }), - ); - - const result = await client.getUIConfig(); - - expect(result).toEqual(mockUIConfig); - expect(mockSecureFetchApi.fetch).toHaveBeenCalledWith( - expect.stringContaining('/uiconfig'), - expect.objectContaining({ method: 'GET' }), - ); - }); - - it('should return empty config if workatoWebHookURL is not present', async () => { - const mockUIConfig = {}; - - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: true, - json: () => Promise.resolve(mockUIConfig), - }), - ); - - const result = await client.getUIConfig(); - - expect(result).toEqual({}); - expect(result.workatoWebHookURL).toBeUndefined(); - }); - - it('should return empty config on unsuccessful response', async () => { - mockSecureFetchApi.fetch.mockResolvedValue( - createMockResponse({ - ok: false, - status: 500, - json: () => Promise.resolve({}), - }), - ); - - const result = await client.getUIConfig(); - - expect(result).toEqual({}); - }); - - it('should return empty config on fetch error', async () => { - mockSecureFetchApi.fetch.mockRejectedValue(new Error('Network failure')); - - const result = await client.getUIConfig(); - - expect(result).toEqual({}); - }); - }); -}); diff --git a/workspaces/sandbox/plugins/sandbox/src/api/index.ts b/workspaces/sandbox/plugins/sandbox/src/api/index.ts deleted file mode 100644 index 04ebe86b6c..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/api/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { - ApiRef, - BackstageIdentityApi, - createApiRef, - OAuthApi, - OpenIdConnectApi, - ProfileInfoApi, - SessionApi, -} from '@backstage/core-plugin-api'; -import { RegistrationService } from './RegistrationBackendClient'; -import { KubeAPIService } from './KubeBackendClient'; -import { AAPService } from './AnsibleBackendClient'; -import { SecureFetchApi } from './SecureFetchClient'; - -export * from './SecureFetchClient'; -export * from './RegistrationBackendClient'; -export * from './KubeBackendClient'; -export * from './AnsibleBackendClient'; - -export const keycloakApiRef: ApiRef< - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi & - OAuthApi -> = createApiRef({ - id: 'plugin.sandbox.keycloak.api-ref', -}); - -export const secureFetchApiRef = createApiRef({ - id: 'plugin.sandbox.secure-fetch.api-ref', -}); - -export const registerApiRef = createApiRef({ - id: 'plugin.sandbox.registration.api-ref', -}); - -export const kubeApiRef = createApiRef({ - id: 'plugin.sandbox.kube.api-ref', -}); - -export const aapApiRef = createApiRef({ - id: 'plugin.sandbox.aap.api-ref', -}); diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/images/art0.svg b/workspaces/sandbox/plugins/sandbox/src/assets/images/art0.svg deleted file mode 100644 index 516e70ac1f..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/images/art0.svg +++ /dev/null @@ -1,224 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/images/art1.svg b/workspaces/sandbox/plugins/sandbox/src/assets/images/art1.svg deleted file mode 100644 index ebc5df3978..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/images/art1.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/images/art2.svg b/workspaces/sandbox/plugins/sandbox/src/assets/images/art2.svg deleted file mode 100644 index 4b524884bf..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/images/art2.svg +++ /dev/null @@ -1,897 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/images/art3.svg b/workspaces/sandbox/plugins/sandbox/src/assets/images/art3.svg deleted file mode 100644 index 4b04293dc3..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/images/art3.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/images/art4.svg b/workspaces/sandbox/plugins/sandbox/src/assets/images/art4.svg deleted file mode 100644 index 9a82ac0159..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/images/art4.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/images/art5.svg b/workspaces/sandbox/plugins/sandbox/src/assets/images/art5.svg deleted file mode 100644 index 65093bacfe..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/images/art5.svg +++ /dev/null @@ -1,341 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/images/sandbox-banner-image.svg b/workspaces/sandbox/plugins/sandbox/src/assets/images/sandbox-banner-image.svg deleted file mode 100644 index caf3712fc2..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/images/sandbox-banner-image.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/logos/ansible.svg b/workspaces/sandbox/plugins/sandbox/src/assets/logos/ansible.svg deleted file mode 100644 index 3329c8a164..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/logos/ansible.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/logos/devspaces.svg b/workspaces/sandbox/plugins/sandbox/src/assets/logos/devspaces.svg deleted file mode 100644 index c9d1c8f11d..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/logos/devspaces.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/logos/logo_hat-only.svg b/workspaces/sandbox/plugins/sandbox/src/assets/logos/logo_hat-only.svg deleted file mode 100644 index 062df671c1..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/logos/logo_hat-only.svg +++ /dev/null @@ -1 +0,0 @@ -RedHat-Logo-Hat-Color \ No newline at end of file diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/logos/openshift-ai.svg b/workspaces/sandbox/plugins/sandbox/src/assets/logos/openshift-ai.svg deleted file mode 100644 index 294f5abefb..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/logos/openshift-ai.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/logos/openshift-virtualization.svg b/workspaces/sandbox/plugins/sandbox/src/assets/logos/openshift-virtualization.svg deleted file mode 100644 index 3b937b4eda..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/logos/openshift-virtualization.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/workspaces/sandbox/plugins/sandbox/src/assets/logos/openshift.svg b/workspaces/sandbox/plugins/sandbox/src/assets/logos/openshift.svg deleted file mode 100644 index efd6c7b3b4..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/assets/logos/openshift.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/AccessCodeInputModal.tsx b/workspaces/sandbox/plugins/sandbox/src/components/Modals/AccessCodeInputModal.tsx deleted file mode 100644 index a323e4fbc2..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/AccessCodeInputModal.tsx +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React, { useEffect, useRef, useState } from 'react'; -import { useTheme } from '@mui/material/styles'; -import Button from '@mui/material/Button'; -import Dialog from '@mui/material/Dialog'; -import DialogTitle from '@mui/material/DialogTitle'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogActions from '@mui/material/DialogActions'; -import ErrorIcon from '@mui/icons-material/Error'; -import TextField from '@mui/material/TextField'; -import IconButton from '@mui/material/IconButton'; -import CloseIcon from '@mui/icons-material/Close'; -import Stack from '@mui/material/Stack'; -import { useApi } from '@backstage/core-plugin-api'; -import { registerApiRef } from '../../api'; -import { useSandboxContext } from '../../hooks/useSandboxContext'; -import Typography from '@mui/material/Typography'; -import CircularProgress from '@mui/material/CircularProgress'; -import { errorMessage } from '../../utils/common'; - -type AccessCodeInputModalProps = { - modalOpen: boolean; - setOpen: React.Dispatch>; -}; - -export const AccessCodeInputModal: React.FC = ({ - modalOpen, - setOpen, -}) => { - const theme = useTheme(); - const registerApi = useApi(registerApiRef); - const { refetchUserData } = useSandboxContext(); - const [loading, setLoading] = useState(false); - const [accessCode, setAccessCode] = useState(['', '', '', '', '']); - const [accessCodeError, setAccessCodeError] = React.useState< - string | undefined - >(); - const inputRefs = useRef([]); - - useEffect(() => { - if (modalOpen) { - // Focus on the first input field when modal opens - setTimeout(() => inputRefs.current[0]?.focus(), 100); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [modalOpen]); - - const handleClose = () => { - setOpen(false); - setAccessCode(['', '', '', '', '']); - setAccessCodeError(undefined); - }; - - const handleChange = ( - index: number, - event: React.ChangeEvent, - ) => { - const value = event.target.value; - - // Only allow alphanumeric characters - if (value && !/^[A-Za-z0-9]$/.test(value)) return; - - const newAccessCode = [...accessCode]; - newAccessCode[index] = value; - setAccessCode(newAccessCode); - - // Move focus to next input - if (value && index < accessCode.length - 1) { - inputRefs.current[index + 1].focus(); - } - }; - - // Handle Backspace: Move focus to previous box if empty - const handleKeyDown = ( - index: number, - event: React.KeyboardEvent, - ) => { - if (event.key === 'Backspace' && !accessCode[index] && index > 0) { - inputRefs.current[index - 1].focus(); - } - }; - - const handleStartTrialClick = async () => { - try { - setAccessCodeError(undefined); - setLoading(true); - await registerApi.verifyActivationCode(accessCode.join('')); - refetchUserData(); - handleClose(); - } catch (e) { - setAccessCodeError(errorMessage(e)); - } finally { - setLoading(false); - } - }; - - return ( - - - Enter the activation code - - - - - - - If you have an activation code, enter it now. - - - {accessCode.map((digit, index) => ( - handleChange(index, e)} - onKeyDown={e => - handleKeyDown(index, e as React.KeyboardEvent) - } - inputRef={el => (inputRefs.current[index] = el)} - variant="outlined" - inputProps={{ - maxLength: 1, - style: { textAlign: 'center', fontWeight: 400 }, - }} - /> - ))} - - {accessCodeError && ( - -
- - {accessCodeError} -
-
- )} -
- - - - -
- ); -}; - -export default AccessCodeInputModal; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/AnsibleDeleteInstanceModal.tsx b/workspaces/sandbox/plugins/sandbox/src/components/Modals/AnsibleDeleteInstanceModal.tsx deleted file mode 100644 index eba4d05726..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/AnsibleDeleteInstanceModal.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import Button from '@mui/material/Button'; -import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import IconButton from '@mui/material/IconButton'; -import WarningIcon from '@mui/icons-material/Warning'; -import Typography from '@mui/material/Typography'; -import { useTheme } from '@mui/material/styles'; -import CloseIcon from '@mui/icons-material/Close'; - -type AnsibleDeleteInstanceModalProps = { - modalOpen: boolean; - setOpen: React.Dispatch>; - handleAnsibleDeleteInstance: () => void; -}; - -export const AnsibleDeleteInstanceModal: React.FC< - AnsibleDeleteInstanceModalProps -> = ({ modalOpen, setOpen, handleAnsibleDeleteInstance }) => { - const theme = useTheme(); - - const handleClose = () => { - setOpen(false); - }; - - return ( - - -
- -
Delete instance?
-
-
- - - {' '} - - - Your AAP instance will be deleted. Consider backing up your work - before continuing. - - - - - - -
- ); -}; - -export default AnsibleDeleteInstanceModal; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/AnsibleLaunchInfoModal.tsx b/workspaces/sandbox/plugins/sandbox/src/components/Modals/AnsibleLaunchInfoModal.tsx deleted file mode 100644 index 1c6e0c3e97..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/AnsibleLaunchInfoModal.tsx +++ /dev/null @@ -1,516 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React, { useState } from 'react'; -import Alert from '@mui/material/Alert'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import CircularProgress from '@mui/material/CircularProgress'; -import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import IconButton from '@mui/material/IconButton'; -import InputAdornment from '@mui/material/InputAdornment'; -import InputLabel from '@mui/material/InputLabel'; -import Stack from '@mui/material/Stack'; -import TextField from '@mui/material/TextField'; -import Typography from '@mui/material/Typography'; -import { useTheme } from '@mui/material/styles'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import CloseIcon from '@mui/icons-material/Close'; -import ContentCopy from '@mui/icons-material/ContentCopy'; -import ErrorIcon from '@mui/icons-material/Error'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import { Visibility, VisibilityOff } from '@mui/icons-material'; -import { Link } from '@backstage/core-components'; -import { useSandboxContext } from '../../hooks/useSandboxContext'; -import { AnsibleStatus } from '../../utils/aap-utils'; -import { useTrackAnalytics } from '../../utils/eddl-utils'; -import { Intcmp } from '../../hooks/useProductURLs'; - -// Import the logos -import AnsibleLogo from '../../assets/logos/ansible.svg'; -import RedHatLogo from '../../assets/logos/logo_hat-only.svg'; - -type AnsibleLaunchInfoModalProps = { - modalOpen: boolean; - setOpen: React.Dispatch>; -}; - -export const AnsibleLaunchInfoModal: React.FC = ({ - modalOpen, - setOpen, -}) => { - const theme = useTheme(); - - const { - ansibleUILink, - ansibleUIUser, - ansibleUIPassword, - ansibleError, - ansibleStatus, - } = useSandboxContext(); - - const trackAnalytics = useTrackAnalytics(); - - // Handle CTA click for analytics - const handleAnsibleCtaClick = async () => { - if (ansibleUILink) { - await trackAnalytics( - 'Get Started - Ansible', - 'Catalog', - ansibleUILink, - Intcmp.AAP, - 'cta', - ); - } - }; - const [showPassword, setShowPassword] = useState(false); - - const handleClose = () => { - setOpen(false); - }; - - const handleTogglePassword = () => { - setShowPassword(prev => !prev); - }; - - return ( - - {ansibleStatus === AnsibleStatus.READY ? ( - <> - -
- -
- Ansible Automation Platform instance provisioned -
-
-
- - - - - - To get started with your AAP instance, you will need{' '} - - two different accounts - - : - - - {/* Section 1: AAP admin account */} - - - - 1. - - - - AAP admin account - - - - - Log in to your AAP admin account within the new tab. Use the - provided username and password to access your AAP instance. - - - - - - Username: - - - - - window.navigator.clipboard.writeText( - ansibleUIUser || '', - ) - } - > - - - - ), - sx: { - fontWeight: 450, - color: theme.palette.text.secondary, - height: '2rem', - width: '20rem', - paddingY: '4px', - '& .v5-MuiInputBase-input': { - paddingY: '10px', - }, - }, - }} - /> - - - - - Password: - - - - - - {showPassword ? ( - - ) : ( - - )} - - - window.navigator.clipboard.writeText( - ansibleUIPassword || '', - ) - } - > - - - - - ), - sx: { - fontWeight: 450, - color: theme.palette.text.secondary, - height: '2rem', - width: '20rem', - paddingY: '4px', - '& .v5-MuiInputBase-input': { - paddingY: '10px', - }, - }, - }} - /> - - - - - {/* Section 2: Red Hat account */} - - - - 2. - - - - Red Hat account - - - - - Once logged in, you'll need to activate your subscription. On - the activation form, select "username and password" and then - enter your Red Hat account credentials. - - - - - Access this information again by clicking the{' '} - Launch button on the Ansible Automation Platform - sandbox card. - - - {ansibleError && - !ansibleUIUser && - !ansibleUIPassword && - !ansibleUILink && ( - -
- - {ansibleError} -
-
- )} -
- - - - - - - ) : ( - <> - {' '} - -
-
- Provisioning Ansible Automation Platform (AAP) instance -
-
-
- - - {' '} - - - Provisioning can take up to 30 minutes. When ready, your instance - will remain active for several hours. - -
- - New to Ansible Automation Platform? Get up to speed with our{' '} - - introductory learning path. - - - -
-
- - - - - - You can close this modal. Follow the status of your instance - on the AAP sandbox card. - - -
-
- - - - - )} -
- ); -}; - -export default AnsibleLaunchInfoModal; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationModal.tsx b/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationModal.tsx deleted file mode 100644 index 6cfaca76cb..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationModal.tsx +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React, { useState } from 'react'; -import { E164Number } from 'libphonenumber-js/types.cjs'; -import { Country, getCountryCallingCode } from 'react-phone-number-input'; -import Dialog from '@mui/material/Dialog'; -import { useTheme } from '@mui/material/styles'; -import { useApi } from '@backstage/core-plugin-api'; -import { registerApiRef } from '../../api'; -import { - PhoneNumberStep, - VerificationCodeStep, -} from './PhoneVerificationSteps'; -import { errorMessage } from '../../utils/common'; -import { Product } from '../SandboxCatalog/productData'; - -type PhoneVerificationModalProps = { - id: Product; - modalOpen: boolean; - setOpen: React.Dispatch>; - setAnsibleCredsModalOpen: React.Dispatch>; - setRefetchingUserData: React.Dispatch>; -}; - -export const PhoneVerificationModal: React.FC = ({ - id, - modalOpen, - setOpen, - setAnsibleCredsModalOpen, - setRefetchingUserData, -}) => { - const theme = useTheme(); - const registerApi = useApi(registerApiRef); - const [enterOTP, setEnterOTP] = useState(false); - const [phoneNumber, setPhoneNumber] = useState(); - const [country, setCountry] = useState(undefined); - const [otp, setOtp] = useState(['', '', '', '', '', '']); - - const [loading, setLoading] = useState(false); - const [phoneSubmitError, setPhoneSubmitError] = React.useState< - string | undefined - >(); - - const handleClose = () => { - setCountry(undefined); - setPhoneNumber(undefined); - setOtp(['', '', '', '', '', '']); - setOpen(false); - setEnterOTP(false); - setPhoneSubmitError(undefined); - }; - - const handlePhoneNumberSubmit = async () => { - if (!country) return; - - const countryCallingCode = `+${getCountryCallingCode(country)}`; - const phoneWithoutCountryCode = (phoneNumber as string)?.replace( - countryCallingCode, - '', - ); - try { - setPhoneSubmitError(undefined); - setLoading(true); - await registerApi.initiatePhoneVerification( - countryCallingCode, - phoneWithoutCountryCode, - ); - - setEnterOTP(true); - } catch (e) { - setPhoneSubmitError(errorMessage(e)); - } finally { - setLoading(false); - } - }; - - const handleEditPhoneNumber = () => { - setEnterOTP(false); - setOtp(['', '', '', '', '', '']); - }; - - return ( - - {!enterOTP ? ( - - ) : ( - - )} - - ); -}; - -export default PhoneVerificationModal; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/PhoneNumberStep.tsx b/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/PhoneNumberStep.tsx deleted file mode 100644 index 1d809048a8..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/PhoneNumberStep.tsx +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React, { ForwardedRef, forwardRef } from 'react'; -import { E164Number } from 'libphonenumber-js/types.cjs'; -import { default as RPNInput, Country } from 'react-phone-number-input'; -import { useTheme } from '@mui/material/styles'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import ErrorIcon from '@mui/icons-material/Error'; -import Select, { SelectChangeEvent } from '@mui/material/Select'; -import MenuItem from '@mui/material/MenuItem'; -import TextField, { TextFieldProps } from '@mui/material/TextField'; -import Typography from '@mui/material/Typography'; -import DialogTitle from '@mui/material/DialogTitle'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogActions from '@mui/material/DialogActions'; -import IconButton from '@mui/material/IconButton'; -import CloseIcon from '@mui/icons-material/Close'; - -import CircularProgress from '@mui/material/CircularProgress'; -import { useTrackAnalytics } from '../../../utils/eddl-utils'; - -const FLAG_FETCH_URL = - 'https://catamphetamine.github.io/country-flag-icons/3x2'; - -type CountrySelectFieldProps = { - value: Country | undefined; - onChange: (value: Country) => void; - options: { label: string; value: Country }[]; -}; - -type PhoneNumberFormProps = { - phoneNumber: E164Number | undefined; - setPhoneNumber: React.Dispatch>; - setCountry: React.Dispatch>; - country: Country | undefined; - handleClose: () => void; - handlePhoneNumberSubmit: () => void; - loading?: boolean; - error?: string; -}; - -export const PhoneNumberStep: React.FC = ({ - phoneNumber, - setPhoneNumber, - setCountry, - country, - handleClose, - handlePhoneNumberSubmit, - loading, - error, -}) => { - const theme = useTheme(); - const trackAnalytics = useTrackAnalytics(); - - // Handle Send Code click for analytics tracking - const handleSendCodeClick = async ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - event.stopPropagation(); - - await trackAnalytics( - 'Send Code', - 'Verification', - window.location.href, - undefined, - 'cta', - ); - handlePhoneNumberSubmit(); - }; - - // Handle Cancel click for analytics tracking - const handleCancelClick = async ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - event.stopPropagation(); - - await trackAnalytics( - 'Cancel Verification', - 'Verification', - window.location.href, - undefined, - 'cta', - ); - handleClose(); - }; - - const PhoneInputField = forwardRef(function PhoneInputField( - props: TextFieldProps, - ref: ForwardedRef>, - ) { - return ( - - ); - }); - - const CountrySelectField = ({ - value, - onChange, - options, - }: CountrySelectFieldProps) => { - const handleSelect = (event: SelectChangeEvent) => { - onChange(event.target.value as Country); - setCountry(event.target.value as Country); - }; - return ( - - ); - }; - - return ( - <> - - Let's verify you - - - - - - - - Enter your phone number and we'll send you a text message with a - verification code. - -
- -
- {error && ( - -
- - {error} -
-
- )} -
- - - - - - ); -}; - -export default PhoneNumberStep; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/VerificationCodeStep.tsx b/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/VerificationCodeStep.tsx deleted file mode 100644 index 8b5f003111..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/VerificationCodeStep.tsx +++ /dev/null @@ -1,401 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React, { useEffect, useRef, useState } from 'react'; -import { E164Number, parsePhoneNumber } from 'libphonenumber-js/min'; -import { useTheme } from '@mui/material/styles'; -import Typography from '@mui/material/Typography'; -import Button from '@mui/material/Button'; -import DialogTitle from '@mui/material/DialogTitle'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogActions from '@mui/material/DialogActions'; -import TextField from '@mui/material/TextField'; -import Stack from '@mui/material/Stack'; -import ErrorIcon from '@mui/icons-material/Error'; -import IconButton from '@mui/material/IconButton'; -import EditIcon from '@mui/icons-material/Edit'; -import CloseIcon from '@mui/icons-material/Close'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import CircularProgress from '@mui/material/CircularProgress'; -import { isValidOTP } from '../../../utils/phone-utils'; -import { Product } from '../../SandboxCatalog/productData'; -import { signupDataToStatus } from '../../../utils/register-utils'; -import { productsURLMapping } from '../../../hooks/useProductURLs'; -import { errorMessage } from '../../../utils/common'; -import { useSandboxContext } from '../../../hooks/useSandboxContext'; -import { Country, getCountryCallingCode } from 'react-phone-number-input'; -import { useApi } from '@backstage/core-plugin-api'; -import { registerApiRef } from '../../../api'; -import { useTrackAnalytics } from '../../../utils/eddl-utils'; - -type VerificationCodeProps = { - id: Product; - otp: string[]; - setOtp: React.Dispatch>; - country: Country | undefined; - phoneNumber: E164Number | undefined; - handleEditPhoneNumber: () => void; - handleClose: () => void; - setAnsibleCredsModalOpen: React.Dispatch>; - setRefetchingUserData: React.Dispatch>; - setLoading: React.Dispatch>; - loading?: boolean; -}; - -export const VerificationCodeStep: React.FC = ({ - id, - otp, - setOtp, - country, - phoneNumber, - handleEditPhoneNumber, - setAnsibleCredsModalOpen, - setRefetchingUserData, - handleClose, - loading, - setLoading, -}) => { - const theme = useTheme(); - const trackAnalytics = useTrackAnalytics(); - - const inputRefs = useRef([]); - const { refetchUserData, handleAAPInstance } = useSandboxContext(); - const [verificationCodeError, setVerificationCodeError] = React.useState< - string | undefined - >(); - const [codeResent, setCodeResent] = useState(false); - const registerApi = useApi(registerApiRef); - - useEffect(() => { - // Focus on the first input box when modal opens - if (!otp[0]) { - // Focus on the first input box when modal opens - setTimeout(() => inputRefs.current[0]?.focus(), 100); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [window.open]); - - const handleChange = ( - index: number, - event: React.ChangeEvent, - ) => { - const value = event.target.value; - if (!isValidOTP(value)) return; - - const newOtp = [...otp]; - newOtp[index] = value; - setOtp(newOtp); - - // Move focus to next input - if (value && index < otp.length - 1) { - inputRefs.current[index + 1].focus(); - } - }; - - // Handle Backspace: Move focus to previous box if empty - const handleKeyDown = ( - index: number, - event: React.KeyboardEvent, - ) => { - if (event.key === 'Backspace' && !otp[index] && index > 0) { - inputRefs.current[index - 1].focus(); - } - }; - - const createPhoneString = (phone: E164Number | undefined) => { - if (!phone) return ''; - try { - const parsedPhoneNumber = parsePhoneNumber(phone); - return `+${parsedPhoneNumber.countryCallingCode} ${parsedPhoneNumber.nationalNumber}`; - } catch { - return ''; - } - }; - - const handleResendCode = async () => { - if (codeResent || !country) return; - - const countryCallingCode = `+${getCountryCallingCode(country)}`; - setOtp(['', '', '', '', '', '']); - try { - setVerificationCodeError(undefined); - setLoading(true); - await registerApi.initiatePhoneVerification( - countryCallingCode, - (phoneNumber as string)?.replace(countryCallingCode, ''), - ); - setCodeResent(true); - } catch (e) { - setVerificationCodeError(errorMessage(e)); - } finally { - setLoading(false); - } - }; - - const handleStartTrialClick = async (pdt: Product) => { - try { - setVerificationCodeError(undefined); - setLoading(true); - await registerApi.completePhoneVerification(otp.join('')); - const maxAttempts = 60; - const retryInterval = 1000; // 1 second - - // Poll until user is found or max attempts reached - let urlToOpen = ''; - let userReady = false; - for (let i = 0; i < maxAttempts; i++) { - setRefetchingUserData(true); - await new Promise(resolve => setTimeout(resolve, retryInterval)); - - // Fetch the latest user data and check if user is found - const userData = await refetchUserData(); - if (userData) { - const userStatus = signupDataToStatus(userData); - userReady = userStatus === 'ready'; - - const verificationRequired = userStatus === 'verify'; - // if verification is required we can stop fetching the data - if (verificationRequired) { - break; - } - - // if user is ready we can stop fetching the data - if (userReady) { - // if namespace is not defined we can continue fetching the data - if (!userData?.defaultUserNamespace) { - // eslint-disable-next-line - console.error( - 'user is ready but default namespace is not defined yet...', - ); - continue; - } - const productURLs = productsURLMapping(userData); - // find the link to open if any - urlToOpen = productURLs.find(pu => pu.id === id)?.url || ''; - // User has signed up and the trial is ready and user selects the AAP Trial - if (pdt === Product.AAP) { - handleAAPInstance(userData.defaultUserNamespace as string); - setAnsibleCredsModalOpen(true); - } else if (urlToOpen) { - window.open(urlToOpen, '_blank'); - } - break; - } - } - } - handleClose(); - } catch (error) { - setVerificationCodeError(errorMessage(error)); - } finally { - setLoading(false); - setRefetchingUserData(false); - } - }; - - // Handle Start Trial click for analytics tracking - const handleStartTrialClickWithTracking = async ( - event: React.MouseEvent, - pdt: Product, - ) => { - event.preventDefault(); - event.stopPropagation(); - - await trackAnalytics( - 'Start Trial', - 'Verification', - window.location.href, - undefined, - 'cta', - ); - handleStartTrialClick(pdt); - }; - - // Handle Resend Code click for analytics tracking - const handleResendCodeClickWithTracking = async ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - event.stopPropagation(); - - await trackAnalytics( - 'Resend Code', - 'Verification', - window.location.href, - undefined, - 'cta', - ); - handleResendCode(); - }; - - // Handle Cancel click for analytics tracking - const handleCancelVerificationClick = async ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - event.stopPropagation(); - - await trackAnalytics( - 'Cancel Verification', - 'Verification', - window.location.href, - undefined, - 'cta', - ); - handleClose(); - }; - - return ( - <> - - Enter the verification code - - - - - - - We've sent a verification code to {createPhoneString(phoneNumber)}.{' '} - - - - - - {otp.map((digit, index) => ( - handleChange(index, e)} - onKeyDown={e => - handleKeyDown(index, e as React.KeyboardEvent) - } - inputRef={el => (inputRefs.current[index] = el)} - variant="outlined" - inputProps={{ - maxLength: 1, - style: { textAlign: 'center', fontWeight: 400 }, - }} - /> - ))} - - { - handleResendCodeClickWithTracking(event); - inputRefs.current[0]?.focus(); - }} - > -
- {codeResent ? ( - <> - Resent successful - - - ) : ( - 'Resend code' - )} -
-
- - {verificationCodeError && ( - -
- - {verificationCodeError} -
-
- )} -
- - - - - - ); -}; - -export default VerificationCodeStep; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/__tests__/PhoneNumberStep.test.tsx b/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/__tests__/PhoneNumberStep.test.tsx deleted file mode 100644 index e8226e650e..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/__tests__/PhoneNumberStep.test.tsx +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { PhoneNumberStep } from '../PhoneNumberStep'; -import { E164Number } from 'libphonenumber-js/types'; -import { Country } from 'react-phone-number-input'; -import { parsePhoneNumber } from 'libphonenumber-js/min'; -import { PhoneNumber } from 'libphonenumber-js'; -import * as eddlUtils from '../../../../utils/eddl-utils'; - -// Mock the useTrackAnalytics hook -jest.mock('../../../../utils/eddl-utils', () => ({ - ...jest.requireActual('../../../../utils/eddl-utils'), - useTrackAnalytics: jest.fn(), -})); - -// Mock the props interface if it's not exported from the component file -declare module '../PhoneNumberStep' { - interface PhoneNumberFormProps { - phoneNumber: E164Number | undefined; - setPhoneNumber: React.Dispatch< - React.SetStateAction - >; - setCountry: React.Dispatch>; - country: Country | undefined; - handleClose: () => void; - handlePhoneNumberSubmit: () => void; - loading?: boolean; - error?: string; - } -} - -describe('PhoneNumberStep', () => { - const mockTrackAnalytics = jest.fn(); - const mockSetPhoneNumber = jest.fn(); - const mockHandleClose = jest.fn(); - const mockHandlePhoneNumberSubmit = jest.fn(); - const mockSetCountry = jest.fn(); - const phoneNumber = parsePhoneNumber(' 8 (800) 555-35-35 ', 'RU'); - - beforeEach(() => { - jest.clearAllMocks(); - // Mock the useTrackAnalytics hook to return a mock function - mockTrackAnalytics.mockResolvedValue(undefined); // Make it async - (eddlUtils.useTrackAnalytics as jest.Mock).mockReturnValue( - mockTrackAnalytics, - ); - }); - - function renderComponent(inputPhoneNumber: PhoneNumber, error?: string) { - render( - , - ); - } - - test('should render modal', () => { - renderComponent(phoneNumber); - // expected text should be there - expect(screen.getByText("Let's verify you")).toBeInTheDocument(); - expect( - screen.getByText( - "Enter your phone number and we'll send you a text message with a verification code.", - ), - ).toBeInTheDocument(); - // country code selection should be there - const phoneInputs = screen.getAllByTestId('tel-input'); - expect(phoneInputs).toHaveLength(1); - // phone number input should be there - const countryInputs = screen.getAllByTestId('country-code-select'); - expect(countryInputs).toHaveLength(1); - // submit phone button should be there - const submitButton = screen.getAllByTestId('submit-phone-button'); - expect(submitButton).toHaveLength(1); - // close button should be there - const closeButton = screen.getAllByTestId('close-phone-button'); - expect(closeButton).toHaveLength(1); - }); - - test('should submit phone number when clicking send code button', async () => { - renderComponent(phoneNumber); - // Find and click send code button - const submitPhoneNumberButton = screen.getByRole('button', { - name: /Send code/i, - }); - fireEvent.click(submitPhoneNumberButton); - - // Wait for the async tracking call to complete - await waitFor(() => { - expect(mockHandlePhoneNumberSubmit).toHaveBeenCalled(); - }); - }); - - test('should show an error when phone number is invalid', async () => { - const invalidPhoneNumber = parsePhoneNumber(' 8 (800) xxxx ', 'RU'); - renderComponent(invalidPhoneNumber, 'invalid phone number error'); - // Find and click send code - const submitPhoneNumberButton = screen.getByRole('button', { - name: /Send code/i, - }); - fireEvent.click(submitPhoneNumberButton); - - // Wait for the async tracking call to complete - await waitFor(() => { - expect(mockHandlePhoneNumberSubmit).toHaveBeenCalled(); - }); - - // expect mock error from backend to be displayed - expect(screen.getByText('invalid phone number error')).toBeInTheDocument(); - // submit button should be enabled so user can retry with new number - expect(screen.getByText(/Send code/i).closest('button')).toBeEnabled(); - }); - - test('closes the modal when the close button is clicked', async () => { - renderComponent(phoneNumber); - const closeButton = screen.getByRole('button', { name: /Cancel/i }); - fireEvent.click(closeButton); - - // Wait for the async tracking call to complete - await waitFor(() => { - expect(mockHandleClose).toHaveBeenCalled(); - }); - }); - - test('should not have a default country when no country is provided', () => { - render( - , - ); - - // The country select should be present but not have a default selected value - const countrySelect = screen.getByTestId('country-code-select'); - expect(countrySelect).toBeInTheDocument(); - - // The input value should be empty (no default country) - const selectInput = countrySelect.querySelector('input'); - expect(selectInput).toHaveValue(''); - - // No country calling code should be displayed when no country is selected - const phoneInput = screen.getByTestId('tel-input'); - expect( - phoneInput.querySelector('[data-testid="tel-input"] input'), - ).toHaveValue(''); - }); - - test('should not display duplicate country code in phone input field', () => { - const usPhoneNumber = parsePhoneNumber('+17373072270', 'US'); - render( - , - ); - - const phoneInput = screen.getByTestId('tel-input'); - const inputElement = phoneInput.querySelector('input'); - const inputValue = inputElement?.value || ''; - expect(inputValue).toBe('+1 737 307 2270'); - }); - - describe('Analytics tracking', () => { - test('calls trackAnalytics with correct parameters when Send Code is clicked', async () => { - renderComponent(phoneNumber); - const sendCodeButton = screen.getByRole('button', { name: /Send code/i }); - - const clickEvent = fireEvent.click(sendCodeButton); - - await waitFor(() => { - expect(mockTrackAnalytics).toHaveBeenCalledWith( - 'Send Code', - 'Verification', - window.location.href, - undefined, - 'cta', - ); - }); - expect(mockHandlePhoneNumberSubmit).toHaveBeenCalled(); - expect(clickEvent).toBe(false); // Event was prevented - }); - - test('calls trackAnalytics with correct parameters when Cancel is clicked', async () => { - renderComponent(phoneNumber); - const cancelButton = screen.getByRole('button', { name: /Cancel/i }); - - const clickEvent = fireEvent.click(cancelButton); - - await waitFor(() => { - expect(mockTrackAnalytics).toHaveBeenCalledWith( - 'Cancel Verification', - 'Verification', - window.location.href, - undefined, - 'cta', - ); - }); - expect(mockHandleClose).toHaveBeenCalled(); - expect(clickEvent).toBe(false); // Event was prevented - }); - }); -}); diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/__tests__/VerificationCodeStep.test.tsx b/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/__tests__/VerificationCodeStep.test.tsx deleted file mode 100644 index 9f3f746465..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/__tests__/VerificationCodeStep.test.tsx +++ /dev/null @@ -1,455 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { E164Number } from 'libphonenumber-js/types'; -import { parsePhoneNumber } from 'libphonenumber-js/min'; -import VerificationCodeStep from '../VerificationCodeStep'; -import { Product } from '../../../SandboxCatalog/productData'; -import { Country } from 'react-phone-number-input'; -import { useSandboxContext } from '../../../../hooks/useSandboxContext'; -import { act } from 'react-dom/test-utils'; -import * as eddlUtils from '../../../../utils/eddl-utils'; - -jest.mock('@backstage/core-plugin-api', () => ({ - ...jest.requireActual('@backstage/core-plugin-api'), - useApi: jest.fn().mockReturnValue({ - completePhoneVerification: jest.fn(), - }), -})); - -jest.useFakeTimers(); // control timers -// mock hooks -jest.mock('../../../../hooks/useSandboxContext'); -jest.mock('../../../../utils/eddl-utils', () => ({ - ...jest.requireActual('../../../../utils/eddl-utils'), - useTrackAnalytics: jest.fn(), -})); - -// Mock the props interface if it's not exported from the component file -declare module '../VerificationCodeStep' { - interface VerificationCodeProps { - id: Product; - otp: string[]; - setOtp: React.Dispatch>; - country: Country | undefined; - phoneNumber: E164Number | undefined; - handleEditPhoneNumber: () => void; - handleClose: () => void; - setAnsibleCredsModalOpen: React.Dispatch>; - setLoading: React.Dispatch>; - loading?: boolean; - } -} - -describe('VerificationCodeStep', () => { - const mockTrackAnalytics = jest.fn(); - const mockSetOpt = jest.fn(); - const mockHandleClose = jest.fn(); - const mockHandleEditPhoneNumber = jest.fn(); - const mockSetLoading = jest.fn(); - const mockSetAnsibleCredsModalOpen = jest.fn(); - const mockRefetchingUserData = jest.fn(); - const mockRefetchAAP = jest.fn(); - const mockOpen = jest.fn(); - const otpCode: string[] = ['1', '2', '3', '4', '5']; - const phoneNumber = parsePhoneNumber('8 (800) 555-35-35', 'RU'); - const mockCountry: Country = 'RU'; - - const defaultUserData = { - name: 'bob', - consoleURL: 'https://sandboxcluster.test/', - cheDashboardURL: 'https://devspaces.test/', - proxyURL: 'https://api-sandboxcluster.test', - rhodsMemberURL: 'https://rhods-dashboard.test/', - apiEndpoint: 'https://api.test:6443', - clusterName: 'sandboxcluster.test', - defaultUserNamespace: 'bob-2-dev', - compliantUsername: 'bob-2', - username: 'bob', - status: { - ready: true, - reason: 'Provisioned', - verificationRequired: false, - }, - } as any; - - const defaultProps = { - id: Product.OPENSHIFT_CONSOLE, - otp: otpCode, - setOtp: mockSetOpt, - phoneNumber: phoneNumber.number, - handleEditPhoneNumber: mockHandleEditPhoneNumber, - handleClose: mockHandleClose, - setLoading: mockSetLoading, - setAnsibleCredsModalOpen: mockSetAnsibleCredsModalOpen, - setRefetchingUserData: mockRefetchingUserData, - loading: false, - country: mockCountry, - }; - - beforeEach(() => { - jest.clearAllMocks(); - mockTrackAnalytics.mockResolvedValue(undefined); - (eddlUtils.useTrackAnalytics as jest.Mock).mockReturnValue( - mockTrackAnalytics, - ); - }); - - function renderComponent(props = {}) { - render(); - } - - test('should render modal', () => { - const mockRefetchUserData = jest.fn(); - const mockHandleAAPInstance = jest.fn(); - const mockUseSandboxContext = useSandboxContext as jest.MockedFunction< - typeof useSandboxContext - >; - mockUseSandboxContext.mockReturnValue({ - refetchUserData: mockRefetchUserData, - refetchAAP: mockRefetchAAP, - handleAAPInstance: mockHandleAAPInstance, - } as any); - - renderComponent(); - // expected text should be there - expect(screen.getByText('Enter the verification code')).toBeInTheDocument(); - const sentMessageDialog = screen.getAllByTestId('sent-message-dialog'); - expect(sentMessageDialog[0]).toHaveTextContent( - "We've sent a verification code to +7 8005553535.", - ); - // submit phone button should be there - const submitButton = screen.getAllByTestId('submit-opt-button'); - expect(submitButton).toHaveLength(1); - // opt inputs are there - const optInputFields = screen.getAllByTestId('opt-inputs'); - expect(optInputFields).toHaveLength(5); - // resend opt should be there - const resendOptLink = screen.getAllByTestId('resend-code-link'); - expect(resendOptLink).toHaveLength(1); - // close button should be there - const closeButton = screen.getAllByTestId('close-opt-button'); - expect(closeButton).toHaveLength(2); - }); - - test('should submit opt code when clicking start trial and open selected url', async () => { - window.open = mockOpen; // override window.open with mock - const mockRefetchUserData = jest.fn().mockReturnValue(defaultUserData); - const mockHandleAAPInstance = jest.fn(); - const mockUseSandboxContext = useSandboxContext as jest.MockedFunction< - typeof useSandboxContext - >; - mockUseSandboxContext.mockReturnValue({ - refetchUserData: mockRefetchUserData, - refetchAAP: mockRefetchAAP, - handleAAPInstance: mockHandleAAPInstance, - } as any); - - renderComponent(); - const submitButton = screen.getByRole('button', { name: /Start trial/i }); - fireEvent.click(submitButton); - - // advance timers to trigger all retries - for (let i = 0; i < 5; i++) { - jest.advanceTimersByTime(1000); - await Promise.resolve(); // allow awaiting the timer to flush - } - - expect(mockRefetchUserData).toHaveBeenCalled(); - expect(mockOpen).toHaveBeenCalledWith( - `https://sandboxcluster.test//k8s/cluster/projects/bob-2-dev`, - '_blank', - ); // check it opens the url after signup - mockOpen.mockRestore(); - }); - - test('should submit opt code when clicking start trial and provision AAP', async () => { - const mockRefetchUserData = jest.fn().mockReturnValue(defaultUserData); - const mockHandleAAPInstance = jest.fn(); - const mockUseSandboxContext = useSandboxContext as jest.MockedFunction< - typeof useSandboxContext - >; - mockUseSandboxContext.mockReturnValue({ - refetchUserData: mockRefetchUserData, - refetchAAP: mockRefetchAAP, - handleAAPInstance: mockHandleAAPInstance, - } as any); - - renderComponent({ id: Product.AAP }); - const submitButton = screen.getByRole('button', { name: /Start trial/i }); - fireEvent.click(submitButton); - - // advance timers to trigger all retries - for (let i = 0; i < 5; i++) { - jest.advanceTimersByTime(1000); - await Promise.resolve(); // allow awaiting the timer to flush - } - - expect(mockRefetchUserData).toHaveBeenCalled(); - expect(mockHandleAAPInstance).toHaveBeenCalledWith('bob-2-dev'); // check it calls the aap specific functionality - expect(mockHandleClose).toHaveBeenCalled(); // it closes the modal after - }); - - test('closes the modal when the close button is clicked', async () => { - const mockRefetchUserData = jest.fn(); - const mockHandleAAPInstance = jest.fn(); - const mockUseSandboxContext = useSandboxContext as jest.MockedFunction< - typeof useSandboxContext - >; - mockUseSandboxContext.mockReturnValue({ - refetchUserData: mockRefetchUserData, - refetchAAP: mockRefetchAAP, - handleAAPInstance: mockHandleAAPInstance, - } as any); - - renderComponent(); - const closeButton = screen.getByRole('button', { name: /Cancel/i }); - fireEvent.click(closeButton); - - await waitFor(() => { - expect(mockHandleClose).toHaveBeenCalled(); - }); - }); - - test('does not open URL when defaultUserNamespace is undefined', async () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); - window.open = mockOpen; // override window.open with mock - const mockHandleAAPInstance = jest.fn(); - const mockRefetchUserData = jest.fn().mockReturnValue({ - ...defaultUserData, - defaultUserNamespace: undefined, - }); - const mockUseSandboxContext = useSandboxContext as jest.MockedFunction< - typeof useSandboxContext - >; - mockUseSandboxContext.mockReturnValue({ - refetchUserData: mockRefetchUserData, - refetchAAP: mockRefetchAAP, - handleAAPInstance: mockHandleAAPInstance, - } as any); - - renderComponent(); - const submitButton = screen.getByRole('button', { name: /Start trial/i }); - await act(async () => { - fireEvent.click(submitButton); - // advance timers to trigger all retries - for (let i = 0; i < 5; i++) { - jest.advanceTimersByTime(1000); - await Promise.resolve(); // allow awaiting the timer to flush - } - }); - - expect(mockRefetchUserData).toHaveBeenCalled(); - await waitFor( - () => { - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'user is ready but default namespace is not defined yet...', - ); - }, - { timeout: 6000 }, - ); - consoleErrorSpy.mockRestore(); - expect(mockOpen).not.toHaveBeenCalled(); // verify window.open is not called - mockOpen.mockRestore(); - }); - - test('does not invoke handleAAP when defaultUserNamespace is undefined', async () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); - const mockHandleAAPInstance = jest.fn(); - const mockRefetchUserData = jest.fn().mockReturnValue({ - ...defaultUserData, - defaultUserNamespace: undefined, - }); - const mockUseSandboxContext = useSandboxContext as jest.MockedFunction< - typeof useSandboxContext - >; - mockUseSandboxContext.mockReturnValue({ - refetchUserData: mockRefetchUserData, - refetchAAP: mockRefetchAAP, - handleAAPInstance: mockHandleAAPInstance, - } as any); - - renderComponent({ id: Product.AAP }); - const submitButton = screen.getByRole('button', { name: /Start trial/i }); - await act(async () => { - fireEvent.click(submitButton); - // advance timers to trigger all retries - for (let i = 0; i < 5; i++) { - jest.advanceTimersByTime(1000); - await Promise.resolve(); // allow awaiting the timer to flush - } - }); - - expect(mockRefetchUserData).toHaveBeenCalled(); - expect(mockHandleAAPInstance).not.toHaveBeenCalled(); // verify handleAAP is not called - await waitFor( - () => { - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'user is ready but default namespace is not defined yet...', - ); - }, - { timeout: 6000 }, - ); - consoleErrorSpy.mockRestore(); - }); - - test('does not open URL when verification is required', async () => { - window.open = mockOpen; // override window.open with mock - const mockHandleAAPInstance = jest.fn(); - const mockRefetchUserData = jest.fn().mockReturnValue({ - ...defaultUserData, - status: { - ready: false, - reason: 'VerificationRequired', - verificationRequired: true, - }, - }); - const mockUseSandboxContext = useSandboxContext as jest.MockedFunction< - typeof useSandboxContext - >; - mockUseSandboxContext.mockReturnValue({ - refetchUserData: mockRefetchUserData, - refetchAAP: mockRefetchAAP, - handleAAPInstance: mockHandleAAPInstance, - } as any); - - renderComponent(); - const submitButton = screen.getByRole('button', { name: /Start trial/i }); - await act(async () => { - fireEvent.click(submitButton); - // advance timers to trigger all retries - for (let i = 0; i < 5; i++) { - jest.advanceTimersByTime(1000); - await Promise.resolve(); // allow awaiting the timer to flush - } - }); - - expect(mockRefetchUserData).toHaveBeenCalled(); - expect(mockOpen).not.toHaveBeenCalled(); // verify window.open is not called - mockOpen.mockRestore(); - }); - - test('does not invoke handleAAP when verification is required', async () => { - const mockHandleAAPInstance = jest.fn(); - const mockRefetchUserData = jest.fn().mockReturnValue({ - ...defaultUserData, - status: { - ready: false, - reason: 'VerificationRequired', - verificationRequired: true, - }, - }); - const mockUseSandboxContext = useSandboxContext as jest.MockedFunction< - typeof useSandboxContext - >; - mockUseSandboxContext.mockReturnValue({ - refetchUserData: mockRefetchUserData, - refetchAAP: mockRefetchAAP, - handleAAPInstance: mockHandleAAPInstance, - } as any); - - renderComponent({ id: Product.AAP }); - const submitButton = screen.getByRole('button', { name: /Start trial/i }); - await act(async () => { - fireEvent.click(submitButton); - // advance timers to trigger all retries - for (let i = 0; i < 5; i++) { - jest.advanceTimersByTime(1000); - await Promise.resolve(); // allow awaiting the timer to flush - } - }); - - expect(mockRefetchUserData).toHaveBeenCalled(); - expect(mockHandleAAPInstance).not.toHaveBeenCalled(); // verify handleAAP is not called - }); - - describe('Analytics tracking', () => { - let mockRefetchUserData: jest.Mock; - let mockHandleAAPInstance: jest.Mock; - - beforeEach(() => { - mockRefetchUserData = jest.fn(); - mockHandleAAPInstance = jest.fn(); - const mockUseSandboxContext = useSandboxContext as jest.MockedFunction< - typeof useSandboxContext - >; - mockUseSandboxContext.mockReturnValue({ - refetchUserData: mockRefetchUserData, - refetchAAP: mockRefetchAAP, - handleAAPInstance: mockHandleAAPInstance, - } as any); - }); - - test('calls trackAnalytics with correct parameters when Start Trial is clicked', async () => { - renderComponent(); - - const startTrialButton = screen.getByRole('button', { - name: /Start trial/i, - }); - const clickEvent = fireEvent.click(startTrialButton); - - await waitFor(() => { - expect(mockTrackAnalytics).toHaveBeenCalledWith( - 'Start Trial', - 'Verification', - window.location.href, - undefined, - 'cta', - ); - }); - expect(clickEvent).toBe(false); // Event was prevented - }); - - test('calls trackAnalytics with correct parameters when Resend Code is clicked', async () => { - renderComponent(); - - const resendCodeLink = screen.getByTestId('resend-code-link'); - const clickEvent = fireEvent.click(resendCodeLink); - - await waitFor(() => { - expect(mockTrackAnalytics).toHaveBeenCalledWith( - 'Resend Code', - 'Verification', - window.location.href, - undefined, - 'cta', - ); - }); - expect(clickEvent).toBe(false); // Event was prevented - }); - - test('calls trackAnalytics with correct parameters when Cancel is clicked', async () => { - renderComponent(); - - const cancelButton = screen.getByRole('button', { name: /Cancel/i }); - const clickEvent = fireEvent.click(cancelButton); - - await waitFor(() => { - expect(mockTrackAnalytics).toHaveBeenCalledWith( - 'Cancel Verification', - 'Verification', - window.location.href, - undefined, - 'cta', - ); - }); - expect(mockHandleClose).toHaveBeenCalled(); - expect(clickEvent).toBe(false); // Event was prevented - }); - }); -}); diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/index.ts b/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/index.ts deleted file mode 100644 index 7ebdb91b2d..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/PhoneVerificationSteps/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { default as PhoneNumberStep } from './PhoneNumberStep'; -export { default as VerificationCodeStep } from './VerificationCodeStep'; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/AccessCodeInputModal.test.tsx b/workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/AccessCodeInputModal.test.tsx deleted file mode 100644 index ea7cc10bea..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/AccessCodeInputModal.test.tsx +++ /dev/null @@ -1,363 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { ThemeProvider } from '@mui/material/styles'; -import { createTheme } from '@mui/material/styles'; -import { TestApiProvider } from '@backstage/test-utils'; -import { AccessCodeInputModal } from '../AccessCodeInputModal'; -import { registerApiRef } from '../../../api'; -import { useSandboxContext } from '../../../hooks/useSandboxContext'; -import { AnsibleStatus } from '../../../utils/aap-utils'; - -const mockRegisterApi = { - verifyActivationCode: jest.fn(), -}; - -const mockRefetchUserData = jest.fn(); - -const theme = createTheme(); - -const renderComponent = (modalOpen: boolean = true) => { - const mockSetOpen = jest.fn(); - - return { - ...render( - - - - - , - ), - mockSetOpen, - }; -}; - -jest.mock('../../../hooks/useSandboxContext'); - -describe('AccessCodeInputModal', () => { - const mockUseSandboxContext = useSandboxContext as jest.MockedFunction< - typeof useSandboxContext - >; - - beforeEach(() => { - jest.clearAllMocks(); - mockUseSandboxContext.mockReturnValue({ - refetchUserData: mockRefetchUserData, - loading: false, - userFound: false, - userReady: false, - ansibleStatus: AnsibleStatus.UNKNOWN, - verificationRequired: false, - userData: undefined, - signupUser: jest.fn(), - refetchAAP: jest.fn(), - ansibleData: undefined, - ansibleUIUser: undefined, - } as any); - }); - - it('renders modal when open', () => { - renderComponent(true); - - expect(screen.getByText('Enter the activation code')).toBeInTheDocument(); - expect( - screen.getByText('If you have an activation code, enter it now.'), - ).toBeInTheDocument(); - }); - - it('does not render modal when closed', () => { - renderComponent(false); - - expect( - screen.queryByText('Enter the activation code'), - ).not.toBeInTheDocument(); - }); - - it('renders 5 input fields', () => { - renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - expect(inputs).toHaveLength(5); - }); - - it('allows uppercase letters in input fields', async () => { - const user = userEvent.setup(); - renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - - await user.type(inputs[0], 'A'); - expect(inputs[0]).toHaveValue('A'); - }); - - it('allows lowercase letters in input fields', async () => { - const user = userEvent.setup(); - renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - - await user.type(inputs[0], 'a'); - expect(inputs[0]).toHaveValue('a'); - }); - - it('allows numbers in input fields', async () => { - const user = userEvent.setup(); - renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - - await user.type(inputs[0], '1'); - expect(inputs[0]).toHaveValue('1'); - }); - - it('rejects special characters', async () => { - const user = userEvent.setup(); - renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - - await user.type(inputs[0], '@'); - expect(inputs[0]).toHaveValue(''); - - await user.type(inputs[0], '#'); - expect(inputs[0]).toHaveValue(''); - - await user.type(inputs[0], ' '); - expect(inputs[0]).toHaveValue(''); - }); - - it('moves focus to next input after entering valid character', async () => { - const user = userEvent.setup(); - renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - - // Click on first input to focus it - await user.click(inputs[0]); - expect(inputs[0]).toHaveFocus(); - - // Type a character - await user.keyboard('A'); - - // Check that value was set and focus moved - expect(inputs[0]).toHaveValue('A'); - await waitFor(() => { - expect(inputs[1]).toHaveFocus(); - }); - }); - - it('moves focus to previous input on backspace when current input is empty', async () => { - const user = userEvent.setup(); - renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - - // Type in first input - await user.type(inputs[0], 'A'); - - // Second input should be focused, press backspace - await user.keyboard('{Backspace}'); - - // First input should now be focused - expect(inputs[0]).toHaveFocus(); - }); - - it('limits input to one character per field', async () => { - const user = userEvent.setup(); - renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - - await user.type(inputs[0], 'ABC'); - expect(inputs[0]).toHaveValue('A'); - }); - - it('disables Start trial button when not all fields are filled', () => { - renderComponent(); - - const startTrialButton = screen.getByText('Start trial'); - expect(startTrialButton).toBeDisabled(); - }); - - it('enables Start trial button when all fields are filled', async () => { - const user = userEvent.setup(); - renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - - // Fill all inputs by typing once in the first input - auto-focus will handle the rest - await user.click(inputs[0]); - await user.type(inputs[0], 'ABCDE'); // Type 5 characters to fill all 5 inputs - - // Wait for all inputs to be filled - await waitFor(() => { - inputs.forEach((input, index) => { - expect(input).toHaveValue(String.fromCharCode(65 + index)); // A, B, C, D, E - }); - }); - - // Wait for the button to become enabled with longer timeout - await waitFor( - () => { - const startTrialButton = screen.getByText('Start trial'); - expect(startTrialButton).not.toBeDisabled(); - }, - { timeout: 5000 }, - ); - }); - - it('calls verifyActivationCode with correct code when Start trial is clicked', async () => { - const user = userEvent.setup(); - mockRegisterApi.verifyActivationCode.mockResolvedValue(undefined); - - renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - const testCode = 'ABCDE'; - - // Fill all inputs by typing the code in the first input - auto-focus will distribute - await user.click(inputs[0]); - await user.type(inputs[0], testCode); - - // Wait for all inputs to be filled - await waitFor(() => { - inputs.forEach((input, index) => { - expect(input).toHaveValue(testCode[index]); - }); - }); - - // Wait for button to be enabled before clicking with longer timeout - const startTrialButton = await waitFor( - () => { - const button = screen.getByText('Start trial'); - expect(button).not.toBeDisabled(); - return button; - }, - { timeout: 5000 }, - ); - - await user.click(startTrialButton); - - await waitFor(() => { - expect(mockRegisterApi.verifyActivationCode).toHaveBeenCalledWith( - 'ABCDE', - ); - }); - }); - - it('displays error message when verification fails', async () => { - const user = userEvent.setup(); - const errorMessage = 'Invalid activation code'; - mockRegisterApi.verifyActivationCode.mockRejectedValue( - new Error(errorMessage), - ); - - renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - - // Fill all inputs by typing the code in the first input - auto-focus will distribute - await user.click(inputs[0]); - await user.type(inputs[0], 'AAAAA'); - - // Wait for all inputs to be filled - await waitFor(() => { - inputs.forEach(input => { - expect(input).toHaveValue('A'); - }); - }); - - // Wait for button to be enabled before clicking with longer timeout - const startTrialButton = await waitFor( - () => { - const button = screen.getByText('Start trial'); - expect(button).not.toBeDisabled(); - return button; - }, - { timeout: 5000 }, - ); - - await user.click(startTrialButton); - - await waitFor(() => { - expect(screen.getByText(errorMessage)).toBeInTheDocument(); - }); - }); - - it('closes modal and resets state when close button is clicked', async () => { - const user = userEvent.setup(); - const { mockSetOpen } = renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - await user.type(inputs[0], 'A'); - - const closeButton = screen.getByLabelText('close'); - await user.click(closeButton); - - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - - it('closes modal and resets state when cancel button is clicked', async () => { - const user = userEvent.setup(); - const { mockSetOpen } = renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - await user.type(inputs[0], 'A'); - - const cancelButton = screen.getByText('Cancel'); - await user.click(cancelButton); - - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - - it('calls refetchUserData after successful verification', async () => { - const user = userEvent.setup(); - mockRegisterApi.verifyActivationCode.mockResolvedValue(undefined); - - renderComponent(); - - const inputs = screen.getAllByRole('textbox'); - - // Fill all inputs by typing the code in the first input - auto-focus will distribute - await user.click(inputs[0]); - await user.type(inputs[0], 'AAAAA'); - - // Wait for all inputs to be filled - await waitFor(() => { - inputs.forEach(input => { - expect(input).toHaveValue('A'); - }); - }); - - // Wait for button to be enabled with a longer timeout - const startTrialButton = await waitFor( - () => { - const button = screen.getByText('Start trial'); - expect(button).not.toBeDisabled(); - return button; - }, - { timeout: 5000 }, - ); - - await user.click(startTrialButton); - - await waitFor(() => { - expect(mockRefetchUserData).toHaveBeenCalled(); - }); - }); -}); diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/AnsibleDeleteInstanceModal.test.tsx b/workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/AnsibleDeleteInstanceModal.test.tsx deleted file mode 100644 index d8f1719fb6..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/AnsibleDeleteInstanceModal.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { ThemeProvider, createTheme } from '@mui/material/styles'; -import AnsibleDeleteInstanceModal from '../AnsibleDeleteInstanceModal'; - -describe('AnsibleDeleteInstanceModal', () => { - const mockSetOpen = jest.fn(); - const mockHandleAnsibleDeleteInstance = jest.fn(); - const theme = createTheme(); - - const renderComponent = (props = {}) => { - return render( - - - , - ); - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('renders the modal with the correct title and warning', () => { - renderComponent(); - - expect(screen.getByText('Delete instance?')).toBeInTheDocument(); - expect( - screen.getByText( - 'Your AAP instance will be deleted. Consider backing up your work before continuing.', - ), - ).toBeInTheDocument(); - }); - - test('closes the modal when the close button is clicked', () => { - renderComponent(); - - const closeButton = screen.getByRole('button', { name: /close/i }); - fireEvent.click(closeButton); - - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - - test('calls setOpen(false) when the cancel button is clicked', () => { - renderComponent(); - - const cancelButton = screen.getByRole('button', { name: /cancel/i }); - fireEvent.click(cancelButton); - - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - - test('calls handleAnsibleDeleteInstance when the delete button is clicked', () => { - renderComponent(); - - const deleteButton = screen.getByRole('button', { - name: /delete instance/i, - }); - fireEvent.click(deleteButton); - - expect(mockHandleAnsibleDeleteInstance).toHaveBeenCalledTimes(1); - }); - - test('displays warning icon', () => { - renderComponent(); - - const warningIcon = document.querySelector( - 'svg[data-testid="WarningIcon"]', - ); - expect(warningIcon).toBeInTheDocument(); - }); - - test('does not render when modalOpen is false', () => { - renderComponent({ modalOpen: false }); - - expect(screen.queryByText('Delete instance?')).not.toBeInTheDocument(); - }); -}); diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/AnsibleLaunchInfoModal.test.tsx b/workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/AnsibleLaunchInfoModal.test.tsx deleted file mode 100644 index 1677b33be2..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/AnsibleLaunchInfoModal.test.tsx +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { ThemeProvider, createTheme } from '@mui/material/styles'; -import { AnsibleLaunchInfoModal } from '../AnsibleLaunchInfoModal'; -import { useSandboxContext } from '../../../hooks/useSandboxContext'; -import { AnsibleStatus } from '../../../utils/aap-utils'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { Intcmp } from '../../../hooks/useProductURLs'; -import * as eddlUtils from '../../../utils/eddl-utils'; - -// Mock the useSandboxContext hook -jest.mock('../../../hooks/useSandboxContext'); - -// Mock the EDDL utils -jest.mock('../../../utils/eddl-utils'); - -// Mock clipboard API -Object.defineProperty(window.navigator, 'clipboard', { - value: { - writeText: jest.fn(), - }, -}); - -describe('AnsibleLaunchInfoModal', () => { - const theme = createTheme(); - const mockSetOpen = jest.fn(); - - const defaultProps = { - modalOpen: true, - setOpen: mockSetOpen, - }; - - const mockUseSandboxContext = useSandboxContext as jest.MockedFunction< - typeof useSandboxContext - >; - const mockUseTrackAnalytics = jest.spyOn(eddlUtils, 'useTrackAnalytics'); - - const mockTrackAnalytics = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - // Setup window.appEventData for tests - (global as any).window = { appEventData: [] }; - - // Mock useTrackAnalytics to return our mock function - mockUseTrackAnalytics.mockReturnValue(mockTrackAnalytics); - }); - - const renderModal = (contextOverrides = {}) => { - mockUseSandboxContext.mockReturnValue({ - ansibleUILink: 'https://ansible.example.com', - ansibleUIUser: 'testuser', - ansibleUIPassword: 'password123', - ansibleError: null, - ansibleStatus: AnsibleStatus.READY, - ...contextOverrides, - } as any); - - return render( - wrapInTestApp( - - - , - ), - ); - }; - - it('renders the ready state with correct content', () => { - renderModal(); - - // Check for header - expect( - screen.getByText(/Ansible Automation Platform instance provisioned/i), - ).toBeInTheDocument(); - - // Check for the introductory text with "two different accounts" - expect( - screen.getByText(/To get started with your AAP instance, you will need/i), - ).toBeInTheDocument(); - expect(screen.getByText(/two different accounts/i)).toBeInTheDocument(); - - // Check for numbered sections - expect(screen.getByText('1.')).toBeInTheDocument(); - expect(screen.getByText('2.')).toBeInTheDocument(); - - // Check for section titles - expect(screen.getByText('AAP admin account')).toBeInTheDocument(); - expect(screen.getByText('Red Hat account')).toBeInTheDocument(); - - // Check for credentials section - const usernameLabel = screen.getByText('Username:'); - expect(usernameLabel).toBeInTheDocument(); - - const passwordLabel = screen.getByText('Password:'); - expect(passwordLabel).toBeInTheDocument(); - - // Check for the new "Get started" button - expect( - screen.getByRole('button', { - name: /Get started/i, - }), - ).toBeInTheDocument(); - - // Check for logos (by alt text) - expect(screen.getByAltText('Ansible')).toBeInTheDocument(); - expect(screen.getByAltText('Red Hat')).toBeInTheDocument(); - }); - - it('renders the provisioning state with correct content', async () => { - renderModal({ ansibleStatus: AnsibleStatus.PROVISIONING }); - - // Check for provisioning title - expect( - screen.getByText(/Provisioning Ansible Automation Platform/i), - ).toBeInTheDocument(); - - // Check for provisioning message - expect( - screen.getByText(/Provisioning can take up to 30 minutes/i), - ).toBeInTheDocument(); - - // Check for modal close hint - expect(screen.getByText(/You can close this modal/i)).toBeInTheDocument(); - - // Check for the loading spinner - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - - // Check for the info alert - expect(screen.getByRole('alert')).toHaveTextContent( - /You can close this modal. Follow the status of your instance on the AAP sandbox card./i, - ); - }); - - it('calls setOpen when the X button is clicked', () => { - renderModal(); - - const closeIcon = screen.getByLabelText('close'); - fireEvent.click(closeIcon); - - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); - - it('toggles password visibility when visibility button is clicked', () => { - renderModal(); - - // Find all password input fields - const inputs = document.querySelectorAll('input[type="password"]'); - expect(inputs.length).toBeGreaterThan(0); // Ensure we have at least one password field - - const passwordField = inputs[0] as HTMLInputElement; - - // Find the visibility toggle button - should have Visibility or VisibilityOff icon - const visibilityButtons = screen - .getAllByRole('button') - .filter(button => button.innerHTML.includes('Visibility')); - - expect(visibilityButtons.length).toBeGreaterThan(0); - const toggleButton = visibilityButtons[0]; - - // Initial state (should be password) - expect(passwordField.type).toBe('password'); - - // Click to show - fireEvent.click(toggleButton); - expect(passwordField.type).toBe('text'); - - // Click to hide - fireEvent.click(toggleButton); - expect(passwordField.type).toBe('password'); - }); - - it('copies username to clipboard when copy button is clicked', () => { - renderModal(); - - // Get all the copy buttons in the document by looking for ContentCopy icon - const copyButtons = screen - .getAllByRole('button') - .filter(button => button.innerHTML.includes('ContentCopy')); - - // First copy button should be for username - fireEvent.click(copyButtons[0]); - expect(window.navigator.clipboard.writeText).toHaveBeenCalledWith( - 'testuser', - ); - }); - - it('copies password to clipboard when copy button is clicked', () => { - renderModal(); - - // Get all the copy buttons in the document by looking for ContentCopy icon - const copyButtons = screen - .getAllByRole('button') - .filter(button => button.innerHTML.includes('ContentCopy')); - - // Second copy button should be for password - fireEvent.click(copyButtons[1]); - expect(window.navigator.clipboard.writeText).toHaveBeenCalledWith( - 'password123', - ); - }); - - it('shows error message when ansibleError is provided', () => { - const errorMessage = 'Failed to provision AAP instance'; - renderModal({ - ansibleStatus: AnsibleStatus.READY, - ansibleError: errorMessage, - ansibleUIUser: undefined, - ansibleUIPassword: '', - ansibleUILink: undefined, - }); - - // Look for error icon and message - const errorIcon = document.querySelector('[data-testid="ErrorIcon"]'); - expect(errorIcon).toBeInTheDocument(); - expect(screen.getByText(errorMessage)).toBeInTheDocument(); - }); - - it('contains section descriptions for both accounts', () => { - renderModal(); - - // Check for AAP admin account description - expect( - screen.getByText(/Log in to your AAP admin account within the new tab/i), - ).toBeInTheDocument(); - - // Check for Red Hat account description - expect( - screen.getByText( - /Once logged in, you'll need to activate your subscription/i, - ), - ).toBeInTheDocument(); - }); - - it('has the correct link target for the Get started button', () => { - renderModal(); - - const getStartedButton = screen.getByRole('button', { - name: /Get started/i, - }); - - // The button should be wrapped in a Link component that targets the ansible URL - const linkElement = getStartedButton.closest('a'); - expect(linkElement).toHaveAttribute('href', `https://ansible.example.com`); - expect(linkElement).toHaveAttribute('target', '_blank'); - }); - - it('displays the launch button instruction text', () => { - renderModal(); - - expect( - screen.getByText(/Access this information again by clicking the/i), - ).toBeInTheDocument(); - expect(screen.getByText(/Launch/i)).toBeInTheDocument(); - expect( - screen.getByText( - /button on the Ansible Automation Platform sandbox card/i, - ), - ).toBeInTheDocument(); - }); - - describe('CTA Event Pushing', () => { - it('should disable automatic tracking and push CTA event when Get started link is clicked', () => { - renderModal(); - - const getStartedButton = screen.getByRole('button', { - name: /Get started/i, - }); - - // The button should be wrapped in a Link component - const linkElement = getStartedButton.closest('a'); - - // Should have automatic tracking disabled - expect(linkElement).toHaveAttribute( - 'data-analytics-track-by-analytics-manager', - 'false', - ); - - // Click the link - fireEvent.click(linkElement!); - - // Should push CTA event with correct parameters - expect(mockTrackAnalytics).toHaveBeenCalledWith( - 'Get Started - Ansible', - 'Catalog', - 'https://ansible.example.com', - Intcmp.AAP, - 'cta', - ); - }); - - it('should not push CTA event if ansibleUILink is not available', () => { - // Clear all mocks to ensure clean state - jest.clearAllMocks(); - - const contextOverrides = { - ansibleStatus: AnsibleStatus.READY, - ansibleUILink: undefined, // No link available - ansibleUIUser: 'admin', - ansibleUIPassword: 'password123', - ansibleError: null, - }; - - renderModal(contextOverrides); - - const getStartedButton = screen.getByRole('button', { - name: /Get started/i, - }); - - const linkElement = getStartedButton.closest('a'); - fireEvent.click(linkElement!); - - // Should not push event when link is not available - expect(mockTrackAnalytics).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/PhoneVerificationModal.test.tsx b/workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/PhoneVerificationModal.test.tsx deleted file mode 100644 index 3da59557bd..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/__tests__/PhoneVerificationModal.test.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { PhoneVerificationModal } from '../PhoneVerificationModal'; -import { registerApiRef } from '../../../api'; -import { useSandboxContext } from '../../../hooks/useSandboxContext'; // Corrected path -import { TestApiProvider } from '@backstage/test-utils'; -import { AnsibleStatus } from '../../../utils/aap-utils'; -import { Product } from '../../SandboxCatalog/productData'; - -// Mock dependencies -jest.mock('../../../hooks/useSandboxContext'); -jest.mock('../../../utils/common', () => ({ - errorMessage: jest.fn(e => e.message || 'Unknown error'), -})); - -// Mock the props interface if it's not exported from the component file -declare module '../PhoneVerificationModal' { - interface PhoneVerificationModalProps { - modalOpen: boolean; - setOpen: (open: boolean) => void; - } -} - -describe('PhoneVerificationModal', () => { - const mockInitiatePhoneVerification = jest.fn(); - const mockCompletePhoneVerification = jest.fn(); - const mockRefetchUserData = jest.fn(); - const mockSetOpen = jest.fn(); - const mockSetRefetchingUserData = jest.fn(); - const mockSetAnsibleCredsOpen = jest.fn(); - - const mockRegisterApi = { - initiatePhoneVerification: mockInitiatePhoneVerification, - completePhoneVerification: mockCompletePhoneVerification, - // Add other required methods from the API - getRecaptchaAPIKey: jest.fn(), - getSignUpData: jest.fn(), - signup: jest.fn(), - verifyActivationCode: jest.fn(), - }; - - const mockUseSandboxContext = useSandboxContext as jest.MockedFunction< - typeof useSandboxContext - >; - - beforeEach(() => { - jest.clearAllMocks(); - mockUseSandboxContext.mockReturnValue({ - refetchUserData: mockRefetchUserData, - loading: false, - userFound: false, - userReady: false, - ansibleStatus: AnsibleStatus.UNKNOWN, - verificationRequired: false, - userData: undefined, - signupUser: jest.fn(), - refetchAAP: jest.fn(), - ansibleData: undefined, - ansibleUIUser: undefined, - } as any); - }); - - const renderComponent = () => { - return render( - - - , - ); - }; - - test('should open the modal', () => { - renderComponent(); - - expect(screen.getByText("Let's verify you")).toBeInTheDocument(); - expect( - screen.getByText( - "Enter your phone number and we'll send you a text message with a verification code.", - ), - ).toBeInTheDocument(); - }); - - test('should handle modal close', () => { - renderComponent(); - - // Find and click close button/icon - const closeButton = screen.getByRole('button', { name: /close/i }); - fireEvent.click(closeButton); - - expect(mockSetOpen).toHaveBeenCalledWith(false); - }); -}); diff --git a/workspaces/sandbox/plugins/sandbox/src/components/Modals/index.ts b/workspaces/sandbox/plugins/sandbox/src/components/Modals/index.ts deleted file mode 100644 index f52e756713..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/Modals/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { default as AccessCodeInputModal } from './AccessCodeInputModal'; -export { default as AnsibleLaunchInfoModal } from './AnsibleLaunchInfoModal'; -export { default as PhoneVerificationModal } from './PhoneVerificationModal'; -export { default as AnsibleDeleteInstanceModal } from './AnsibleDeleteInstanceModal'; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/SandboxActivitiesCard.tsx b/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/SandboxActivitiesCard.tsx deleted file mode 100644 index 688915ecb8..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/SandboxActivitiesCard.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import Typography from '@mui/material/Typography'; -import CardMedia from '@mui/material/CardMedia'; -import { useTheme } from '@mui/material/styles'; -import { Link } from '@backstage/core-components'; -import { - getEddlDataAttributes, - useTrackAnalytics, -} from '../../utils/eddl-utils'; - -type SandboxActivitiesCardProps = { - article: { - img: string; - title: string; - description: string; - link: string; - }; -}; - -export const SandboxActivitiesCard: React.FC = ({ - article: { img, title, description, link }, -}) => { - const theme = useTheme(); - const trackAnalytics = useTrackAnalytics(); - const eddlAttributes = getEddlDataAttributes(title, 'Activities'); - - // Handle activity click for analytics tracking - const handleActivityClick = async () => { - await trackAnalytics(title, 'Activities', link); - }; - - return ( - - - - - - {title} - - - {description} - - - - - ); -}; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/SandboxActivitiesGrid.tsx b/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/SandboxActivitiesGrid.tsx deleted file mode 100644 index 2f02da729a..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/SandboxActivitiesGrid.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import { useTheme } from '@mui/material/styles'; -import { ItemCardGrid } from '@backstage/core-components'; -import { SandboxActivitiesCard } from './SandboxActivitiesCard'; -import { articleData } from './articleData'; - -const FeaturedArticles: React.FC = () => { - const theme = useTheme(); - - return ( - - - Featured - - - {articleData?.featured?.map(article => ( - - ))} - - - ); -}; - -const Articles: React.FC = () => { - const theme = useTheme(); - - return ( - - - {articleData?.other?.map(article => ( - - ))} - - - ); -}; - -export const SandboxActivitiesGrid: React.FC = () => { - return ( - <> - - - - ); -}; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/SandboxActivitiesPage.tsx b/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/SandboxActivitiesPage.tsx deleted file mode 100644 index 985ffe7747..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/SandboxActivitiesPage.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import { Page, Content } from '@backstage/core-components'; -import { SandboxHeader } from '../SandboxHeader'; -import { SandboxActivitiesGrid } from './SandboxActivitiesGrid'; -import { SandboxCatalogFooter } from '../SandboxCatalog/SandboxCatalogFooter'; -import { SandboxProvider } from '../../hooks/useSandboxContext'; - -export const SandboxActivitiesPage = () => { - return ( - - - - - - - - - - ); -}; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/__tests__/SandboxActivitiesCard.test.tsx b/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/__tests__/SandboxActivitiesCard.test.tsx deleted file mode 100644 index 8a3ad43236..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/__tests__/SandboxActivitiesCard.test.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { SandboxActivitiesCard } from '../SandboxActivitiesCard'; -import { ThemeProvider } from '@mui/material/styles'; -import { lightTheme } from '@backstage/theme'; -import { wrapInTestApp } from '@backstage/test-utils'; -import * as eddlUtils from '../../../utils/eddl-utils'; - -// Mock the useTrackAnalytics hook -jest.mock('../../../utils/eddl-utils', () => ({ - ...jest.requireActual('../../../utils/eddl-utils'), - useTrackAnalytics: jest.fn(), -})); - -describe('SandboxActivitiesCard', () => { - const mockTrackAnalytics = jest.fn(); - const mockArticle = { - img: 'test-image.jpg', - title: 'Test Article', - description: 'This is a test article description', - link: '/test-link', - }; - - beforeEach(() => { - jest.clearAllMocks(); - // Mock the useTrackAnalytics hook to return a mock function - (eddlUtils.useTrackAnalytics as jest.Mock).mockReturnValue( - mockTrackAnalytics, - ); - }); - - const renderCard = () => { - return render( - wrapInTestApp( - - - , - ), - ); - }; - - it('renders without errors', () => { - renderCard(); - expect(screen.getByText(mockArticle.title)).toBeInTheDocument(); - }); - - it('displays the correct article title', () => { - renderCard(); - const titleElement = screen.getByText(mockArticle.title); - expect(titleElement).toBeInTheDocument(); - expect(titleElement.tagName).toBe('H5'); - }); - - it('displays the correct article description', () => { - renderCard(); - const descriptionElement = screen.getByText(mockArticle.description); - expect(descriptionElement).toBeInTheDocument(); - }); - - it('renders the image with correct attributes', () => { - renderCard(); - const imageElement = screen.getByRole('img'); - expect(imageElement).toHaveAttribute('src', mockArticle.img); - expect(imageElement).toHaveAttribute('alt', mockArticle.title); - expect(imageElement).toHaveAttribute('height', '120'); - }); - - it('links to the correct destination', () => { - renderCard(); - const linkElement = screen.getByRole('link'); - expect(linkElement).toHaveAttribute('href', mockArticle.link); - }); - - it('has no text decoration on the link', () => { - renderCard(); - const linkElement = screen.getByRole('link'); - expect(linkElement).toHaveStyle('text-decoration: none'); - }); - - it('creates a card with the expected styling', () => { - renderCard(); - const cardElement = screen.getByRole('link').firstChild; - - // Check that it's using the Card component by inspecting class names - expect(cardElement).toHaveClass(/MuiCard-root/); - - // Check it has no elevation - expect(cardElement).toHaveClass(/MuiPaper-elevation0/); - }); - - it('applies appropriate typographic styles', () => { - renderCard(); - const titleElement = screen.getByText(mockArticle.title); - const descriptionElement = screen.getByText(mockArticle.description); - - // Check title is primary color - expect(titleElement).toHaveClass(/MuiTypography-h5/); - - // Check description is secondary color - expect(descriptionElement).toHaveClass(/MuiTypography-body2/); - - // Check description has italic style - expect(descriptionElement).toHaveStyle('font-style: italic'); - }); - - describe('EDDL data attributes', () => { - it('should have correct Red Hat EDDL data attributes for activities', () => { - renderCard(); - - const linkElement = screen.getByRole('link'); - - expect(linkElement).toHaveAttribute( - 'data-analytics-category', - 'Developer Sandbox|Activities', - ); - expect(linkElement).toHaveAttribute( - 'data-analytics-text', - 'Test Article', - ); - expect(linkElement).toHaveAttribute( - 'data-analytics-region', - 'sandbox-activities', - ); - }); - }); -}); diff --git a/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/__tests__/SandboxActivitiesGrid.test.tsx b/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/__tests__/SandboxActivitiesGrid.test.tsx deleted file mode 100644 index 30cc321045..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/__tests__/SandboxActivitiesGrid.test.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { SandboxActivitiesGrid } from '../SandboxActivitiesGrid'; -import { articleData } from '../articleData'; -import { ThemeProvider } from '@mui/material/styles'; -import { lightTheme } from '@backstage/theme'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { SandboxActivitiesCard } from '../SandboxActivitiesCard'; - -// Mock the SandboxActivitiesCard component -jest.mock('../SandboxActivitiesCard', () => ({ - SandboxActivitiesCard: jest.fn(() =>
), -})); - -// Mock the articleData -jest.mock('../articleData', () => ({ - articleData: { - featured: [ - { - img: 'featured-image.jpg', - title: 'Featured Article', - description: 'This is a featured article', - link: '/featured-link', - }, - ], - other: [ - { - img: 'other-image-1.jpg', - title: 'Other Article 1', - description: 'This is another article', - link: '/other-link-1', - }, - { - img: 'other-image-2.jpg', - title: 'Other Article 2', - description: 'This is yet another article', - link: '/other-link-2', - }, - ], - }, -})); - -describe('SandboxActivitiesGrid', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - const renderGrid = () => { - return render( - wrapInTestApp( - - - , - ), - ); - }; - - it('renders the featured and other articles sections', () => { - renderGrid(); - - // Check for Featured heading - expect(screen.getByText('Featured')).toBeInTheDocument(); - - // Check that we have the right number of cards rendered (1 featured + 2 other) - const cards = screen.getAllByTestId('mock-card'); - expect(cards).toHaveLength(3); - }); - - it('renders the correct number of cards for other articles', () => { - renderGrid(); - - // SandboxActivitiesCard should be called once for each featured article - // and once for each other article - expect(SandboxActivitiesCard).toHaveBeenCalledTimes( - articleData.featured.length + articleData.other.length, - ); - }); - - describe('Articles component', () => { - // Directly testing the Articles component would require exposing it, - // but we can test its behavior through the parent component - - it('renders correctly when there are no other articles', () => { - // Temporarily modify the mock to have no other articles - const originalOther = articleData.other; - articleData.other = []; - - renderGrid(); - - // Should only render the featured article cards - expect(SandboxActivitiesCard).toHaveBeenCalledTimes( - articleData.featured.length, - ); - - // Restore the original mock - articleData.other = originalOther; - }); - }); -}); diff --git a/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/articleData.ts b/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/articleData.ts deleted file mode 100644 index 62560d5560..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/SandboxActivities/articleData.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import Art0 from '../../assets/images/art0.svg'; -import Art1 from '../../assets/images/art1.svg'; -import Art2 from '../../assets/images/art2.svg'; -import Art3 from '../../assets/images/art3.svg'; -import Art4 from '../../assets/images/art4.svg'; -import Art5 from '../../assets/images/art5.svg'; - -type ArticleData = { - img: string; - title: string; - description: string; - link: string; -}; - -type Articles = { - featured: ArticleData[]; - other: ArticleData[]; -}; - -export const articleData: Articles = { - featured: [ - { - img: Art0, - title: 'Get started with your Developer Sandbox', - description: - 'Learn how to set up and use the Developer Sandbox, and learn to develop quicker than ever before.', - link: 'https://developers.redhat.com/learn/openshift/get-started-your-developer-sandbox', - }, - { - img: Art1, - title: 'Streamline automation in OpenShift Dev Spaces with Ansible', - description: - 'Learn how to transform the way you develop and test Ansible automations by using OpenShift Dev Spaces, which provides isolated and tailored development environments.', - link: 'https://developers.redhat.com/learn/openshift/streamline-automation-openshift-dev-spaces-ansible', - }, - { - img: Art2, - title: 'How to deploy a Java application on Kubernetes in minutes', - description: - 'Modernize a legacy Java application by creating microservices, moving it into a container, then deploying it to Red Hat OpenShift using only Kubernetes commands.', - link: 'https://developers.redhat.com/learn/java/how-deploy-java-application-kubernetes-minutes', - }, - ], - other: [ - { - img: Art3, - title: 'Foundations of OpenShift', - description: - 'Learn the foundations of OpenShift through hands-on experience deploying and working with applications.', - link: 'https://developers.redhat.com/learn/openshift/foundations-openshift', - }, - { - img: Art4, - title: 'Using OpenShift Pipelines', - description: - 'Learn how to use OpenShift Pipelines for automated builds and deployments – known as CI/CD – of container-based applications to reduce mistakes, improve productivity, and promote more thorough testing.', - link: 'https://developers.redhat.com/learn/openshift/using-openshift-pipelines', - }, - { - img: Art5, - title: - 'OpenShift virtualization and application modernization using the Developer Sandbox', - description: - 'Learn how to create and manage your virtual machines (VMs) using Red Hat OpenShift and the Developer Sandbox, a no-cost OpenShift cluster with no need for setup or configuration.', - link: 'https://developers.redhat.com/learn/openshift/openshift-virtualization-and-application-modernization-using-developer-sandbox', - }, - ], -}; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogBanner.tsx b/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogBanner.tsx deleted file mode 100644 index 252863153b..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogBanner.tsx +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import Card from '@mui/material/Card'; -import Stack from '@mui/material/Stack'; -import Skeleton from '@mui/material/Skeleton'; -import Popover from '@mui/material/Popover'; -import IconButton from '@mui/material/IconButton'; -import Link from '@mui/material/Link'; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import CloseIcon from '@mui/icons-material/Close'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import { useTheme } from '@mui/material/styles'; -import Image from '../../assets/images/sandbox-banner-image.svg'; -import { useSandboxContext } from '../../hooks/useSandboxContext'; -import { calculateDaysBetweenDates } from '../../utils/common'; - -export const SandboxCatalogBanner: React.FC = () => { - const theme = useTheme(); - const { userData, pendingApproval, verificationRequired, loading } = - useSandboxContext(); - const [anchorEl, setAnchorEl] = React.useState(null); - - const calculateDaysLeft = React.useCallback(() => { - if (userData?.endDate) { - const trialEndDate = new Date(userData?.endDate); - return calculateDaysBetweenDates(new Date(), trialEndDate); - } - // unable to compute days - return undefined; - }, [userData?.endDate]); - - const handleInfoClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handlePopoverClose = () => { - setAnchorEl(null); - }; - - const open = Boolean(anchorEl); - - return ( - - - - Red Hat Trial - - - - {loading ? ( - <> - - - - - ) : ( - <> - {userData ? ( - <> - - Welcome, {userData.givenName || userData.compliantUsername} - - - - {(() => { - if (verificationRequired) { - return 'Click on "Try it" to initiate your free, no commitment 30-day trial.'; - } - if (pendingApproval) { - return 'Please wait for your trial to be approved.'; - } - if (userData?.endDate) { - const daysLeft = calculateDaysLeft(); - return `Your free trial expires in ${daysLeft} ${ - daysLeft === 1 ? 'day' : 'days' - }`; - } - return ''; - })()} - - {userData?.endDate && ( - - - - )} - - - ) : ( - <> - - Try Red Hat products - - - Explore, experiment, and see what's possible - - - Click on "Try it" to initiate your free, no commitment - 30-day trial. - - - )} - - )} - - - - - - - - - - - Trial expiration - - - - Once this trial expires, you can start a new one immediately. Your - work from this trial will be deleted. To save your work, follow the - instructions in our documentation. - - - - View documentation - - - - - - ); -}; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogCard.tsx b/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogCard.tsx deleted file mode 100644 index a1b59fcfdd..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogCard.tsx +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React, { useState } from 'react'; -import { useTheme } from '@mui/material/styles'; -import Box from '@mui/material/Box'; -import Stack from '@mui/material/Stack'; -import Card from '@mui/material/Card'; -import Tooltip from '@mui/material/Tooltip'; -import DoneIcon from '@mui/icons-material/Done'; -import Typography from '@mui/material/Typography'; -import CardMedia from '@mui/material/CardMedia'; -import CardContent from '@mui/material/CardContent'; -import CardActions from '@mui/material/CardActions'; -import { SandboxCatalogCardButton } from './SandboxCatalogCardButton'; -import { SandboxCatalogCardDeleteButton } from './SandboxCatalogCardDeleteButton'; -import { - AnsibleDeleteInstanceModal, - AnsibleLaunchInfoModal, - PhoneVerificationModal, -} from '../Modals'; -import { useSandboxContext } from '../../hooks/useSandboxContext'; -import { useApi } from '@backstage/core-plugin-api'; -import { aapApiRef, kubeApiRef } from '../../api'; -import { Product } from './productData'; -import { signupDataToStatus } from '../../utils/register-utils'; -import { productsURLMapping } from '../../hooks/useProductURLs'; - -type SandboxCatalogCardProps = { - id: Product; - title: string; - image: string; - description: { - icon: React.ReactNode; - value: string; - }[]; - link: string; - greenCorner: boolean; - showGreenCorner: () => void; -}; - -const CatalogCardGreenCorner = ({ show }: { show: boolean }) => { - const theme = useTheme(); - if (!show) { - return ( - // Empty box to keep the layout consistent - - ); - } - return ( - - - - - - ); -}; - -export const SandboxCatalogCard: React.FC = ({ - id, - title, - image, - description, - link, - greenCorner, - showGreenCorner, -}) => { - const theme = useTheme(); - const kubeApi = useApi(kubeApiRef); - const aapApi = useApi(aapApiRef); - let { userData, userFound, userReady, verificationRequired } = - useSandboxContext(); - const { handleAAPInstance, signupUser, refetchUserData, refetchAAP } = - useSandboxContext(); - const [ansibleCredsModalOpen, setAnsibleCredsModalOpen] = - React.useState(false); - const [verifyPhoneModalOpen, setVerifyPhoneModalOpen] = useState(false); - const [refetchingUserData, setRefetchingUserData] = useState(false); - const [deleteAnsibleModalOpen, setDeleteAnsibleModalOpen] = useState(false); - const [deleting, setDeleting] = useState(false); - - const handleTryButtonClick = async (pdt: Product) => { - // User is not yet signed up - let urlToOpen = link; - if (!userFound) { - signupUser(); - - const maxAttempts = 5; - const retryInterval = 1000; // 1 second - - // Poll until user is found or max attempts reached - for (let i = 0; i < maxAttempts; i++) { - setRefetchingUserData(true); - await new Promise(resolve => setTimeout(resolve, retryInterval)); - - try { - // Fetch the latest user data and check if user is found - userData = await refetchUserData(); - if (userData) { - userFound = true; - const userStatus = signupDataToStatus(userData); - verificationRequired = userStatus === 'verify'; - userReady = userStatus === 'ready'; - const userNamespaceReady = - userData?.defaultUserNamespace !== undefined; - // if user is ready or verification is required we can stop fetching the data - if ((userReady || verificationRequired) && userNamespaceReady) { - const productURLs = productsURLMapping(userData); - // find the link to open if any - urlToOpen = productURLs.find(pu => pu.id === id)?.url || ''; - if (urlToOpen && !verificationRequired) { - // open url if user is ready and no further verification is required - window.open(urlToOpen, '_blank'); - showGreenCorner(); - } - break; - } - } - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error fetching user data:', error); - } finally { - setRefetchingUserData(false); - } - } - } - - // User has signed up but require verification - if (userFound && verificationRequired) { - setVerifyPhoneModalOpen(true); - return; - } - - // User has signed up and the trial is ready and user selects the AAP Trial - if (userFound && userReady && pdt === Product.AAP) { - if (!userData?.defaultUserNamespace) { - // eslint-disable-next-line - console.error( - 'unable to provision AAP. user namespace is not defined.', - ); - return; - } - handleAAPInstance(userData.defaultUserNamespace); - setAnsibleCredsModalOpen(true); - showGreenCorner(); - } - }; - - const handleDeleteButtonClick = async (pdt: Product) => { - if (deleting) { - return; - } - - setDeleteAnsibleModalOpen(false); - setDeleting(true); - const userNamespace = userData?.defaultUserNamespace; - if (!userNamespace) { - // eslint-disable-next-line - console.error( - 'unable to delete aap instance. user namespace is undefined', - ); - return; - } - if (pdt === Product.AAP) { - refetchAAP(userNamespace); - const aapLabelSelector = - 'app.kubernetes.io%2Fmanaged-by+in+%28aap-gateway-operator%2Caap-operator%2Cautomationcontroller-operator%2Cautomationhub-operator%2Ceda-operator%2Clightspeed-operator%29&limit=50'; - - try { - const aapDeployments = await kubeApi.getDeployments( - userNamespace, - aapLabelSelector, - ); - const aapStatefulSets = await kubeApi.getStatefulSets( - userNamespace, - aapLabelSelector, - ); - await aapApi.deleteAAPCR(userNamespace); - await kubeApi.deleteSecretsAndPVCs(aapDeployments, userNamespace); - await kubeApi.deleteSecretsAndPVCs(aapStatefulSets, userNamespace); - await kubeApi.deletePVCsForSTS(aapStatefulSets, userNamespace); - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - } - } - refetchAAP(userNamespace); - setDeleting(false); - }; - - return ( - <> - - - - - {title} - - {title} - - - - - {description?.map(point => ( - -
- {point.icon} {point.value} -
-
- ))} -
- - - setDeleteAnsibleModalOpen(true)} - isDeleting={deleting} - /> - -
- - - handleDeleteButtonClick(id)} - /> - - ); -}; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogCardButton.tsx b/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogCardButton.tsx deleted file mode 100644 index 09bcf160be..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogCardButton.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import Button from '@mui/material/Button'; -import { Theme } from '@mui/material/styles'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import CircularProgress from '@mui/material/CircularProgress'; -import { Link } from '@backstage/core-components'; -import { useSandboxContext } from '../../hooks/useSandboxContext'; -import { AnsibleStatus } from '../../utils/aap-utils'; -import { Product } from './productData'; -import { useTrackAnalytics } from '../../utils/eddl-utils'; -import { Intcmp } from '../../hooks/useProductURLs'; - -type SandboxCatalogCardButtonProps = { - link: string; - id: Product; - title: string; - handleTryButtonClick: (id: Product) => void; - theme: Theme; - refetchingUserData?: boolean; -}; - -export const SandboxCatalogCardButton: React.FC< - SandboxCatalogCardButtonProps -> = ({ link, id, title, handleTryButtonClick, theme, refetchingUserData }) => { - const { loading, userFound, verificationRequired, userReady, ansibleStatus } = - useSandboxContext(); - const [clicked, setClicked] = React.useState(false); - const trackAnalytics = useTrackAnalytics(); - - const handleClick = () => { - if (!clicked) setClicked(true); - handleTryButtonClick(id); - }; - - const label = (() => { - if (id === Product.AAP) { - if (ansibleStatus === AnsibleStatus.IDLED) { - return 'Reprovision'; - } - if (ansibleStatus === AnsibleStatus.PROVISIONING) { - return 'Provisioning'; - } - if (ansibleStatus === AnsibleStatus.READY) { - return 'Launch'; - } - return 'Provision'; - } - return 'Try it'; - })(); - - let endIcon; - if ( - (loading && clicked) || - (userFound && !userReady && !verificationRequired && clicked) || - (refetchingUserData && clicked) - ) { - endIcon = ; - } else if (id !== Product.AAP) { - endIcon = ; - } else if ( - ansibleStatus === AnsibleStatus.UNKNOWN || - ansibleStatus === AnsibleStatus.PROVISIONING - ) { - endIcon = ; - } else { - endIcon = null; - } - - const buttonSx = { - border: `1px solid ${ - theme.palette.mode === 'dark' ? '#92c5f9' : theme.palette.primary.main - }`, - marginTop: theme.spacing(0.5), - '&:hover': { - backgroundColor: 'rgba(25, 118, 210, 0.04)', - borderColor: '#1976d2', - }, - }; - - // Get the intcmp parameter for this product - const getIntcmpFromProduct = (productId: Product): string | undefined => { - switch (productId) { - case Product.OPENSHIFT_CONSOLE: - return Intcmp.OPENSHIFT_CONSOLE; - case Product.DEVSPACES: - return Intcmp.DEVSPACES; - case Product.OPENSHIFT_AI: - return Intcmp.RHODS; - case Product.OPENSHIFT_VIRT: - return Intcmp.OPENSHIFT_VIRT; - case Product.AAP: - return Intcmp.AAP; - default: - return undefined; - } - }; - - const intcmp = getIntcmpFromProduct(id); - - // Handle CTA click for analytics - const handleCtaClick = async () => { - await trackAnalytics(title, 'Catalog', link, intcmp, 'cta'); - }; - - const buttonContent = ( - - ); - - return userFound && !loading && !verificationRequired ? ( - - {buttonContent} - - ) : ( - // When there's no link, we push CTA event on button click - - ); -}; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogCardDeleteButton.tsx b/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogCardDeleteButton.tsx deleted file mode 100644 index fb7bd08825..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogCardDeleteButton.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import Button from '@mui/material/Button'; -import { Theme } from '@mui/material/styles'; -import CircularProgress from '@mui/material/CircularProgress'; -import { useSandboxContext } from '../../hooks/useSandboxContext'; -import { AnsibleStatus } from '../../utils/aap-utils'; -import { Product } from './productData'; - -type SandboxCatalogCardDeleteButtonProps = { - id: Product; - handleDeleteButtonClick: (id: Product) => void; - theme: Theme; - isDeleting: boolean; -}; - -export const SandboxCatalogCardDeleteButton: React.FC< - SandboxCatalogCardDeleteButtonProps -> = ({ id, handleDeleteButtonClick, theme, isDeleting }) => { - const { ansibleStatus } = useSandboxContext(); - - if ( - id === Product.AAP && - (ansibleStatus === AnsibleStatus.READY || - ansibleStatus === AnsibleStatus.PROVISIONING || - ansibleStatus === AnsibleStatus.UNKNOWN) - ) { - return ( - - ); - } - return null; -}; diff --git a/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogFooter.tsx b/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogFooter.tsx deleted file mode 100644 index bc9d1292a9..0000000000 --- a/workspaces/sandbox/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogFooter.tsx +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import { useTheme } from '@mui/material/styles'; -import Link from '@mui/material/Link'; -import { AccessCodeInputModal } from '../Modals/AccessCodeInputModal'; -import { FooterCopyright } from '@rhds/elements/react/rh-footer/rh-footer-copyright'; -import { FooterUniversal } from '@rhds/elements/react/rh-footer/rh-footer-universal'; - -import '@rhds/elements/rh-footer/rh-footer-lightdom.css'; -import { useEffect, useRef } from 'react'; - -// Create the TrustArc element once, outside of any component -let trustArcElement: HTMLSpanElement | null = null; - -const createTrustArcElement = () => { - if (!trustArcElement) { - trustArcElement = document.createElement('span'); - trustArcElement.id = 'teconsent'; - trustArcElement.style.display = 'none'; - } - return trustArcElement; -}; - -const CookieConsentElement = () => { - const consentRef = useRef(null); - - useEffect(() => { - const spanElement = createTrustArcElement(); - - if (consentRef.current && !consentRef.current.contains(spanElement)) { - consentRef.current.appendChild(spanElement); - } - }); - - return
  • ; -}; - -export const SandboxCatalogFooter = () => { - const theme = useTheme(); - const [accessCodeModalOpen, setAccessCodeModalOpen] = React.useState(false); - - const handleClick = () => { - setAccessCodeModalOpen(true); - }; - - return ( - <> - - - Have an activation code?{' '} - - Click here - - -